builder: enforce target, source sync, and timeout contracts

This commit is contained in:
2026-11-07 12:32:45 +00:00
parent a27d579264
commit 2f18f0fa4c
4 changed files with 182 additions and 23 deletions
+117
View File
@@ -33,6 +33,41 @@ async fn discovers_workspace_and_skips_target() -> Result<()> {
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())?;
@@ -69,6 +104,58 @@ fn relative_host_directory_uses_absolute_namespace_paths() -> Result<()> {
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);
@@ -96,6 +183,36 @@ fn disk_cap_removes_workspaces() -> Result<()> {
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());
let exited = tokio::io::unix::AsyncFd::new(unsafe { 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);