use super::{
ChildExit, ChildFailure, ChildLimits, ChildRequest, FinishedChild, deadline_reached, exceeds,
next_pause, run_bounded_child, sleep_for,
};
use crate::test_support::{executable, fixture, remove};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
const ROOMY: ChildLimits = ChildLimits {
timeout: Duration::from_secs(60),
max_stdout: 1 << 20,
max_stderr: 1 << 20,
};
fn path_only() -> Vec<(OsString, OsString)> {
return vec![(OsString::from("PATH"), OsString::from("/usr/bin:/bin"))];
}
fn stand_in(root: &Path, name: &str, script: &str) -> PathBuf {
let path: PathBuf = root.join(name);
executable(path.as_path(), format!("#!/bin/sh\n{script}\n").as_bytes());
return path;
}
fn run(
root: &Path,
program: &Path,
input: &[u8],
environment: &[(OsString, OsString)],
limits: ChildLimits,
) -> Result<FinishedChild, ChildFailure> {
let arguments: Vec<OsString> = vec![OsString::from("first"), OsString::from("sec ond")];
return run_bounded_child(&ChildRequest {
executable: program,
arguments: arguments.as_slice(),
directory: root,
environment,
input,
scratch: root,
limits,
});
}
#[test]
fn a_child_reads_its_input_and_returns_both_outputs() {
let root: PathBuf = fixture("bounded-echo");
let program: PathBuf = stand_in(
root.as_path(),
"echo",
"cat\nprintf '%s|%s|%s|%s|%s' \"$1\" \"$2\" \"$ONLY\" \"${HOME-unset}\" \"$(pwd)\" >&2\nexit 3",
);
let input: Vec<u8> = (0..=255).cycle().take(70_000).collect();
let environment: Vec<(OsString, OsString)> = vec![
(OsString::from("ONLY"), OsString::from("value")),
(OsString::from("PATH"), OsString::from("/usr/bin:/bin")),
];
let finished: FinishedChild = run(
root.as_path(),
program.as_path(),
input.as_slice(),
environment.as_slice(),
ROOMY,
)
.expect("finished");
assert_eq!(finished.exit, ChildExit::Code(3));
assert_eq!(finished.stdout, input);
assert_eq!(
String::from_utf8(finished.stderr).expect("text"),
format!("first|sec ond|value|unset|{}", root.display())
);
remove(root.as_path());
}
#[test]
fn a_child_that_ignores_its_input_ends_quickly() {
let root: PathBuf = fixture("bounded-ignore");
let program: PathBuf = stand_in(root.as_path(), "ignore", "printf done");
let started: Instant = Instant::now();
let finished: FinishedChild = run(
root.as_path(),
program.as_path(),
vec![b'x'; 4 << 20].as_slice(),
path_only().as_slice(),
ROOMY,
)
.expect("finished");
assert_eq!(
(finished.exit, finished.stdout),
(ChildExit::Code(0), b"done".to_vec())
);
assert!(
started.elapsed() < Duration::from_secs(10),
"{:?}",
started.elapsed()
);
remove(root.as_path());
}
#[test]
fn a_child_ended_by_a_signal_reports_it() {
let root: PathBuf = fixture("bounded-signal");
let program: PathBuf = stand_in(root.as_path(), "signal", "kill -TERM $$");
let finished: FinishedChild = run(
root.as_path(),
program.as_path(),
b"",
path_only().as_slice(),
ROOMY,
)
.expect("finished");
assert_eq!(finished.exit, ChildExit::Signal(15));
remove(root.as_path());
}
fn running(pid: &str) -> bool {
return Path::new("/proc").join(pid).exists();
}
#[test]
fn a_child_past_its_bound_is_killed() {
let root: PathBuf = fixture("bounded-timeout");
let pid_file: PathBuf = root.join("pid");
let program: PathBuf = stand_in(
root.as_path(),
"sleeper",
format!("echo $$ > '{}'\nexec sleep 30", pid_file.display()).as_str(),
);
let started: Instant = Instant::now();
let failure: ChildFailure = run(
root.as_path(),
program.as_path(),
b"",
path_only().as_slice(),
ChildLimits {
timeout: Duration::from_millis(300),
..ROOMY
},
)
.expect_err("timed out");
assert_eq!(failure, ChildFailure::TimedOut(Duration::from_millis(300)));
assert!(started.elapsed() >= Duration::from_millis(300));
assert!(
started.elapsed() < Duration::from_secs(10),
"{:?}",
started.elapsed()
);
let pid: String = std::fs::read_to_string(&pid_file)
.expect("pid")
.trim()
.to_owned();
assert!(!running(pid.as_str()), "process {pid} is still running");
remove(root.as_path());
}
#[test]
fn output_past_a_cap_stops_the_child() {
let root: PathBuf = fixture("bounded-cap");
let flood: PathBuf = stand_in(root.as_path(), "flood", "exec head -c 50000000 /dev/zero");
let failure: ChildFailure = run(
root.as_path(),
flood.as_path(),
b"",
path_only().as_slice(),
ChildLimits {
max_stdout: 1000,
..ROOMY
},
)
.expect_err("too large");
assert_eq!(
failure,
ChildFailure::TooLarge {
stream: "standard output",
limit: 1000
}
);
let errors: PathBuf = stand_in(
root.as_path(),
"errors",
"exec head -c 50000000 /dev/zero >&2",
);
assert_eq!(
run(
root.as_path(),
errors.as_path(),
b"",
path_only().as_slice(),
ChildLimits {
max_stderr: 10,
..ROOMY
}
),
Err(ChildFailure::TooLarge {
stream: "standard error",
limit: 10
})
);
let exact: PathBuf = stand_in(root.as_path(), "exact", "printf 12345\nprintf abc >&2");
let at_cap: FinishedChild = run(
root.as_path(),
exact.as_path(),
b"",
path_only().as_slice(),
ChildLimits {
timeout: ROOMY.timeout,
max_stdout: 5,
max_stderr: 3,
},
)
.expect("at the cap");
assert_eq!(
(at_cap.stdout, at_cap.stderr),
(b"12345".to_vec(), b"abc".to_vec())
);
assert_eq!(
run(
root.as_path(),
exact.as_path(),
b"",
path_only().as_slice(),
ChildLimits {
timeout: ROOMY.timeout,
max_stdout: 4,
max_stderr: 3,
}
),
Err(ChildFailure::TooLarge {
stream: "standard output",
limit: 4
})
);
remove(root.as_path());
}
#[test]
fn a_child_that_cannot_start_reports_why() {
let root: PathBuf = fixture("bounded-start");
let missing: PathBuf = root.join("missing");
match run(
root.as_path(),
missing.as_path(),
b"",
path_only().as_slice(),
ROOMY,
) {
Err(ChildFailure::Start(reason)) => assert!(reason.contains("No such file"), "{reason}"),
other => panic!("{other:?}"),
}
let program: PathBuf = stand_in(root.as_path(), "fine", "exit 0");
let nowhere: PathBuf = root.join("no-scratch");
let arguments: Vec<OsString> = Vec::new();
match run_bounded_child(&ChildRequest {
executable: program.as_path(),
arguments: arguments.as_slice(),
directory: root.as_path(),
environment: &[],
input: b"",
scratch: nowhere.as_path(),
limits: ROOMY,
}) {
Err(ChildFailure::Files(reason)) => assert!(reason.contains("No such file"), "{reason}"),
other => panic!("{other:?}"),
}
remove(root.as_path());
}
#[test]
fn caps_and_bounds_are_inclusive_where_they_should_be() {
assert!(!exceeds(5, 5));
assert!(exceeds(6, 5));
assert!(!exceeds(0, 0));
assert!(exceeds(u64::MAX, usize::MAX - 1));
let five: Duration = Duration::from_millis(5);
assert!(deadline_reached(five, five));
assert!(!deadline_reached(Duration::from_millis(4), five));
assert!(deadline_reached(Duration::from_millis(6), five));
}
#[test]
fn pauses_double_and_never_pass_the_bound() {
let mut pauses: Vec<u128> = Vec::new();
let mut pause: Duration = Duration::from_millis(1);
for _ in 0..7 {
pauses.push(pause.as_millis());
pause = next_pause(pause);
}
assert_eq!(pauses, [1, 2, 4, 8, 16, 16, 16]);
assert_eq!(
sleep_for(Duration::from_millis(16), Duration::from_millis(3)),
Duration::from_millis(3)
);
assert_eq!(
sleep_for(Duration::from_millis(2), Duration::from_millis(3)),
Duration::from_millis(2)
);
}