From 42e746aa8bb5c66fd41e076710564aa71c1e2196 Mon Sep 17 00:00:00 2001 From: Jakob Beckmann Date: Wed, 8 Oct 2025 17:18:29 +0200 Subject: [PATCH 1/4] test(config): start implementing a test suite for config precedence Relates-to: #16 Signed-off-by: Jakob Beckmann --- crates/wash/src/cli/mod.rs | 61 ++++++++++++++++++++++++++++++++++++++ crates/wash/src/config.rs | 16 ++++++++++ 2 files changed, 77 insertions(+) diff --git a/crates/wash/src/cli/mod.rs b/crates/wash/src/cli/mod.rs index b63c81f9..ff5e4c54 100644 --- a/crates/wash/src/cli/mod.rs +++ b/crates/wash/src/cli/mod.rs @@ -647,3 +647,64 @@ impl CliContext { Ok(()) } } + + +#[cfg(test)] +pub mod test { + use super::*; + use tempfile::{TempDir, tempdir}; + + #[derive(Debug)] + struct TestAppStrategy { + home: TempDir + } + + impl TestAppStrategy { + fn new() -> anyhow::Result { + Ok(TestAppStrategy { + home: tempdir()? + }) + } + } + + impl DirectoryStrategy for TestAppStrategy { + fn home_dir(&self) -> &Path { + self.home.path() + } + fn config_dir(&self) -> PathBuf { + self.home.path().join("config") + } + fn data_dir(&self) -> PathBuf { + self.home.path().join("data") + } + fn cache_dir(&self) -> PathBuf { + self.home.path().join("cache") + } + fn state_dir(&self) -> Option { + Some(self.home.path().join("state")) + } + fn runtime_dir(&self) -> Option { + Some(self.home.path().join("runtime")) + } + } + + pub async fn create_test_cli_context() -> anyhow::Result { + let app_strategy = Arc::new(TestAppStrategy::new()?); + let (plugin_runtime, thread) = new_runtime() + .await + .context("failed to create wasmcloud runtime")?; + + let plugin_manager = PluginManager::initialize(&plugin_runtime, app_strategy.data_dir()) + .await + .context("failed to initialize plugin manager")?; + + Ok(CliContext { + app_strategy, + runtime: plugin_runtime, + runtime_thread: Arc::new(thread), + plugin_manager: Arc::new(plugin_manager), + background_processes: Arc::default(), + non_interactive: true, + }) + } +} diff --git a/crates/wash/src/config.rs b/crates/wash/src/config.rs index feac7ac0..7fbe1ef7 100644 --- a/crates/wash/src/config.rs +++ b/crates/wash/src/config.rs @@ -277,3 +277,19 @@ pub async fn generate_default_config( info!(config_path = %path.display(), "Generated default configuration"); Ok(()) } + +#[cfg(test)] +mod test { + use super::*; + use crate::cli::test::create_test_cli_context; + + #[tokio::test] + async fn test_load_config_only_defaults() -> anyhow::Result<()> { + let ctx = create_test_cli_context().await?; + let config = load_config(&ctx.config_path(), None, None::)?; + assert!(config.build.is_none()); + assert!(config.templates.is_empty()); + assert!(config.wit.is_none()); + Ok(()) + } +} From 7126a0c3407ed687ac4ed0dbd267b222275a6417 Mon Sep 17 00:00:00 2001 From: Jakob Beckmann Date: Wed, 8 Oct 2025 17:39:46 +0200 Subject: [PATCH 2/4] test(config): add additional tests to check precedence on config Relates-to: #16 Signed-off-by: Jakob Beckmann --- crates/wash/src/component_build.rs | 12 ++++---- crates/wash/src/config.rs | 48 +++++++++++++++++++++++++++--- crates/wash/src/new.rs | 2 +- crates/wash/src/wit.rs | 4 +-- 4 files changed, 53 insertions(+), 13 deletions(-) diff --git a/crates/wash/src/component_build.rs b/crates/wash/src/component_build.rs index 867d34df..bdfdeb3f 100644 --- a/crates/wash/src/component_build.rs +++ b/crates/wash/src/component_build.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use std::str::FromStr; /// Build configuration for different language toolchains -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] pub struct BuildConfig { /// Rust-specific build configuration #[serde(skip_serializing_if = "Option::is_none")] @@ -26,7 +26,7 @@ pub struct BuildConfig { } /// Types of projects that can be built -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum ProjectType { /// Rust project (Cargo.toml found) @@ -40,7 +40,7 @@ pub enum ProjectType { } /// Rust-specific build configuration with explicit defaults -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct RustBuildConfig { /// Custom build command that overrides all other Rust build settings /// When specified, all other Rust build flags are ignored @@ -155,8 +155,8 @@ impl FromStr for TinyGoGarbageCollector { } } -/// TinyGo-specific build configuration with explicit defaults -#[derive(Debug, Clone, Serialize, Deserialize)] +/// TinyGo-specific build configuration with explicit defaults +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct TinyGoBuildConfig { /// Custom build command that overrides all other TinyGo build settings /// When specified, all other TinyGo build flags are ignored @@ -259,7 +259,7 @@ fn default_tinygo_no_debug() -> bool { } /// TypeScript-specific build configuration with explicit defaults -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct TypeScriptBuildConfig { /// Custom build command that overrides all other TypeScript build settings /// When specified, all other TypeScript build flags are ignored diff --git a/crates/wash/src/config.rs b/crates/wash/src/config.rs index 7fbe1ef7..b5d9c9c9 100644 --- a/crates/wash/src/config.rs +++ b/crates/wash/src/config.rs @@ -29,7 +29,7 @@ pub const PROJECT_CONFIG_DIR: &str = ".wash"; /// (typically `~/.config/wash/config.json`), while the "local" project configuration /// is stored in the project's `.wash/config.json` file. This allows for both reasonable /// global defaults and project-specific overrides. -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] pub struct Config { /// Build configuration for different project types (default: empty/optional) #[serde(skip_serializing_if = "Option::is_none")] @@ -283,13 +283,53 @@ mod test { use super::*; use crate::cli::test::create_test_cli_context; + use tempfile::tempdir; + #[tokio::test] async fn test_load_config_only_defaults() -> anyhow::Result<()> { let ctx = create_test_cli_context().await?; let config = load_config(&ctx.config_path(), None, None::)?; - assert!(config.build.is_none()); - assert!(config.templates.is_empty()); - assert!(config.wit.is_none()); + assert_eq!(config, Config::default()); + Ok(()) + } + + #[tokio::test] + async fn test_load_config_with_global_config() -> anyhow::Result<()> { + let ctx = create_test_cli_context().await?; + + let global_config = Config::default_with_templates(); + save_config(&global_config, &ctx.config_path()).await?; + + let config = load_config(&ctx.config_path(), None, None::)?; + assert_eq!(config, global_config); + Ok(()) + } + + #[tokio::test] + async fn test_load_config_with_local_config() -> anyhow::Result<()> { + let ctx = create_test_cli_context().await?; + + let global_config = Config::default_with_templates(); + save_config(&global_config, &ctx.config_path()).await?; + + let project = tempdir()?; + let project_dir = project.path(); + let local_config_file = project_dir.join(PROJECT_CONFIG_DIR).join(CONFIG_FILE_NAME); + let mut local_config = Config::default_with_templates(); + local_config.build = Some(BuildConfig { + rust: Some(RustBuildConfig { + release: true, + ..RustBuildConfig::default() + }), + ..BuildConfig::default() + }); // should override global + local_config.templates = Vec::new(); // should take templates from global + save_config(&local_config, &local_config_file).await?; + + let config = load_config(&ctx.config_path(), Some(&project_dir), None::)?; + assert_eq!(config.wit, Config::default().wit); + assert_eq!(config.templates, global_config.templates); + assert_eq!(config.build, local_config.build); Ok(()) } } diff --git a/crates/wash/src/new.rs b/crates/wash/src/new.rs index 1cea622e..7695c96d 100644 --- a/crates/wash/src/new.rs +++ b/crates/wash/src/new.rs @@ -18,7 +18,7 @@ pub enum TemplateLanguage { Other(String), } -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct NewTemplate { pub name: String, #[serde(default)] diff --git a/crates/wash/src/wit.rs b/crates/wash/src/wit.rs index 2d30ae49..965dd7cc 100644 --- a/crates/wash/src/wit.rs +++ b/crates/wash/src/wit.rs @@ -24,7 +24,7 @@ pub const LOCK_FILE_NAME: &str = "wasmcloud.lock"; pub const WKG_LOCK_FILE_NAME: &str = "wkg.lock"; /// Configuration for WIT dependency management -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] pub struct WitConfig { /// Registries for WIT package fetching (default: wasm.pkg registry) #[serde(default = "default_wit_registries")] @@ -50,7 +50,7 @@ fn default_wit_registries() -> Vec { } /// WIT registry configuration -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct WitRegistry { /// Registry URL pub url: String, From d4f3d0d5724f9a6bb4206fa26008600924786fd4 Mon Sep 17 00:00:00 2001 From: Jakob Beckmann Date: Wed, 8 Oct 2025 18:05:22 +0200 Subject: [PATCH 3/4] test(config): add test for environment variable precedence Relates-to: #16 Signed-off-by: Jakob Beckmann --- Cargo.lock | 2 ++ Cargo.toml | 1 + crates/wash/src/config.rs | 46 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 808a88e9..492fd27d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1546,9 +1546,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" dependencies = [ "atomic", + "parking_lot", "pear", "serde", "serde_json", + "tempfile", "uncased", "version_check", ] diff --git a/Cargo.toml b/Cargo.toml index c8fd403a..6c127ad8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ http-body-util = { version = "0.1.3", default-features = false } rcgen = { version = "0.13", default-features = false, features = ["crypto", "ring", "pem"] } tempfile = { version = "3.0", default-features = false } tokio = { version = "1.45.1", default-features = false, features = ["full"] } +figment = { version = "0.10.19", default-features = false, features = ["json", "env", "test"] } [package.metadata.binstall] pkg-url = "{ repo }/releases/download/{name}-v{version}/wash-{ target }{ binary-ext }" diff --git a/crates/wash/src/config.rs b/crates/wash/src/config.rs index b5d9c9c9..a4069153 100644 --- a/crates/wash/src/config.rs +++ b/crates/wash/src/config.rs @@ -148,7 +148,7 @@ where } // Environment variables with WASH_ prefix - figment = figment.merge(Env::prefixed("WASH_")); + figment = figment.merge(Env::prefixed("WASH_").split("_")); // TODO(#16): There's more testing to be done here to ensure that CLI args can override existing // config without replacing present values with empty values. @@ -283,6 +283,7 @@ mod test { use super::*; use crate::cli::test::create_test_cli_context; + use figment::Jail; use tempfile::tempdir; #[tokio::test] @@ -322,8 +323,8 @@ mod test { ..RustBuildConfig::default() }), ..BuildConfig::default() - }); // should override global - local_config.templates = Vec::new(); // should take templates from global + }); // should override global + local_config.templates = Vec::new(); // should take templates from global save_config(&local_config, &local_config_file).await?; let config = load_config(&ctx.config_path(), Some(&project_dir), None::)?; @@ -332,4 +333,43 @@ mod test { assert_eq!(config.build, local_config.build); Ok(()) } + + #[tokio::test] + async fn test_load_config_with_env_vars() -> anyhow::Result<()> { + let ctx = create_test_cli_context().await?; + + let project = tempdir()?; + let project_dir = project.path(); + let local_config_file = project_dir.join(PROJECT_CONFIG_DIR).join(CONFIG_FILE_NAME); + let mut local_config = Config::default_with_templates(); + local_config.build = Some(BuildConfig { + rust: Some(RustBuildConfig { + release: true, + ..RustBuildConfig::default() + }), + ..BuildConfig::default() + }); + save_config(&local_config, &local_config_file).await?; + + Jail::expect_with(|jail| { + // should override whatever was set in local configuration + jail.set_env("WASH_BUILD_RUST_RELEASE", "false"); + + let config = load_config(&ctx.config_path(), Some(&project_dir), None::) + .expect("configuration should be loadable"); + + assert_eq!( + config + .build + .ok_or("build config should contain information")? + .rust + .ok_or("rust build config should contain information")? + .release, + false + ); + + Ok(()) + }); + Ok(()) + } } From 8cb9379103cabb2b9a3b97aa9e8ba695e788f7ae Mon Sep 17 00:00:00 2001 From: Jakob Beckmann Date: Thu, 9 Oct 2025 10:55:03 +0200 Subject: [PATCH 4/4] test(config): add final test to validate CLI configuration overrides Relates-to: #16 Signed-off-by: Jakob Beckmann --- crates/wash/src/cli/mod.rs | 7 +--- crates/wash/src/config.rs | 84 ++++++++++++++++++++++++++++++++------ 2 files changed, 74 insertions(+), 17 deletions(-) diff --git a/crates/wash/src/cli/mod.rs b/crates/wash/src/cli/mod.rs index ff5e4c54..445fdeec 100644 --- a/crates/wash/src/cli/mod.rs +++ b/crates/wash/src/cli/mod.rs @@ -648,7 +648,6 @@ impl CliContext { } } - #[cfg(test)] pub mod test { use super::*; @@ -656,14 +655,12 @@ pub mod test { #[derive(Debug)] struct TestAppStrategy { - home: TempDir + home: TempDir, } impl TestAppStrategy { fn new() -> anyhow::Result { - Ok(TestAppStrategy { - home: tempdir()? - }) + Ok(TestAppStrategy { home: tempdir()? }) } } diff --git a/crates/wash/src/config.rs b/crates/wash/src/config.rs index a4069153..8e6f090f 100644 --- a/crates/wash/src/config.rs +++ b/crates/wash/src/config.rs @@ -148,7 +148,7 @@ where } // Environment variables with WASH_ prefix - figment = figment.merge(Env::prefixed("WASH_").split("_")); + figment = figment.merge(Env::prefixed("WASH_").split("__")); // TODO(#16): There's more testing to be done here to ensure that CLI args can override existing // config without replacing present values with empty values. @@ -320,10 +320,10 @@ mod test { local_config.build = Some(BuildConfig { rust: Some(RustBuildConfig { release: true, - ..RustBuildConfig::default() + ..Default::default() }), - ..BuildConfig::default() - }); // should override global + ..Default::default() + }); // Should override global local_config.templates = Vec::new(); // should take templates from global save_config(&local_config, &local_config_file).await?; @@ -345,27 +345,87 @@ mod test { local_config.build = Some(BuildConfig { rust: Some(RustBuildConfig { release: true, - ..RustBuildConfig::default() + ..Default::default() }), - ..BuildConfig::default() + ..Default::default() }); save_config(&local_config, &local_config_file).await?; Jail::expect_with(|jail| { - // should override whatever was set in local configuration - jail.set_env("WASH_BUILD_RUST_RELEASE", "false"); + // Should override whatever was set in local configuration + jail.set_env("WASH_BUILD__RUST__RELEASE", "false"); + // Using double underscore as delimiter allows to use multi-words for configuration via + // env variables + jail.set_env("WASH_BUILD__RUST__CUSTOM_COMMAND", "[cargo,build]"); let config = load_config(&ctx.config_path(), Some(&project_dir), None::) .expect("configuration should be loadable"); + let rust_build_config = config + .clone() + .build + .ok_or("build config should contain information")? + .rust + .ok_or("rust build config should contain information")?; + + assert_eq!(rust_build_config.release, false); + + assert_eq!( + rust_build_config.custom_command, + Some(vec!["cargo".into(), "build".into()]) + ); + + Ok(()) + }); + Ok(()) + } + + #[tokio::test] + async fn test_load_config_with_cli_args() -> anyhow::Result<()> { + let ctx = create_test_cli_context().await?; + + let some_path = "/this/is/some/path"; + let custom_command = vec!["cargo".into(), "component".into(), "bindings".into()]; + let mut cli_config = Config::default_with_templates(); + cli_config.build = Some(BuildConfig { + component_path: Some(some_path.into()), + rust: Some(RustBuildConfig { + custom_command: Some(custom_command.clone()), + ..Default::default() + }), + ..Default::default() + }); + + Jail::expect_with(|jail| { + // Should be irrelevant, as is overwritten by CLI configuration. + jail.set_env("WASH_BUILD__RUST__CUSTOM_COMMAND", "[cargo,build]"); + + let config = load_config(&ctx.config_path(), None, Some(cli_config)) + .expect("configuration should be loadable"); + + let build_config = config + .clone() + .build + .ok_or("build config should contain information")?; + + assert_eq!( + build_config + .clone() + .rust + .ok_or("rust build config should contain information")? + .custom_command, + Some(custom_command) + ); + + assert_eq!(build_config.clone().component_path, Some(some_path.into())); + assert_eq!( - config - .build - .ok_or("build config should contain information")? + build_config + .clone() .rust .ok_or("rust build config should contain information")? .release, - false + RustBuildConfig::default().release ); Ok(())