Files
sausage/host/src/builder/tests.rs
T

237 lines
8.3 KiB
Rust

//! Focused tests for source discovery, argument forwarding, eviction, and process cancellation.
use super::*;
#[tokio::test]
async fn discovers_workspace_and_skips_target() -> Result<()> {
let temp = tempfile::tempdir()?;
let world = crate::world::World::open(temp.path())?;
files::write(
world.root.as_ref(),
"/project/Cargo.toml",
b"[workspace]\nmembers=['member']".to_vec(),
)
.await?;
files::write(
world.root.as_ref(),
"/project/member/Cargo.toml",
b"[package]\nname='member'\nversion='0.1.0'".to_vec(),
)
.await?;
files::write(
world.root.as_ref(),
"/project/target/junk",
b"junk".to_vec(),
)
.await?;
assert_eq!(
files::workspace(world.root.as_ref(), "/project/member/src").await?,
"/project"
);
let copy = temp.path().join("copy");
files::copy_out(world.root.as_ref(), "/project", &copy).await?;
assert!(copy.join("member/Cargo.toml").exists());
assert!(!copy.join("target").exists());
Ok(())
}
#[tokio::test]
async fn copies_only_changed_and_new_sources() -> Result<()> {
let temp = tempfile::tempdir()?;
let world = crate::world::World::open(temp.path())?;
for name in ["unchanged.rs", "changed.rs"] {
files::write(
world.root.as_ref(),
&format!("/project/{name}"),
b"initial".to_vec(),
)
.await?;
}
let copy = temp.path().join("copy");
let before = files::copy_out(world.root.as_ref(), "/project", &copy).await?;
files::write(
world.root.as_ref(),
"/project/unchanged.rs",
b"concurrent edit".to_vec(),
)
.await?;
std::fs::write(copy.join("changed.rs"), b"changed by command")?;
std::fs::write(copy.join("Cargo.lock"), b"created by command")?;
files::copy_back(world.root.as_ref(), &copy, "/project", Some(&before)).await?;
for (name, expected) in [
("unchanged.rs", "concurrent edit"),
("changed.rs", "changed by command"),
("Cargo.lock", "created by command"),
] {
assert_eq!(
files::read(world.root.as_ref(), &format!("/project/{name}")).await?,
expected.as_bytes()
);
}
Ok(())
}
#[tokio::test]
async fn clean_removes_only_deleted_artifacts() -> Result<()> {
let temp = tempfile::tempdir()?;
let world = crate::world::World::open(temp.path())?;
let native = temp.path().join("native");
std::fs::create_dir_all(&native)?;
std::fs::write(native.join("keep.wasm"), b"keep")?;
for name in ["keep.wasm", "remove.wasm", "note.txt"] {
files::write(
world.root.as_ref(),
&format!("/target/{name}"),
b"data".to_vec(),
)
.await?;
}
files::clean_artifacts(world.root.as_ref(), &native, "/target").await?;
assert!(
files::read(world.root.as_ref(), "/target/remove.wasm")
.await
.is_err()
);
for name in ["keep.wasm", "note.txt"] {
assert_eq!(
files::read(world.root.as_ref(), &format!("/target/{name}")).await?,
b"data"
);
}
Ok(())
}
#[test]
fn relative_host_directory_uses_absolute_namespace_paths() -> Result<()> {
let temp = tempfile::tempdir()?;
let world = crate::world::World::open(temp.path())?;
let builder = Builder::new(world.root, Path::new("."))?;
assert!(builder.directory.is_absolute());
Ok(())
}
#[tokio::test]
async fn target_options_preserve_the_platform_target_and_cache() -> Result<()> {
let temp = tempfile::tempdir()?;
let world = crate::world::World::open(temp.path())?;
let builder = Builder::new(world.root, &world.host_dir)?;
let (tx, _rx) = mpsc::channel(1);
for options in [
vec!["--target", "aarch64-unknown-linux-gnu"],
vec!["--target=aarch64-unknown-linux-gnu"],
vec!["--target-dir", "/other"],
vec!["--target-dir=/other"],
] {
let error = builder
.execute(
Request {
argv: std::iter::once("build")
.chain(options)
.map(str::to_string)
.collect(),
cwd: "/".into(),
timeout_secs: None,
},
&tx,
)
.await
.unwrap_err();
assert!(error.to_string().contains("target"), "{error}");
}
for (action, options) in [
("run", vec!["--target", "wasm32-wasip2"]),
("run", vec!["--target=wasm32-wasip2"]),
("run", vec!["--", "--target-dir", "/argument"]),
("tree", vec!["--target", "all"]),
] {
let error = builder
.execute(
Request {
argv: std::iter::once(action)
.chain(options)
.map(str::to_string)
.collect(),
cwd: "/".into(),
timeout_secs: None,
},
&tx,
)
.await
.unwrap_err();
assert_eq!(error.to_string(), "no Cargo.toml above cwd");
}
Ok(())
}
#[test]
fn test_filter_is_not_a_build_argument() {
let args = ["test", "-p", "example", "filter", "--release"].map(str::to_string);
assert_eq!(
test_build_args(&args),
["test", "-p", "example", "--release"]
);
}
#[test]
fn disk_cap_removes_workspaces() -> Result<()> {
let temp = tempfile::tempdir()?;
let world = crate::world::World::open(temp.path())?;
let builder = Builder::new(world.root, &world.host_dir)?;
for name in ["a", "b"] {
let path = builder.directory.join(name);
std::fs::create_dir_all(&path)?;
std::fs::write(path.join("last-used"), [])?;
std::fs::write(path.join("data"), [0; 32])?;
}
std::fs::File::open(builder.directory.join("a/last-used"))?
.set_times(std::fs::FileTimes::new().set_modified(std::time::UNIX_EPOCH))?;
builder.evict(32)?;
assert!(!builder.directory.join("a").exists());
assert!(builder.directory.join("b").exists());
assert_eq!(files::size(&builder.directory)?, 32);
Ok(())
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn timeout_kills_before_output_can_drain() -> Result<()> {
use std::os::fd::{FromRawFd, OwnedFd};
let (tx, mut rx) = mpsc::channel(1);
let output = tx.clone();
let mut cmd = Command::new("sh");
cmd.args(["-c", "echo $$; exec sleep 1000"]);
let task =
tokio::spawn(
async move { run(cmd, &output, Instant::now() + Duration::from_secs(1)).await },
);
let Some(Event::Stdout { bytes }) =
tokio::time::timeout(Duration::from_secs(2), rx.recv()).await?
else {
anyhow::bail!("missing native PID");
};
let pid: u32 = std::str::from_utf8(&bytes)?.trim().parse()?;
let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) };
ensure!(fd >= 0, "pidfd_open: {}", std::io::Error::last_os_error());
// OwnedFd keeps the PID descriptor valid and unchanged until AsyncFd drops it.
let exited = unsafe { tokio::io::unix::AsyncFd::register(OwnedFd::from_raw_fd(fd as i32)) }?;
// Fill the output channel, so the timeout message cannot enter it.
tx.send(Event::Stdout { bytes: Vec::new() }).await?;
let _ready = tokio::time::timeout(Duration::from_secs(2), exited.readable()).await??;
assert!(matches!(rx.recv().await, Some(Event::Stdout { .. })));
assert!(matches!(rx.recv().await, Some(Event::Stderr { bytes })
if bytes.as_slice() == b"cargo: command timed out\n"));
assert_eq!(task.await??.0, 124);
Ok(())
}
#[tokio::test]
async fn closed_receiver_cancels_command() -> Result<()> {
let (tx, mut rx) = mpsc::channel(1);
let mut cmd = Command::new("sh");
cmd.args(["-c", "printf ready; exec sleep 1000"]);
let task =
tokio::spawn(async move { run(cmd, &tx, Instant::now() + Duration::from_secs(60)).await });
// Output must arrive before the command exits, not after collection completes.
let event = tokio::time::timeout(Duration::from_secs(2), rx.recv())
.await?
.context("missing streamed output")?;
assert!(matches!(event, Event::Stdout { bytes } if bytes == b"ready"));
drop(rx);
assert!(
tokio::time::timeout(Duration::from_secs(2), task)
.await??
.is_err()
);
Ok(())
}