|
17 | 17 | //! live OODA dispatcher. |
18 | 18 |
|
19 | 19 | use crate::status::FoundryLocalStatus; |
| 20 | +use scxml::model::{State, Statechart, Transition}; |
20 | 21 |
|
21 | 22 | /// Windows Foundry Local's observed lifecycle state. |
22 | 23 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
@@ -51,6 +52,37 @@ pub(crate) fn state_id(state: FoundryLocalLifecycle) -> &'static str { |
51 | 52 | } |
52 | 53 | } |
53 | 54 |
|
| 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 | + |
54 | 86 | #[cfg(test)] |
55 | 87 | mod tests { |
56 | 88 | use super::*; |
@@ -88,4 +120,79 @@ mod tests { |
88 | 120 | assert_eq!(state_id(FoundryLocalLifecycle::InstalledStopped), "installed_stopped"); |
89 | 121 | assert_eq!(state_id(FoundryLocalLifecycle::InstalledRunning), "installed_running"); |
90 | 122 | } |
| 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 | + } |
91 | 198 | } |
0 commit comments