use std::sync::Arc;
use std::sync::atomic::{AtomicU8, Ordering};
use crate::error::MobError;
use crate::runtime::MobState;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MobLifecycleInput {
#[cfg_attr(
not(test),
expect(
dead_code,
reason = "schema-aligned authority input retained even when current shell paths do not construct it"
)
)]
#[cfg_attr(test, allow(dead_code))]
Start,
Stop,
Resume,
MarkCompleted,
Destroy,
StartRun,
FinishRun,
BeginCleanup,
FinishCleanup,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum MobLifecycleEffect {
EmitLifecycleNotice,
RequestCleanup,
}
#[cfg_attr(
not(test),
expect(
dead_code,
reason = "transition snapshot retained for schema-aligned authority surface and tests"
)
)]
#[cfg_attr(test, allow(dead_code))]
#[derive(Debug)]
pub(crate) struct MobLifecycleTransition {
pub next_phase: MobState,
pub active_run_count: u32,
pub cleanup_pending: bool,
pub effects: Vec<MobLifecycleEffect>,
}
#[derive(Debug, Clone)]
struct MobLifecycleFields {
active_run_count: u32,
cleanup_pending: bool,
}
mod sealed {
pub trait Sealed {}
}
pub(crate) trait MobLifecycleMutator: sealed::Sealed {
fn apply(&mut self, input: MobLifecycleInput) -> Result<MobLifecycleTransition, MobError>;
}
#[derive(Clone)]
pub(crate) struct MobLifecycleAuthority {
phase: MobState,
fields: MobLifecycleFields,
observable: Arc<AtomicU8>,
}
impl sealed::Sealed for MobLifecycleAuthority {}
impl MobLifecycleAuthority {
pub(crate) fn with_phase(observable: Arc<AtomicU8>, phase: MobState) -> Self {
observable.store(phase as u8, Ordering::Release);
Self {
phase,
fields: MobLifecycleFields {
active_run_count: 0,
cleanup_pending: false,
},
observable,
}
}
pub(crate) fn phase(&self) -> MobState {
self.phase
}
#[cfg(test)]
pub(crate) fn active_run_count(&self) -> u32 {
self.fields.active_run_count
}
pub(crate) fn can_accept(&self, input: MobLifecycleInput) -> bool {
self.evaluate(input).is_ok()
}
pub(crate) fn require_phase(
&self,
allowed: &[MobState],
hint_to: MobState,
) -> Result<(), MobError> {
if allowed.contains(&self.phase) {
Ok(())
} else {
Err(MobError::InvalidTransition {
from: self.phase,
to: hint_to,
})
}
}
fn evaluate(
&self,
input: MobLifecycleInput,
) -> Result<(MobState, MobLifecycleFields, Vec<MobLifecycleEffect>), MobError> {
use MobLifecycleInput::{
BeginCleanup, Destroy, FinishCleanup, FinishRun, MarkCompleted, Resume, Start,
StartRun, Stop,
};
use MobState::{Completed, Creating, Destroyed, Running, Stopped};
let phase = self.phase;
let mut fields = self.fields.clone();
let mut effects = Vec::new();
let next_phase = match (phase, input) {
(Creating | Stopped, Start) => Running,
(Running, Stop) => Stopped,
(Stopped, Resume) => Running,
(Running | Stopped, MarkCompleted) => {
if fields.active_run_count != 0 {
return Err(MobError::InvalidTransition {
from: phase,
to: Completed,
});
}
Completed
}
(Creating | Running | Stopped | Completed, Destroy) => {
fields.active_run_count = 0;
fields.cleanup_pending = false;
effects.push(MobLifecycleEffect::EmitLifecycleNotice);
Destroyed
}
(Running, StartRun) => {
fields.active_run_count = fields.active_run_count.saturating_add(1);
Running
}
(Running | Stopped, FinishRun) => {
if fields.active_run_count == 0 {
return Err(MobError::InvalidTransition {
from: phase,
to: Running,
});
}
fields.active_run_count -= 1;
Running
}
(Stopped | Completed, BeginCleanup) => {
fields.cleanup_pending = true;
effects.push(MobLifecycleEffect::RequestCleanup);
Stopped
}
(Stopped | Completed, FinishCleanup) => {
fields.cleanup_pending = false;
Stopped
}
_ => {
let target = match input {
Start | Resume | StartRun => Running,
Stop | BeginCleanup | FinishCleanup | FinishRun => Stopped,
MarkCompleted => Completed,
Destroy => Destroyed,
};
return Err(MobError::InvalidTransition {
from: phase,
to: target,
});
}
};
Ok((next_phase, fields, effects))
}
}
impl MobLifecycleMutator for MobLifecycleAuthority {
fn apply(&mut self, input: MobLifecycleInput) -> Result<MobLifecycleTransition, MobError> {
let (next_phase, next_fields, effects) = self.evaluate(input)?;
self.phase = next_phase;
self.fields = next_fields.clone();
self.observable.store(next_phase as u8, Ordering::Release);
Ok(MobLifecycleTransition {
next_phase,
active_run_count: next_fields.active_run_count,
cleanup_pending: next_fields.cleanup_pending,
effects,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_authority(phase: MobState) -> MobLifecycleAuthority {
MobLifecycleAuthority::with_phase(Arc::new(AtomicU8::new(phase as u8)), phase)
}
#[test]
fn start_from_creating_transitions_to_running() {
let mut auth = make_authority(MobState::Creating);
let result = auth.apply(MobLifecycleInput::Start);
let transition = result.expect("start from creating should succeed");
assert_eq!(transition.next_phase, MobState::Running);
assert_eq!(auth.phase(), MobState::Running);
}
#[test]
fn start_from_stopped_transitions_to_running() {
let mut auth = make_authority(MobState::Stopped);
let t = auth
.apply(MobLifecycleInput::Start)
.expect("start from stopped");
assert_eq!(t.next_phase, MobState::Running);
}
#[test]
fn stop_from_running_transitions_to_stopped() {
let mut auth = make_authority(MobState::Running);
let t = auth
.apply(MobLifecycleInput::Stop)
.expect("stop should succeed");
assert_eq!(t.next_phase, MobState::Stopped);
}
#[test]
fn resume_from_stopped_transitions_to_running() {
let mut auth = make_authority(MobState::Stopped);
let t = auth
.apply(MobLifecycleInput::Resume)
.expect("resume should succeed");
assert_eq!(t.next_phase, MobState::Running);
}
#[test]
fn start_run_increments_active_run_count() {
let mut auth = make_authority(MobState::Running);
let t1 = auth
.apply(MobLifecycleInput::StartRun)
.expect("start run 1");
assert_eq!(t1.active_run_count, 1);
let t2 = auth
.apply(MobLifecycleInput::StartRun)
.expect("start run 2");
assert_eq!(t2.active_run_count, 2);
assert_eq!(auth.active_run_count(), 2);
}
#[test]
fn finish_run_decrements_active_run_count() {
let mut auth = make_authority(MobState::Running);
auth.apply(MobLifecycleInput::StartRun).expect("start run");
auth.apply(MobLifecycleInput::StartRun)
.expect("start run 2");
let t = auth
.apply(MobLifecycleInput::FinishRun)
.expect("finish run");
assert_eq!(t.active_run_count, 1);
}
#[test]
fn finish_run_rejects_zero_active_runs() {
let mut auth = make_authority(MobState::Running);
let result = auth.apply(MobLifecycleInput::FinishRun);
assert!(
result.is_err(),
"should reject FinishRun with zero active runs"
);
}
#[test]
fn mark_completed_requires_no_active_runs() {
let mut auth = make_authority(MobState::Running);
auth.apply(MobLifecycleInput::StartRun).expect("start run");
let result = auth.apply(MobLifecycleInput::MarkCompleted);
assert!(
result.is_err(),
"should reject MarkCompleted with active runs"
);
}
#[test]
fn mark_completed_succeeds_with_zero_active_runs() {
let mut auth = make_authority(MobState::Running);
let t = auth
.apply(MobLifecycleInput::MarkCompleted)
.expect("mark completed");
assert_eq!(t.next_phase, MobState::Completed);
}
#[test]
fn destroy_emits_lifecycle_notice() {
let mut auth = make_authority(MobState::Running);
let t = auth.apply(MobLifecycleInput::Destroy).expect("destroy");
assert_eq!(t.next_phase, MobState::Destroyed);
assert!(t.effects.contains(&MobLifecycleEffect::EmitLifecycleNotice));
}
#[test]
fn destroy_clears_fields() {
let mut auth = make_authority(MobState::Running);
auth.apply(MobLifecycleInput::StartRun).expect("start run");
let t = auth.apply(MobLifecycleInput::Destroy).expect("destroy");
assert_eq!(t.active_run_count, 0);
assert!(!t.cleanup_pending);
}
#[test]
fn begin_cleanup_emits_request_cleanup() {
let mut auth = make_authority(MobState::Stopped);
let t = auth
.apply(MobLifecycleInput::BeginCleanup)
.expect("begin cleanup");
assert!(t.cleanup_pending);
assert!(t.effects.contains(&MobLifecycleEffect::RequestCleanup));
}
#[test]
fn finish_cleanup_clears_cleanup_pending() {
let mut auth = make_authority(MobState::Stopped);
auth.apply(MobLifecycleInput::BeginCleanup)
.expect("begin cleanup");
let t = auth
.apply(MobLifecycleInput::FinishCleanup)
.expect("finish cleanup");
assert!(!t.cleanup_pending);
}
#[test]
fn observable_cache_is_updated_on_transition() {
let observable = Arc::new(AtomicU8::new(0));
let mut auth = MobLifecycleAuthority::with_phase(observable.clone(), MobState::Creating);
assert_eq!(observable.load(Ordering::Acquire), MobState::Creating as u8);
auth.apply(MobLifecycleInput::Start).expect("start");
assert_eq!(observable.load(Ordering::Acquire), MobState::Running as u8);
}
#[test]
fn reject_invalid_transition() {
let mut auth = make_authority(MobState::Completed);
let result = auth.apply(MobLifecycleInput::Start);
assert!(result.is_err());
assert_eq!(auth.phase(), MobState::Completed);
}
#[test]
fn require_phase_accepts_allowed() {
let auth = make_authority(MobState::Running);
assert!(
auth.require_phase(&[MobState::Running, MobState::Creating], MobState::Running)
.is_ok()
);
}
#[test]
fn require_phase_rejects_disallowed() {
let auth = make_authority(MobState::Stopped);
let result = auth.require_phase(&[MobState::Running], MobState::Running);
assert!(matches!(result, Err(MobError::InvalidTransition { .. })));
}
#[test]
fn can_accept_probes_without_mutation() {
let auth = make_authority(MobState::Running);
assert!(auth.can_accept(MobLifecycleInput::Stop));
assert!(!auth.can_accept(MobLifecycleInput::Resume));
assert_eq!(auth.phase(), MobState::Running);
}
#[test]
fn destroy_from_all_valid_phases() {
for phase in [
MobState::Creating,
MobState::Running,
MobState::Stopped,
MobState::Completed,
] {
let mut auth = make_authority(phase);
let t = auth
.apply(MobLifecycleInput::Destroy)
.unwrap_or_else(|_| panic!("destroy should work from {phase}"));
assert_eq!(t.next_phase, MobState::Destroyed);
}
}
#[test]
fn destroy_from_destroyed_is_rejected() {
let mut auth = make_authority(MobState::Destroyed);
assert!(auth.apply(MobLifecycleInput::Destroy).is_err());
}
#[test]
fn stop_from_non_running_is_rejected() {
for phase in [MobState::Creating, MobState::Completed, MobState::Destroyed] {
let mut auth = make_authority(phase);
assert!(
auth.apply(MobLifecycleInput::Stop).is_err(),
"stop should be rejected from {phase}"
);
}
}
#[test]
fn start_run_from_non_running_is_rejected() {
let mut auth = make_authority(MobState::Stopped);
assert!(auth.apply(MobLifecycleInput::StartRun).is_err());
}
}