Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }"
Expand Down
61 changes: 61 additions & 0 deletions crates/wash/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TestAppStrategy> {
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<PathBuf> {
Some(self.home.path().join("state"))
}
fn runtime_dir(&self) -> Option<PathBuf> {
Some(self.home.path().join("runtime"))
}
}

pub async fn create_test_cli_context() -> anyhow::Result<CliContext> {
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,
})
}
}
12 changes: 6 additions & 6 deletions crates/wash/src/component_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
100 changes: 98 additions & 2 deletions crates/wash/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -148,7 +148,7 @@ where
}

// Environment variables with WASH_ prefix
figment = figment.merge(Env::prefixed("WASH_"));
figment = figment.merge(Env::prefixed("WASH_").split("_"));
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not quite done with the testing, but wanted to make sure this is desired. I have the feeling this is the typical way to do this, instead of using the default from figment (which is a dot - .).

Not setting this does not allow configurations via the env vars as shown in the test below (https://github.com/wasmCloud/wash/pull/97/files#diff-f7f9d11319f33b9af91da4ef6ce4b7111c1baceef1b82db506c4612ba6f808b1R356). If this is removed, the test fails as the splitting does not work on underscores.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will need to add a test on how this works on "multi-word" configuration options though such as here:

pub custom_command: Option<Vec<String>>,

Copy link
Copy Markdown
Contributor Author

@f4z3r f4z3r Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated this to use two underscored (__) as the separator. Otherwise multi-word configurations cannot be parsed from the environment. This results in the following sample configuration via environment variables:

WASH_BUILD__RUST__CUSTOM_COMMAND="[cargo,build]"

The only slightly weird stuff is the single underscore in the prefix. We could update that to a double underscore as well to have it be consistent. Or do you see any other separator instead of double underscore that might make sense (note single underscore will not work properly) @brooksmtownsend ? The figment default is a dot, but this is not supported properly in many shells AFAIK.


// 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.
Expand Down Expand Up @@ -277,3 +277,99 @@ 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;

use figment::Jail;
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::<Config>)?;
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::<Config>)?;
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::<Config>)?;
assert_eq!(config.wit, Config::default().wit);
assert_eq!(config.templates, global_config.templates);
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::<Config>)
.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(())
}
}
2 changes: 1 addition & 1 deletion crates/wash/src/new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
4 changes: 2 additions & 2 deletions crates/wash/src/wit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -50,7 +50,7 @@ fn default_wit_registries() -> Vec<WitRegistry> {
}

/// WIT registry configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WitRegistry {
/// Registry URL
pub url: String,
Expand Down
Loading