use super::*;
use crate::test_support::executable;
use crate::test_support::{fixture, remove};
const GENEROUS: Duration = Duration::from_secs(60);
const ROOMY: Duration = Duration::from_secs(3600);
fn limits(window: Duration) -> ToolLimits {
return ToolLimits { window, ceiling: ROOMY };
}
const BRIEF: Duration = Duration::from_millis(300);
const FAILING_STATUS: i32 = 7;
const SLOW_SECONDS: u64 = 30;
#[test]
fn finds_the_first_executable_in_search_order() {
let root: PathBuf = fixture("finds-first");
let first: PathBuf = root.join("first");
let second: PathBuf = root.join("second");
std::fs::create_dir_all(&first).expect("first");
std::fs::create_dir_all(&second).expect("second");
executable(first.join("tool").as_path(), b"#!/bin/sh\nexit 0\n");
executable(second.join("tool").as_path(), b"#!/bin/sh\nexit 0\n");
let search: OsString = std::env::join_paths([&first, &second]).expect("search path");
assert_eq!(find_program("tool", Some(search.as_os_str())), Some(first.join("tool")));
remove(&root);
}
#[test]
fn reports_missing_and_non_executable_programs_as_absent() {
let root: PathBuf = fixture("finds-absent");
std::fs::write(root.join("plain"), b"data").expect("plain file");
let search: OsString = root.clone().into_os_string();
assert_eq!(find_program("absent", Some(search.as_os_str())), None);
assert_eq!(find_program("plain", Some(search.as_os_str())), None);
assert_eq!(find_program("plain", None), None);
remove(&root);
}
#[test]
fn runs_in_the_directory_with_the_variables_and_reports_success() {
let root: PathBuf = fixture("runs-success");
let program: PathBuf = root.join("tool");
let record: PathBuf = root.join("record");
executable(
program.as_path(),
format!("#!/bin/sh\npwd -P > '{}'\nprintf '%s' \"$REPAIR_PROBE\" >> '{}'\n", record.display(), record.display()).as_bytes(),
);
let command: ToolCommand = ToolCommand { arguments: Vec::new(), directory: root.clone(), set: vec![(OsString::from("REPAIR_PROBE"), OsString::from("kept"))] };
let run: ToolRun = run_tool(program.as_path(), &command, limits(GENEROUS));
assert_eq!(run.end, ToolEnd::Succeeded);
let recorded: String = std::fs::read_to_string(&record).expect("record");
assert!(recorded.starts_with(root.to_str().expect("utf8 root")), "ran in {recorded}");
assert!(recorded.ends_with("kept"), "variable missing in {recorded}");
remove(&root);
}
#[test]
fn reports_a_failure_with_its_status_and_output() {
let root: PathBuf = fixture("runs-failure");
let program: PathBuf = root.join("tool");
executable(program.as_path(), format!("#!/bin/sh\necho 'reason printed on stderr' >&2\nexit {FAILING_STATUS}\n").as_bytes());
let command: ToolCommand = ToolCommand { arguments: Vec::new(), directory: root.clone(), set: Vec::new() };
let run: ToolRun = run_tool(program.as_path(), &command, limits(GENEROUS));
let ToolEnd::Failed(text) = run.end else { panic!("expected a failure, got {:?}", run.end) };
assert!(text.contains(FAILING_STATUS.to_string().as_str()), "status missing in {text}");
assert!(text.contains("reason printed on stderr"), "output missing in {text}");
remove(&root);
}
#[test]
fn stops_a_program_that_shows_no_sign_of_work_for_its_window() {
let root: PathBuf = fixture("runs-limit");
let program: PathBuf = root.join("tool");
executable(program.as_path(), format!("#!/bin/sh\nexec sleep {SLOW_SECONDS}\n").as_bytes());
let command: ToolCommand = ToolCommand { arguments: Vec::new(), directory: root.clone(), set: Vec::new() };
let run: ToolRun = run_tool(program.as_path(), &command, limits(BRIEF));
assert_eq!(run.end, ToolEnd::TimedOut);
assert!(run.millis < u128::from(SLOW_SECONDS) * 1000, "the run waited {} ms for a program stopped at its limit", run.millis);
remove(&root);
}
#[test]
fn reports_a_program_that_cannot_start() {
let root: PathBuf = fixture("runs-unstartable");
let command: ToolCommand = ToolCommand { arguments: Vec::new(), directory: root.clone(), set: Vec::new() };
let run: ToolRun = run_tool(root.join("absent").as_path(), &command, limits(GENEROUS));
assert!(matches!(run.end, ToolEnd::Failed(_)), "got {:?}", run.end);
remove(&root);
}
#[test]
fn a_failure_text_drops_wait_notices() {
let text: String = tail_of(b"Blocking waiting for file lock on package cache\nerror: the real reason\n");
assert_eq!(text, "error: the real reason");
}
const WORKING_WINDOW: Duration = Duration::from_millis(1200);
#[test]
fn a_program_that_keeps_working_is_never_stopped_however_long_it_runs() {
let root: PathBuf = fixture("runs-working");
let program: PathBuf = root.join("tool");
let rounds: u32 = 15;
executable(
program.as_path(),
format!("#!/bin/sh\nn=0\nwhile [ $n -lt {rounds} ]; do echo progress; sleep 0.2; n=$((n + 1)); done\n").as_bytes(),
);
let command: ToolCommand = ToolCommand { arguments: Vec::new(), directory: root.clone(), set: Vec::new() };
let run: ToolRun = run_tool(program.as_path(), &command, limits(WORKING_WINDOW));
assert_eq!(run.end, ToolEnd::Succeeded);
assert!(run.millis > WORKING_WINDOW.as_millis() * 2, "the fixture ran only {} ms", run.millis);
remove(&root);
}
const SHORT_CEILING: Duration = Duration::from_secs(2);
fn is_running(pid: i32) -> bool {
if unsafe { libc::kill(pid, 0) } != 0 {
return false;
}
return std::fs::read_to_string(format!("/proc/{pid}/stat")).map_or(true, |stat| { return !stat.rsplit(')').next().unwrap_or("").trim_start().starts_with('Z'); });
}
fn is_gone_soon(pid: i32) -> bool {
let waiting_since: Instant = Instant::now();
while is_running(pid) {
if waiting_since.elapsed() > Duration::from_secs(10) {
return false;
}
std::thread::sleep(Duration::from_millis(20));
}
return true;
}
#[test]
fn a_program_that_keeps_working_is_ended_at_its_ceiling_with_its_whole_process_group() {
let root: PathBuf = fixture("runs-ceiling");
let program: PathBuf = root.join("tool");
let printing_record: PathBuf = root.join("printing-helper-pid");
let silent_record: PathBuf = root.join("silent-helper-pid");
let rounds: u32 = 100;
executable(
program.as_path(),
format!(
"#!/bin/sh\n(n=0; while [ $n -lt {rounds} ]; do echo helper; sleep 0.2; n=$((n + 1)); done) &\necho $! > '{printing}'\n(sleep 60) >/dev/null 2>&1 &\necho $! > '{silent}'\nn=0\nwhile [ $n -lt {rounds} ]; do echo progress; sleep 0.2; n=$((n + 1)); done\n",
printing = printing_record.display(),
silent = silent_record.display()
)
.as_bytes(),
);
let command: ToolCommand = ToolCommand { arguments: Vec::new(), directory: root.clone(), set: Vec::new() };
let run: ToolRun = run_tool(program.as_path(), &command, ToolLimits { window: WORKING_WINDOW, ceiling: SHORT_CEILING });
assert_eq!(run.end, ToolEnd::CeilingReached(SHORT_CEILING), "ran {} ms", run.millis);
assert!(run.millis >= SHORT_CEILING.as_millis() && run.millis < u128::from(SLOW_SECONDS) * 1000 / 2, "the run lasted {} ms against a ceiling of {SHORT_CEILING:?}", run.millis);
for record in [&printing_record, &silent_record] {
let pid: i32 = std::fs::read_to_string(record).expect("the helper's number").trim().parse().expect("a process number");
assert!(is_gone_soon(pid), "the helper {pid} of the tool outlived the stop of its group");
}
remove(&root);
}
#[test]
fn a_helper_that_keeps_the_output_open_after_the_leader_exits_does_not_hold_the_run() {
let root: PathBuf = fixture("runs-helper-holds-output");
let program: PathBuf = root.join("tool");
let record: PathBuf = root.join("helper-pid");
executable(program.as_path(), format!("#!/bin/sh\nsleep 40 &\necho $! > '{}'\necho leader-output\n", record.display()).as_bytes());
let command: ToolCommand = ToolCommand { arguments: Vec::new(), directory: root.clone(), set: Vec::new() };
let (sender, receiver) = std::sync::mpsc::channel();
let started_in: PathBuf = program.clone();
std::thread::spawn(move || { let _sent = sender.send(run_tool(started_in.as_path(), &command, ToolLimits { window: GENEROUS, ceiling: GENEROUS })); });
let run: ToolRun = receiver.recv_timeout(Duration::from_secs(15)).expect("the run ended without waiting for the helper to close the output");
assert_eq!(run.end, ToolEnd::Succeeded);
assert!(String::from_utf8_lossy(run.stdout.as_slice()).contains("leader-output"));
let pid: i32 = std::fs::read_to_string(&record).expect("the helper's number").trim().parse().expect("a process number");
let _ended = unsafe { libc::kill(pid, libc::SIGKILL) };
remove(&root);
}
#[test]
fn a_helper_outside_the_process_group_that_holds_the_output_does_not_hold_the_ceiling() {
let root: PathBuf = fixture("runs-helper-outside-group");
let program: PathBuf = root.join("tool");
let record: PathBuf = root.join("helper-pid");
executable(
program.as_path(),
format!("#!/bin/sh\nsetsid sleep 40 &\necho $! > '{}'\nn=0\nwhile [ $n -lt 100 ]; do echo progress; sleep 0.2; n=$((n + 1)); done\n", record.display()).as_bytes(),
);
let command: ToolCommand = ToolCommand { arguments: Vec::new(), directory: root.clone(), set: Vec::new() };
let (sender, receiver) = std::sync::mpsc::channel();
let started_in: PathBuf = program.clone();
std::thread::spawn(move || { let _sent = sender.send(run_tool(started_in.as_path(), &command, ToolLimits { window: WORKING_WINDOW, ceiling: SHORT_CEILING })); });
let run: ToolRun = receiver.recv_timeout(Duration::from_secs(15)).expect("the ceiling ended the run without waiting for the helper");
assert_eq!(run.end, ToolEnd::CeilingReached(SHORT_CEILING));
let pid: i32 = std::fs::read_to_string(&record).expect("the helper's number").trim().parse().expect("a process number");
let _ended = unsafe { libc::kill(pid, libc::SIGKILL) };
remove(&root);
}
#[test]
fn a_program_that_finishes_before_its_ceiling_succeeds() {
let root: PathBuf = fixture("runs-under-ceiling");
let program: PathBuf = root.join("tool");
executable(program.as_path(), b"#!/bin/sh\necho done\n");
let command: ToolCommand = ToolCommand { arguments: Vec::new(), directory: root.clone(), set: Vec::new() };
let run: ToolRun = run_tool(program.as_path(), &command, ToolLimits { window: GENEROUS, ceiling: GENEROUS });
assert_eq!(run.end, ToolEnd::Succeeded);
remove(&root);
}