1pub mod core;
54pub mod doc;
55pub mod instance;
56pub mod macros;
57pub mod query;
58
59pub use core::StateMachine;
61pub use doc::StateMachineDoc;
62pub use instance::StateMachineInstance;
63pub use query::StateMachineQuery;
64
65pub const DEFAULT_MAX_HISTORY_SIZE: usize = 512;
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 define_state_machine! {
74 name: TrafficLight,
75 states: { Red, Yellow, Green },
76 inputs: { Timer, Emergency },
77 initial: Red,
78 transitions: {
79 Red + Timer => Green,
80 Green + Timer => Yellow,
81 Yellow + Timer => Red,
82 Red + Emergency => Yellow,
83 Green + Emergency => Red,
84 Yellow + Emergency => Red
85 }
86 }
87
88 mod test_machine {
90 use super::super::*;
91
92 define_state_machine! {
93 name: TestMachine,
94 states: { StateA, StateB },
95 inputs: { Action, _HiddenAction, _Debug },
96 initial: StateA,
97 transitions: {
98 StateA + Action => StateB,
99 StateB + Action => StateA,
100 StateA + _HiddenAction => StateA,
101 StateB + _HiddenAction => StateB,
102 StateA + _Debug => StateA,
103 StateB + _Debug => StateB
104 }
105 }
106 }
107
108 #[test]
109 fn test_deterministic_state_machine_basic() {
110 let mut sm = StateMachineInstance::<TrafficLight>::new();
111 assert_eq!(*sm.current_state(), State::Red);
112
113 let result = sm.transition(Input::Timer);
115 assert!(result.is_ok());
116 assert_eq!(*sm.current_state(), State::Green);
117 assert_eq!(result.unwrap(), State::Green);
118
119 let result = sm.transition(Input::Timer);
121 assert!(result.is_ok());
122 assert_eq!(*sm.current_state(), State::Yellow);
123 }
124
125 #[test]
126 fn test_invalid_transition() {
127 let mut sm = StateMachineInstance::<TrafficLight>::new();
128 assert_eq!(*sm.current_state(), State::Red);
129
130 let result = sm.transition(Input::Emergency);
132 assert!(result.is_ok());
133 assert_eq!(*sm.current_state(), State::Yellow);
134 }
135
136 #[test]
137 fn test_state_machine_instance_methods() {
138 let mut sm = StateMachineInstance::<TrafficLight>::new();
139
140 assert_eq!(*sm.current_state(), State::Red);
142 assert!(sm.history_is_empty());
143 assert_eq!(sm.history_len(), 0);
144
145 assert!(sm.can_accept(&Input::Timer));
147 assert!(sm.can_accept(&Input::Emergency));
148
149 let valid_inputs = sm.valid_inputs();
151 assert!(valid_inputs.contains(&Input::Timer));
152 assert!(valid_inputs.contains(&Input::Emergency));
153
154 sm.transition(Input::Timer).unwrap();
156 assert_eq!(sm.history_len(), 1);
157 assert!(!sm.history_is_empty());
158
159 sm.reset();
161 assert_eq!(*sm.current_state(), State::Red);
162 assert!(sm.history_is_empty());
163 }
164
165 #[test]
166 fn test_query_functions() {
167 let reachable = StateMachineQuery::<TrafficLight>::reachable_states(&State::Red);
168 assert!(reachable.contains(&State::Green));
169 assert!(reachable.contains(&State::Yellow));
170
171 let leading_to_red = StateMachineQuery::<TrafficLight>::states_leading_to(&State::Red);
172 assert!(leading_to_red.contains(&State::Yellow));
173 assert!(leading_to_red.contains(&State::Green));
174
175 assert!(StateMachineQuery::<TrafficLight>::has_path(
177 &State::Red,
178 &State::Green
179 ));
180
181 let path = StateMachineQuery::<TrafficLight>::shortest_path(&State::Red, &State::Green);
183 assert!(path.is_some());
184 let path = path.unwrap();
185 assert_eq!(path[0], State::Red);
186 assert_eq!(path[1], State::Green);
187 }
188
189 #[test]
190 fn test_mermaid_generation() {
191 let mermaid = StateMachineDoc::<TrafficLight>::generate_mermaid();
192 assert!(mermaid.contains("stateDiagram-v2"));
193 assert!(mermaid.contains("Red"));
194 assert!(mermaid.contains("Green"));
195 assert!(mermaid.contains("Yellow"));
196 assert!(mermaid.contains("Timer"));
197 assert!(mermaid.contains("Emergency"));
198 }
199
200 #[test]
201 fn test_history_size_limit() {
202 let mut sm = StateMachineInstance::<TrafficLight>::with_max_history(2);
203 assert_eq!(sm.max_history_size(), 2);
204
205 sm.transition(Input::Timer).unwrap(); sm.transition(Input::Timer).unwrap(); sm.transition(Input::Timer).unwrap(); assert_eq!(sm.history().len(), 2);
212 assert_eq!(sm.history()[0], (State::Green, Input::Timer));
213 assert_eq!(sm.history()[1], (State::Yellow, Input::Timer));
214 }
215
216 #[test]
217 fn test_history_size_default() {
218 let sm = StateMachineInstance::<TrafficLight>::new();
219 assert_eq!(sm.max_history_size(), DEFAULT_MAX_HISTORY_SIZE);
220
221 let sm_default = StateMachineInstance::<TrafficLight>::default();
222 assert_eq!(sm_default.max_history_size(), DEFAULT_MAX_HISTORY_SIZE);
223 }
224
225 #[test]
226 fn test_underscore_inputs_excluded_from_docs() {
227 let mermaid = StateMachineDoc::<test_machine::TestMachine>::generate_mermaid();
228
229 assert!(mermaid.contains("Action"));
231
232 assert!(!mermaid.contains("_HiddenAction"));
234 assert!(!mermaid.contains("_Debug"));
235
236 let table = StateMachineDoc::<test_machine::TestMachine>::generate_transition_table();
237
238 assert!(table.contains("Action"));
240
241 assert!(!table.contains("_HiddenAction"));
243 assert!(!table.contains("_Debug"));
244 }
245
246 #[test]
247 fn test_underscore_inputs_still_functional() {
248 use test_machine::{Input, State, TestMachine};
249
250 let mut sm = StateMachineInstance::<TestMachine>::new();
251 assert_eq!(*sm.current_state(), State::StateA);
252
253 let valid_inputs = sm.valid_inputs();
255 assert!(valid_inputs.contains(&Input::Action));
256 assert!(valid_inputs.contains(&Input::_HiddenAction));
257 assert!(valid_inputs.contains(&Input::_Debug));
258
259 let result = sm.transition(Input::_HiddenAction);
261 assert!(result.is_ok());
262 assert_eq!(*sm.current_state(), State::StateA);
263
264 let result = sm.transition(Input::_Debug);
265 assert!(result.is_ok());
266 assert_eq!(*sm.current_state(), State::StateA);
267
268 let result = sm.transition(Input::Action);
270 assert!(result.is_ok());
271 assert_eq!(*sm.current_state(), State::StateB);
272 }
273
274 #[test]
275 fn test_display_implementation() {
276 assert_eq!(State::Red.to_string(), "Red");
277 assert_eq!(Input::Timer.to_string(), "Timer");
278 }
279
280 #[test]
281 fn test_documentation_generation() {
282 let stats = StateMachineDoc::<TrafficLight>::generate_statistics();
283 assert!(stats.contains("Number of States"));
284 assert!(stats.contains("Number of Transitions"));
285
286 let full_doc = StateMachineDoc::<TrafficLight>::generate_full_documentation();
287 assert!(full_doc.contains("State Machine Documentation"));
288 assert!(full_doc.contains("State Transition Table"));
289 assert!(full_doc.contains("State Diagram"));
290 }
291
292 #[cfg(feature = "serde")]
293 #[test]
294 fn test_serde_serialization() {
295 let state = State::Red;
297 let serialized = serde_json::to_string(&state).unwrap();
298 assert_eq!(serialized, "\"Red\"");
299
300 let deserialized: State = serde_json::from_str(&serialized).unwrap();
301 assert_eq!(deserialized, State::Red);
302
303 let input = Input::Timer;
305 let serialized = serde_json::to_string(&input).unwrap();
306 assert_eq!(serialized, "\"Timer\"");
307
308 let deserialized: Input = serde_json::from_str(&serialized).unwrap();
309 assert_eq!(deserialized, Input::Timer);
310
311 let states = vec![State::Red, State::Yellow, State::Green];
313 let serialized = serde_json::to_string(&states).unwrap();
314 let deserialized: Vec<State> = serde_json::from_str(&serialized).unwrap();
315 assert_eq!(deserialized, states);
316 }
317}