use super::{TemporaryProject, process}; use std::fs; use std::process::Command; use std::time::{Duration, Instant}; const DESCENDANTS: &str = r#" /bin/sh -c 'echo $$ > descendant; /bin/sleep 30 & echo $! > grandchild; wait' & while [ ! -s grandchild ]; do :; done "#; // subreaper 是进程级状态;此单例测试顺序覆盖所有真实进程场景。 #[test] fn case_process_captures_output_times_out_and_reaps_only_owned_descendants() { let original_reaper = rustix::process::child_subreaper().unwrap(); let mut unrelated = Command::new("/bin/sleep").arg("30").spawn().unwrap(); let result = std::panic::catch_unwind(|| { for (body, descendants, expected) in [ ("printf hidden; printf hidden-error >&2; exit 0", false, None), ( "printf output; printf diagnostic >&2; exit 7", false, Some("exit status: 7"), ), ( "printf before-timeout; /bin/sleep 30", false, Some("timed out after 100 ms"), ), ("exit 0", true, None), ("printf owned-error >&2; exit 7", true, Some("exit status: 7")), ("printf started; wait", true, Some("timed out after 100 ms")), ] { let root = TemporaryProject::new().unwrap(); let started = Instant::now(); let script = if descendants { format!("{DESCENDANTS}\n{body}") } else { body.to_owned() }; let result = process::run( Command::new("/bin/sh") .arg("-c") .arg(&script) .env_clear() .current_dir(root.path()), root.path(), Duration::from_millis(100), ); assert!(started.elapsed() < Duration::from_secs(5)); match expected { None => result.unwrap(), Some(message) => { let failure = result.unwrap_err(); assert!(failure.message.contains(message), "{failure:?}"); if script.contains("printf output") { assert_eq!(failure.stdout, b"output"); assert_eq!(failure.stderr, b"diagnostic"); } if script.contains("printf before-timeout") { assert_eq!(failure.stdout, b"before-timeout"); } if script.contains("printf owned-error") { assert_eq!(failure.stderr, b"owned-error"); } } } for name in ["descendant", "grandchild"] { let path = root.path().join(name); if descendants { let pid = fs::read_to_string(path) .unwrap() .trim() .parse::() .unwrap(); assert!( !std::path::Path::new(&format!("/proc/{pid}")).exists(), "unreaped {name} {pid}" ); } } assert_eq!(rustix::process::child_subreaper().unwrap(), original_reaper); } let root = TemporaryProject::new().unwrap(); let failure = process::run( Command::new("/bin/sh") .arg("-c") .arg("/bin/dd if=/dev/zero bs=131072 count=2 2>/dev/null; printf end >&2; exit 9") .env_clear(), root.path(), Duration::from_secs(5), ) .unwrap_err(); assert!(failure.message.contains("exit status: 9")); assert_eq!(failure.stdout.len(), 262_144); assert_eq!(failure.stderr, b"end"); let root = TemporaryProject::new().unwrap(); let failure = process::run( &mut Command::new(root.path().join("missing-executable")), root.path(), Duration::from_millis(100), ) .unwrap_err(); assert!(failure.message.contains("cannot run test process")); assert_eq!(rustix::process::child_subreaper().unwrap(), original_reaper); }); // 即使断言失败也回收此对照进程,不能让测试自身遗留资源。 let unrelated_running = unrelated.try_wait().unwrap().is_none(); let _ = unrelated.kill(); unrelated.wait().unwrap(); result.unwrap(); assert!(unrelated_running, "case cleanup affected an unrelated child"); }