use argonauts_core::events::task::TaskState;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Trigger {
DependenciesMerged,
Start,
LimitNear,
RaiseLimit,
Question,
Answered,
NeedsDecision,
Decided,
AskYou,
TakeOver,
GiveBack,
SaidDone,
KeptWork,
ReportedDone,
Broke,
FirstCheckFailure,
ChecksFailedAgain,
ChecksPassed,
GoalNotMet,
ChangesRequested,
ChangesRequestedAgain,
Approved,
AffectedTestsFailed,
Merged,
SentBack,
Retry,
NewWorker,
Defer,
Split,
Drop,
}
use TaskState::*;
pub const TABLE: &[(TaskState, Trigger, TaskState)] = &[
(Waiting, Trigger::DependenciesMerged, Ready),
(Ready, Trigger::Start, Running),
(Ready, Trigger::LimitNear, Deferred),
(Deferred, Trigger::RaiseLimit, Ready),
(Running, Trigger::Question, Blocked),
(Blocked, Trigger::Answered, Running),
(Running, Trigger::NeedsDecision, NeedsDecision),
(NeedsDecision, Trigger::Decided, Ready),
(NeedsDecision, Trigger::AskYou, NeedsYou),
(NeedsDecision, Trigger::TakeOver, TakenOver),
(Running, Trigger::TakeOver, TakenOver),
(TakenOver, Trigger::GiveBack, Ready),
(TakenOver, Trigger::SaidDone, Checking),
(Running, Trigger::ReportedDone, Checking),
(Running, Trigger::Broke, Failed),
(Checking, Trigger::FirstCheckFailure, Running),
(Checking, Trigger::ChecksFailedAgain, Failed),
(Checking, Trigger::ChecksPassed, InReview),
(Checking, Trigger::GoalNotMet, Running),
(Checking, Trigger::NewWorker, Running),
(InReview, Trigger::ChangesRequested, Running),
(InReview, Trigger::ChangesRequestedAgain, NeedsYou),
(InReview, Trigger::Approved, Merging),
(InReview, Trigger::NewWorker, Running),
(Merging, Trigger::AffectedTestsFailed, Running),
(Merging, Trigger::Merged, Merged),
(Merged, Trigger::SentBack, Running),
(Failed, Trigger::Retry, Ready),
(Failed, Trigger::AskYou, NeedsYou),
(NeedsYou, Trigger::NewWorker, Ready),
(NeedsYou, Trigger::TakeOver, TakenOver),
(NeedsYou, Trigger::KeptWork, Checking),
(NeedsYou, Trigger::Defer, Deferred),
(NeedsYou, Trigger::Split, Cancelled),
(NeedsYou, Trigger::Drop, Cancelled),
];
pub fn next(from: TaskState, trigger: Trigger) -> Option<TaskState> {
TABLE
.iter()
.find(|(state, row, _)| *state == from && *row == trigger)
.map(|(_, _, to)| *to)
}
pub fn is_final(state: TaskState) -> bool {
matches!(state, Merged | Cancelled)
}
pub fn waits_on_decision(state: TaskState) -> bool {
matches!(
state,
Blocked | NeedsDecision | NeedsYou | Deferred | TakenOver | Failed
)
}
#[cfg(test)]
mod tests {
use super::*;
const ALL_STATES: [TaskState; 14] = [
Waiting,
Ready,
Running,
Blocked,
NeedsDecision,
Checking,
InReview,
Merging,
Merged,
Failed,
NeedsYou,
Deferred,
Cancelled,
TakenOver,
];
#[test]
fn task_state_machine_table() {
let rows = [
(Waiting, Trigger::DependenciesMerged, Ready),
(Ready, Trigger::Start, Running),
(Ready, Trigger::LimitNear, Deferred),
(Deferred, Trigger::RaiseLimit, Ready),
(Running, Trigger::Question, Blocked),
(Blocked, Trigger::Answered, Running),
(Running, Trigger::NeedsDecision, NeedsDecision),
(NeedsDecision, Trigger::Decided, Ready),
(NeedsDecision, Trigger::AskYou, NeedsYou),
(NeedsDecision, Trigger::TakeOver, TakenOver),
(Running, Trigger::TakeOver, TakenOver),
(TakenOver, Trigger::GiveBack, Ready),
(TakenOver, Trigger::SaidDone, Checking),
(Running, Trigger::ReportedDone, Checking),
(Running, Trigger::Broke, Failed),
(Checking, Trigger::FirstCheckFailure, Running),
(Checking, Trigger::ChecksFailedAgain, Failed),
(Checking, Trigger::ChecksPassed, InReview),
(Checking, Trigger::GoalNotMet, Running),
(Checking, Trigger::NewWorker, Running),
(InReview, Trigger::ChangesRequested, Running),
(InReview, Trigger::ChangesRequestedAgain, NeedsYou),
(InReview, Trigger::Approved, Merging),
(InReview, Trigger::NewWorker, Running),
(Merging, Trigger::AffectedTestsFailed, Running),
(Merging, Trigger::Merged, Merged),
(Merged, Trigger::SentBack, Running),
(Failed, Trigger::Retry, Ready),
(Failed, Trigger::AskYou, NeedsYou),
(NeedsYou, Trigger::NewWorker, Ready),
(NeedsYou, Trigger::TakeOver, TakenOver),
(NeedsYou, Trigger::KeptWork, Checking),
(NeedsYou, Trigger::Defer, Deferred),
(NeedsYou, Trigger::Split, Cancelled),
(NeedsYou, Trigger::Drop, Cancelled),
];
for (from, trigger, to) in rows {
assert_eq!(next(from, trigger), Some(to), "{from:?} --{trigger:?}-->");
}
assert_eq!(
TABLE.len(),
rows.len(),
"the table has rows the brief doesn't"
);
let triggers: Vec<Trigger> = TABLE.iter().map(|(_, t, _)| *t).collect();
for from in ALL_STATES {
for trigger in &triggers {
let listed = rows.iter().any(|(f, t, _)| *f == from && t == trigger);
if !listed {
assert_eq!(
next(from, *trigger),
None,
"{from:?} --{trigger:?}--> allowed"
);
}
}
}
}
#[test]
fn each_state_and_trigger_pair_has_one_row_at_most() {
for (i, (from, trigger, _)) in TABLE.iter().enumerate() {
let twins = TABLE[i + 1..]
.iter()
.filter(|(f, t, _)| f == from && t == trigger)
.count();
assert_eq!(twins, 0, "{from:?} {trigger:?}");
}
}
#[test]
fn final_states_have_no_way_out_but_the_merge_approval() {
for state in ALL_STATES.into_iter().filter(|s| is_final(*s)) {
let ways_out: Vec<Trigger> = TABLE
.iter()
.filter(|(from, _, _)| *from == state)
.map(|(_, trigger, _)| *trigger)
.collect();
let expected = if state == Merged {
vec![Trigger::SentBack]
} else {
Vec::new()
};
assert_eq!(ways_out, expected, "{state:?}");
}
assert!(!is_final(NeedsYou));
assert!(waits_on_decision(NeedsYou));
assert!(!waits_on_decision(Ready));
}
}