Skip to content

Commit 1176a46

Browse files
feat(lifecycle): export FoundryLocalLifecycle as an SCXML statechart
1 parent 888ee86 commit 1176a46

1 file changed

Lines changed: 107 additions & 0 deletions

File tree

crates/ledgerr-desktop-agent/src/lifecycle.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
//! live OODA dispatcher.
1818
1919
use crate::status::FoundryLocalStatus;
20+
use scxml::model::{State, Statechart, Transition};
2021

2122
/// Windows Foundry Local's observed lifecycle state.
2223
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -51,6 +52,37 @@ pub(crate) fn state_id(state: FoundryLocalLifecycle) -> &'static str {
5152
}
5253
}
5354

55+
/// Exports [`FoundryLocalLifecycle`]'s state shape as a W3C SCXML
56+
/// statechart, following `ufo-types::statechart::ooda_phases_to_statechart`'s
57+
/// exact pattern. See this module's doc comment for why these transitions
58+
/// describe the *shape* of the lifecycle rather than a literally-dispatched
59+
/// event stream.
60+
pub fn foundry_lifecycle_to_statechart() -> Statechart {
61+
let mut not_installed = State::atomic(state_id(FoundryLocalLifecycle::NotInstalled));
62+
not_installed.transitions.push(Transition::new(
63+
"InstallSucceeded",
64+
state_id(FoundryLocalLifecycle::InstalledStopped),
65+
));
66+
67+
let mut installed_stopped = State::atomic(state_id(FoundryLocalLifecycle::InstalledStopped));
68+
installed_stopped.transitions.push(Transition::new(
69+
"ServiceStarted",
70+
state_id(FoundryLocalLifecycle::InstalledRunning),
71+
));
72+
73+
let mut installed_running = State::atomic(state_id(FoundryLocalLifecycle::InstalledRunning));
74+
installed_running.transitions.push(Transition::new(
75+
"ServiceStopped",
76+
state_id(FoundryLocalLifecycle::InstalledStopped),
77+
));
78+
79+
Statechart::new(
80+
state_id(FoundryLocalLifecycle::NotInstalled),
81+
vec![not_installed, installed_stopped, installed_running],
82+
)
83+
.with_name("foundry_local_lifecycle")
84+
}
85+
5486
#[cfg(test)]
5587
mod tests {
5688
use super::*;
@@ -88,4 +120,79 @@ mod tests {
88120
assert_eq!(state_id(FoundryLocalLifecycle::InstalledStopped), "installed_stopped");
89121
assert_eq!(state_id(FoundryLocalLifecycle::InstalledRunning), "installed_running");
90122
}
123+
124+
use scxml::export::xml::to_xml;
125+
use scxml::model::StateKind;
126+
use scxml::parse_xml;
127+
use scxml::validate;
128+
129+
#[test]
130+
fn statechart_has_exactly_the_three_lifecycle_states() {
131+
let chart = foundry_lifecycle_to_statechart();
132+
assert_eq!(chart.states.len(), 3);
133+
for state in [
134+
FoundryLocalLifecycle::NotInstalled,
135+
FoundryLocalLifecycle::InstalledStopped,
136+
FoundryLocalLifecycle::InstalledRunning,
137+
] {
138+
let found = chart.find_state(state_id(state));
139+
assert!(found.is_some(), "missing state {}", state_id(state));
140+
assert_eq!(found.unwrap().kind, StateKind::Atomic);
141+
}
142+
}
143+
144+
#[test]
145+
fn not_installed_transitions_to_installed_stopped_on_install_succeeded() {
146+
let chart = foundry_lifecycle_to_statechart();
147+
let not_installed = chart
148+
.find_state(state_id(FoundryLocalLifecycle::NotInstalled))
149+
.unwrap();
150+
let transition = not_installed
151+
.transitions
152+
.iter()
153+
.find(|t| t.event.as_deref() == Some("InstallSucceeded"))
154+
.expect("NotInstalled must accept InstallSucceeded");
155+
assert_eq!(
156+
transition.targets,
157+
vec![state_id(FoundryLocalLifecycle::InstalledStopped)]
158+
);
159+
}
160+
161+
#[test]
162+
fn installed_stopped_and_running_transition_to_each_other() {
163+
let chart = foundry_lifecycle_to_statechart();
164+
let stopped = chart
165+
.find_state(state_id(FoundryLocalLifecycle::InstalledStopped))
166+
.unwrap();
167+
let start = stopped
168+
.transitions
169+
.iter()
170+
.find(|t| t.event.as_deref() == Some("ServiceStarted"))
171+
.expect("InstalledStopped must accept ServiceStarted");
172+
assert_eq!(start.targets, vec![state_id(FoundryLocalLifecycle::InstalledRunning)]);
173+
174+
let running = chart
175+
.find_state(state_id(FoundryLocalLifecycle::InstalledRunning))
176+
.unwrap();
177+
let stop = running
178+
.transitions
179+
.iter()
180+
.find(|t| t.event.as_deref() == Some("ServiceStopped"))
181+
.expect("InstalledRunning must accept ServiceStopped");
182+
assert_eq!(stop.targets, vec![state_id(FoundryLocalLifecycle::InstalledStopped)]);
183+
}
184+
185+
#[test]
186+
fn statechart_passes_scxml_structural_validation() {
187+
let chart = foundry_lifecycle_to_statechart();
188+
validate(&chart).expect("exported statechart should be structurally valid SCXML");
189+
}
190+
191+
#[test]
192+
fn statechart_round_trips_through_xml_export_and_parse() {
193+
let chart = foundry_lifecycle_to_statechart();
194+
let xml = to_xml(&chart);
195+
let parsed = parse_xml(&xml).expect("exported XML must parse back as valid SCXML");
196+
assert_eq!(parsed, chart);
197+
}
91198
}

0 commit comments

Comments
 (0)