diff --git a/CHANGELOG.md b/CHANGELOG.md index 0db14a23..0983ff5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,9 @@ * feat: `icp canister metadata ` now fetches metadata sections from specified canisters * fix: Validate explicit canister paths and throw an error if `canister.yaml` is not found * feat!: Rename the implicit "mainnet" network to "ic" - * The corresponding environment "ic" is defined implicitly which can be overwritten by user configuration + * The corresponding environment "ic" is defined implicitly which can be overwritten by user configuration. * The `--mainnet` and `--ic` flags are removed. Use `-n/--network ic`, `-e/--environment ic` instead. +* feat: Allow overriding the implicit `local` network and environment. # v0.1.0-beta.3 diff --git a/crates/icp/src/lib.rs b/crates/icp/src/lib.rs index 0cddc242..ee72ae4c 100644 --- a/crates/icp/src/lib.rs +++ b/crates/icp/src/lib.rs @@ -510,3 +510,169 @@ impl ProjectLoad for NoProjectLoader { Ok(false) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::canister::recipe::{Resolve, ResolveError}; + use crate::manifest::{ + ProjectRootLocate, ProjectRootLocateError, + canister::{BuildSteps, SyncSteps}, + recipe::Recipe, + }; + use camino_tempfile::Utf8TempDir; + use indoc::indoc; + + struct MockProjectRootLocate { + path: PathBuf, + } + + impl MockProjectRootLocate { + fn new(path: PathBuf) -> Self { + Self { path } + } + } + + impl ProjectRootLocate for MockProjectRootLocate { + fn locate(&self) -> Result { + Ok(self.path.clone()) + } + } + + struct MockRecipeResolver; + + #[async_trait] + impl Resolve for MockRecipeResolver { + async fn resolve(&self, _recipe: &Recipe) -> Result<(BuildSteps, SyncSteps), ResolveError> { + use crate::manifest::adapter::prebuilt::{ + Adapter as PrebuiltAdapter, LocalSource, SourceField, + }; + use crate::manifest::canister::BuildStep; + + // Create a minimal BuildSteps with a dummy prebuilt step + let build_steps = BuildSteps { + steps: vec![BuildStep::Prebuilt(PrebuiltAdapter { + source: SourceField::Local(LocalSource { + path: "dummy.wasm".into(), + }), + sha256: None, + })], + }; + + Ok((build_steps, SyncSteps::default())) + } + } + + #[tokio::test] + async fn test_load_minimal_project() { + // Create temp directory with icp.yaml + let temp_dir = Utf8TempDir::new().unwrap(); + let project_dir = temp_dir.path(); + + // Write a minimal icp.yaml + let manifest_content = indoc! {r#" + canisters: + - name: backend + build: + steps: + - type: pre-built + path: backend.wasm + "#}; + std::fs::write(project_dir.join("icp.yaml"), manifest_content).unwrap(); + + // Create ProjectLoadImpl with mocks + let loader = ProjectLoadImpl { + project_root_locate: Arc::new(MockProjectRootLocate::new(project_dir.to_path_buf())), + recipe: Arc::new(MockRecipeResolver), + }; + + // Call load + let result = loader.load().await; + + // Assert success and check project contents + assert!(result.is_ok()); + let project = result.unwrap(); + assert_eq!(project.dir, project_dir); + assert!( + project.canisters.contains_key("backend"), + "The backend canister was not found" + ); + assert!( + project.environments.contains_key("local"), + "The default `local` environment was not injected" + ); + assert!( + project.environments.contains_key("ic"), + "The default `ic` environment was not injected" + ); + assert!( + project.networks.contains_key("local"), + "The default `local` network was not injected" + ); + assert!( + project.networks.contains_key("ic"), + "The default `ic` network was not injected" + ); + } + + #[tokio::test] + async fn test_load_project_local_override() { + // Create temp directory with icp.yaml + let temp_dir = Utf8TempDir::new().unwrap(); + let project_dir = temp_dir.path(); + + // Write a minimal icp.yaml + let manifest_content = indoc! {r#" + networks: + - name: test-network + mode: connected + url: https://somenetwork.icp + environments: + - name: local + network: test-network + canisters: + - name: backend + build: + steps: + - type: pre-built + path: backend.wasm + "#}; + std::fs::write(project_dir.join("icp.yaml"), manifest_content).unwrap(); + + // Create ProjectLoadImpl with mocks + let loader = ProjectLoadImpl { + project_root_locate: Arc::new(MockProjectRootLocate::new(project_dir.to_path_buf())), + recipe: Arc::new(MockRecipeResolver), + }; + + // Call load + let result = loader.load().await; + + // Assert success and check project contents + assert!(result.is_ok(), "The project did not load: {:?}", result); + let project = result.unwrap(); + assert_eq!(project.dir, project_dir); + assert!( + project.canisters.contains_key("backend"), + "The backend canister was not found" + ); + assert!( + project.environments.contains_key("local"), + "The default `local` environment was not injected" + ); + let e = project.environments.get("local").unwrap(); + assert_eq!(e.network.name, "test-network"); + assert!( + project.environments.contains_key("ic"), + "The default `ic` environment was not injected" + ); + assert!( + project.networks.contains_key("local"), + "The default `local` network was not injected" + ); + assert!( + project.networks.contains_key("ic"), + "The default `ic` network was not injected" + ); + } +} diff --git a/crates/icp/src/manifest/environment.rs b/crates/icp/src/manifest/environment.rs index b4aad4f7..358dd34c 100644 --- a/crates/icp/src/manifest/environment.rs +++ b/crates/icp/src/manifest/environment.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use schemars::JsonSchema; use serde::{Deserialize, Deserializer}; -use snafu::prelude::*; use crate::{canister::Settings, prelude::LOCAL}; @@ -52,16 +51,8 @@ pub struct EnvironmentManifest { pub init_args: Option>, } -#[derive(Debug, Snafu)] -pub enum ParseError { - #[snafu(display("Overriding the local environment is not supported."))] - OverrideLocal, -} - -impl TryFrom for EnvironmentManifest { - type Error = ParseError; - - fn try_from(v: EnvironmentInner) -> Result { +impl From for EnvironmentManifest { + fn from(v: EnvironmentInner) -> Self { let EnvironmentInner { name, network, @@ -70,11 +61,6 @@ impl TryFrom for EnvironmentManifest { init_args, } = v; - // Name - if name == LOCAL { - return OverrideLocalSnafu.fail(); - } - // Network let network = network.unwrap_or(LOCAL.to_string()); @@ -93,7 +79,7 @@ impl TryFrom for EnvironmentManifest { None => CanisterSelection::Everything, }; - Ok(Self { + Self { name, network, canisters, @@ -101,14 +87,14 @@ impl TryFrom for EnvironmentManifest { // Keep as-is, setting overrides is optional settings, init_args, - }) + } } } impl<'de> Deserialize<'de> for EnvironmentManifest { fn deserialize>(d: D) -> Result { let inner: EnvironmentInner = Deserialize::deserialize(d)?; - inner.try_into().map_err(serde::de::Error::custom) + Ok(inner.into()) } } @@ -134,21 +120,4 @@ mod tests { }, ); } - - #[test] - fn override_local() { - match serde_yaml::from_str::(r#"name: local"#) { - // No Error - Ok(_) => { - panic!("an environment named local should result in an error"); - } - - // Wrong Error - Err(err) => { - if !format!("{err}").starts_with("Overriding the local environment") { - panic!("an environment named local resulted in the wrong error: {err}"); - }; - } - }; - } }