//! Focused tests for source discovery, argument forwarding, eviction, and process cancellation. use super::*; #[test] fn native_command_defaults_to_bwrap_and_can_bypass_it() -> Result<()> { let temp = tempfile::tempdir()?; let world = crate::world::World::open(temp.path())?; assert!(!crate::process::RuntimeOptions::default().no_builder_sandbox); let default = Builder::new(world.root.clone(), &world.host_dir)?; assert_eq!(default.command().as_std().get_program(), "bwrap"); let direct = Builder::new_with_options(world.root, &world.host_dir, true)?; assert_eq!(direct.command().as_std().get_program(), "cargo"); Ok(()) } #[tokio::test] async fn source_links_survive_round_trip() -> Result<()> { let temp = tempfile::tempdir()?; let world = crate::world::World::open(temp.path())?; files::write(world.root.as_ref(), "/project/real.rs", b"source".to_vec()).await?; for (name, target) in [("dangling", "missing"), ("linked.rs", "real.rs")] { std::os::unix::fs::symlink(target, temp.path().join("world/project").join(name))?; } let copy = temp.path().join("copy"); let before = files::copy_out(world.root.as_ref(), "/project", ©).await?; assert_eq!( std::fs::read_link(copy.join("dangling"))?, Path::new("missing") ); assert_eq!(std::fs::read(copy.join("linked.rs"))?, b"source"); files::copy_back(world.root.as_ref(), ©, "/project", Some(&before)).await?; assert_eq!(world.root.readlink("/project/dangling").await?, "missing"); assert_eq!(world.root.readlink("/project/linked.rs").await?, "real.rs"); Ok(()) } #[test] fn display_world_paths_and_hide_runner() { let display = DisplayPaths { replacements: [ ("/private/source".into(), "/project".into()), ("/private/target/cargo".into(), "/project/target".into()), ], }; assert_eq!( display.render(b"Compiling app (/private/source/member)\n"), b"Compiling app (/project/member)\n" ); assert_eq!( display.render(b"error: /private/target/cargo/app.wasm\n"), b"error: /project/target/app.wasm\n" ); assert!( display .render(b" Running `nu /private/tools/run.nu /private/target/cargo/app.wasm`\n") .is_empty() ); } #[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", ©).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", ©).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(), ©, "/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)?; std::fs::create_dir_all(builder.directory.join("incomplete/source"))?; 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), false, None, ) .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), false, None, ) .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(()) }