Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
58 changes: 58 additions & 0 deletions crates/wash/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -647,3 +647,61 @@ 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
160 changes: 158 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("__"));

// 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,159 @@ 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,
..Default::default()
}),
..Default::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,
..Default::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");
// 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::<Config>)
.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!(
build_config
.clone()
.rust
.ok_or("rust build config should contain information")?
.release,
RustBuildConfig::default().release
);

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