use crate::run_graph::RunGraphOutcome;
use crate::run_task::{RunOutcome, RunState};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CancellationSignal {
Interrupt,
Terminate,
}
impl CancellationSignal {
#[must_use]
pub const fn posix_number(self) -> i32 {
match self {
Self::Interrupt => 2,
Self::Terminate => 15,
}
}
}
pub const EXIT_TASK_FAILURE: i32 = 1;
#[must_use]
pub fn exit_code_for(outcome: &RunGraphOutcome, signal: Option<CancellationSignal>) -> i32 {
if let Some(sig) = signal {
return 128 + sig.posix_number();
}
if is_failure(outcome) {
return EXIT_TASK_FAILURE;
}
0
}
fn is_failure(outcome: &RunGraphOutcome) -> bool {
if !outcome.task_errors.is_empty() {
return true;
}
if !outcome.invariant_violations.is_empty() {
return true;
}
outcome.outcomes.values().any(|o| match o {
RunOutcome::Completed(rec) => rec.state == RunState::Failed,
RunOutcome::Skipped(_) | RunOutcome::Cancelled(_) => false,
})
}
#[cfg(test)]
mod tests {
use std::collections::{BTreeMap, BTreeSet};
use std::str::FromStr;
use haz_domain::name::{ProjectName, TaskName};
use haz_domain::task_id::TaskId;
use crate::exit_code::{CancellationSignal, EXIT_TASK_FAILURE, exit_code_for};
use crate::run_graph::{RunGraphOutcome, RuntimeInvariantViolation};
use crate::run_task::{CompletedRecord, RunOutcome, RunSource, RunState};
fn tid(project: &str, task: &str) -> TaskId {
TaskId {
project: ProjectName::from_str(project).unwrap(),
task: TaskName::from_str(task).unwrap(),
}
}
fn completed(task: TaskId, state: RunState) -> RunOutcome {
RunOutcome::Completed(CompletedRecord {
task,
source: RunSource::FreshRun,
state,
exit_status: None,
stdout_hash: [0; 32],
stderr_hash: [0; 32],
materialised_outputs: Vec::new(),
})
}
fn empty_outcome() -> RunGraphOutcome {
RunGraphOutcome {
outcomes: BTreeMap::new(),
task_errors: BTreeMap::new(),
invariant_violations: Vec::new(),
}
}
fn outcome_with_completed(entries: Vec<(TaskId, RunState)>) -> RunGraphOutcome {
let mut out = empty_outcome();
for (task, state) in entries {
out.outcomes.insert(task.clone(), completed(task, state));
}
out
}
#[test]
fn exec_021_empty_outcome_returns_zero() {
assert_eq!(exit_code_for(&empty_outcome(), None), 0);
}
#[test]
fn exec_021_all_succeeded_returns_zero() {
let outcome = outcome_with_completed(vec![
(tid("p", "a"), RunState::Succeeded),
(tid("p", "b"), RunState::Succeeded),
]);
assert_eq!(exit_code_for(&outcome, None), 0);
}
#[test]
fn exec_021_failed_task_returns_task_failure_code() {
let outcome = outcome_with_completed(vec![
(tid("p", "a"), RunState::Succeeded),
(tid("p", "b"), RunState::Failed),
]);
assert_eq!(exit_code_for(&outcome, None), EXIT_TASK_FAILURE);
}
#[test]
fn exec_021_invariant_violation_alone_returns_task_failure_code() {
let mut outcome = outcome_with_completed(vec![
(tid("lib", "produce"), RunState::Succeeded),
(tid("app", "consume"), RunState::Succeeded),
]);
outcome
.invariant_violations
.push(RuntimeInvariantViolation::RuntimeCycle {
nodes: BTreeSet::from([tid("lib", "produce"), tid("app", "consume")]),
offending_edge: (tid("lib", "produce"), tid("app", "consume")),
});
assert_eq!(exit_code_for(&outcome, None), EXIT_TASK_FAILURE);
}
#[test]
fn exec_021_signal_interrupt_returns_130() {
let outcome = empty_outcome();
assert_eq!(
exit_code_for(&outcome, Some(CancellationSignal::Interrupt)),
130
);
}
#[test]
fn exec_021_signal_terminate_returns_143() {
let outcome = empty_outcome();
assert_eq!(
exit_code_for(&outcome, Some(CancellationSignal::Terminate)),
143
);
}
#[test]
fn exec_021_signal_wins_over_task_failure() {
let outcome = outcome_with_completed(vec![(tid("p", "a"), RunState::Failed)]);
assert_eq!(
exit_code_for(&outcome, Some(CancellationSignal::Interrupt)),
130,
);
assert_eq!(
exit_code_for(&outcome, Some(CancellationSignal::Terminate)),
143,
);
}
#[test]
fn exec_021_signal_wins_over_invariant_violation() {
let mut outcome = empty_outcome();
outcome
.invariant_violations
.push(RuntimeInvariantViolation::RuntimeCycle {
nodes: BTreeSet::from([tid("p", "a"), tid("p", "b")]),
offending_edge: (tid("p", "a"), tid("p", "b")),
});
assert_eq!(
exit_code_for(&outcome, Some(CancellationSignal::Interrupt)),
130,
);
}
#[test]
fn cancellation_signal_posix_number_matches_spec() {
assert_eq!(CancellationSignal::Interrupt.posix_number(), 2);
assert_eq!(CancellationSignal::Terminate.posix_number(), 15);
}
#[test]
fn exit_task_failure_constant_is_one() {
assert_eq!(EXIT_TASK_FAILURE, 1);
}
}