Skip to content

Commit f53cfcd

Browse files
authored
fix: Allow overloading the local environment (#281)
1 parent 82c5670 commit f53cfcd

File tree

3 files changed

+173
-37
lines changed

3 files changed

+173
-37
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
* feat: `icp canister metadata <canister> <metadata section>` now fetches metadata sections from specified canisters
55
* fix: Validate explicit canister paths and throw an error if `canister.yaml` is not found
66
* feat!: Rename the implicit "mainnet" network to "ic"
7-
* The corresponding environment "ic" is defined implicitly which can be overwritten by user configuration
7+
* The corresponding environment "ic" is defined implicitly which can be overwritten by user configuration.
88
* The `--mainnet` and `--ic` flags are removed. Use `-n/--network ic`, `-e/--environment ic` instead.
9+
* feat: Allow overriding the implicit `local` network and environment.
910

1011
# v0.1.0-beta.3
1112

crates/icp/src/lib.rs

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,3 +510,169 @@ impl ProjectLoad for NoProjectLoader {
510510
Ok(false)
511511
}
512512
}
513+
514+
#[cfg(test)]
515+
mod tests {
516+
use super::*;
517+
use crate::canister::recipe::{Resolve, ResolveError};
518+
use crate::manifest::{
519+
ProjectRootLocate, ProjectRootLocateError,
520+
canister::{BuildSteps, SyncSteps},
521+
recipe::Recipe,
522+
};
523+
use camino_tempfile::Utf8TempDir;
524+
use indoc::indoc;
525+
526+
struct MockProjectRootLocate {
527+
path: PathBuf,
528+
}
529+
530+
impl MockProjectRootLocate {
531+
fn new(path: PathBuf) -> Self {
532+
Self { path }
533+
}
534+
}
535+
536+
impl ProjectRootLocate for MockProjectRootLocate {
537+
fn locate(&self) -> Result<PathBuf, ProjectRootLocateError> {
538+
Ok(self.path.clone())
539+
}
540+
}
541+
542+
struct MockRecipeResolver;
543+
544+
#[async_trait]
545+
impl Resolve for MockRecipeResolver {
546+
async fn resolve(&self, _recipe: &Recipe) -> Result<(BuildSteps, SyncSteps), ResolveError> {
547+
use crate::manifest::adapter::prebuilt::{
548+
Adapter as PrebuiltAdapter, LocalSource, SourceField,
549+
};
550+
use crate::manifest::canister::BuildStep;
551+
552+
// Create a minimal BuildSteps with a dummy prebuilt step
553+
let build_steps = BuildSteps {
554+
steps: vec![BuildStep::Prebuilt(PrebuiltAdapter {
555+
source: SourceField::Local(LocalSource {
556+
path: "dummy.wasm".into(),
557+
}),
558+
sha256: None,
559+
})],
560+
};
561+
562+
Ok((build_steps, SyncSteps::default()))
563+
}
564+
}
565+
566+
#[tokio::test]
567+
async fn test_load_minimal_project() {
568+
// Create temp directory with icp.yaml
569+
let temp_dir = Utf8TempDir::new().unwrap();
570+
let project_dir = temp_dir.path();
571+
572+
// Write a minimal icp.yaml
573+
let manifest_content = indoc! {r#"
574+
canisters:
575+
- name: backend
576+
build:
577+
steps:
578+
- type: pre-built
579+
path: backend.wasm
580+
"#};
581+
std::fs::write(project_dir.join("icp.yaml"), manifest_content).unwrap();
582+
583+
// Create ProjectLoadImpl with mocks
584+
let loader = ProjectLoadImpl {
585+
project_root_locate: Arc::new(MockProjectRootLocate::new(project_dir.to_path_buf())),
586+
recipe: Arc::new(MockRecipeResolver),
587+
};
588+
589+
// Call load
590+
let result = loader.load().await;
591+
592+
// Assert success and check project contents
593+
assert!(result.is_ok());
594+
let project = result.unwrap();
595+
assert_eq!(project.dir, project_dir);
596+
assert!(
597+
project.canisters.contains_key("backend"),
598+
"The backend canister was not found"
599+
);
600+
assert!(
601+
project.environments.contains_key("local"),
602+
"The default `local` environment was not injected"
603+
);
604+
assert!(
605+
project.environments.contains_key("ic"),
606+
"The default `ic` environment was not injected"
607+
);
608+
assert!(
609+
project.networks.contains_key("local"),
610+
"The default `local` network was not injected"
611+
);
612+
assert!(
613+
project.networks.contains_key("ic"),
614+
"The default `ic` network was not injected"
615+
);
616+
}
617+
618+
#[tokio::test]
619+
async fn test_load_project_local_override() {
620+
// Create temp directory with icp.yaml
621+
let temp_dir = Utf8TempDir::new().unwrap();
622+
let project_dir = temp_dir.path();
623+
624+
// Write a minimal icp.yaml
625+
let manifest_content = indoc! {r#"
626+
networks:
627+
- name: test-network
628+
mode: connected
629+
url: https://somenetwork.icp
630+
environments:
631+
- name: local
632+
network: test-network
633+
canisters:
634+
- name: backend
635+
build:
636+
steps:
637+
- type: pre-built
638+
path: backend.wasm
639+
"#};
640+
std::fs::write(project_dir.join("icp.yaml"), manifest_content).unwrap();
641+
642+
// Create ProjectLoadImpl with mocks
643+
let loader = ProjectLoadImpl {
644+
project_root_locate: Arc::new(MockProjectRootLocate::new(project_dir.to_path_buf())),
645+
recipe: Arc::new(MockRecipeResolver),
646+
};
647+
648+
// Call load
649+
let result = loader.load().await;
650+
651+
// Assert success and check project contents
652+
assert!(result.is_ok(), "The project did not load: {:?}", result);
653+
let project = result.unwrap();
654+
assert_eq!(project.dir, project_dir);
655+
assert!(
656+
project.canisters.contains_key("backend"),
657+
"The backend canister was not found"
658+
);
659+
assert!(
660+
project.environments.contains_key("local"),
661+
"The default `local` environment was not injected"
662+
);
663+
let e = project.environments.get("local").unwrap();
664+
assert_eq!(e.network.name, "test-network");
665+
assert!(
666+
project.environments.contains_key("ic"),
667+
"The default `ic` environment was not injected"
668+
);
669+
assert!(
670+
project.networks.contains_key("local"),
671+
"The default `local` network was not injected"
672+
);
673+
assert!(
674+
project.networks.contains_key("ic"),
675+
"The default `ic` network was not injected"
676+
);
677+
}
678+
}

crates/icp/src/manifest/environment.rs

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ use std::collections::HashMap;
22

33
use schemars::JsonSchema;
44
use serde::{Deserialize, Deserializer};
5-
use snafu::prelude::*;
65

76
use crate::{canister::Settings, prelude::LOCAL};
87

@@ -52,16 +51,8 @@ pub struct EnvironmentManifest {
5251
pub init_args: Option<HashMap<String, String>>,
5352
}
5453

55-
#[derive(Debug, Snafu)]
56-
pub enum ParseError {
57-
#[snafu(display("Overriding the local environment is not supported."))]
58-
OverrideLocal,
59-
}
60-
61-
impl TryFrom<EnvironmentInner> for EnvironmentManifest {
62-
type Error = ParseError;
63-
64-
fn try_from(v: EnvironmentInner) -> Result<Self, Self::Error> {
54+
impl From<EnvironmentInner> for EnvironmentManifest {
55+
fn from(v: EnvironmentInner) -> Self {
6556
let EnvironmentInner {
6657
name,
6758
network,
@@ -70,11 +61,6 @@ impl TryFrom<EnvironmentInner> for EnvironmentManifest {
7061
init_args,
7162
} = v;
7263

73-
// Name
74-
if name == LOCAL {
75-
return OverrideLocalSnafu.fail();
76-
}
77-
7864
// Network
7965
let network = network.unwrap_or(LOCAL.to_string());
8066

@@ -93,22 +79,22 @@ impl TryFrom<EnvironmentInner> for EnvironmentManifest {
9379
None => CanisterSelection::Everything,
9480
};
9581

96-
Ok(Self {
82+
Self {
9783
name,
9884
network,
9985
canisters,
10086

10187
// Keep as-is, setting overrides is optional
10288
settings,
10389
init_args,
104-
})
90+
}
10591
}
10692
}
10793

10894
impl<'de> Deserialize<'de> for EnvironmentManifest {
10995
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
11096
let inner: EnvironmentInner = Deserialize::deserialize(d)?;
111-
inner.try_into().map_err(serde::de::Error::custom)
97+
Ok(inner.into())
11298
}
11399
}
114100

@@ -134,21 +120,4 @@ mod tests {
134120
},
135121
);
136122
}
137-
138-
#[test]
139-
fn override_local() {
140-
match serde_yaml::from_str::<EnvironmentManifest>(r#"name: local"#) {
141-
// No Error
142-
Ok(_) => {
143-
panic!("an environment named local should result in an error");
144-
}
145-
146-
// Wrong Error
147-
Err(err) => {
148-
if !format!("{err}").starts_with("Overriding the local environment") {
149-
panic!("an environment named local resulted in the wrong error: {err}");
150-
};
151-
}
152-
};
153-
}
154123
}

0 commit comments

Comments
 (0)