use std::time::{Duration, Instant};
#[cfg(windows)]
use processkit::Command;
use processkit::{Mechanism, ProcessGroup};
use crate::common::*;
#[tokio::test]
#[ignore = "creates an OS job/cgroup"]
async fn group_reports_the_platforms_mechanism() {
let group = ProcessGroup::new().expect("create group");
let mechanism = group.mechanism();
#[cfg(windows)]
assert_eq!(mechanism, Mechanism::JobObject);
#[cfg(target_os = "linux")]
assert!(
matches!(mechanism, Mechanism::CgroupV2 | Mechanism::ProcessGroup),
"linux is cgroup v2 or its pgroup fallback, got {mechanism:?}"
);
#[cfg(all(unix, not(target_os = "linux")))]
assert_eq!(mechanism, Mechanism::ProcessGroup);
#[cfg(not(any(unix, windows)))]
assert_eq!(mechanism, Mechanism::None);
}
#[tokio::test]
#[ignore = "spawns a long-lived subprocess and asserts kill-on-drop"]
async fn dropping_group_kills_children() {
if cfg!(not(any(windows, unix))) {
return;
}
let group = ProcessGroup::new().expect("create group");
let process = group.start(&sleeper()).await.expect("spawn sleeper");
let pid = process.pid();
assert!(
pid.is_some(),
"sleeper should report a pid right after spawn"
);
drop(group);
let start = Instant::now();
let _exit = tokio::time::timeout(Duration::from_secs(10), process.wait())
.await
.expect("child outlived its group — kill-on-close did not fire")
.expect("wait completed");
assert!(
start.elapsed() < Duration::from_secs(5),
"child was not reaped promptly (took {:?})",
start.elapsed()
);
}
#[cfg(windows)]
#[tokio::test]
#[ignore = "spawns a real process tree; proves a grandchild is contained (race fix)"]
async fn windows_grandchild_is_contained() {
let tmp = std::env::temp_dir();
let tag = std::process::id();
let pidfile = tmp.join(format!("processkit_gc_{tag}.pid"));
let grandchild_ps1 = tmp.join(format!("processkit_gc_{tag}.ps1"));
let parent_ps1 = tmp.join(format!("processkit_parent_{tag}.ps1"));
let _ = std::fs::remove_file(&pidfile);
std::fs::write(
&grandchild_ps1,
format!(
"$PID | Set-Content -Encoding ascii '{}'\nStart-Sleep -Seconds 30\n",
pidfile.display()
),
)
.expect("write grandchild script");
std::fs::write(
&parent_ps1,
format!(
"Start-Process -WindowStyle Hidden -FilePath powershell \
-ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','{}'\n",
grandchild_ps1.display()
),
)
.expect("write parent script");
let group = ProcessGroup::new().expect("create group");
group
.start(&Command::new("powershell").args([
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
&parent_ps1.to_string_lossy(),
]))
.await
.expect("spawn parent")
.wait()
.await
.expect("parent waits");
let mut grandchild_pid = None;
for _ in 0..50 {
if let Ok(text) = std::fs::read_to_string(&pidfile)
&& let Ok(pid) = text.trim().parse::<u32>()
{
grandchild_pid = Some(pid);
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
let pid = grandchild_pid.expect("grandchild never recorded its PID");
assert!(
windows_pid_alive(pid),
"grandchild should be alive before drop"
);
drop(group);
let mut reaped = false;
for _ in 0..50 {
if !windows_pid_alive(pid) {
reaped = true;
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
let _ = std::fs::remove_file(&pidfile);
let _ = std::fs::remove_file(&grandchild_ps1);
let _ = std::fs::remove_file(&parent_ps1);
assert!(
reaped,
"grandchild {pid} outlived its job — containment leaked"
);
}
#[tokio::test]
#[ignore = "spawns a real subprocess and kills it twice"]
async fn terminate_all_is_idempotent() {
let group = ProcessGroup::new().expect("create group");
let child = group.start(&sleep_secs(30)).await.expect("start sleeper");
group.terminate_all().expect("first terminate");
group
.terminate_all()
.expect("second terminate must be a no-op success, not an error");
let again = group
.start(&sleep_secs(1))
.await
.expect("group usable after terminate");
drop(again);
let _ = tokio::time::timeout(Duration::from_secs(10), child.wait())
.await
.expect("child reaped");
}