use crate::daemon::{log, Coordinator};
use crate::job::{self, JobState, Usage};
use crate::paths;
use crate::sys;
use anyhow::{Context, Result};
use std::os::unix::process::CommandExt;
use std::sync::Arc;
use std::time::Duration;
pub fn spawn(id: uuid::Uuid) -> Result<i32> {
let exe = paths::program_path()?;
let dir = paths::job_dir(&id)?;
let log_path = dir.join("supervisor.log");
use std::os::unix::fs::OpenOptionsExt;
let log_file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.mode(0o600)
.open(&log_path)
.with_context(|| format!("opening {}", log_path.display()))?;
let log_err = log_file
.try_clone()
.context("copying the log file handle")?;
let mut cmd = std::process::Command::new(exe);
cmd.arg("supervise")
.arg(id.to_string())
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::from(log_file))
.stderr(std::process::Stdio::from(log_err))
.current_dir("/");
unsafe {
cmd.pre_exec(|| {
if libc::setsid() == -1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
let child = cmd.spawn().context("starting the supervisor")?;
Ok(child.id() as i32)
}
pub fn record_supervisor_pid(id: &uuid::Uuid, pid: i32) {
let Ok(dir) = paths::job_dir(id) else { return };
crate::job::write_atomic(
&dir.join("supervisor.pid"),
pid.to_string().as_bytes(),
0o600,
)
.ok();
}
pub fn supervisor_pid_of(dir: &std::path::Path) -> Option<i32> {
std::fs::read_to_string(dir.join("supervisor.pid"))
.ok()?
.trim()
.parse()
.ok()
}
pub fn reap(coord: Arc<Coordinator>, id: uuid::Uuid, pid: i32) {
let mut wait_status: libc::c_int = 0;
let rc = unsafe { libc::waitpid(pid, &mut wait_status, 0) };
if rc < 0 {
let e = std::io::Error::last_os_error();
if e.raw_os_error() == Some(libc::ECHILD) {
watch_until_gone(pid);
} else {
log(&format!(
"qex could not wait for the supervisor {pid} of the job {id}: {e}"
));
}
}
let dir = match paths::job_dir(&id) {
Ok(d) => d,
Err(_) => return,
};
let mut state = coord.state.lock().unwrap();
if let Some(job) = state.jobs.get_mut(&id) {
job.supervisor_pid = None;
match job::read_status(&dir) {
Ok(status) if status.state.is_terminal() => {
job.status = status;
}
other => {
let job_pid = other.ok().and_then(|s| s.pid).or(job.status.pid);
let mut note = "the supervisor stopped without a result".to_string();
if let Some(text) = supervisor_log_tail(&dir) {
note.push_str(&format!(". The supervisor said: {text}"));
}
if let Some(pid) = job_pid {
if sys::pid_alive(pid) {
log(&format!(
"the supervisor of the job {id} stopped, and the job {pid} \
continues; qex stops the job now"
));
stop_process_group(pid);
note.push_str("; qex stopped the job process");
}
}
job.status.state = JobState::Failed;
job.status.finished_at = Some(sys::now_secs());
job.status.error = Some(note);
job.status.blocked_reason = None;
let status = job.status.clone();
job::write_status(&dir, &status).ok();
log(&format!("the supervisor of the job {id} left no result"));
}
}
}
drop(state);
coord.notify();
}
fn watch_until_gone(pid: i32) {
while sys::pid_alive(pid) {
std::thread::sleep(Duration::from_millis(500));
}
}
fn stop_process_group(pid: i32) {
unsafe {
libc::killpg(pid, libc::SIGTERM);
}
for _ in 0..20 {
std::thread::sleep(Duration::from_millis(100));
if !sys::pid_alive(pid) {
return;
}
}
unsafe {
libc::killpg(pid, libc::SIGKILL);
}
}
pub fn main(id: uuid::Uuid) -> Result<i32> {
let dir = paths::job_dir(&id)?;
let spec = job::read_spec(&dir).context("reading the job specification")?;
let mut status = job::read_status(&dir).context("reading the job status")?;
status.supervisor_pid = Some(std::process::id() as i32);
job::write_status(&dir, &status).context("writing the job status")?;
let earlier_drops = status.logs_dropped.unwrap_or_default();
let mut config_fault: Option<String> = None;
let cfg = match crate::config::Config::load_short() {
Ok(cfg) => cfg,
Err(e) => {
let message = format!(
"qex could not read the configuration ({e}). This job uses the default values, \
SO NO LIMIT OPERATES. Correct the file, and start the job again with \
`qex rerun {id}`. Run `qex config show` for the complete message."
);
log(&message);
eprintln!("{message}");
config_fault = Some(message);
crate::config::Config::default()
}
};
let log_limit = match cfg.log_max_bytes() {
Ok(limit) => limit,
Err(e) => {
let message = format!("{e}. This job uses the default limit.");
log(&message);
eprintln!("qex: {message}");
crate::config::Config::default()
.log_max_bytes()
.ok()
.flatten()
}
};
let again = status.attempts > 0;
let out_path = dir.join("stdout.log");
let err_path = dir.join("stderr.log");
let stdout =
create_private(&out_path, again).context("opening the standard output file of the job")?;
let stderr =
create_private(&err_path, again).context("opening the standard error file of the job")?;
if again {
use std::io::Write;
let mark = format!("\n--- attempt {} ---\n", status.attempts + 1);
(&stdout).write_all(mark.as_bytes()).ok();
(&stderr).write_all(mark.as_bytes()).ok();
}
let out_len = std::fs::metadata(&out_path).map(|m| m.len()).unwrap_or(0);
let err_len = std::fs::metadata(&err_path).map(|m| m.len()).unwrap_or(0);
let out_cap = crate::logcap::CapWriter::new(&out_path, stdout, out_len, log_limit);
let err_cap = crate::logcap::CapWriter::new(&err_path, stderr, err_len, log_limit);
let mut cmd = std::process::Command::new(&spec.command[0]);
cmd.args(&spec.command[1..])
.current_dir(&spec.cwd)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.env_clear()
.envs(&spec.env);
let mut cgroup_dir: Option<std::path::PathBuf> = None;
let mut enforce_warning: Option<String> = None;
if cfg.enforce.mode.is_on() {
match crate::enforce::create_job_cgroup(&cfg, &id, spec.mem) {
Ok(cgroup) => match crate::enforce::add_process(&cgroup, std::process::id() as i32) {
Ok(()) => {
crate::enforce::record_cgroup_path(&dir, &cgroup);
cgroup_dir = Some(cgroup);
}
Err(e) => {
enforce_warning = Some(e);
crate::enforce::remove_cgroup(&cgroup);
}
},
Err(e) => {
enforce_warning = Some(e);
}
}
}
if let Some(warning) = &enforce_warning {
eprintln!("qex: the memory limit is not active for this job: {warning}");
status.error = Some(format!(
"the memory limit is not active for this job: {warning}"
));
}
if let Some(fault) = &config_fault {
status.error = Some(fault.clone());
}
let _ = &cgroup_dir;
let politeness = match cfg.politeness_values() {
Ok(()) => cfg.politeness.clone(),
Err(e) => {
let message = format!(
"{e} This job uses the default politeness values, so it gives way as a job \
of qex did before."
);
log(&message);
add_fault(&mut status.error, message);
crate::config::PolitenessConfig::default()
}
};
let nice = spec.nice.unwrap_or(politeness.nice);
let io_class = politeness.io.clone();
let oom_adj = politeness.oom_score_adj;
unsafe {
cmd.pre_exec(move || {
if libc::setpgid(0, 0) == -1 {
return Err(std::io::Error::last_os_error());
}
apply_politeness(nice, &io_class, oom_adj);
Ok(())
});
}
let mut child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
let message = format!(
"qex could not start `{}`: {e}. Test the program name and the PATH value.",
spec.command[0]
);
eprintln!("{message}");
status.state = JobState::Failed;
status.finished_at = Some(sys::now_secs());
status.error = Some(message);
status.blocked_reason = None;
job::write_status(&dir, &status)?;
return Ok(1);
}
};
let (tx, rx) = std::sync::mpsc::channel::<(bool, crate::logcap::Report)>();
let mut copies = 0;
if let Some(pipe) = child.stdout.take() {
let done = tx.clone();
let eof = tx.clone();
copies += 1;
std::thread::spawn(move || {
let dropped = crate::logcap::pump(pipe, out_cap, || {
eof.send((false, crate::logcap::Report::Eof)).ok();
});
done.send((false, crate::logcap::Report::Done(dropped)))
.ok();
});
}
if let Some(pipe) = child.stderr.take() {
let done = tx.clone();
let eof = tx.clone();
copies += 1;
std::thread::spawn(move || {
let dropped = crate::logcap::pump(pipe, err_cap, || {
eof.send((true, crate::logcap::Report::Eof)).ok();
});
done.send((true, crate::logcap::Report::Done(dropped))).ok();
});
}
drop(tx);
let watch_cgroup = cgroup_dir.clone().or_else(crate::enforce::own_cgroup);
let oom_before = watch_cgroup
.as_ref()
.map(|c| crate::enforce::oom_count(c))
.unwrap_or(0);
let pid = child.id() as i32;
status.state = JobState::Running;
status.pid = Some(pid);
status.supervisor_pid = Some(std::process::id() as i32);
status.started_at = Some(sys::now_secs());
status.attempts += 1;
job::write_status(&dir, &status)?;
let outcome = Arc::new(std::sync::atomic::AtomicU8::new(RACE_OPEN));
if let Some(limit) = spec.timeout {
let outcome = Arc::clone(&outcome);
std::thread::spawn(move || {
std::thread::sleep(Duration::from_secs(limit));
if outcome
.compare_exchange(
RACE_OPEN,
RACE_TIMER,
std::sync::atomic::Ordering::SeqCst,
std::sync::atomic::Ordering::SeqCst,
)
.is_err()
{
return;
}
unsafe {
libc::killpg(pid, libc::SIGTERM);
}
std::thread::sleep(Duration::from_secs(10));
unsafe {
libc::killpg(pid, libc::SIGKILL);
}
});
}
let reserved = match wait_without_reaping(pid) {
Ok(()) => true,
Err(e) => {
log(&format!(
"the wait for the job {id} (pid {pid}) failed: {e}. qex sends no signal to that \
process group, because the machine can give that pid to another process."
));
false
}
};
let _ = outcome.compare_exchange(
RACE_OPEN,
RACE_JOB,
std::sync::atomic::Ordering::SeqCst,
std::sync::atomic::Ordering::SeqCst,
);
if reserved {
unsafe {
libc::killpg(pid, libc::SIGKILL);
}
}
if let Some(cgroup) = crate::enforce::job_cgroup_path(&dir) {
if crate::enforce::cgroup_had_oom(&cgroup) {
crate::enforce::mark_oom(&dir);
}
crate::enforce::kill_cgroup(&cgroup);
}
if let Some(cgroup) = &watch_cgroup {
if crate::enforce::oom_count(cgroup) > oom_before {
crate::enforce::mark_oom(&dir);
}
}
let exit = child.wait().context("waiting for the job")?;
let mut drops = crate::job::LogsDropped {
limit: log_limit.unwrap_or(0),
..earlier_drops
};
let incomplete = drain_copies(
&rx,
copies,
&mut drops,
std::time::Instant::now() + EOF_LIMIT,
COPY_LIMIT,
);
if incomplete {
log(&format!(
"the output of the job {id} did not close; qex writes the result now, and the \
last part of a log file can be missing"
));
for log_file in [&out_path, &err_path] {
std::fs::remove_file(crate::logcap::tail_path(log_file)).ok();
}
let note = "the output of this job did not close, so a log file can be missing its \
last part. A process of the job kept the pipe open. Read the log file, \
and start the job again if you need the full output.";
add_fault(&mut status.error, note.to_string());
drops.incomplete = true;
}
let usage = read_usage();
if let Some(cgroup) = crate::enforce::job_cgroup_path(&dir) {
crate::enforce::leave_cgroup(&cgroup);
crate::enforce::remove_cgroup(&cgroup);
}
let signal = exit_signal(&exit);
let code = exit.code();
let timed_out = outcome.load(std::sync::atomic::Ordering::SeqCst) == RACE_TIMER;
status.state = classify(&spec, code, signal, timed_out, &dir);
status.exit_code = code;
status.signal = signal;
status.finished_at = Some(sys::now_secs());
status.usage = usage;
status.logs_dropped = drops.any().then_some(drops);
status.pid = None;
status.last_pid = Some(pid);
let retrying = status.state == JobState::Failed && status.retries_left > 0;
if retrying {
status.retries_left -= 1;
status.state = JobState::Queued;
status.error = Some(format!(
"attempt {} failed with the exit code {}; qex starts the job again",
status.attempts,
code.unwrap_or(-1)
));
status.finished_at = None;
}
job::write_status(&dir, &status)?;
if retrying {
log(&format!(
"job {id} failed and starts again; {} attempt(s) left",
status.retries_left
));
std::thread::sleep(Duration::from_secs(1));
return main(id);
}
crate::usage::record(&spec, &status);
Ok(code.unwrap_or(0))
}
const EOF_LIMIT: Duration = Duration::from_secs(30);
const COPY_LIMIT: Duration = Duration::from_secs(600);
fn drain_copies(
rx: &std::sync::mpsc::Receiver<(bool, crate::logcap::Report)>,
copies: usize,
drops: &mut crate::job::LogsDropped,
eof_limit: std::time::Instant,
copy_limit: Duration,
) -> bool {
let mut open = copies;
let mut waiting_for_eof = copies;
while open > 0 {
let wait = if waiting_for_eof > 0 {
eof_limit.saturating_duration_since(std::time::Instant::now())
} else {
copy_limit
};
match rx.recv_timeout(wait) {
Ok((_, crate::logcap::Report::Eof)) => waiting_for_eof -= 1,
Ok((is_err, crate::logcap::Report::Done(d))) => {
open -= 1;
if is_err {
drops.stderr_bytes += d.bytes;
drops.stderr_lines += d.lines;
} else {
drops.stdout_bytes += d.bytes;
drops.stdout_lines += d.lines;
}
}
Err(_) => return true,
}
}
false
}
fn create_private(path: &std::path::Path, append: bool) -> std::io::Result<std::fs::File> {
use std::os::unix::fs::OpenOptionsExt;
std::fs::OpenOptions::new()
.write(true)
.read(true)
.create(true)
.truncate(!append)
.append(append)
.mode(0o600)
.open(path)
}
const RACE_OPEN: u8 = 0;
const RACE_JOB: u8 = 1;
const RACE_TIMER: u8 = 2;
fn add_fault(error: &mut Option<String>, message: String) {
match error {
Some(already) => {
already.push_str("; ");
already.push_str(&message);
}
None => *error = Some(message),
}
}
fn apply_politeness(nice: i32, io_class: &str, oom_score_adj: i32) {
unsafe {
libc::setpriority(libc::PRIO_PROCESS, 0, nice);
}
#[cfg(target_os = "linux")]
{
const IOPRIO_WHO_PROCESS: libc::c_int = 1;
const IOPRIO_CLASS_SHIFT: libc::c_int = 13;
const CLASS_BEST_EFFORT: libc::c_int = 2;
const CLASS_IDLE: libc::c_int = 3;
let value = match io_class {
"best-effort" => Some((CLASS_BEST_EFFORT << IOPRIO_CLASS_SHIFT) | 4),
"idle" => Some(CLASS_IDLE << IOPRIO_CLASS_SHIFT),
_ => None,
};
if let Some(value) = value {
unsafe {
libc::syscall(libc::SYS_ioprio_set, IOPRIO_WHO_PROCESS, 0, value);
}
}
if oom_score_adj != 0 {
write_oom_score(oom_score_adj);
}
}
#[cfg(not(target_os = "linux"))]
{
let _ = (io_class, oom_score_adj);
}
}
#[cfg(target_os = "linux")]
fn write_oom_score(value: i32) {
let mut out = [0u8; OOM_TEXT];
let len = write_i32(value, &mut out);
unsafe {
let path = c"/proc/self/oom_score_adj";
let fd = libc::open(path.as_ptr(), libc::O_WRONLY);
if fd >= 0 {
libc::write(fd, out.as_ptr() as *const libc::c_void, len);
libc::close(fd);
}
}
}
#[cfg(target_os = "linux")]
const OOM_TEXT: usize = 11;
#[cfg(target_os = "linux")]
fn write_i32(value: i32, out: &mut [u8; OOM_TEXT]) -> usize {
let mut digits = [0u8; 10];
let mut n = 0;
let mut v = value.unsigned_abs();
loop {
digits[n] = b'0' + (v % 10) as u8;
v /= 10;
n += 1;
if v == 0 {
break;
}
}
let mut len = 0;
if value < 0 {
out[0] = b'-';
len = 1;
}
for i in (0..n).rev() {
out[len] = digits[i];
len += 1;
}
len
}
fn supervisor_log_tail(dir: &std::path::Path) -> Option<String> {
const KEEP: usize = 3;
const LIMIT: usize = 400;
let raw = std::fs::read(dir.join("supervisor.log")).ok()?;
let text = String::from_utf8_lossy(&raw);
let lines: Vec<&str> = text
.lines()
.map(|l| l.trim())
.filter(|l| !l.is_empty())
.collect();
if lines.is_empty() {
return None;
}
let start = lines.len().saturating_sub(KEEP);
let mut joined = lines[start..].join(" / ");
if joined.chars().count() > LIMIT {
joined = joined.chars().take(LIMIT).collect::<String>() + "...";
}
Some(joined)
}
fn wait_without_reaping(pid: i32) -> std::io::Result<()> {
let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
loop {
let result = unsafe {
libc::waitid(
libc::P_PID,
pid as libc::id_t,
&mut info,
libc::WEXITED | libc::WNOWAIT,
)
};
if result == 0 {
return Ok(());
}
let e = std::io::Error::last_os_error();
if e.kind() == std::io::ErrorKind::Interrupted {
continue;
}
return Err(e);
}
}
fn classify(
_spec: &crate::spec::JobSpec,
code: Option<i32>,
signal: Option<i32>,
timed_out: bool,
dir: &std::path::Path,
) -> JobState {
if code == Some(0) {
return JobState::Completed;
}
if timed_out {
return JobState::Timeout;
}
if signal == Some(libc::SIGKILL) && crate::enforce::was_oom_killed(dir) {
return JobState::Oom;
}
match (code, signal) {
(Some(0), _) => JobState::Completed,
(Some(_), _) => JobState::Failed,
(None, Some(libc::SIGTERM)) | (None, Some(libc::SIGKILL)) => JobState::Killed,
(None, Some(_)) => JobState::Failed,
(None, None) => JobState::Failed,
}
}
fn exit_signal(exit: &std::process::ExitStatus) -> Option<i32> {
use std::os::unix::process::ExitStatusExt;
exit.signal()
}
fn read_usage() -> Usage {
let mut ru: libc::rusage = unsafe { std::mem::zeroed() };
let rc = unsafe { libc::getrusage(libc::RUSAGE_CHILDREN, &mut ru) };
if rc != 0 {
return Usage::default();
}
#[cfg(target_os = "linux")]
let max_rss = (ru.ru_maxrss as u64).saturating_mul(1024);
#[cfg(not(target_os = "linux"))]
let max_rss = ru.ru_maxrss as u64;
let cpu_secs = ru.ru_utime.tv_sec as f64
+ ru.ru_utime.tv_usec as f64 / 1e6
+ ru.ru_stime.tv_sec as f64
+ ru.ru_stime.tv_usec as f64 / 1e6;
Usage { max_rss, cpu_secs }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spec::JobSpec;
#[test]
fn a_job_with_two_faults_keeps_both_of_them() {
let mut error = None;
add_fault(&mut error, "the memory limit is not active.".into());
assert_eq!(error.as_deref(), Some("the memory limit is not active."));
add_fault(&mut error, "the politeness values have a fault.".into());
let both = error.unwrap();
assert!(
both.contains("memory limit") && both.contains("politeness"),
"the record must keep both faults, and it said: {both}"
);
}
#[test]
#[cfg(target_os = "linux")]
fn the_oom_score_text_fits_the_buffer_for_every_number() {
for value in [
i32::MIN,
i32::MIN + 1,
-100000,
-1000,
-1,
0,
1,
9,
10,
500,
1000,
999999,
i32::MAX,
] {
let mut out = [0u8; OOM_TEXT];
let len = write_i32(value, &mut out);
assert_eq!(
std::str::from_utf8(&out[..len]).unwrap(),
value.to_string(),
"the text of {value} is wrong"
);
}
}
#[test]
fn the_last_words_of_the_supervisor_reach_the_record() {
let dir = std::env::temp_dir().join(format!("qex-tail-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
assert_eq!(supervisor_log_tail(&dir), None);
std::fs::write(dir.join("supervisor.log"), b"\n \n").unwrap();
assert_eq!(supervisor_log_tail(&dir), None);
std::fs::write(
dir.join("supervisor.log"),
b"one\ntwo\nthree\nfour\nError: renaming status.json into place\n",
)
.unwrap();
let tail = supervisor_log_tail(&dir).unwrap();
assert!(tail.contains("renaming status.json"), "got: {tail}");
assert!(!tail.contains("one"), "the oldest lines must go: {tail}");
std::fs::write(dir.join("supervisor.log"), "x".repeat(5000).as_bytes()).unwrap();
let tail = supervisor_log_tail(&dir).unwrap();
assert!(tail.chars().count() <= 405, "the text must have a limit");
assert!(!tail.contains('\n'), "the text must be one line");
std::fs::write(dir.join("supervisor.log"), b"bad \xff\xfe byte").unwrap();
assert!(supervisor_log_tail(&dir).unwrap().contains("bad"));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_copy_that_completes_gives_a_complete_record() {
use crate::logcap::{Dropped, Report};
let (tx, rx) = std::sync::mpsc::channel();
tx.send((false, Report::Eof)).unwrap();
tx.send((
false,
Report::Done(Dropped {
bytes: 4096,
lines: 20,
}),
))
.unwrap();
tx.send((true, Report::Eof)).unwrap();
tx.send((
true,
Report::Done(Dropped {
bytes: 16,
lines: 1,
}),
))
.unwrap();
let mut drops = crate::job::LogsDropped::default();
let incomplete = drain_copies(
&rx,
2,
&mut drops,
std::time::Instant::now() + Duration::from_secs(30),
Duration::from_secs(600),
);
assert!(!incomplete, "each copy reported, so the record is complete");
assert_eq!(drops.stdout_bytes, 4096);
assert_eq!(drops.stdout_lines, 20);
assert_eq!(drops.stderr_bytes, 16);
assert_eq!(drops.stderr_lines, 1);
}
#[test]
fn an_output_that_never_closes_stops_the_wait_and_keeps_what_arrived() {
use crate::logcap::{Dropped, Report};
let (tx, rx) = std::sync::mpsc::channel();
tx.send((false, Report::Eof)).unwrap();
tx.send((
false,
Report::Done(Dropped {
bytes: 1024,
lines: 8,
}),
))
.unwrap();
let mut drops = crate::job::LogsDropped::default();
let start = std::time::Instant::now();
let incomplete = drain_copies(
&rx,
2,
&mut drops,
start + Duration::from_millis(50),
Duration::from_secs(10),
);
let took = start.elapsed();
assert!(
incomplete,
"the record must say that a log file is not complete"
);
assert!(
took < Duration::from_secs(5),
"the wait took {took:?}; the limit did not operate"
);
assert_eq!(drops.stdout_bytes, 1024, "a count that arrived must stay");
assert_eq!(drops.stdout_lines, 8, "a count that arrived must stay");
drop(tx);
}
#[test]
fn a_copy_that_reached_the_end_of_the_output_is_not_cut_short() {
use crate::logcap::{Dropped, Report};
let (tx, rx) = std::sync::mpsc::channel();
tx.send((false, Report::Eof)).unwrap();
std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(120));
tx.send((
false,
Report::Done(Dropped {
bytes: 77,
lines: 3,
}),
))
.ok();
});
let mut drops = crate::job::LogsDropped::default();
let incomplete = drain_copies(
&rx,
1,
&mut drops,
std::time::Instant::now(),
Duration::from_secs(600),
);
assert!(
!incomplete,
"the copy reached the end of the output, so the long limit applies to it"
);
assert_eq!(drops.stdout_bytes, 77);
assert_eq!(drops.stdout_lines, 3);
}
fn spec() -> JobSpec {
JobSpec {
id: uuid::Uuid::new_v4(),
name: "t".into(),
cwd: "/".into(),
command: vec!["true".into()],
env: Default::default(),
cpu: 1,
mem: 1 << 30,
timeout: None,
tags: vec![],
priority: 0,
env_capture: crate::config::EnvCapture::None,
claim_source: "explicit".into(),
group: None,
group_name: None,
locks: vec![],
retries: 0,
nice: None,
needs: vec![],
after: vec![],
submitted_at: 0,
dedupe_key: None,
dedupe_window: 0,
}
}
#[test]
fn the_exit_code_gives_the_final_state() {
let dir = std::path::Path::new("/nonexistent");
assert_eq!(
classify(&spec(), Some(0), None, false, dir),
JobState::Completed
);
assert_eq!(
classify(&spec(), Some(1), None, false, dir),
JobState::Failed
);
assert_eq!(
classify(&spec(), Some(127), None, false, dir),
JobState::Failed
);
}
#[test]
fn a_signal_gives_the_state_killed() {
let dir = std::path::Path::new("/nonexistent");
assert_eq!(
classify(&spec(), None, Some(libc::SIGTERM), false, dir),
JobState::Killed
);
assert_eq!(
classify(&spec(), None, Some(libc::SIGKILL), false, dir),
JobState::Killed
);
}
#[test]
fn a_time_limit_gives_the_state_timeout() {
let dir = std::path::Path::new("/nonexistent");
assert_eq!(
classify(&spec(), None, Some(libc::SIGTERM), true, dir),
JobState::Timeout
);
}
#[test]
fn a_fault_signal_gives_the_state_failed() {
let dir = std::path::Path::new("/nonexistent");
assert_eq!(
classify(&spec(), None, Some(libc::SIGSEGV), false, dir),
JobState::Failed
);
}
#[test]
fn the_use_measurement_gives_a_value_after_a_child_stops() {
std::process::Command::new("sh")
.args(["-c", "head -c 4000000 /dev/zero > /dev/null"])
.status()
.expect("the test could not start a child process");
let usage = read_usage();
assert!(usage.max_rss > 0, "the memory measurement gave zero");
assert!(
usage.max_rss > 64 * 1024 && usage.max_rss < 8 * (1 << 30),
"the memory measurement {} is not plausible; test the unit",
crate::units::format_size(usage.max_rss)
);
assert!(usage.cpu_secs >= 0.0);
}
}