use super::harness::{BUDGET_MS, machine, persist, reach, ready_machine, release, step};
use crate::lifecycle::effect::{
Answer, Durability, Effect, Effects, Notify, RecordEnd, ReleaseScope, Timer, Unconfirmed,
};
use crate::lifecycle::event::{Event, PauseReason, Provision, RestoreOrigin};
use crate::lifecycle::machine::State;
use crate::sandbox::record::PersistPhase;
use crate::sandbox::workload::WorkloadClaim;
use crate::sandbox::{IdleAction, SandboxState};
#[test]
fn a_cold_create_stages_boots_gates_and_only_then_announces_ready() {
let mut context = Effects::default();
let mut sm = machine(&mut context);
let (state, effects) = step(
&mut sm,
&mut context,
&Event::Provision(Provision::Boot { warm: true }),
);
assert_eq!(state.to_public(), SandboxState::Starting);
assert_eq!(
effects,
vec![
persist(PersistPhase::Starting, Durability::Report(Unconfirmed::Ack)),
Effect::SpawnBoot { warm: true },
Effect::Publish(Notify::Created),
Effect::ArmTimer(Timer::Ttl),
]
);
let (state, effects) = step(&mut sm, &mut context, &Event::ResourcesHandedOff);
assert!(matches!(state, State::Booting {}));
assert!(effects.is_empty());
let (_, effects) = step(&mut sm, &mut context, &Event::AgentReady);
assert_eq!(effects, vec![Effect::SpawnGate]);
let (state, effects) = step(&mut sm, &mut context, &Event::Gated);
assert_eq!(state.to_public(), SandboxState::Ready);
assert_eq!(
effects,
vec![
persist(
PersistPhase::Ready,
Durability::Report(Unconfirmed::Unavailable)
),
Effect::Publish(Notify::Ready),
Effect::Answer(Answer::Ready),
Effect::ArmTimer(Timer::Idle),
]
);
}
#[test]
fn a_restore_commits_ready_in_one_hop_before_its_gate() {
let mut context = Effects::default();
let mut sm = machine(&mut context);
let (_, effects) = step(
&mut sm,
&mut context,
&Event::Provision(Provision::Restore {
origin: RestoreOrigin::WarmCreate,
}),
);
assert_eq!(
effects,
vec![Effect::SpawnRestore {
origin: RestoreOrigin::WarmCreate
}]
);
let (state, effects) = step(&mut sm, &mut context, &Event::Restored);
assert_eq!(state.to_public(), SandboxState::Starting);
assert!(matches!(
state,
State::Gating {
committed: true,
claimed: false
}
));
assert_eq!(
effects,
vec![
Effect::CommitRestored {
durability: Durability::Report(Unconfirmed::Ack),
},
Effect::Publish(Notify::Created),
Effect::ArmTimer(Timer::Ttl),
Effect::SpawnGate,
]
);
let (state, effects) = step(&mut sm, &mut context, &Event::Gated);
assert_eq!(state.to_public(), SandboxState::Ready);
assert_eq!(
effects,
vec![
Effect::Publish(Notify::Ready),
Effect::Answer(Answer::Ready),
Effect::ArmTimer(Timer::Idle),
]
);
}
#[test]
fn the_boots_own_cmd_claims_the_slot_the_gate_reserved_for_it() {
let (mut sm, mut context) = reach(&[
Event::Provision(Provision::Boot { warm: false }),
Event::ResourcesHandedOff,
Event::AgentReady,
]);
let (state, effects) = step(
&mut sm,
&mut context,
&Event::ClaimWorkload {
claim: WorkloadClaim::Initial,
},
);
assert!(matches!(
state,
State::Gating {
committed: false,
claimed: true
}
));
assert_eq!(state.to_public(), SandboxState::Running);
assert_eq!(effects, vec![Effect::Publish(Notify::Running)]);
let (_, effects) = step(
&mut sm,
&mut context,
&Event::ClaimWorkload {
claim: WorkloadClaim::Initial,
},
);
assert!(effects.is_empty());
let (state, effects) = step(&mut sm, &mut context, &Event::Gated);
assert!(matches!(state, State::Running {}));
assert_eq!(
effects,
vec![
persist(
PersistPhase::Ready,
Durability::Report(Unconfirmed::Unavailable)
),
Effect::Publish(Notify::Ready),
Effect::Answer(Answer::Ready),
]
);
let (state, effects) = step(&mut sm, &mut context, &Event::WorkloadExited);
assert_eq!(state.to_public(), SandboxState::Ready);
assert_eq!(
effects,
vec![Effect::Publish(Notify::Idle), Effect::ArmTimer(Timer::Idle)]
);
}
#[test]
fn only_the_warm_create_reroute_announces_a_restore_as_a_create() {
let (mut sm, mut context) = reach(&[Event::Provision(Provision::Restore {
origin: RestoreOrigin::Restore,
})]);
let (_, effects) = step(&mut sm, &mut context, &Event::Restored);
assert!(
!effects.contains(&Effect::Publish(Notify::Created)),
"{effects:?}"
);
}
#[test]
fn a_workload_round_trip_cancels_and_re_arms_the_idle_timer() {
let (mut sm, mut context) = ready_machine();
let (state, effects) = step(
&mut sm,
&mut context,
&Event::ClaimWorkload {
claim: WorkloadClaim::Api,
},
);
assert_eq!(state.to_public(), SandboxState::Running);
assert_eq!(
effects,
vec![
Effect::CancelTimer(Timer::Idle),
Effect::Publish(Notify::Running),
]
);
let (state, effects) = step(&mut sm, &mut context, &Event::WorkloadExited);
assert_eq!(state.to_public(), SandboxState::Ready);
assert_eq!(
effects,
vec![Effect::Publish(Notify::Idle), Effect::ArmTimer(Timer::Idle)]
);
}
#[test]
fn a_pause_captures_releases_and_parks_at_paused() {
let (mut sm, mut context) = ready_machine();
let (state, effects) = step(
&mut sm,
&mut context,
&Event::Pause {
reason: PauseReason::Requested,
},
);
assert_eq!(state.to_public(), SandboxState::Pausing);
assert_eq!(
effects,
vec![
persist(PersistPhase::Pausing, Durability::Warn),
Effect::CancelTimer(Timer::Idle),
Effect::Publish(Notify::Pausing),
Effect::SpawnCheckpoint { hold: true },
]
);
let (state, effects) = step(
&mut sm,
&mut context,
&Event::CaptureDone {
snapshot_id: "snap".to_owned(),
},
);
assert_eq!(state.to_public(), SandboxState::Pausing);
assert_eq!(effects, vec![release(ReleaseScope::KeepDisk)]);
let (state, effects) = step(&mut sm, &mut context, &Event::ReleasedForPause);
assert_eq!(state.to_public(), SandboxState::Paused);
assert_eq!(
effects,
vec![
persist(PersistPhase::Paused, Durability::GateJournal),
Effect::ClearJournal,
Effect::Publish(Notify::Paused),
Effect::Answer(Answer::Paused),
]
);
}
#[test]
fn a_recoverable_capture_failure_leaves_a_usable_computer() {
let (mut sm, mut context) = ready_machine();
step(
&mut sm,
&mut context,
&Event::Pause {
reason: PauseReason::Requested,
},
);
let (state, effects) = step(&mut sm, &mut context, &Event::Failure);
assert_eq!(state.to_public(), SandboxState::Ready);
assert_eq!(
effects,
vec![
persist(PersistPhase::Ready, Durability::Warn),
Effect::Publish(Notify::Ready),
Effect::ArmTimer(Timer::Idle),
]
);
}
#[test]
fn a_frozen_guest_fails_the_computer_from_either_capture_path() {
for pause in [true, false] {
let (mut sm, mut context) = ready_machine();
let event = if pause {
Event::Pause {
reason: PauseReason::Requested,
}
} else {
Event::Checkpoint
};
step(&mut sm, &mut context, &event);
let (state, effects) = step(&mut sm, &mut context, &Event::Frozen);
assert_eq!(state.to_public(), SandboxState::Failed, "pause={pause}");
assert_eq!(
effects,
vec![
persist(PersistPhase::Failed, Durability::GateJournal),
release(ReleaseScope::Runtime),
Effect::ClearJournal,
Effect::CancelTimer(Timer::Idle),
Effect::CancelTimer(Timer::Ttl),
Effect::Publish(Notify::Failed),
]
);
}
}
#[test]
fn a_user_checkpoint_holds_ready_and_writes_no_record() {
let (mut sm, mut context) = ready_machine();
let (state, effects) = step(&mut sm, &mut context, &Event::Checkpoint);
assert_eq!(state.to_public(), SandboxState::Ready);
assert_eq!(
effects,
vec![
Effect::CancelTimer(Timer::Idle),
Effect::SpawnCheckpoint { hold: false },
]
);
for blocked in [
Event::ClaimWorkload {
claim: WorkloadClaim::Api,
},
Event::Pause {
reason: PauseReason::Requested,
},
Event::IdleExpired {
action: IdleAction::Kill,
},
] {
let (state, effects) = step(&mut sm, &mut context, &blocked);
assert!(matches!(state, State::Checkpointing {}), "{blocked:?}");
assert!(effects.is_empty(), "{blocked:?}");
}
let (state, effects) = step(&mut sm, &mut context, &Event::Failure);
assert_eq!(state.to_public(), SandboxState::Ready);
assert_eq!(effects, vec![Effect::ArmTimer(Timer::Idle)]);
}
#[test]
fn resume_reverts_to_paused_when_the_restore_unwinds() {
let (mut sm, mut context) = reach(&[Event::Recovered {
phase: PersistPhase::Paused,
}]);
let (state, effects) = step(&mut sm, &mut context, &Event::Resume);
assert_eq!(state.to_public(), SandboxState::Starting);
assert_eq!(
effects,
vec![
persist(
PersistPhase::Resuming,
Durability::Report(Unconfirmed::RevertToPaused)
),
Effect::SpawnResume,
]
);
let (state, effects) = step(&mut sm, &mut context, &Event::Failure);
assert_eq!(state.to_public(), SandboxState::Paused);
assert_eq!(
effects,
vec![
persist(PersistPhase::Paused, Durability::Warn),
Effect::Publish(Notify::Paused),
]
);
step(&mut sm, &mut context, &Event::Resume);
let (state, effects) = step(&mut sm, &mut context, &Event::Restored);
assert_eq!(state.to_public(), SandboxState::Ready);
assert_eq!(
effects,
vec![
persist(PersistPhase::Ready, Durability::Warn),
Effect::Publish(Notify::Resumed),
Effect::Answer(Answer::Resumed),
Effect::ArmTimer(Timer::Idle),
]
);
}
#[test]
fn a_resume_that_could_not_unwind_fails_instead_of_parking_at_paused() {
let (mut sm, mut context) = reach(&[
Event::Recovered {
phase: PersistPhase::Paused,
},
Event::Resume,
]);
let (state, effects) = step(&mut sm, &mut context, &Event::Stranded);
assert_eq!(state.to_public(), SandboxState::Failed);
assert_eq!(
effects,
vec![
persist(PersistPhase::Failed, Durability::GateJournal),
release(ReleaseScope::Runtime),
Effect::ClearJournal,
Effect::CancelTimer(Timer::Idle),
Effect::CancelTimer(Timer::Ttl),
Effect::Publish(Notify::Failed),
]
);
}
#[test]
fn a_stop_drains_through_stopping_and_clears_the_journal() {
let (mut sm, mut context) = ready_machine();
let (state, effects) = step(
&mut sm,
&mut context,
&Event::Stop {
budget_ms: BUDGET_MS,
},
);
assert_eq!(state.to_public(), SandboxState::Stopping);
assert_eq!(
effects,
vec![
persist(PersistPhase::Stopping, Durability::Warn),
Effect::CancelTimer(Timer::Idle),
Effect::Publish(Notify::Stopping),
Effect::SpawnStop {
budget_ms: BUDGET_MS,
drain: false,
},
]
);
let (state, effects) = step(&mut sm, &mut context, &Event::VmExited);
assert_eq!(state.to_public(), SandboxState::Stopping);
assert!(effects.is_empty());
let (state, effects) = step(&mut sm, &mut context, &Event::StopDone);
assert_eq!(state.to_public(), SandboxState::Stopped);
assert_eq!(
effects,
vec![
persist(PersistPhase::Stopped, Durability::GateJournal),
Effect::ClearJournal,
Effect::CancelTimer(Timer::Ttl),
Effect::Publish(Notify::Stopped),
Effect::Answer(Answer::Stopped),
]
);
}
#[test]
fn an_unacknowledged_provision_forgets_its_record() {
let mut context = Effects::default();
let mut sm = machine(&mut context);
let (state, effects) = step(&mut sm, &mut context, &Event::Failure);
assert!(matches!(state, State::Gone {}));
assert_eq!(effects, vec![Effect::ForgetRecord(RecordEnd::Aborted)]);
}
#[test]
fn the_pause_reasons_are_the_ones_the_event_stream_carries() {
assert_eq!(PauseReason::Requested.as_str(), "pause");
assert_eq!(PauseReason::IdleTimeout.as_str(), "idle_timeout");
}
#[test]
fn a_restore_that_fails_before_its_commit_is_rolled_back_not_parked() {
for failure in [Event::Failure, Event::VmExited, Event::Frozen] {
let (mut sm, mut context) = reach(&[Event::Provision(Provision::Restore {
origin: RestoreOrigin::WarmCreate,
})]);
let (state, effects) = step(&mut sm, &mut context, &failure);
assert!(matches!(state, State::Removing {}), "{failure:?}");
assert_eq!(
effects,
vec![
Effect::AbortInflight,
Effect::CancelTimer(Timer::Ttl),
Effect::CancelTimer(Timer::Idle),
persist(PersistPhase::Removing, Durability::Warn),
release(ReleaseScope::Full),
],
"{failure:?}"
);
let (state, effects) = step(&mut sm, &mut context, &Event::RemoveDone);
assert!(matches!(state, State::Gone {}), "{failure:?}");
assert_eq!(effects[0], Effect::ForgetRecord(RecordEnd::Removed));
}
}
#[test]
fn a_released_claim_reopens_the_computer_and_balances_its_running() {
let (mut sm, mut context) = ready_machine();
let (state, _) = step(
&mut sm,
&mut context,
&Event::ClaimWorkload {
claim: WorkloadClaim::Api,
},
);
assert_eq!(state.to_public(), SandboxState::Running);
let (state, effects) = step(&mut sm, &mut context, &Event::WorkloadReleased);
assert_eq!(state.to_public(), SandboxState::Ready);
assert_eq!(
effects,
vec![Effect::Publish(Notify::Idle), Effect::ArmTimer(Timer::Idle)]
);
let (mut sm, mut context) = reach(&[
Event::Provision(Provision::Boot { warm: false }),
Event::ResourcesHandedOff,
Event::AgentReady,
Event::ClaimWorkload {
claim: WorkloadClaim::Initial,
},
]);
assert_eq!(sm.state().to_public(), SandboxState::Running);
let (state, effects) = step(&mut sm, &mut context, &Event::WorkloadReleased);
assert_eq!(state.to_public(), SandboxState::Starting);
assert_eq!(effects, vec![Effect::Publish(Notify::Idle)]);
let (state, effects) = step(&mut sm, &mut context, &Event::Gated);
assert_eq!(state.to_public(), SandboxState::Ready);
assert!(effects.contains(&Effect::ArmTimer(Timer::Idle)));
}
#[test]
fn only_a_running_computer_drains_before_its_guest_is_shut_down() {
let (mut sm, mut context) = ready_machine();
let (_, effects) = step(
&mut sm,
&mut context,
&Event::Stop {
budget_ms: BUDGET_MS,
},
);
assert!(effects.contains(&Effect::SpawnStop {
budget_ms: BUDGET_MS,
drain: false,
}));
let (mut sm, mut context) = ready_machine();
step(
&mut sm,
&mut context,
&Event::ClaimWorkload {
claim: WorkloadClaim::Api,
},
);
let (state, effects) = step(
&mut sm,
&mut context,
&Event::Stop {
budget_ms: BUDGET_MS,
},
);
assert_eq!(state.to_public(), SandboxState::Stopping);
assert!(effects.contains(&Effect::SpawnStop {
budget_ms: BUDGET_MS,
drain: true,
}));
}
#[test]
fn a_gate_failure_unwinds_the_way_its_own_half_of_the_transaction_can() {
let (mut sm, mut context) = reach(&[
Event::Provision(Provision::Boot { warm: false }),
Event::ResourcesHandedOff,
Event::AgentReady,
]);
let (state, effects) = step(&mut sm, &mut context, &Event::Failure);
assert_eq!(state.to_public(), SandboxState::Failed);
assert!(effects.contains(&persist(PersistPhase::Failed, Durability::GateJournal)));
let (mut sm, mut context) = reach(&[
Event::Provision(Provision::Restore {
origin: RestoreOrigin::WarmCreate,
}),
Event::Restored,
]);
let (state, effects) = step(&mut sm, &mut context, &Event::Failure);
assert_eq!(state.to_public(), SandboxState::Stopping);
assert!(matches!(state, State::Removing {}));
assert!(effects.contains(&persist(PersistPhase::Removing, Durability::Warn)));
assert!(effects.contains(&release(ReleaseScope::Full)));
}