1use serde::{Deserialize, Serialize};
7
8#[cfg(test)]
9fn can_transition(from: &RuntimeState, next: &RuntimeState) -> bool {
10 use RuntimeState::{Attached, Destroyed, Idle, Initializing, Retired, Running, Stopped};
11
12 matches!(
13 (from, next),
14 (Initializing, Idle | Stopped | Destroyed)
15 | (Idle, Attached | Running | Retired | Stopped | Destroyed)
16 | (Attached, Running | Idle | Retired | Stopped | Destroyed)
17 | (Running, Idle | Attached | Retired | Stopped | Destroyed)
18 | (Retired, Running | Stopped | Destroyed)
19 | (Stopped, Destroyed)
20 )
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26#[non_exhaustive]
27pub enum RuntimeState {
28 Initializing,
30 Idle,
32 Attached,
34 Running,
36 Retired,
38 Stopped,
40 Destroyed,
42}
43
44impl RuntimeState {
45 pub fn is_terminal(&self) -> bool {
50 matches!(self, Self::Destroyed)
51 }
52
53 pub fn can_accept_input(&self) -> bool {
55 matches!(self, Self::Idle | Self::Attached | Self::Running)
56 }
57
58 pub fn can_process_queue(&self) -> bool {
60 matches!(self, Self::Idle | Self::Attached | Self::Retired)
61 }
62
63 pub fn is_attached(&self) -> bool {
65 matches!(self, Self::Attached)
66 }
67
68 pub fn is_idle_or_attached(&self) -> bool {
70 matches!(self, Self::Idle | Self::Attached)
71 }
72}
73
74pub fn run_return_phase_from_pre_run_phase(pre_run_phase: Option<RuntimeState>) -> RuntimeState {
81 match pre_run_phase {
82 Some(RuntimeState::Attached) => RuntimeState::Attached,
83 Some(RuntimeState::Retired) => RuntimeState::Retired,
84 Some(
85 RuntimeState::Idle
86 | RuntimeState::Initializing
87 | RuntimeState::Running
88 | RuntimeState::Stopped
89 | RuntimeState::Destroyed,
90 )
91 | None => RuntimeState::Idle,
92 }
93}
94
95pub fn run_start_pre_phase_from_phase(
98 phase: RuntimeState,
99) -> Result<RuntimeState, RuntimeStateTransitionError> {
100 match phase {
101 RuntimeState::Idle | RuntimeState::Attached | RuntimeState::Retired => Ok(phase),
102 from => Err(RuntimeStateTransitionError {
103 from,
104 to: RuntimeState::Running,
105 }),
106 }
107}
108
109impl std::fmt::Display for RuntimeState {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 match self {
112 Self::Initializing => write!(f, "initializing"),
113 Self::Idle => write!(f, "idle"),
114 Self::Attached => write!(f, "attached"),
115 Self::Running => write!(f, "running"),
116 Self::Retired => write!(f, "retired"),
117 Self::Stopped => write!(f, "stopped"),
118 Self::Destroyed => write!(f, "destroyed"),
119 }
120 }
121}
122
123#[derive(Debug, Clone, thiserror::Error)]
125#[error("Invalid runtime state transition: {from} -> {to}")]
126pub struct RuntimeStateTransitionError {
127 pub from: RuntimeState,
128 pub to: RuntimeState,
129}
130
131#[cfg(test)]
132#[allow(clippy::unwrap_used)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn terminal_states() {
138 assert!(!RuntimeState::Stopped.is_terminal());
139 assert!(RuntimeState::Destroyed.is_terminal());
140 assert!(!RuntimeState::Initializing.is_terminal());
141 assert!(!RuntimeState::Idle.is_terminal());
142 assert!(!RuntimeState::Attached.is_terminal());
143 assert!(!RuntimeState::Running.is_terminal());
144 assert!(!RuntimeState::Retired.is_terminal());
145 }
146
147 #[test]
148 fn input_and_queue_capabilities() {
149 assert!(RuntimeState::Idle.can_accept_input());
150 assert!(RuntimeState::Attached.can_accept_input());
151 assert!(RuntimeState::Running.can_accept_input());
152 assert!(!RuntimeState::Retired.can_accept_input());
153
154 assert!(RuntimeState::Idle.can_process_queue());
155 assert!(RuntimeState::Attached.can_process_queue());
156 assert!(RuntimeState::Retired.can_process_queue());
157 assert!(!RuntimeState::Running.can_process_queue());
158 }
159
160 #[test]
161 fn attachment_predicates() {
162 assert!(RuntimeState::Attached.is_attached());
163 assert!(RuntimeState::Idle.is_idle_or_attached());
164 assert!(RuntimeState::Attached.is_idle_or_attached());
165 assert!(!RuntimeState::Running.is_idle_or_attached());
166 }
167
168 #[test]
169 fn transition_table_matches_spec_examples() {
170 assert!(can_transition(
171 &RuntimeState::Initializing,
172 &RuntimeState::Idle
173 ));
174 assert!(can_transition(&RuntimeState::Idle, &RuntimeState::Attached));
175 assert!(can_transition(
176 &RuntimeState::Attached,
177 &RuntimeState::Running
178 ));
179 assert!(can_transition(
180 &RuntimeState::Running,
181 &RuntimeState::Retired
182 ));
183 assert!(can_transition(
184 &RuntimeState::Retired,
185 &RuntimeState::Stopped
186 ));
187
188 assert!(!can_transition(&RuntimeState::Stopped, &RuntimeState::Idle));
189 assert!(!can_transition(
190 &RuntimeState::Destroyed,
191 &RuntimeState::Running
192 ));
193 assert!(!can_transition(&RuntimeState::Retired, &RuntimeState::Idle));
194 }
195
196 #[test]
197 fn run_return_phase_classifier_matches_machine_projection() {
198 assert_eq!(
199 run_return_phase_from_pre_run_phase(Some(RuntimeState::Idle)),
200 RuntimeState::Idle
201 );
202 assert_eq!(
203 run_return_phase_from_pre_run_phase(Some(RuntimeState::Attached)),
204 RuntimeState::Attached
205 );
206 assert_eq!(
207 run_return_phase_from_pre_run_phase(Some(RuntimeState::Retired)),
208 RuntimeState::Retired
209 );
210 assert_eq!(
211 run_return_phase_from_pre_run_phase(None),
212 RuntimeState::Idle
213 );
214 }
215
216 #[test]
217 fn run_start_pre_phase_classifier_matches_machine_projection() {
218 assert!(
219 matches!(
220 run_start_pre_phase_from_phase(RuntimeState::Idle),
221 Ok(RuntimeState::Idle)
222 ),
223 "idle should be a legal run start phase"
224 );
225 assert!(
226 matches!(
227 run_start_pre_phase_from_phase(RuntimeState::Attached),
228 Ok(RuntimeState::Attached)
229 ),
230 "attached should be a legal run start phase"
231 );
232 assert!(
233 matches!(
234 run_start_pre_phase_from_phase(RuntimeState::Retired),
235 Ok(RuntimeState::Retired)
236 ),
237 "retired should be a legal drain start phase"
238 );
239 assert!(
240 run_start_pre_phase_from_phase(RuntimeState::Stopped).is_err(),
241 "stopped should not be a legal run start phase"
242 );
243 }
244
245 #[test]
246 fn transition_failure_shape_matches_runtime_error() {
247 let result = if can_transition(&RuntimeState::Stopped, &RuntimeState::Idle) {
248 Ok(())
249 } else {
250 Err(RuntimeStateTransitionError {
251 from: RuntimeState::Stopped,
252 to: RuntimeState::Idle,
253 })
254 };
255
256 assert!(result.is_err());
257 assert!(matches!(
258 result.unwrap_err(),
259 RuntimeStateTransitionError {
260 from: RuntimeState::Stopped,
261 to: RuntimeState::Idle
262 }
263 ));
264 }
265
266 #[test]
267 fn serde_roundtrip_all_states() {
268 for state in [
269 RuntimeState::Initializing,
270 RuntimeState::Idle,
271 RuntimeState::Attached,
272 RuntimeState::Running,
273 RuntimeState::Retired,
274 RuntimeState::Stopped,
275 RuntimeState::Destroyed,
276 ] {
277 let json = serde_json::to_value(state).unwrap();
278 let parsed: RuntimeState = serde_json::from_value(json).unwrap();
279 assert_eq!(state, parsed);
280 }
281 }
282
283 #[test]
284 fn display() {
285 assert_eq!(RuntimeState::Idle.to_string(), "idle");
286 assert_eq!(RuntimeState::Attached.to_string(), "attached");
287 assert_eq!(RuntimeState::Running.to_string(), "running");
288 assert_eq!(RuntimeState::Destroyed.to_string(), "destroyed");
289 }
290}