use std::{
fs,
future::pending,
path::PathBuf,
process::{self, Child, Command, Stdio},
thread,
time::Duration,
};
use super::{Clock, parse_start_time};
use crate::cpu::{self, Failure, Monitor};
fn sleeper() -> Child {
Command::new("sleep").arg("60").stdin(Stdio::null()).spawn().unwrap()
}
#[test]
fn start_time_is_field_22_even_when_the_command_name_has_spaces_and_parens() {
let stat = "4242 (a) b (c)) S 1 4242 4242 0 -1 4194560 100 0 0 0 7 3 0 0 20 0 1 0 987654 1000 200";
assert_eq!(parse_start_time(stat).unwrap(), 987_654);
}
#[test]
fn malformed_stat_is_rejected() {
assert_eq!(
parse_start_time("4242 no-parens S 1").unwrap_err().to_string(),
"malformed /proc stat: no command name"
);
assert_eq!(
parse_start_time("4242 (short) S 1 2").unwrap_err().to_string(),
"malformed /proc stat: no field 22"
);
}
#[test]
fn a_process_that_is_not_our_child_is_refused() {
let own = process::id();
assert_eq!(
Clock::for_child(own, own).err().unwrap().to_string(),
format!("/proc/{own} is not this process's child; /proc may belong to another PID namespace")
);
}
#[test]
fn live_child_samples_and_confirms() {
let mut child = sleeper();
let clock = Clock::for_child(child.id(), process::id()).unwrap();
assert!(clock.sample().is_ok());
assert!(clock.confirm().is_ok());
child.kill().unwrap();
child.wait().unwrap();
}
#[test]
fn a_different_start_time_fails_confirmation() {
let mut child = sleeper();
let mut clock = Clock::for_child(child.id(), process::id()).unwrap();
clock.start_time += 1;
assert!(clock.sample().is_ok(), "sampling alone does not check identity");
assert_eq!(clock.confirm().unwrap_err().to_string(), "child has already exited");
child.kill().unwrap();
child.wait().unwrap();
}
#[test]
fn a_reaped_child_fails_confirmation() {
let mut child = sleeper();
let clock = Clock::for_child(child.id(), process::id()).unwrap();
child.kill().unwrap();
child.wait().unwrap();
assert_eq!(clock.confirm().unwrap_err().to_string(), "child has already exited");
}
#[test]
fn an_unrelated_proc_read_failure_is_reported_as_itself() {
let mut child = sleeper();
let mut clock = Clock::for_child(child.id(), process::id()).unwrap();
clock.stat_path = PathBuf::from("/proc");
assert_eq!(clock.confirm().unwrap_err().to_string(), "Is a directory (os error 21)");
child.kill().unwrap();
child.wait().unwrap();
}
#[tokio::test]
async fn a_crossed_limit_is_confirmed_before_the_worker_is_failed() {
let mut child = sleeper();
let clock = Clock::for_child(child.id(), process::id()).unwrap();
let mut impostor = clock.clone();
impostor.start_time += 1;
let genuine = Monitor::new(clock, Duration::ZERO).unwrap();
let outcome = cpu::run(Some(&genuine), None, pending::<()>()).await;
assert!(matches!(outcome, Err(Failure::Limit { .. })));
let reused = Monitor::new(impostor, Duration::ZERO).unwrap();
let Err(Failure::Clock(error)) = cpu::run(Some(&reused), None, pending::<()>()).await else {
panic!("an unconfirmed sample over the limit must not fail the worker as a limit");
};
assert_eq!(error.to_string(), "child has already exited");
child.kill().unwrap();
child.wait().unwrap();
}
#[test]
fn the_final_charge_is_confirmed() {
let mut child = sleeper();
let mut clock = Clock::for_child(child.id(), process::id()).unwrap();
clock.start_time += 1;
let monitor = Monitor::new(clock, Duration::MAX).unwrap();
assert_eq!(monitor.elapsed().unwrap_err().to_string(), "child has already exited");
child.kill().unwrap();
child.wait().unwrap();
}
#[test]
fn an_unreaped_zombie_is_still_read() {
let mut child = sleeper();
let clock = Clock::for_child(child.id(), process::id()).unwrap();
child.kill().unwrap();
let stat = format!("/proc/{}/stat", child.id());
while !fs::read_to_string(&stat).unwrap().contains(") Z ") {
thread::yield_now();
}
assert!(clock.sample().is_ok());
assert!(clock.confirm().is_ok());
child.wait().unwrap();
}