use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
JsonSchema,
tatara_lisp::DeriveClosedSet,
)]
#[closed_set(
via = "as_str",
unknown = "UnknownPhase",
display,
generate_unknown = "process phase"
)]
pub enum ProcessPhase {
Pending,
Forking,
Execing,
Running,
Attested,
Reconverging,
Releasing,
Exiting,
Failed,
Zombie,
Reaped,
}
impl Default for ProcessPhase {
fn default() -> Self {
Self::Pending
}
}
impl ProcessPhase {
pub const ALL: [Self; 11] = [
Self::Pending,
Self::Forking,
Self::Execing,
Self::Running,
Self::Attested,
Self::Reconverging,
Self::Releasing,
Self::Exiting,
Self::Failed,
Self::Zombie,
Self::Reaped,
];
pub const fn as_str(self) -> &'static str {
match self {
Self::Pending => "Pending",
Self::Forking => "Forking",
Self::Execing => "Execing",
Self::Running => "Running",
Self::Attested => "Attested",
Self::Reconverging => "Reconverging",
Self::Releasing => "Releasing",
Self::Exiting => "Exiting",
Self::Failed => "Failed",
Self::Zombie => "Zombie",
Self::Reaped => "Reaped",
}
}
pub const fn is_terminal(self) -> bool {
matches!(self, Self::Reaped)
}
pub const fn is_running(self) -> bool {
matches!(self, Self::Running | Self::Attested)
}
pub const fn is_alive(self) -> bool {
!matches!(self, Self::Zombie | Self::Reaped | Self::Failed)
}
pub const fn is_releasing(self) -> bool {
matches!(self, Self::Releasing)
}
pub const fn is_terminal_reached(self) -> bool {
matches!(self, Self::Attested | Self::Failed)
}
pub const fn can_transition_to(self, next: Self) -> bool {
use ProcessPhase::*;
matches!(
(self, next),
(Pending, Forking)
| (Forking, Execing)
| (Execing, Running)
| (Execing, Failed)
| (Running, Attested)
| (Running, Exiting)
| (Running, Failed)
| (Running, Reconverging)
| (Attested, Reconverging)
| (Attested, Releasing)
| (Attested, Exiting)
| (Failed, Releasing)
| (Failed, Zombie)
| (Releasing, Exiting)
| (Releasing, Zombie)
| (Reconverging, Execing)
| (Exiting, Zombie)
| (Zombie, Reaped)
)
}
}
#[cfg(test)]
mod tests {
use super::ProcessPhase::*;
#[test]
fn canonical_path_is_legal() {
assert!(Pending.can_transition_to(Forking));
assert!(Forking.can_transition_to(Execing));
assert!(Execing.can_transition_to(Running));
assert!(Running.can_transition_to(Attested));
assert!(Attested.can_transition_to(Reconverging));
assert!(Reconverging.can_transition_to(Execing));
assert!(Attested.can_transition_to(Exiting));
assert!(Exiting.can_transition_to(Zombie));
assert!(Zombie.can_transition_to(Reaped));
}
#[test]
fn releasing_path_is_legal() {
assert!(Attested.can_transition_to(Releasing));
assert!(Failed.can_transition_to(Releasing));
assert!(Releasing.can_transition_to(Exiting));
assert!(Releasing.can_transition_to(Zombie));
assert!(Releasing.is_alive());
assert!(!Releasing.is_terminal_reached());
}
#[test]
fn terminal_reached_gates_are_attested_and_failed() {
assert!(Attested.is_terminal_reached());
assert!(Failed.is_terminal_reached());
for p in super::ProcessPhase::ALL {
if matches!(p, Attested | Failed) {
continue;
}
assert!(!p.is_terminal_reached(), "{p:?} is not a terminal gate");
}
}
#[test]
fn releasing_can_only_be_entered_from_terminal_gates() {
let entries: Vec<_> = super::ProcessPhase::ALL
.into_iter()
.filter(|p| p.can_transition_to(Releasing))
.collect();
assert_eq!(entries, vec![Attested, Failed]);
}
#[test]
fn reaped_is_sink() {
assert!(Reaped.is_terminal());
for next in super::ProcessPhase::ALL {
if next == Reaped {
continue;
}
assert!(
!Reaped.can_transition_to(next),
"Reaped → {next:?} should be illegal"
);
}
}
#[test]
fn cannot_skip_forking() {
assert!(!Pending.can_transition_to(Execing));
assert!(!Pending.can_transition_to(Running));
}
#[test]
fn running_is_alive() {
assert!(Running.is_alive());
assert!(Attested.is_alive());
assert!(!Zombie.is_alive());
assert!(!Reaped.is_alive());
}
#[test]
fn process_phase_is_well_formed_closed_set() {
tatara_lisp::assert_closed_set_well_formed::<super::ProcessPhase>();
}
#[test]
fn display_matches_as_str() {
for phase in super::ProcessPhase::ALL {
assert_eq!(phase.to_string(), phase.as_str());
}
}
#[test]
fn unknown_phase_errors() {
use std::str::FromStr;
for bad in ["attested", "FAILED", "Cancelled", "Reapped"] {
let err = super::ProcessPhase::from_str(bad).unwrap_err();
assert_eq!(err.0, bad, "error payload should echo input verbatim");
}
}
}