304 lines
11 KiB
Rust
304 lines
11 KiB
Rust
//! 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<PathBuf> = OnceLock::new();
|
|
PATH.get_or_init(|| {
|
|
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
|
|
crate::common::cwd_init();
|
|
assert!(
|
|
std::process::Command::new("cargo")
|
|
.args(["build", "--release", "-p", "world-cargo"])
|
|
.current_dir(root.join("userland"))
|
|
.status()
|
|
.unwrap()
|
|
.success()
|
|
);
|
|
assert!(crate::common::build_brush().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<Runtime>, args: &[&str]) -> anyhow::Result<(i32, String)> {
|
|
cargo_at(runtime, args, "/project").await
|
|
}
|
|
async fn cargo_at(
|
|
runtime: &Arc<Runtime>,
|
|
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<u64> {
|
|
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::<u64>().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(())
|
|
}
|