//! Real-world Cargo acceptance: native namespaces, source changes, cache, and WASI execution. use sausage::{ exec::{Command, Exec, Options}, process::Runtime, world::World, }; use std::{ path::{Path, PathBuf}, sync::{Arc, OnceLock}, time::Duration, }; fn programs() -> PathBuf { static PATH: OnceLock = OnceLock::new(); PATH.get_or_init(|| { let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap(); assert!( std::process::Command::new("nu") .arg(root.join("userland/cwd-init/build.nu")) .status() .unwrap() .success() ); assert!( std::process::Command::new("cargo") .args(["build", "--release", "-p", "world-cargo"]) .current_dir(root.join("userland")) .status() .unwrap() .success() ); assert!( std::process::Command::new("nu") .arg(root.join("userland/brush/build.nu")) .status() .unwrap() .success() ); PathBuf::from(std::env::var_os("CARGO_TARGET_DIR").expect("executor target required")) .join("wasm32-wasip2/release") }) .clone() } async fn cargo(runtime: &Arc, args: &[&str]) -> anyhow::Result<(i32, String)> { cargo_at(runtime, args, "/project").await } async fn cargo_at( runtime: &Arc, args: &[&str], cwd: &str, ) -> anyhow::Result<(i32, String)> { let mut bytes = Vec::new(); let code = runtime .exec( Command::Argv( std::iter::once("/bin/cargo.wasm".into()) .chain(args.iter().map(|a| a.to_string())) .collect(), ), Options { cwd: cwd.into(), timeout: Some(Duration::from_secs(120)), ..Default::default() }, &mut |_, b| bytes.extend_from_slice(b), ) .await?; Ok((code, String::from_utf8_lossy(&bytes).into_owned())) } fn cache_hits(base: &Path) -> anyhow::Result { let stats = std::process::Command::new("kache") .arg("stats") .env("KACHE_CACHE_DIR", base.join("kache")) .output()?; assert!( stats.status.success(), "{}", String::from_utf8_lossy(&stats.stderr) ); let text = String::from_utf8_lossy(&stats.stdout); println!("PRIVATE CACHE: {text}"); let words: Vec<_> = text.split_whitespace().collect(); let hits = words.windows(2).find_map(|pair| { (pair[1] == "hits") .then(|| pair[0].replace(',', "").parse::().ok()) .flatten() }); assert!( std::process::Command::new("kache") .args(["daemon", "stop"]) .env("KACHE_CACHE_DIR", base.join("kache")) .status()? .success() ); Ok(hits.expect("kache stats must report its lifetime hits")) } #[tokio::test(flavor = "multi_thread")] async fn cargo_build_test_and_source_changes() -> anyhow::Result<()> { let binaries = programs(); let temp = tempfile::tempdir()?; let world = World::open(temp.path())?; let bin = temp.path().join("world/bin"); std::fs::create_dir_all(&bin)?; std::fs::copy(binaries.join("cargo.wasm"), bin.join("cargo.wasm"))?; std::fs::copy(binaries.join("brush.wasm"), bin.join("sh.wasm"))?; let source = temp.path().join("world/project"); std::fs::create_dir_all(source.join("src"))?; std::fs::create_dir_all(source.join("tests"))?; for (path, bytes) in [ ( "Cargo.toml", include_bytes!("builder-fixture/Cargo.toml").as_slice(), ), ( "build.rs", include_bytes!("builder-fixture/build.rs").as_slice(), ), ( "src/main.rs", include_bytes!("builder-fixture/src/main.rs").as_slice(), ), ( "tests/integration.rs", include_bytes!("builder-fixture/tests/integration.rs").as_slice(), ), ( "src/add.c", include_bytes!("builder-fixture/src/add.c").as_slice(), ), ] { std::fs::write(source.join(path), bytes)?; } let runtime = Runtime::new(world)?; let (code, out) = cargo(&runtime, &["build"]).await?; assert_eq!(code, 0, "{out}"); assert!(out.contains("namespace assertions passed"), "{out}"); assert!(source.join("Cargo.lock").exists()); let mut shell_output = Vec::new(); let shell_code = runtime .exec( Command::Argv(vec![ "/bin/sh.wasm".into(), "-c".into(), "./target/wasm32-wasip2/debug/builder-fixture.wasm".into(), ]), Options { cwd: "/project".into(), ..Default::default() }, &mut |_, b| shell_output.extend_from_slice(b), ) .await?; assert_eq!(shell_code, 0, "{}", String::from_utf8_lossy(&shell_output)); assert!(String::from_utf8_lossy(&shell_output).contains("builder fixture cwd: /project")); let (code, out) = cargo(&runtime, &["run"]).await?; assert_eq!(code, 0, "{out}"); assert!(out.contains("builder fixture cwd: /project"), "{out}"); let (code, out) = cargo(&runtime, &["run", "--", "--no-run"]).await?; assert_eq!(code, 0, "{out}"); assert!(out.contains("builder fixture cwd: /project"), "{out}"); let (code, out) = cargo(&runtime, &["run", "--", "--exit"]).await?; assert_eq!(code, 23, "{out}"); let (code, out) = cargo(&runtime, &["test", "pass", "--", "--nocapture"]).await?; assert_eq!(code, 0, "{out}"); assert!(out.contains("1 passed"), "{out}"); assert!(out.contains("INTEGRATION_CASE"), "{out}"); let (code, out) = cargo_at( &runtime, &["test", "pass", "--", "--nocapture"], "/project/src", ) .await?; assert_eq!(code, 0, "{out}"); assert!(out.contains("INTEGRATION_CASE"), "{out}"); // WASI aborts on panic, so disable capture to expose the panic message. let (code, out) = cargo(&runtime, &["test", "fail", "--", "--nocapture"]).await?; assert_eq!(code, 101, "{out}"); assert!(out.contains("intentional fixture failure"), "{out}"); let (code, out) = cargo(&runtime, &["test", "--no-run"]).await?; assert_eq!(code, 0, "{out}"); assert!(!out.contains("intentional fixture failure"), "{out}"); std::fs::create_dir_all(source.join("src/bin"))?; std::fs::write( source.join("src/bin/other.rs"), "fn main() { println!(\"OTHER_BIN\"); }\n", )?; let (code, out) = cargo(&runtime, &["run"]).await?; assert_eq!(code, 101, "{out}"); assert!(out.contains("--bin"), "{out}"); let manifest = std::fs::read_to_string(source.join("Cargo.toml"))?; std::fs::write( source.join("Cargo.toml"), manifest.replace("[package]\n", "[package]\ndefault-run = \"other\"\n"), )?; let (code, out) = cargo(&runtime, &["run"]).await?; assert_eq!(code, 0, "{out}"); assert!(out.contains("OTHER_BIN"), "{out}"); let (code, out) = cargo(&runtime, &["run", "--bin", "builder-fixture"]).await?; assert_eq!(code, 0, "{out}"); assert!(out.contains("builder fixture cwd: /project"), "{out}"); for action in ["check", "clippy", "fetch", "tree", "metadata", "update"] { let (code, out) = cargo(&runtime, &[action]).await?; assert_eq!(code, 0, "{action}: {out}"); } let (code, out) = cargo(&runtime, &["clippy", "--", "-D", "clippy::print_stdout"]).await?; assert_ne!(code, 0, "{out}"); assert!(out.contains("stdout"), "{out}"); let base = std::fs::read_dir(temp.path().join("host/builds"))? .next() .unwrap()? .path(); let before = cache_hits(&base)?; let (code, out) = cargo(&runtime, &["clean"]).await?; assert_eq!(code, 0, "{out}"); assert!( !source .join("target/wasm32-wasip2/debug/builder-fixture.wasm") .exists() ); let (code, out) = cargo(&runtime, &["build"]).await?; assert_eq!(code, 0, "{out}"); let after = cache_hits(&base)?; println!("CACHE AFTER CLEAN: {before} -> {after} hits"); assert!( after > before, "the second build did not restore cached actions" ); std::fs::write( source.join("src/main.rs"), "fn main(){println!(\"format me\");}\n", )?; let unformatted = std::fs::read(source.join("src/main.rs"))?; let (code, out) = cargo(&runtime, &["fmt", "--", "--check"]).await?; assert_ne!(code, 0, "{out}"); assert_eq!(std::fs::read(source.join("src/main.rs"))?, unformatted); let (code, out) = cargo(&runtime, &["fmt"]).await?; assert_eq!(code, 0, "{out}"); assert!(std::fs::read_to_string(source.join("src/main.rs"))?.contains("fn main() {")); let (code, out) = cargo(&runtime, &["remove", "serde"]).await?; assert_eq!(code, 0, "{out}"); let (code, out) = cargo(&runtime, &["add", "serde"]).await?; assert_eq!(code, 0, "{out}"); assert!(std::fs::read_to_string(source.join("Cargo.toml"))?.contains("serde")); std::fs::write( source.join("src/main.rs"), "fn main() { undefined_function(); }", )?; let (code, out) = cargo(&runtime, &["build"]).await?; assert_ne!(code, 0, "{out}"); assert!(out.contains("undefined_function"), "{out}"); Ok(()) } #[tokio::test] async fn timeout_and_cancellation() -> anyhow::Result<()> { use sausage::builder::{Builder, Event, Request}; let temp = tempfile::tempdir()?; let world = World::open(temp.path())?; let source = temp.path().join("world/project"); std::fs::create_dir_all(source.join("src"))?; std::fs::write( source.join("Cargo.toml"), "[package]\nname='hang'\nversion='0.1.0'\n[workspace]\n", )?; std::fs::write(source.join("src/main.rs"), "fn main() {}")?; std::fs::write( source.join("build.rs"), "fn main() { std::process::Command::new(\"sh\").args([\"-c\", \"echo $$ > child.pid; exec sleep 1000\"]).status().unwrap(); }", )?; let builder = Arc::new(Builder::new(world.root.clone(), &world.host_dir)?); let (tx, mut rx) = tokio::sync::mpsc::channel(16); let task = tokio::spawn(async move { builder .execute( Request { argv: vec!["build".into()], cwd: "/project".into(), timeout_secs: Some(3), }, &tx, ) .await }); let mut timed_out = false; while let Some(event) = rx.recv().await { if let Event::Exit { code, .. } = event { timed_out = code == 124; } } task.await??; assert!(timed_out); let binaries = programs(); let bin = temp.path().join("world/bin"); std::fs::create_dir_all(&bin)?; std::fs::copy(binaries.join("cargo.wasm"), bin.join("cargo.wasm"))?; let runtime = Runtime::new(world)?; let error = runtime .exec( Command::Argv(vec!["/bin/cargo.wasm".into(), "build".into()]), Options { cwd: "/project".into(), timeout: Some(Duration::from_secs(1)), ..Default::default() }, &mut |_, _| {}, ) .await .unwrap_err(); assert!(error.to_string().contains("timed out"), "{error}"); let (code, out) = tokio::time::timeout(Duration::from_secs(20), cargo(&runtime, &["metadata"])).await??; assert_eq!( code, 0, "the canceled build still held the builder lock: {out}" ); Ok(()) }