use std::fs; use std::path::Path; use std::time::Duration; use dever_runtime::bytes::Bytes; use dever_runtime::component::{self, Definition, Reply}; use dever_runtime::external::{self, Resource}; use dever_runtime::wire::Encoder; use serde_json::json; use sha2::{Digest, Sha256}; #[path = "support/process_runtime.rs"] mod process_runtime; #[path = "support/temp.rs"] mod temp; const PORT: &str = "tools.command"; struct Fixture { directory: temp::TemporaryDirectory, resources: Vec>, } impl Fixture { fn new(timeout_ms: u64, output_limit: usize) -> Self { Self::with_program( timeout_ms, output_limit, fs::read(env!("CARGO_BIN_EXE_command-fixture")).unwrap(), ) } fn with_program(timeout_ms: u64, output_limit: usize, program: Vec) -> Self { let directory = temp::TemporaryDirectory::new(); fs::create_dir(directory.path().join("markers")).unwrap(); fs::create_dir(directory.path().join("config")).unwrap(); let settings = json!({"adapter": {PORT: { "command": {"timeout_ms":timeout_ms, "output_limit":output_limit} }}}); fs::write( directory.path().join("config/setting.json"), settings.to_string(), ) .unwrap(); let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); let mut assets = process_runtime::assets(&workspace); assets.push(process_runtime::Asset { path: "commands/fixture/tool".into(), bytes: program, executable: true, }); assets.push(process_runtime::Asset { path: "tool.dever-command.json".into(), bytes: json!({"format":"dever-command-launch-v1", "ecosystem":"command", "entry":"tool", "executable":"commands/fixture/tool", "arguments":[], "working_directory":"commands/fixture"}) .to_string().into_bytes(), executable: false, }); let resources = assets .into_iter() .map(|asset| Resource { path: Box::leak(asset.path.into_boxed_str()), sha256: Box::leak(format!("{:x}", Sha256::digest(&asset.bytes)).into_boxed_str()), bytes: Box::leak(asset.bytes.into_boxed_slice()), executable: asset.executable, }) .collect(); Self { directory, resources, } } async fn start(&self) -> Result<(), String> { external::prepare( self.directory.path(), &external::bundle_digest(&self.resources), &self.resources, )?; component::start(Definition { key: PORT.into(), ecosystem: "command".into(), entry: "tool".into(), port: PORT.into(), adapter: "default".into(), schema: "command-fixture-v1".into(), capabilities: vec!["file".into(), "process".into()], operations: vec!["execute".into()], setting: None, timeout_ms: 30_000, }) .await } fn marker(&self, name: &str) -> std::path::PathBuf { self.directory.path().join("markers").join(name) } } struct Output { code: i64, stdout: Vec, stderr: Vec, } async fn tree(directory: std::path::PathBuf) -> Result<(), String> { invoke(&["tree", directory.to_str().unwrap()], None) .await .map(|_| ()) } async fn invoke(args: &[&str], stdin: Option<&[u8]>) -> Result { let mut writer = Encoder::default(); writer.begin_object()?; writer.key("args")?; writer.begin_array()?; for arg in args { writer.text(arg)?; } writer.end()?; if let Some(stdin) = stdin { writer.key("stdin")?; writer.bytes(&Bytes::new(stdin.to_vec()))?; } writer.end()?; let reply = component::call(PORT, "execute", &writer.finish()?).await?; let Reply::Result(payload) = reply else { return Err("unexpected business error".into()); }; let node = dever_runtime::wire::parse(&payload)?; let output = node .object()? .get("output") .ok_or("missing output")? .object()?; Ok(Output { code: output.get("code").ok_or("missing code")?.int()?, stdout: output .get("stdout") .ok_or("missing stdout")? .bytes()? .values() .to_vec(), stderr: output .get("stderr") .ok_or("missing stderr")? .bytes()? .values() .to_vec(), }) } #[test] fn ordinary_commands_preserve_argv_binary_io_and_nonzero_status() { let fixture = Fixture::new(5000, 1024 * 1024); dever_runtime::task::run_entry(async { fixture.start().await?; assert!( !fixture.marker("starts").exists(), "startup must not execute the tool" ); let output = invoke( &[ "echo", "a b", "\"quoted\"", "$(literal);*", "/data/markers", "", ], None, ) .await?; assert_eq!( output.stdout, b"a b\0\"quoted\"\0$(literal);*\0/data/markers\0\0" ); assert_eq!(output.code, 0); let binary: Vec = (0..=255).cycle().take(131_072).collect(); let output = invoke(&["stdin"], Some(&binary)).await?; assert_eq!(output.stdout, binary); assert_eq!(output.stderr, [0, 255, 128, 10]); assert!( invoke(&["stdin"], None).await?.stdout.is_empty(), "omitted stdin must send EOF" ); let output = invoke(&["duplex", "262144"], Some(&binary)).await?; assert_eq!(output.stdout, vec![254; 262_144]); assert_eq!(output.stderr, vec![253; 262_144]); assert_eq!(invoke(&["exit", "37"], None).await?.code, 37); assert_eq!(invoke(&["exit", "1"], None).await?.code, 1); assert_eq!(invoke(&["exit", "127"], None).await?.code, 127); assert!( invoke(&["fds"], None).await?.stdout.is_empty(), "observation descriptors leaked into the command" ); // Early stdin close must not replace the program's ordinary result. assert_eq!(invoke(&["exit", "13"], Some(&binary)).await?.code, 13); let nul = invoke(&["echo", "bad\0arg"], None) .await .err() .ok_or("NUL accepted")?; assert!(nul.contains("NUL")); let huge = "a".repeat(65_536); assert!(invoke(&["echo", &huge], None).await.is_err()); assert!( invoke(&["stdin"], Some(&vec![0; 8 * 1024 * 1024 + 1])) .await .err() .is_some_and(|error| error.contains("stdin exceeds")) ); for _ in 0..5 { assert_eq!(invoke(&["exit", "0"], None).await?.code, 0); } component::shutdown().await }) .unwrap(); } #[test] fn combined_output_limit_closes_process_then_allows_the_next_call() { let fixture = Fixture::new(5000, 262_144); dever_runtime::task::run_entry(async { fixture.start().await?; let error = invoke(&["duplex", "200000"], Some(&vec![7; 131_072])) .await .err() .ok_or("limit accepted")?; assert!(error.contains("output_limit"), "{error}"); assert_eq!(invoke(&["exit", "0"], None).await?.code, 0); component::shutdown().await }) .unwrap(); } async fn heartbeat(fixture: &Fixture) -> Result<(), String> { // Verified-resource preparation precedes process startup; debug hashing is // not the command deadline and may dominate on a busy author machine. tokio::time::timeout(Duration::from_secs(15), async { loop { if fs::read_to_string(fixture.marker("heartbeat")) .ok() .and_then(|text| text.parse::().ok()) .is_some_and(|count| count >= 2) { break; } tokio::time::sleep(Duration::from_millis(10)).await; } }) .await .map_err(|_| { format!( "descendant heartbeat did not start; starts={:?}; ready={}; error={:?}", fs::read_to_string(fixture.marker("starts")), fixture.marker("ready").exists(), fs::read_to_string(fixture.marker("fixture-error")) ) }) } #[test] fn invalid_executable_is_rejected_before_a_program_exit_code_exists() { let fixture = Fixture::with_program(5000, 1024 * 1024, b"not an ELF executable\n".to_vec()); dever_runtime::task::run_entry(async { let error = fixture.start().await.unwrap_err(); assert!(error.contains("invalid executable ELF"), "{error}"); assert!(!fixture.marker("starts").exists()); component::shutdown().await }) .unwrap(); } async fn assert_reaped(fixture: &Fixture) { let lifetime = fs::OpenOptions::new() .write(true) .open(fixture.marker("lifetime.lock")) .unwrap(); lifetime .try_lock() .expect("descendant still holds its lifetime lock after cleanup returned"); let before = fs::read(fixture.marker("heartbeat")).unwrap(); tokio::time::sleep(Duration::from_millis(100)).await; assert_eq!( fs::read(fixture.marker("heartbeat")).unwrap(), before, "descendant survived cleanup" ); assert_eq!( fs::read_to_string(fixture.marker("starts")).unwrap(), "started\n", "command was replayed" ); } #[test] fn successful_root_exit_stops_descendants_holding_output_pipes() { let fixture = Fixture::new(5000, 1024 * 1024); dever_runtime::task::run_entry(async { fixture.start().await?; let directory = fixture.marker(""); let output = invoke(&["tree-exit", directory.to_str().unwrap()], None).await?; assert_eq!(output.code, 0); assert_reaped(&fixture).await; component::shutdown().await }) .unwrap(); } #[test] fn timeout_reaps_descendants_and_keeps_the_adapter_available() { let fixture = Fixture::new(800, 1024 * 1024); dever_runtime::task::run_entry(async { fixture.start().await?; let call = dever_runtime::task::run(tree(fixture.marker(""))).await?; heartbeat(&fixture).await?; let error = dever_runtime::task::wait(call) .await .expect_err("command did not time out"); assert!(error.contains("timed out"), "{error}"); assert_reaped(&fixture).await; assert_eq!(invoke(&["exit", "0"], None).await?.code, 0); component::shutdown().await }) .unwrap(); } #[test] fn cancellation_and_root_close_reap_descendants_without_replay() { for root_close in [false, true] { let fixture = Fixture::new(5000, 1024 * 1024); dever_runtime::task::run_entry(async { fixture.start().await?; let call = dever_runtime::task::run(tree(fixture.marker(""))).await?; heartbeat(&fixture).await?; if root_close { // Return while the call is active: the invocation closes its // component owner before draining the structured child task. return Ok::<(), String>(()); } dever_runtime::task::stop(call).await?; assert_reaped(&fixture).await; assert_eq!(invoke(&["exit", "0"], None).await?.code, 0); component::shutdown().await }) .unwrap_or_else(|error| { assert!(root_close && error.contains("stopped"), "{error}"); }); let before = fs::read(fixture.marker("heartbeat")).unwrap(); let lifetime = fs::OpenOptions::new() .write(true) .open(fixture.marker("lifetime.lock")) .unwrap(); lifetime .try_lock() .expect("root close returned before descendant cleanup"); std::thread::sleep(Duration::from_millis(100)); assert_eq!(fs::read(fixture.marker("heartbeat")).unwrap(), before); assert_eq!( fs::read_to_string(fixture.marker("starts")).unwrap(), "started\n" ); } } #[test] fn queued_cancellation_does_not_wait_for_an_unrelated_running_command() { let fixture = Fixture::new(5000, 1024 * 1024); dever_runtime::task::run_entry(async { fixture.start().await?; let active = dever_runtime::task::run(tree(fixture.marker(""))).await?; heartbeat(&fixture).await?; let (entered, admitted) = tokio::sync::oneshot::channel(); let queued = dever_runtime::task::run(async move { let _ = entered.send(()); invoke(&["echo", "queued"], None).await.map(|_| ()) }) .await?; admitted.await.map_err(|error| error.to_string())?; tokio::time::sleep(Duration::from_millis(20)).await; tokio::time::timeout( Duration::from_millis(500), dever_runtime::task::stop(queued), ) .await .map_err(|_| "queued cancellation waited for the active command")??; let before = fs::read(fixture.marker("heartbeat")).unwrap(); tokio::time::sleep(Duration::from_millis(50)).await; assert_ne!(fs::read(fixture.marker("heartbeat")).unwrap(), before); dever_runtime::task::stop(active).await?; assert_reaped(&fixture).await; component::shutdown().await }) .unwrap(); } #[test] fn task_deadline_waits_for_command_process_cleanup() { let fixture = Fixture::new(5000, 1024 * 1024); dever_runtime::task::run_entry(async { fixture.start().await?; let call = dever_runtime::task::run(tree(fixture.marker(""))).await?; heartbeat(&fixture).await?; assert_eq!(dever_runtime::task::wait_timeout(call, 10).await?, None); assert_reaped(&fixture).await; component::shutdown().await }) .unwrap(); } #[test] fn explicit_shutdown_drains_active_and_queued_commands() { let fixture = Fixture::new(5000, 1024 * 1024); dever_runtime::task::run_entry(async { fixture.start().await?; let active = dever_runtime::task::run(tree(fixture.marker(""))).await?; heartbeat(&fixture).await?; let queued = dever_runtime::task::run(async { invoke(&["echo", "queued"], None).await.map(|_| ()) }) .await?; component::shutdown().await?; assert_reaped(&fixture).await; assert!(dever_runtime::task::wait(active).await.is_err()); assert!(dever_runtime::task::wait(queued).await.is_err()); Ok::<(), String>(()) }) .unwrap(); } #[test] fn command_settings_validate_limits_without_affecting_worker_selection() { let directory = temp::TemporaryDirectory::new(); fs::create_dir(directory.path().join("config")).unwrap(); for command in [ json!({"timeout_ms":0}), json!({"timeout_ms":3_600_001}), json!({"output_limit":0}), json!({"output_limit":8_388_609}), json!({"unknown":1}), ] { fs::write( directory.path().join("config/setting.json"), json!({"adapter":{PORT:{"command":command}}}).to_string(), ) .unwrap(); let settings = dever_runtime::config::Settings::load_project(directory.path()).unwrap(); assert!(settings.command_limits(PORT).is_err()); assert!(settings.select_adapter(PORT, &["default"]).is_ok()); } }