1pub mod callbacks;
54pub mod core;
55pub mod doc;
56pub mod instance;
57pub mod macros;
58pub mod query;
59
60pub use callbacks::CallbackRegistry;
62pub use core::StateMachine;
63pub use doc::StateMachineDoc;
64pub use instance::StateMachineInstance;
65pub use query::StateMachineQuery;
66
67pub const DEFAULT_MAX_HISTORY_SIZE: usize = 512;
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 define_state_machine! {
76 name: TrafficLight,
77 states: { Red, Yellow, Green },
78 inputs: { Timer, Emergency },
79 initial: Red,
80 transitions: {
81 Red + Timer => Green,
82 Green + Timer => Yellow,
83 Yellow + Timer => Red,
84 Red + Emergency => Yellow,
85 Green + Emergency => Red,
86 Yellow + Emergency => Red
87 }
88 }
89
90 mod test_machine {
92 use super::super::*;
93
94 define_state_machine! {
95 name: TestMachine,
96 states: { StateA, StateB },
97 inputs: { Action, _HiddenAction, _Debug },
98 initial: StateA,
99 transitions: {
100 StateA + Action => StateB,
101 StateB + Action => StateA,
102 StateA + _HiddenAction => StateA,
103 StateB + _HiddenAction => StateB,
104 StateA + _Debug => StateA,
105 StateB + _Debug => StateB
106 }
107 }
108 }
109
110 #[test]
111 fn test_deterministic_state_machine_basic() {
112 let mut sm = StateMachineInstance::<TrafficLight>::new();
113 assert_eq!(*sm.current_state(), State::Red);
114
115 let result = sm.transition(Input::Timer);
117 assert!(result.is_ok());
118 assert_eq!(*sm.current_state(), State::Green);
119 assert_eq!(result.unwrap(), State::Green);
120
121 let result = sm.transition(Input::Timer);
123 assert!(result.is_ok());
124 assert_eq!(*sm.current_state(), State::Yellow);
125 }
126
127 #[test]
128 fn test_invalid_transition() {
129 let mut sm = StateMachineInstance::<TrafficLight>::new();
130 assert_eq!(*sm.current_state(), State::Red);
131
132 let result = sm.transition(Input::Emergency);
134 assert!(result.is_ok());
135 assert_eq!(*sm.current_state(), State::Yellow);
136 }
137
138 #[test]
139 fn test_state_machine_instance_methods() {
140 let mut sm = StateMachineInstance::<TrafficLight>::new();
141
142 assert_eq!(*sm.current_state(), State::Red);
144 assert!(sm.history_is_empty());
145 assert_eq!(sm.history_len(), 0);
146
147 assert!(sm.can_accept(&Input::Timer));
149 assert!(sm.can_accept(&Input::Emergency));
150
151 let valid_inputs = sm.valid_inputs();
153 assert!(valid_inputs.contains(&Input::Timer));
154 assert!(valid_inputs.contains(&Input::Emergency));
155
156 sm.transition(Input::Timer).unwrap();
158 assert_eq!(sm.history_len(), 1);
159 assert!(!sm.history_is_empty());
160
161 sm.reset();
163 assert_eq!(*sm.current_state(), State::Red);
164 assert!(sm.history_is_empty());
165 }
166
167 #[test]
168 fn test_query_functions() {
169 let reachable = StateMachineQuery::<TrafficLight>::reachable_states(&State::Red);
170 assert!(reachable.contains(&State::Green));
171 assert!(reachable.contains(&State::Yellow));
172
173 let leading_to_red = StateMachineQuery::<TrafficLight>::states_leading_to(&State::Red);
174 assert!(leading_to_red.contains(&State::Yellow));
175 assert!(leading_to_red.contains(&State::Green));
176
177 assert!(StateMachineQuery::<TrafficLight>::has_path(
179 &State::Red,
180 &State::Green
181 ));
182
183 let path = StateMachineQuery::<TrafficLight>::shortest_path(&State::Red, &State::Green);
185 assert!(path.is_some());
186 let path = path.unwrap();
187 assert_eq!(path[0], State::Red);
188 assert_eq!(path[1], State::Green);
189 }
190
191 #[test]
192 fn test_mermaid_generation() {
193 let mermaid = StateMachineDoc::<TrafficLight>::generate_mermaid();
194 assert!(mermaid.contains("stateDiagram-v2"));
195 assert!(mermaid.contains("Red"));
196 assert!(mermaid.contains("Green"));
197 assert!(mermaid.contains("Yellow"));
198 assert!(mermaid.contains("Timer"));
199 assert!(mermaid.contains("Emergency"));
200 }
201
202 #[test]
203 fn test_history_size_limit() {
204 let mut sm = StateMachineInstance::<TrafficLight>::with_max_history(2);
205 assert_eq!(sm.max_history_size(), 2);
206
207 sm.transition(Input::Timer).unwrap(); sm.transition(Input::Timer).unwrap(); sm.transition(Input::Timer).unwrap(); assert_eq!(sm.history().len(), 2);
214 assert_eq!(sm.history()[0], (State::Green, Input::Timer));
215 assert_eq!(sm.history()[1], (State::Yellow, Input::Timer));
216 }
217
218 #[test]
219 fn test_history_size_default() {
220 let sm = StateMachineInstance::<TrafficLight>::new();
221 assert_eq!(sm.max_history_size(), DEFAULT_MAX_HISTORY_SIZE);
222
223 let sm_default = StateMachineInstance::<TrafficLight>::default();
224 assert_eq!(sm_default.max_history_size(), DEFAULT_MAX_HISTORY_SIZE);
225 }
226
227 #[test]
228 fn test_underscore_inputs_excluded_from_docs() {
229 let mermaid = StateMachineDoc::<test_machine::TestMachine>::generate_mermaid();
230
231 assert!(mermaid.contains("Action"));
233
234 assert!(!mermaid.contains("_HiddenAction"));
236 assert!(!mermaid.contains("_Debug"));
237
238 let table = StateMachineDoc::<test_machine::TestMachine>::generate_transition_table();
239
240 assert!(table.contains("Action"));
242
243 assert!(!table.contains("_HiddenAction"));
245 assert!(!table.contains("_Debug"));
246 }
247
248 #[test]
249 fn test_underscore_inputs_still_functional() {
250 use test_machine::{Input, State, TestMachine};
251
252 let mut sm = StateMachineInstance::<TestMachine>::new();
253 assert_eq!(*sm.current_state(), State::StateA);
254
255 let valid_inputs = sm.valid_inputs();
257 assert!(valid_inputs.contains(&Input::Action));
258 assert!(valid_inputs.contains(&Input::_HiddenAction));
259 assert!(valid_inputs.contains(&Input::_Debug));
260
261 let result = sm.transition(Input::_HiddenAction);
263 assert!(result.is_ok());
264 assert_eq!(*sm.current_state(), State::StateA);
265
266 let result = sm.transition(Input::_Debug);
267 assert!(result.is_ok());
268 assert_eq!(*sm.current_state(), State::StateA);
269
270 let result = sm.transition(Input::Action);
272 assert!(result.is_ok());
273 assert_eq!(*sm.current_state(), State::StateB);
274 }
275
276 #[test]
277 fn test_display_implementation() {
278 assert_eq!(State::Red.to_string(), "Red");
279 assert_eq!(Input::Timer.to_string(), "Timer");
280 }
281
282 #[test]
283 fn test_documentation_generation() {
284 let stats = StateMachineDoc::<TrafficLight>::generate_statistics();
285 assert!(stats.contains("Number of States"));
286 assert!(stats.contains("Number of Transitions"));
287
288 let full_doc = StateMachineDoc::<TrafficLight>::generate_full_documentation();
289 assert!(full_doc.contains("State Machine Documentation"));
290 assert!(full_doc.contains("State Transition Table"));
291 assert!(full_doc.contains("State Diagram"));
292 }
293
294 #[test]
295 fn test_state_from_str() {
296 let red_state = State::from("Red");
298 assert_eq!(red_state, State::Red);
299
300 let yellow_state = State::from("Yellow");
301 assert_eq!(yellow_state, State::Yellow);
302
303 let green_state = State::from("Green");
304 assert_eq!(green_state, State::Green);
305 }
306
307 #[test]
308 #[should_panic(expected = "Invalid state")]
309 fn test_state_from_str_invalid() {
310 let _ = State::from("InvalidState");
312 }
313
314 #[test]
315 fn test_input_from_str() {
316 let timer_input = Input::from("Timer");
318 assert_eq!(timer_input, Input::Timer);
319
320 let emergency_input = Input::from("Emergency");
321 assert_eq!(emergency_input, Input::Emergency);
322 }
323
324 #[test]
325 #[should_panic(expected = "Invalid input")]
326 fn test_input_from_str_invalid() {
327 let _ = Input::from("InvalidInput");
329 }
330
331 #[cfg(feature = "serde")]
332 #[test]
333 fn test_serde_serialization() {
334 let state = State::Red;
336 let serialized = serde_json::to_string(&state).unwrap();
337 assert_eq!(serialized, "\"Red\"");
338
339 let deserialized: State = serde_json::from_str(&serialized).unwrap();
340 assert_eq!(deserialized, State::Red);
341
342 let input = Input::Timer;
344 let serialized = serde_json::to_string(&input).unwrap();
345 assert_eq!(serialized, "\"Timer\"");
346
347 let deserialized: Input = serde_json::from_str(&serialized).unwrap();
348 assert_eq!(deserialized, Input::Timer);
349
350 let states = vec![State::Red, State::Yellow, State::Green];
352 let serialized = serde_json::to_string(&states).unwrap();
353 let deserialized: Vec<State> = serde_json::from_str(&serialized).unwrap();
354 assert_eq!(deserialized, states);
355 }
356}