use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExitClass {
Success,
Failure(i32),
KilledBySignal,
}
pub fn classify_exit(exit_code: Option<i32>) -> ExitClass {
match exit_code {
Some(0) => ExitClass::Success,
Some(code) => ExitClass::Failure(code),
None => ExitClass::KilledBySignal,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StageOutcome {
Passed,
Failed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MergeOutcome {
Merged,
Diverged,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RunEvidence {
pub cancelled: bool,
pub exit: ExitClass,
pub commit_count: u64,
pub tests: Option<StageOutcome>,
pub merge: Option<MergeOutcome>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Outcome {
Merged,
Failed,
NeedsReview,
Cancelled,
Orphaned,
}
impl Outcome {
pub fn as_str(self) -> &'static str {
match self {
Self::Merged => "merged",
Self::Failed => "failed",
Self::NeedsReview => "needs_review",
Self::Cancelled => "cancelled",
Self::Orphaned => "orphaned",
}
}
}
pub fn wants_tests(exit: ExitClass, commit_count: u64) -> bool {
exit == ExitClass::Success && commit_count > 0
}
pub fn wants_merge(exit: ExitClass, commit_count: u64, tests: Option<StageOutcome>) -> bool {
wants_tests(exit, commit_count) && tests != Some(StageOutcome::Failed)
}
pub fn derive_outcome(evidence: &RunEvidence) -> Outcome {
if evidence.cancelled {
return Outcome::Cancelled;
}
if evidence.merge == Some(MergeOutcome::Merged) {
return Outcome::Merged;
}
if evidence.commit_count == 0 {
return Outcome::Failed;
}
Outcome::NeedsReview
}
#[cfg(test)]
mod tests {
use super::*;
fn evidence() -> RunEvidence {
RunEvidence {
cancelled: false,
exit: ExitClass::Success,
commit_count: 0,
tests: None,
merge: None,
}
}
#[test]
fn exit_zero_without_commits_is_not_successful_work() {
let outcome = derive_outcome(&evidence());
assert_ne!(outcome, Outcome::Merged);
assert_ne!(outcome, Outcome::Cancelled);
}
#[test]
fn a_successful_merge_is_the_only_path_to_merged() {
let merged = derive_outcome(&RunEvidence {
commit_count: 2,
tests: Some(StageOutcome::Passed),
merge: Some(MergeOutcome::Merged),
..evidence()
});
assert_eq!(merged, Outcome::Merged);
let diverged = derive_outcome(&RunEvidence {
commit_count: 2,
tests: Some(StageOutcome::Passed),
merge: Some(MergeOutcome::Diverged),
..evidence()
});
assert_ne!(diverged, Outcome::Merged);
}
#[test]
fn failed_tests_never_reach_merged() {
let outcome = derive_outcome(&RunEvidence {
commit_count: 1,
tests: Some(StageOutcome::Failed),
..evidence()
});
assert_ne!(outcome, Outcome::Merged);
assert_ne!(outcome, Outcome::Cancelled);
}
#[test]
fn a_crashed_agent_with_commits_preserves_the_work_for_a_human() {
let outcome = derive_outcome(&RunEvidence {
exit: ExitClass::Failure(1),
commit_count: 3,
..evidence()
});
assert_eq!(outcome, Outcome::NeedsReview);
}
#[test]
fn cancellation_wins_over_every_other_reading() {
let outcome = derive_outcome(&RunEvidence {
cancelled: true,
exit: ExitClass::KilledBySignal,
commit_count: 5,
tests: Some(StageOutcome::Passed),
merge: Some(MergeOutcome::Merged),
});
assert_eq!(outcome, Outcome::Cancelled);
}
#[test]
fn exit_classification_reads_the_wait_status() {
assert_eq!(classify_exit(Some(0)), ExitClass::Success);
assert_eq!(classify_exit(Some(2)), ExitClass::Failure(2));
assert_eq!(classify_exit(None), ExitClass::KilledBySignal);
}
#[test]
fn test_and_merge_gates_follow_the_evidence() {
assert!(wants_tests(ExitClass::Success, 1));
assert!(!wants_tests(ExitClass::Success, 0));
assert!(!wants_tests(ExitClass::Failure(1), 4));
assert!(!wants_tests(ExitClass::KilledBySignal, 4));
assert!(wants_merge(
ExitClass::Success,
1,
Some(StageOutcome::Passed)
));
assert!(wants_merge(ExitClass::Success, 1, None));
assert!(!wants_merge(
ExitClass::Success,
1,
Some(StageOutcome::Failed)
));
assert!(!wants_merge(ExitClass::Failure(1), 1, None));
}
}