//! Dependency report CLI behavior (`SMH-SPEC-SPEC0001`). use std::{fs, path::PathBuf, time::Duration}; fn workspace_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..") } #[test] fn e2e_deps_rejects_invalid_arguments() { let mut command = assert_cmd::Command::new(env!("CARGO_BIN_EXE_xtask")); let output = command .current_dir(workspace_root()) .args(["deps", "--unknown"]) .timeout(Duration::from_secs(10)) .output() .unwrap(); assert_eq!(output.status.code(), Some(1)); assert!(String::from_utf8_lossy(&output.stderr).contains("usage: cargo x deps [--offline]")); } #[test] fn e2e_dependency_resolution_rejects_invalid_mode_without_mutating_lockfile() { let root = workspace_root(); let lockfile = root.join("Cargo.lock"); let before = fs::read(&lockfile).unwrap(); let output = assert_cmd::Command::new(env!("CARGO_BIN_EXE_xtask")) .current_dir(root) .args(["deps", "--verify", "unknown", "-p", "smith-core"]) .timeout(Duration::from_secs(10)) .output() .unwrap(); assert_eq!(output.status.code(), Some(1)); assert!(String::from_utf8_lossy(&output.stderr).contains("usage: cargo x deps --verify")); assert_eq!(fs::read(&lockfile).unwrap(), before); } #[test] fn e2e_dependency_resolution_rejects_missing_or_invalid_target_without_mutating_lockfile() { let root = workspace_root(); let lockfile = root.join("Cargo.lock"); let before = fs::read(&lockfile).unwrap(); for arguments in [ vec!["deps", "--verify", "locked", "--target"], vec!["deps", "--verify", "locked", "--target", ""], vec![ "deps", "--verify", "locked", "--target", "x86_64-unknown-linux-gnu", "--unknown", ], ] { let output = assert_cmd::Command::new(env!("CARGO_BIN_EXE_xtask")) .current_dir(&root) .args(arguments) .timeout(Duration::from_secs(10)) .output() .unwrap(); assert_eq!(output.status.code(), Some(1)); assert!(String::from_utf8_lossy(&output.stderr).contains("usage: cargo x deps --verify")); } let invalid_target = assert_cmd::Command::new(env!("CARGO_BIN_EXE_xtask")) .current_dir(&root) .args([ "deps", "--verify", "locked", "--target", "not-a-rust-target", ]) .timeout(Duration::from_secs(120)) .output() .unwrap(); assert_eq!(invalid_target.status.code(), Some(1)); assert!(String::from_utf8_lossy(&invalid_target.stderr).contains("not-a-rust-target")); assert_eq!(fs::read(&lockfile).unwrap(), before); } #[test] fn e2e_default_resolution_checks_host_despite_ambient_build_target() { let root = workspace_root(); let before = fs::read(root.join("Cargo.lock")).unwrap(); assert_cmd::Command::new(env!("CARGO_BIN_EXE_xtask")) .current_dir(&root) .env("CARGO_BUILD_TARGET", "not-a-rust-target") .args(["deps", "--verify", "locked", "-p", "smith-core"]) .timeout(Duration::from_secs(120)) .assert() .success(); assert_eq!(fs::read(root.join("Cargo.lock")).unwrap(), before); } #[test] fn e2e_dependency_resolution_scopes_locked_check_without_rewriting_lockfile() { let root = workspace_root(); let lockfile = root.join("Cargo.lock"); let before = fs::read(&lockfile).unwrap(); let native_target = String::from_utf8( assert_cmd::Command::new("rustc") .arg("-vV") .timeout(Duration::from_secs(10)) .output() .unwrap() .stdout, ) .unwrap() .lines() .find_map(|line| line.strip_prefix("host: ")) .unwrap() .to_owned(); for arguments in [ vec![ "deps", "--verify", "locked", "-p", "smith-core", "--target", &native_target, ], vec![ "deps", "--verify", "locked", "--target", &native_target, "--package", "smith-core", ], ] { let output = assert_cmd::Command::new(env!("CARGO_BIN_EXE_xtask")) .current_dir(&root) .args(arguments) .timeout(Duration::from_secs(120)) .assert() .success() .get_output() .stdout .clone(); assert!(String::from_utf8_lossy(&output).contains(&format!("target {native_target}"))); } assert_eq!(fs::read(&lockfile).unwrap(), before); } #[test] #[cfg(unix)] fn e2e_deps_offline_report_uses_cargo_protocol_without_registry_cache() { use std::{ os::unix::fs::PermissionsExt, sync::atomic::{AtomicUsize, Ordering}, }; static SCRATCH_COUNTER: AtomicUsize = AtomicUsize::new(0); let root = workspace_root(); let lockfile = root.join("Cargo.lock"); let before = fs::read(&lockfile).unwrap(); let scratch = Scratch(std::env::temp_dir().join(format!( "smith-dependency-e2e-{}-{}", std::process::id(), SCRATCH_COUNTER.fetch_add(1, Ordering::Relaxed) ))); fs::create_dir(&scratch.0).unwrap(); let bin = scratch.0.join("bin"); let fixtures = scratch.0.join("fixtures"); let cargo_home = scratch.0.join("cargo-home"); fs::create_dir(&bin).unwrap(); fs::create_dir(&fixtures).unwrap(); fs::create_dir(&cargo_home).unwrap(); let helper = bin.join("cargo"); fs::copy( PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/cargo-offline.sh"), &helper, ) .unwrap(); fs::set_permissions(&helper, fs::Permissions::from_mode(0o755)).unwrap(); write_metadata_fixtures(&fixtures); let mut paths = vec![bin]; paths.extend(std::env::split_paths(&std::env::var_os("PATH").unwrap())); let output = assert_cmd::Command::new(env!("CARGO_BIN_EXE_xtask")) .current_dir(&root) .env("PATH", std::env::join_paths(paths).unwrap()) .env("CARGO_HOME", &cargo_home) .env("SMITH_METADATA", fixtures.join("workspace.json")) .env("SMITH_FIXTURES", &fixtures) .args(["deps", "--offline"]) .timeout(Duration::from_secs(120)) .assert() .success() .get_output() .stdout .clone(); let report = String::from_utf8(output).unwrap(); assert_eq!( report, concat!( "Offline registry-cache evidence only; upstream freshness is unverified.\n", "Crates.io only: git, path, and alternate registries excluded; compatibility resolves with default features disabled.\n", "member | scope | target | dependency | requirement | locked | latest compatible | latest stable | constraint reason\n", "smith-fixture | normal | all-targets | ureq | ~3.4 | 3.4.1 | 3.4.1 | 3.5.0 | smith-harness, http stack; tilde pin: the chunk-gap timeout uses ureq::unversioned\n", "smith-fixture | normal | all-targets | wasmtime | ^49.0.2 | 49.0.2 | 49.0.2 | 50.0.0 | smith-harness: Wasmtime hosts components; guest wit-bindgen must align with its component tooling release, not independently chase latest. Wasmi or Wasmer remain alternatives only after component compatibility and Smith workload benchmarks justify replacing Wasmtime. smith-harness, wasm plugin engine\n", "smith-fixture | normal | all-targets | wit-bindgen | =0.62.0 | 0.62.0 | 0.62.0 | 0.63.0 | plugins/echo: guest component bindings aligned with Wasmtime's release set.\n", ) ); assert_eq!(fs::read(&lockfile).unwrap(), before); } fn write_metadata_fixtures(directory: &std::path::Path) { let registry = "registry+https://github.com/rust-lang/crates.io-index"; let workspace_id = "smith-fixture 0.0.0 (path+file:///fixture)"; let dependencies = [ ("ureq", "~3.4", "3.4.1", "ureq"), ("wasmtime", "^49.0.2", "49.0.2", "wasmtime"), ("wit-bindgen", "=0.62.0", "0.62.0", "wit_bindgen"), ]; let workspace_dependencies = dependencies .iter() .map(|(name, requirement, _, _)| { serde_json::json!({ "name": name, "source": registry, "req": requirement, "kind": null, "rename": null, "optional": false, "uses_default_features": false, "features": [] }) }) .collect::>(); let packages = std::iter::once(serde_json::json!({ "id": workspace_id, "name": "smith-fixture", "version": "0.0.0", "source": null, "dependencies": workspace_dependencies })) .chain(dependencies.iter().map(|(name, _, locked, _)| { serde_json::json!({ "id": format!("{name} {locked} ({registry})"), "name": name, "version": locked, "source": registry, "dependencies": [] }) })) .collect::>(); let edges = dependencies .iter() .map(|(name, _, locked, edge)| { serde_json::json!({ "name": edge, "pkg": format!("{name} {locked} ({registry})"), "dep_kinds": [{"kind": null, "target": null}] }) }) .collect::>(); let metadata = serde_json::json!({ "packages": packages, "workspace_members": [workspace_id], "resolve": {"nodes": [{"id": workspace_id, "deps": edges}]} }); fs::write( directory.join("workspace.json"), serde_json::to_vec(&metadata).unwrap(), ) .unwrap(); for (name, _, compatible, _) in dependencies { let latest = match name { "ureq" => "3.5.0", "wasmtime" => "50.0.0", "wit-bindgen" => "0.63.0", _ => unreachable!(), }; for (label, version) in [("compatible", compatible), ("latest", latest)] { let package = serde_json::json!({ "id": format!("{name} {version} ({registry})"), "name": name, "version": version, "source": registry, "dependencies": [] }); let metadata = serde_json::json!({ "packages": [package], "workspace_members": [], "resolve": {"nodes": []} }); fs::write( directory.join(format!("{name}-{label}.json")), serde_json::to_vec(&metadata).unwrap(), ) .unwrap(); } } } struct Scratch(PathBuf); impl Drop for Scratch { fn drop(&mut self) { fs::remove_dir_all(&self.0).unwrap(); } }