use super::harness::{
ALL_PHASES, BUDGET_MS, detached_machine, explore, ordinal, reach, ready_machine, step,
};
use crate::lifecycle::effect::{
Answer, Durability, Effect, Effects, Notify, RecordEnd, ReleaseScope, Timer,
};
use crate::lifecycle::event::Event;
use crate::lifecycle::machine::State;
use crate::sandbox::policy::recovery::{JournalEvidence, RecoveryAction, plan};
use crate::sandbox::record::PersistPhase;
use crate::sandbox::workload::WorkloadClaim;
use crate::sandbox::{IdleAction, SandboxState};
fn recovery_plan(phase: PersistPhase) -> RecoveryAction {
plan(phase, JournalEvidence::Swept)
}
#[test]
fn recovery_seeds_the_state_its_own_verdict_leaves_behind() {
for phase in ALL_PHASES {
let (sm, _) = reach(&[Event::Recovered { phase }]);
let state = *sm.state();
match recovery_plan(phase) {
RecoveryAction::LeaveResumable => {
assert_eq!(state.durable(), Some(PersistPhase::Creating), "{phase:?}");
assert_eq!(state.to_public(), SandboxState::Starting, "{phase:?}");
}
RecoveryAction::Fail => {
assert_eq!(state.durable(), Some(PersistPhase::Failed), "{phase:?}");
assert_eq!(state.to_public(), SandboxState::Failed, "{phase:?}");
}
RecoveryAction::Reinstate(public) => {
assert_eq!(state.durable(), Some(phase), "{phase:?}");
assert_eq!(state.to_public(), public, "{phase:?}");
}
RecoveryAction::FinishRemove => {
assert_eq!(state.durable(), Some(PersistPhase::Removing), "{phase:?}");
}
refused @ (RecoveryAction::RefuseUnjournaled | RecoveryAction::RefuseAdopted) => {
panic!("{phase:?} with a swept journal cannot yield {refused:?}")
}
}
let mut context = Effects::default();
let mut sm = sm;
step(&mut sm, &mut context, &Event::Remove { force: true });
let (state, _) = step(&mut sm, &mut context, &Event::RemoveDone);
assert!(
matches!(state, State::Gone {} | State::Provisioning {}),
"{phase:?} did not reach a terminal state"
);
}
}
#[test]
fn an_adopted_computer_is_seeded_ready_without_a_launch() {
assert_eq!(
plan(PersistPhase::Ready, JournalEvidence::Adopted),
RecoveryAction::Reinstate(SandboxState::Ready),
);
let (sm, mut context) = reach(&[Event::Adopted]);
let state = *sm.state();
assert_eq!(state.to_public(), SandboxState::Ready);
assert_eq!(state.durable(), Some(PersistPhase::Ready));
let mut sm = sm;
let (state, _) = step(
&mut sm,
&mut context,
&Event::ClaimWorkload {
claim: WorkloadClaim::Api,
},
);
assert_eq!(state.to_public(), SandboxState::Running);
}
#[test]
fn only_ready_and_the_gates_own_cmd_take_the_workload_slot() {
for node in explore() {
for claim in [WorkloadClaim::Api, WorkloadClaim::Initial] {
let (mut sm, mut context) = reach(&node.path);
let before = *sm.state();
let (after, effects) = step(&mut sm, &mut context, &Event::ClaimWorkload { claim });
let allowed = matches!(before, State::Ready {})
|| (claim == WorkloadClaim::Initial
&& matches!(before, State::Gating { claimed: false, .. }));
assert_eq!(
after.to_public() == SandboxState::Running && !effects.is_empty(),
allowed,
"{before:?} with {claim:?}"
);
if !allowed {
assert_eq!(before, after, "a refused claim moved {before:?}");
assert!(effects.is_empty(), "a refused claim acted in {before:?}");
}
}
}
}
#[test]
fn a_stop_only_acts_while_the_guest_serves() {
for node in explore() {
let (mut sm, mut context) = reach(&node.path);
let (state, effects) = step(
&mut sm,
&mut context,
&Event::Stop {
budget_ms: BUDGET_MS,
},
);
let acts = matches!(
node.state,
State::Ready {} | State::Running {} | State::Checkpointing {}
);
assert_eq!(
!effects.is_empty(),
acts,
"stop from {:?} emitted {effects:?}",
node.state
);
if !acts {
assert_eq!(ordinal(state), ordinal(node.state), "{:?}", node.state);
}
}
}
#[test]
fn a_non_forced_remove_is_refused_where_the_computer_is_busy_or_handed_over() {
for node in explore() {
let (mut sm, mut context) = reach(&node.path);
let (_, effects) = step(&mut sm, &mut context, &Event::Remove { force: false });
let refused = matches!(
node.state.to_public(),
SandboxState::Starting | SandboxState::Running
) || matches!(
node.state,
State::Removing {} | State::Gone {} | State::Detached {}
);
assert_eq!(
effects.is_empty(),
refused,
"non-forced remove from {:?} emitted {effects:?}",
node.state
);
}
}
#[test]
fn a_forced_remove_tears_down_from_every_live_state() {
for node in explore() {
if matches!(
node.state,
State::Removing {} | State::Gone {} | State::Detached {}
) {
continue;
}
let (mut sm, mut context) = reach(&node.path);
let (state, effects) = step(&mut sm, &mut context, &Event::Remove { force: true });
assert!(matches!(state, State::Removing {}), "{:?}", node.state);
assert_eq!(
effects,
vec![
Effect::AbortInflight,
Effect::CancelTimer(Timer::Ttl),
Effect::CancelTimer(Timer::Idle),
Effect::PersistPhase {
phase: PersistPhase::Removing,
durability: Durability::Warn,
},
Effect::SpawnRelease {
scope: ReleaseScope::Full
},
],
"{:?}",
node.state
);
let (state, effects) = step(&mut sm, &mut context, &Event::RemoveDone);
assert!(matches!(state, State::Gone {}));
assert_eq!(
effects,
vec![
Effect::ForgetRecord(RecordEnd::Removed),
Effect::Publish(Notify::Removed),
Effect::Answer(Answer::Removed),
]
);
}
}
#[test]
fn the_ttl_cap_destroys_everywhere_except_a_resting_removed_or_handed_over_computer() {
for node in explore() {
let (mut sm, mut context) = reach(&node.path);
let (_, effects) = step(&mut sm, &mut context, &Event::TtlExpired);
let inert = matches!(
node.state,
State::Stopped {}
| State::Failed {}
| State::Removing {}
| State::Gone {}
| State::Detached {}
);
assert_eq!(
effects.is_empty(),
inert,
"ttl from {:?} emitted {effects:?}",
node.state
);
}
}
#[test]
fn a_handover_only_acts_where_a_live_handle_is_settled() {
for node in explore() {
let (mut sm, mut context) = reach(&node.path);
let (after, effects) = step(&mut sm, &mut context, &Event::Detach);
let acts = matches!(node.state, State::Ready {} | State::Running {});
assert_eq!(
effects.is_empty(),
!acts,
"detach from {:?} emitted {effects:?}",
node.state
);
if acts {
assert_eq!(effects, vec![Effect::Detach], "{:?}", node.state);
}
assert_eq!(
ordinal(after),
ordinal(node.state),
"detach moved {:?} before its port call reported",
node.state
);
}
}
#[test]
fn a_handover_completion_only_lands_where_the_handover_was_asked_for() {
for node in explore() {
let (mut sm, mut context) = reach(&node.path);
let (after, effects) = step(&mut sm, &mut context, &Event::Detached);
let asked = matches!(node.state, State::Ready {} | State::Running {});
assert_eq!(
effects.is_empty(),
!asked,
"a handover completion in {:?} emitted {effects:?}",
node.state
);
if asked {
assert!(matches!(after, State::Detached {}), "{:?}", node.state);
} else {
assert_eq!(
ordinal(after),
ordinal(node.state),
"a stray handover completion moved {:?}",
node.state
);
}
}
}
#[test]
fn a_handed_over_computer_refuses_every_teardown() {
for event in [
Event::Stop {
budget_ms: BUDGET_MS,
},
Event::Remove { force: false },
Event::Remove { force: true },
Event::TtlExpired,
Event::IdleExpired {
action: IdleAction::Kill,
},
Event::IdleExpired {
action: IdleAction::Pause,
},
Event::Pause {
reason: crate::lifecycle::event::PauseReason::Requested,
},
Event::Checkpoint,
Event::VmExited,
Event::Failure,
Event::Detach,
] {
let (mut sm, mut context) = detached_machine();
assert!(matches!(sm.state(), State::Detached {}));
let (after, effects) = step(&mut sm, &mut context, &event);
assert!(
matches!(after, State::Detached {}),
"{event:?} moved a handed-over computer to {after:?}"
);
assert!(
effects.is_empty(),
"{event:?} acted on a handed-over computer: {effects:?}"
);
}
}
#[test]
fn a_handover_is_refused_while_a_capture_is_in_flight() {
let (mut sm, mut context) = ready_machine();
let (state, _) = step(&mut sm, &mut context, &Event::Checkpoint);
assert!(matches!(state, State::Checkpointing {}));
let (after, effects) = step(&mut sm, &mut context, &Event::Detach);
assert!(
matches!(after, State::Checkpointing {}),
"the handover moved a computer mid-capture to {after:?}"
);
assert!(
effects.is_empty(),
"the handover acted while a capture held the handle: {effects:?}"
);
let (state, _) = step(
&mut sm,
&mut context,
&Event::CaptureDone {
snapshot_id: "snap".to_owned(),
},
);
assert!(matches!(state, State::Ready {}));
let (_, effects) = step(&mut sm, &mut context, &Event::Detach);
assert_eq!(effects, vec![Effect::Detach]);
}
#[test]
fn a_handover_drops_the_timers_it_no_longer_owns() {
let (mut sm, mut context) = ready_machine();
let (_, effects) = step(&mut sm, &mut context, &Event::Detach);
assert_eq!(effects, vec![Effect::Detach]);
let (state, effects) = step(&mut sm, &mut context, &Event::Detached);
assert!(matches!(state, State::Detached {}));
assert_eq!(
effects,
vec![
Effect::Answer(Answer::Detached),
Effect::CancelTimer(Timer::Ttl),
Effect::CancelTimer(Timer::Idle),
]
);
assert_eq!(state.durable(), Some(PersistPhase::Ready));
}
#[test]
fn the_idle_policy_only_fires_on_a_ready_computer() {
for node in explore() {
for action in [IdleAction::Kill, IdleAction::Pause] {
let (mut sm, mut context) = reach(&node.path);
let (_, effects) = step(&mut sm, &mut context, &Event::IdleExpired { action });
assert_eq!(
!effects.is_empty(),
matches!(node.state, State::Ready {}),
"idle {action:?} from {:?} emitted {effects:?}",
node.state
);
}
}
let (mut sm, mut context) = ready_machine();
let (state, effects) = step(
&mut sm,
&mut context,
&Event::IdleExpired {
action: IdleAction::Pause,
},
);
assert_eq!(state.to_public(), SandboxState::Pausing);
assert!(effects.contains(&Effect::Publish(Notify::Pausing)));
let (mut sm, mut context) = ready_machine();
let (state, _) = step(
&mut sm,
&mut context,
&Event::IdleExpired {
action: IdleAction::Kill,
},
);
assert!(matches!(state, State::Removing {}));
}
#[test]
fn an_idle_expiry_cannot_reach_a_busy_computer() {
for action in [IdleAction::Kill, IdleAction::Pause] {
for node in explore() {
let (mut sm, mut context) = reach(&node.path);
let before = *sm.state();
let (after, effects) = step(&mut sm, &mut context, &Event::IdleExpired { action });
if matches!(before, State::Ready {}) {
assert_ne!(before, after, "{before:?} must act on an idle expiry");
continue;
}
assert_eq!(before, after, "{before:?} acted on an idle expiry");
assert!(effects.is_empty(), "{before:?} acted on an idle expiry");
}
}
}
#[test]
fn an_idle_kill_is_the_teardown_a_forced_remove_runs() {
let (mut sm, mut context) = ready_machine();
let (state, killed) = step(
&mut sm,
&mut context,
&Event::IdleExpired {
action: IdleAction::Kill,
},
);
assert!(matches!(state, State::Removing {}));
let (mut sm, mut context) = ready_machine();
let (state, removed) = step(&mut sm, &mut context, &Event::Remove { force: true });
assert!(matches!(state, State::Removing {}));
assert_eq!(killed, removed);
}