use std::io::{BufRead as _, BufReader, Write as _};
use std::os::unix::process::{CommandExt as _, ExitStatusExt as _};
use std::process::{Child, ChildStdout, Command, Stdio};
use std::time::{Duration, Instant};
use super::gate::last_gated_for_test;
pub use super::gate::{GuardFault, set_guard_fault_for_test};
pub use super::spawner::{
break_helper_for_test, fail_next_release_send_for_test, fail_next_spawn_send_for_test,
helper_pid_for_test, kill_helper_for_test, set_helper_fault_for_test,
};
use super::{
CallGuard, GroupEnd, GuardOutcome, Refusal, end_pinned_group, live_members, start_behind_guard,
};
use crate::shell::exec::{reap_within, wait_with_limits_captured};
pub(super) const HELPER_ENV: &str = "LEAN_CTX_TEST_CONTAINMENT_HELPER";
pub(super) const GROUP_ENV: &str = "LEAN_CTX_TEST_CONTAINMENT_GROUP";
pub(super) const HELPER_TEST: &str = "shell::containment::tests::containment_helper";
const SCRIPT_ENV: &str = "LEAN_CTX_TEST_CONTAINMENT_SCRIPT";
const CONTROL_ENV: &str = "LEAN_CTX_TEST_CONTAINMENT_CONTROL_FD";
const WATCH_PARENT_ENV: &str = "LEAN_CTX_TEST_CONTAINMENT_WATCH_PARENT";
const SAME_GROUP_ENV: &str = "LEAN_CTX_TEST_CONTAINMENT_SAME_GROUP";
pub(super) const PAUSE_ENV: &str = "LEAN_CTX_TEST_CONTAINMENT_PAUSE";
pub(super) const FAULT_ENV: &str = "LEAN_CTX_TEST_CONTAINMENT_HELPER_FAULT";
pub(super) const NO_SUCH_GROUP: libc::pid_t = libc::pid_t::MAX;
const LEAVES_BEHIND: &str =
"trap '' TERM; sleep 60 & echo \"ENGINE $$\"; echo \"DESCENDANT $!\"; exit 7";
const RUNS: &str = "sleep 60 & echo \"ENGINE $$\"; echo \"DESCENDANT $!\"; sleep 60";
const STORM: &str = "i=0; while [ \"$i\" -lt 200 ]; do sleep 30 & i=$((i+1)); done; wait";
fn helper(mode: &str) -> Command {
let mut command = Command::new(std::env::current_exe().unwrap());
command
.args([HELPER_TEST, "--exact", "--ignored", "--nocapture"])
.env(HELPER_ENV, mode);
command
}
#[test]
#[ignore = "subprocess helper for the containment tests"]
fn containment_helper() {
containment_helper_main();
}
pub fn containment_helper_main() {
let mode = std::env::var(HELPER_ENV).expect("helper mode");
match mode.as_str() {
"group-guard" => {
let group = std::env::var(GROUP_ENV).unwrap().parse().unwrap();
std::process::exit(super::guard::run(group));
}
"session-starter" => std::process::exit(super::starter::run(&starter_options())),
"spawn-helper" => std::process::exit(super::spawn_helper::serve(libc::STDIN_FILENO)),
"engine-standin" => engine_standin(),
"capture-caller" => capture_caller(),
"starter-parent" => starter_parent(),
other => fixture(other.strip_prefix("fixture:").expect("known helper mode")),
}
}
fn starter_options() -> super::starter::Options {
super::starter::Options {
ack: true,
watch_parent: std::env::var_os(WATCH_PARENT_ENV).is_some(),
control_fd: std::env::var(CONTROL_ENV)
.ok()
.map(|fd| fd.parse().unwrap()),
program: "sh".into(),
args: vec!["-c".into(), std::env::var_os(SCRIPT_ENV).unwrap()],
engine_in_own_group: std::env::var_os(SAME_GROUP_ENV).is_none(),
}
}
#[allow(clippy::zombie_processes)]
fn engine_standin() {
let (child, _guard) = guarded_call(&std::env::var(SCRIPT_ENV).unwrap(), false);
println!("CALL-GROUP {}", child.id());
std::io::stdout().flush().unwrap();
loop {
std::thread::sleep(Duration::from_secs(1));
}
}
#[allow(clippy::zombie_processes)]
fn starter_parent() {
let _starter = helper("session-starter")
.env(WATCH_PARENT_ENV, "1")
.stdin(Stdio::null())
.spawn()
.unwrap();
loop {
std::thread::sleep(Duration::from_secs(1));
}
}
pub fn alive(pid: libc::pid_t) -> bool {
unsafe { libc::kill(pid, 0) == 0 }
}
fn gone(pid: libc::pid_t) -> bool {
let deadline = Instant::now() + Duration::from_secs(2);
while alive(pid) {
if Instant::now() >= deadline {
return false;
}
std::thread::sleep(Duration::from_millis(20));
}
true
}
pub fn wait_gone(group: libc::pid_t) -> bool {
let deadline = Instant::now() + Duration::from_secs(5);
while live_members(group) != Some(0) {
if Instant::now() >= deadline {
return false;
}
std::thread::sleep(Duration::from_millis(20));
}
true
}
fn call_command(script: &str, piped: bool) -> Command {
let output = || {
if piped { Stdio::piped() } else { Stdio::null() }
};
let mut command = Command::new("sh");
command
.args(["-c", script])
.stdin(Stdio::null())
.stdout(output())
.stderr(output())
.process_group(0);
command
}
fn guarded_call(script: &str, piped: bool) -> (Child, CallGuard) {
start_behind_guard(&mut call_command(script, piped))
.unwrap_or_else(|_| panic!("the call starts behind its guard"))
}
fn read_marker(lines: &mut BufReader<ChildStdout>, prefix: &str) -> libc::pid_t {
let mut line = String::new();
loop {
line.clear();
assert_ne!(
lines.read_line(&mut line).unwrap(),
0,
"no line `{prefix}…`"
);
if let Some(value) = line.trim_end().strip_prefix(prefix) {
return value.parse().unwrap();
}
}
}
fn read_line_equal(lines: &mut BufReader<ChildStdout>, expected: &[u8]) {
let mut line = Vec::new();
loop {
line.clear();
assert_ne!(
lines.read_until(b'\n', &mut line).unwrap(),
0,
"line missing"
);
if line == expected {
return;
}
}
}
fn spawn_starter(
script: &str,
configure: impl FnOnce(&mut Command),
) -> (Child, BufReader<ChildStdout>) {
let mut command = helper("session-starter");
command
.env(SCRIPT_ENV, script)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
configure(&mut command);
let mut starter = command.spawn().unwrap();
let lines = BufReader::new(starter.stdout.take().unwrap());
(starter, lines)
}
const EXTERNAL_FIXTURE: &str = "external-fixture";
const EXTERNAL_COMPLETE: &str = "FIXTURE-COMPLETE external";
pub fn run_external_fixture(module: &str, name: &str) -> String {
let module = module.split_once("::").map_or(module, |(_, rest)| rest);
let mut command = Command::new(std::env::current_exe().unwrap());
command
.args([
format!("{module}::{name}").as_str(),
"--exact",
"--ignored",
"--nocapture",
])
.env(HELPER_ENV, EXTERNAL_FIXTURE)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.process_group(0);
let child = command.spawn().unwrap();
let captured = wait_with_limits_captured(child, 256 * 1024, Duration::from_mins(3), true);
let stdout = String::from_utf8_lossy(&captured.output.stdout);
let stderr = String::from_utf8_lossy(&captured.output.stderr);
assert!(
stdout.contains(EXTERNAL_COMPLETE),
"fixture status {:?}\n{stdout}\n{stderr}",
captured.output.status
);
stdout.into_owned()
}
const EXTERNAL_HELPER: &str = "external-helper";
pub fn external_helper(module: &str, name: &str) -> Command {
let module = module.split_once("::").map_or(module, |(_, rest)| rest);
let mut command = Command::new(std::env::current_exe().unwrap());
command
.args([
format!("{module}::{name}").as_str(),
"--exact",
"--ignored",
"--nocapture",
])
.env(HELPER_ENV, EXTERNAL_HELPER);
command
}
pub fn external_helper_body() {
assert_eq!(
std::env::var(HELPER_ENV).as_deref(),
Ok(EXTERNAL_HELPER),
"this test body runs only as a fixture's helper process"
);
}
const CALLER_STANDIN: &str = "caller-standin";
pub fn caller_killed_before_its_guard(module: &str, standin: &str) {
let module = module.split_once("::").map_or(module, |(_, rest)| rest);
let scratch = tempfile::tempdir().unwrap();
let marker = scratch.path().canonicalize().unwrap().join("ran");
std::fs::write(&marker, b"").unwrap();
let mut caller = Command::new(std::env::current_exe().unwrap());
caller
.args([
format!("{module}::{standin}").as_str(),
"--exact",
"--ignored",
"--nocapture",
])
.env(HELPER_ENV, CALLER_STANDIN)
.env(SCRIPT_ENV, &marker)
.env(PAUSE_ENV, "1")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let mut caller = caller.spawn().unwrap();
let mut lines = BufReader::new(caller.stdout.take().unwrap());
let group = read_marker(&mut lines, "FORKED ");
caller.kill().unwrap();
caller.wait().unwrap();
assert!(
wait_gone(group),
"a command without a guard was left running"
);
assert!(
std::fs::read_to_string(&marker).unwrap().is_empty(),
"the command's program ran"
);
}
pub fn caller_standin_marker() -> std::path::PathBuf {
assert_eq!(
std::env::var(HELPER_ENV).as_deref(),
Ok(CALLER_STANDIN),
"this test body runs only as a stand-in caller"
);
std::env::var_os(SCRIPT_ENV)
.expect("the mark's path")
.into()
}
pub struct ExternalFixture(#[allow(dead_code)] EndOwnGroup);
pub fn external_fixture() -> ExternalFixture {
assert_eq!(
std::env::var(HELPER_ENV).as_deref(),
Ok(EXTERNAL_FIXTURE),
"this test body runs only as a fixture"
);
let own = unsafe { libc::getpgrp() };
assert_eq!(own, std::process::id() as libc::pid_t, "fixture leads");
ExternalFixture(EndOwnGroup(own))
}
impl ExternalFixture {
pub fn complete(self) {
println!("{EXTERNAL_COMPLETE}");
}
}
struct EndOwnGroup(libc::pid_t);
impl Drop for EndOwnGroup {
fn drop(&mut self) {
let _ = std::io::stdout().flush();
unsafe { libc::killpg(self.0, libc::SIGKILL) };
}
}
fn fixture(name: &str) {
let own = unsafe { libc::getpgrp() };
assert_eq!(own, std::process::id() as libc::pid_t, "fixture leads");
let _end = EndOwnGroup(own);
let mut sentinel = Command::new("sleep")
.arg("60")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap();
match name {
"call-timeout" => call_timeout(),
"engine-killed" => engine_killed(),
"leader-first" => leader_first(),
"mid-fork" => mid_fork(),
"cannot-join" => cannot_join(),
"starter-engine-exits" => starter_engine_exits(),
"starter-control" => starter_control(),
"starter-killed" => starter_killed(),
"starter-killed-early" => starter_killed_early(),
"starter-parent-gone" => starter_parent_gone(),
"starter-own-group" => starter_own_group(),
"capture-ownership" => capture_ownership(),
"helper-timeout" => helper_timeout(),
"capture-caller-killed" => capture_caller_killed(),
"costs" => costs(),
other => panic!("unknown fixture: {other}"),
}
assert!(
alive(sentinel.id() as libc::pid_t),
"a process outside the contained groups was ended"
);
sentinel.kill().unwrap();
sentinel.wait().unwrap();
println!("FIXTURE-COMPLETE {name}");
}
fn run_fixture(name: &str) -> String {
let mut command = helper(&format!("fixture:{name}"));
command
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.process_group(0);
let child = command.spawn().unwrap();
let captured = wait_with_limits_captured(child, 256 * 1024, Duration::from_mins(3), true);
let stdout = String::from_utf8_lossy(&captured.output.stdout);
let stderr = String::from_utf8_lossy(&captured.output.stderr);
assert!(
stdout.contains(&format!("FIXTURE-COMPLETE {name}")),
"fixture status {:?}\n{stdout}\n{stderr}",
captured.output.status
);
stdout.into_owned()
}
fn call_timeout() {
let (child, guard) = guarded_call("sleep 60 & sleep 60", true);
let group = child.id() as libc::pid_t;
let captured = wait_with_limits_captured(child, 1024, Duration::from_millis(200), true);
assert!(captured.completeness.timed_out);
assert_eq!(guard.finish(), GuardOutcome::Ended);
assert_eq!(live_members(group), Some(0), "the call's tree is gone");
}
fn engine_killed() {
let mut engine = helper("engine-standin");
engine
.env(SCRIPT_ENV, "trap '' TERM; sleep 60 & sleep 60")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let mut engine = engine.spawn().unwrap();
let mut lines = BufReader::new(engine.stdout.take().unwrap());
let group = read_marker(&mut lines, "CALL-GROUP ");
assert!(
live_members(group).is_some_and(|live| live >= 1),
"the call runs"
);
engine.kill().unwrap();
engine.wait().unwrap();
assert!(wait_gone(group), "the guard ends the call of a dead Engine");
}
fn leader_first() {
let (child, guard) = guarded_call("trap '' TERM; sleep 60 & echo started", true);
let group = child.id() as libc::pid_t;
let start = Instant::now();
let captured = wait_with_limits_captured(child, 1024, Duration::from_secs(30), true);
assert!(start.elapsed() < Duration::from_secs(8));
assert!(String::from_utf8_lossy(&captured.output.stdout).contains("started"));
assert_eq!(guard.finish(), GuardOutcome::Ended);
assert_eq!(live_members(group), Some(0));
let (child, guard) = guarded_call(
"trap '' TERM; sleep 60 >/dev/null 2>&1 & echo started",
true,
);
let group = child.id() as libc::pid_t;
let captured = wait_with_limits_captured(child, 1024, Duration::from_secs(30), true);
assert!(!captured.completeness.timed_out);
assert!(!captured.completeness.reader_incomplete);
assert!(
live_members(group).is_some_and(|live| live >= 1),
"the limit path alone leaves a descendant that holds no pipe"
);
assert_eq!(guard.finish(), GuardOutcome::Ended);
assert_eq!(live_members(group), Some(0));
let (child, guard) = guarded_call("sleep 60 >/dev/null 2>&1 & exit 0", true);
let group = child.id() as libc::pid_t;
let captured = wait_with_limits_captured(child, 1024, Duration::from_secs(30), true);
assert!(captured.output.status.success());
assert_eq!(guard.finish(), GuardOutcome::Ended);
assert_eq!(live_members(group), Some(0));
}
fn mid_fork() {
for _ in 0..10 {
let (child, guard) = guarded_call(STORM, true);
let group = child.id() as libc::pid_t;
let captured = wait_with_limits_captured(child, 1024, Duration::from_millis(30), true);
assert!(captured.completeness.timed_out);
assert_eq!(guard.finish(), GuardOutcome::Ended);
assert_eq!(live_members(group), Some(0));
let (mut child, guard) = guarded_call(STORM, false);
let group = child.id() as libc::pid_t;
std::thread::sleep(Duration::from_millis(30));
assert_eq!(guard.finish(), GuardOutcome::Ended);
assert_eq!(live_members(group), Some(0));
child.wait().unwrap();
}
}
fn cannot_join() {
let marker = tempfile::NamedTempFile::new().unwrap();
let script = format!("echo ran >> {}; sleep 60", marker.path().display());
let nothing_ran = |case: &str| {
let process = last_gated_for_test();
assert!(process > 1, "{case}: a process waited at the gate");
assert!(!alive(process), "{case}: the refused process is reaped");
assert_eq!(live_members(process), Some(0), "{case}: nothing is left");
assert!(
std::fs::read_to_string(marker.path()).unwrap().is_empty(),
"{case}: the refused call's program ran"
);
};
for fault in [GuardFault::CannotJoin, GuardFault::WrongGroup] {
let mut command = call_command(&script, true);
set_guard_fault_for_test(Some(fault));
let refused = start_behind_guard(&mut command);
set_guard_fault_for_test(None);
assert!(
matches!(refused, Err(Refusal::NoGuard(_))),
"{fault:?}: no guard, no call"
);
nothing_ran(&format!("{fault:?}"));
}
let mut ungrouped = Command::new("sh");
ungrouped
.args(["-c", &script])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
assert!(
matches!(start_behind_guard(&mut ungrouped), Err(Refusal::NotLeader)),
"a process that leads no group of its own is not started"
);
nothing_ran("not a leader");
}
fn starter_engine_exits() {
let (mut starter, mut lines) = spawn_starter(LEAVES_BEHIND, |_| {});
read_line_equal(&mut lines, super::SESSION_ACK);
let group = read_marker(&mut lines, "ENGINE ");
let descendant = read_marker(&mut lines, "DESCENDANT ");
let status =
reap_within(&mut starter, Duration::from_secs(15)).expect("the starter exits by itself");
assert_eq!(status.code(), Some(7), "the Engine's status is passed on");
assert_eq!(live_members(group), Some(0));
assert!(gone(descendant));
}
fn starter_control() {
use std::os::fd::AsRawFd as _;
let (reader, writer) = std::io::pipe().unwrap();
let fd = reader.as_raw_fd();
assert_eq!(unsafe { libc::fcntl(fd, libc::F_SETFD, 0) }, 0);
let (mut starter, mut lines) = spawn_starter(RUNS, |command| {
command.env(CONTROL_ENV, fd.to_string());
});
drop(reader);
let group = read_marker(&mut lines, "ENGINE ");
std::thread::sleep(Duration::from_millis(300));
assert!(
starter.try_wait().unwrap().is_none(),
"the session runs while its control is open"
);
assert!(live_members(group).is_some_and(|live| live >= 1));
drop(writer);
let status = reap_within(&mut starter, Duration::from_secs(15)).expect("the starter exits");
assert_eq!(status.signal(), Some(libc::SIGKILL));
assert_eq!(live_members(group), Some(0));
}
fn starter_killed() {
let (mut starter, mut lines) = spawn_starter(RUNS, |_| {});
let group = read_marker(&mut lines, "ENGINE ");
starter.kill().unwrap();
starter.wait().unwrap();
assert!(wait_gone(group), "the backstop ends the Engine's group");
}
fn starter_killed_early() {
let (mut starter, mut lines) = spawn_starter(RUNS, |command| {
command.env(PAUSE_ENV, "1");
});
let group = read_marker(&mut lines, "FORKED ");
starter.kill().unwrap();
starter.wait().unwrap();
assert!(
wait_gone(group),
"an Engine without a backstop was left running"
);
let mut rest = String::new();
std::io::Read::read_to_string(&mut lines, &mut rest).unwrap();
assert!(
!rest.contains("ENGINE "),
"the Engine's program ran: {rest}"
);
}
fn starter_parent_gone() {
let mut parent = helper("starter-parent");
parent
.env(SCRIPT_ENV, RUNS)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let mut parent = parent.spawn().unwrap();
let mut lines = BufReader::new(parent.stdout.take().unwrap());
let group = read_marker(&mut lines, "ENGINE ");
parent.kill().unwrap();
parent.wait().unwrap();
assert!(
wait_gone(group),
"a starter without its parent ends the group"
);
}
fn starter_own_group() {
let own = unsafe { libc::getpgrp() };
assert_eq!(
end_pinned_group(own, Duration::from_secs(1)),
GroupEnd::Refused
);
assert_eq!(
end_pinned_group(1, Duration::from_secs(1)),
GroupEnd::Refused
);
let (mut starter, _lines) = spawn_starter(RUNS, |command| {
command.env(SAME_GROUP_ENV, "1");
});
let status = reap_within(&mut starter, Duration::from_secs(15)).expect("the starter exits");
assert_eq!(status.code(), Some(super::EXIT_CONTAINMENT));
}
fn captured_group(stdout: &[u8]) -> libc::pid_t {
String::from_utf8_lossy(stdout)
.lines()
.next()
.and_then(|line| line.trim().parse().ok())
.expect("the captured command prints its pid first")
}
fn capture_ownership() {
use crate::core::process_capture::{spawn_capture, take_group_signal_log, wait_for_capture};
let capture = |script: &str, timeout: Duration| {
let mut command = Command::new("sh");
command.args(["-c", script]).stdin(Stdio::null());
let _ = take_group_signal_log();
let captured = wait_for_capture(spawn_capture(&mut command).unwrap(), timeout).unwrap();
(captured, take_group_signal_log())
};
let (captured, signals) = capture(
"echo $$; trap '' TERM; sleep 60 >/dev/null 2>&1 & exit 0",
Duration::from_secs(20),
);
assert!(!captured.timed_out);
assert!(captured.output.status.success());
assert!(!signals.is_empty(), "the leftover's group is signalled");
assert!(
signals.iter().all(|unreaped| *unreaped),
"a group was signalled after its leader was reaped: {signals:?}"
);
assert!(wait_gone(captured_group(&captured.output.stdout)));
let (captured, signals) = capture("echo $$; sleep 60 & sleep 60", Duration::from_millis(300));
assert!(captured.timed_out);
assert!(!signals.is_empty());
assert!(
signals.iter().all(|unreaped| *unreaped),
"a group was signalled after its leader was reaped: {signals:?}"
);
assert!(wait_gone(captured_group(&captured.output.stdout)));
}
pub fn temp_dir_text() -> String {
std::env::temp_dir()
.canonicalize()
.unwrap()
.to_str()
.unwrap()
.to_string()
}
pub fn pid_from_file(path: &std::path::Path) -> libc::pid_t {
let deadline = Instant::now() + Duration::from_secs(20);
loop {
if let Some(pid) = std::fs::read_to_string(path)
.ok()
.and_then(|text| text.trim().parse().ok())
{
return pid;
}
assert!(Instant::now() < deadline, "no pid in {}", path.display());
std::thread::sleep(Duration::from_millis(20));
}
}
fn capture_caller() {
use crate::core::process_capture::{spawn_capture, wait_for_capture};
let mut command = Command::new("sh");
command
.args(["-c", &std::env::var(SCRIPT_ENV).unwrap()])
.stdin(Stdio::null());
let _ = wait_for_capture(spawn_capture(&mut command).unwrap(), Duration::from_mins(1));
}
fn capture_caller_killed() {
let marker = tempfile::NamedTempFile::new().unwrap();
let mut caller = helper("capture-caller");
caller
.env(
SCRIPT_ENV,
format!("echo ran > {}; sleep 60", marker.path().display()),
)
.env(PAUSE_ENV, "1")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let mut caller = caller.spawn().unwrap();
let mut lines = BufReader::new(caller.stdout.take().unwrap());
let group = read_marker(&mut lines, "FORKED ");
caller.kill().unwrap();
caller.wait().unwrap();
assert!(
wait_gone(group),
"a command without a guard was left running"
);
assert!(
std::fs::read_to_string(marker.path()).unwrap().is_empty(),
"the command's program ran"
);
}
fn helper_timeout() {
let (signals, group, reaped) =
super::spawn_helper::kill_path_for_test(super::wire::SpawnSpec {
program: "sh".into(),
args: vec!["-c".into(), "sleep 60 & sleep 60".into()],
env: std::env::vars_os().collect(),
cwd: None,
});
assert_eq!(signals, [true], "one signal, while the leader is unreaped");
assert!(reaped, "the leader is reaped after the signal");
assert!(wait_gone(group));
}
fn costs() {
use super::spawner::{break_helper_for_test, kill_helper_for_test};
use crate::shell::process_tree::ShellChild;
let note = |name: &str, samples: &mut Vec<Duration>| {
samples.sort();
let total: Duration = samples.iter().sum();
println!(
"COST {name}: n={} min={:?} median={:?} mean={:?} max={:?}",
samples.len(),
samples[0],
samples[samples.len() / 2],
total / samples.len() as u32,
samples[samples.len() - 1]
);
};
let start_once = || {
let mut command = Command::new("true");
command.stdin(Stdio::null());
let begun = Instant::now();
let started = ShellChild::start(&mut command).unwrap();
let elapsed = begun.elapsed();
let mut child = started.child;
while child.try_wait().unwrap().is_none() {
std::thread::sleep(Duration::from_millis(1));
}
child.release();
elapsed
};
note(
"helper: first start (incl. helper start)",
&mut vec![start_once()],
);
note(
"helper: start, steady state",
&mut (0..50).map(|_| start_once()).collect(),
);
kill_helper_for_test();
break_helper_for_test(true);
note(
"direct: start",
&mut (0..50).map(|_| start_once()).collect(),
);
break_helper_for_test(false);
let mut attach = Vec::new();
let mut finish = Vec::new();
for _ in 0..20 {
let begun = Instant::now();
let (child, guard) = guarded_call("sleep 5", false);
attach.push(begun.elapsed());
let _ = wait_with_limits_captured(child, 1024, Duration::ZERO, true);
let begun = Instant::now();
assert_eq!(guard.finish(), GuardOutcome::Ended);
finish.push(begun.elapsed());
}
note(
"call: start behind the gate, guard included (test binary as guard)",
&mut attach,
);
note("call guard: finish", &mut finish);
let mut table = Vec::new();
for _ in 0..20 {
let begun = Instant::now();
let _ = live_members(1);
table.push(begun.elapsed());
}
note("one process-table read", &mut table);
let mut captures = Vec::new();
let mut plain = Vec::new();
for _ in 0..20 {
use crate::core::process_capture::{spawn_capture, wait_for_capture};
let mut command = Command::new("true");
command.stdin(Stdio::null());
let begun = Instant::now();
let _ = wait_for_capture(
spawn_capture(&mut command).unwrap(),
Duration::from_secs(20),
);
captures.push(begun.elapsed());
let begun = Instant::now();
let _ = Command::new("true").stdin(Stdio::null()).output();
plain.push(begun.elapsed());
}
note("bounded capture of `true` (with guard)", &mut captures);
note("plain output() of `true`", &mut plain);
let mut bare = Vec::new();
for _ in 0..20 {
let begun = Instant::now();
let _ = helper("fixture:none-such")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
bare.push(begun.elapsed());
}
note("start of the test binary itself (not lean-ctx)", &mut bare);
}
#[test]
#[ignore = "measurement, not a test: prints what containment costs per call"]
fn containment_costs_on_this_machine() {
print!("{}", run_fixture("costs"));
}
#[test]
fn a_helper_timeout_ends_the_session_and_signals_only_while_unreaped() {
run_fixture("helper-timeout");
}
#[test]
fn the_helper_notice_is_one_fixed_content_free_line() {
for notice in [
super::HELPER_NOTICE,
super::UNCLEAR_NOTICE,
super::RELEASE_NOTICE,
] {
assert!(!notice.chars().any(|c| c.is_ascii_digit()));
assert!(!notice.contains('\n'));
assert!(notice.starts_with("[lean-ctx: "));
}
}
#[test]
fn a_captured_command_never_runs_before_its_guard_is_ready() {
run_fixture("capture-caller-killed");
}
#[test]
fn a_capture_signals_its_group_only_while_the_leader_is_unreaped() {
run_fixture("capture-ownership");
}
#[test]
fn call_timeout_ends_the_call_tree_and_its_guard() {
run_fixture("call-timeout");
}
#[test]
fn a_killed_engine_leaves_the_guard_to_end_the_call() {
run_fixture("engine-killed");
}
#[test]
fn a_leader_that_exits_first_leaves_nothing_of_the_call() {
run_fixture("leader-first");
}
#[test]
fn the_group_kill_is_repeated_for_members_that_fork() {
run_fixture("mid-fork");
}
#[test]
fn a_call_without_a_guard_is_refused_and_never_runs() {
run_fixture("cannot-join");
}
#[test]
fn the_starter_ends_what_an_engine_leaves_behind() {
run_fixture("starter-engine-exits");
}
#[test]
fn the_starter_ends_the_group_when_its_control_closes() {
run_fixture("starter-control");
}
#[test]
fn a_killed_starter_leaves_the_backstop_to_end_the_group() {
run_fixture("starter-killed");
}
#[test]
fn an_engine_never_runs_before_its_backstop_is_ready() {
run_fixture("starter-killed-early");
}
#[test]
fn the_starter_ends_the_group_when_its_parent_is_gone() {
run_fixture("starter-parent-gone");
}
#[test]
fn the_starter_never_signals_a_group_it_belongs_to() {
run_fixture("starter-own-group");
}
#[test]
fn an_unconfirmed_end_has_one_fixed_content_free_notice() {
assert_eq!(super::end_notice(GuardOutcome::Ended), None);
for outcome in [GuardOutcome::Unconfirmed, GuardOutcome::Failed] {
assert_eq!(super::end_notice(outcome), Some(super::END_NOTICE));
}
assert!(!super::END_NOTICE.chars().any(|c| c.is_ascii_digit()));
assert!(!super::END_NOTICE.contains('\n'));
}
#[test]
fn live_member_count_ignores_zombies_and_other_groups() {
let table = " 100 S\n 100 Z\n 100 R+\n 1000 S\n 10 S\n 100 Zs\nnot a row\n";
assert_eq!(super::count_live(table, 100), 2);
assert_eq!(super::count_live(table, 7), 0);
}
#[test]
fn starter_arguments_are_parsed_strictly() {
let parse = |args: &[&str]| super::starter::parse(args.iter().map(std::ffi::OsString::from));
let options = parse(&[
"--ack",
"--watch-parent",
"--control-fd",
"5",
"--",
"prog",
"a",
"b",
])
.unwrap();
assert!(options.ack && options.watch_parent);
assert_eq!(options.control_fd, Some(5));
assert_eq!(options.program, "prog");
assert_eq!(options.args.len(), 2);
let refused: [&[&str]; 6] = [
&[],
&["prog"],
&["--"],
&["--control-fd", "2", "--", "prog"],
&["--control-fd", "x", "--", "prog"],
&["--unknown", "--", "prog"],
];
for args in refused {
assert!(parse(args).is_none(), "{args:?}");
}
}
#[test]
fn guard_arguments_are_parsed_strictly() {
let main = |args: &[&str]| super::guard::main(args.iter().map(std::ffi::OsString::from));
let refused: [&[&str]; 5] = [
&[],
&["--group"],
&["--group", "x"],
&["--group", "1"],
&["--group", "5", "extra"],
];
for args in refused {
assert_eq!(main(args), super::EXIT_USAGE, "{args:?}");
}
assert_eq!(main(&["--group", "2147483647"]), super::EXIT_CONTAINMENT);
}