1use super::executor::{ActionExecutor, ExecutorError, PlaybookExecutor};
11use super::schema::{OutputAssertion, PathAssertion, Playbook, PlaybookAction, PlaybookStep};
12use std::collections::HashMap;
13use std::time::{Duration, Instant};
14
15#[derive(Debug)]
17pub struct PlaybookRunResult {
18 pub passed: bool,
20 pub variables: HashMap<String, String>,
22 pub state_path: Vec<String>,
24 pub step_results: Vec<StepResult>,
26 pub assertion_results: Vec<AssertionCheckResult>,
28 pub total_time: Duration,
30 pub error: Option<String>,
32}
33
34#[derive(Debug, Clone)]
36pub struct StepResult {
37 pub name: String,
39 pub passed: bool,
41 pub duration: Duration,
43 pub captured: HashMap<String, String>,
45 pub error: Option<String>,
47}
48
49#[derive(Debug, Clone)]
51pub struct AssertionCheckResult {
52 pub description: String,
54 pub passed: bool,
56 pub error: Option<String>,
58}
59
60pub struct PlaybookRunner<E: ActionExecutor> {
62 playbook: Playbook,
63 #[allow(dead_code)] executor: PlaybookExecutor<E>,
65 variables: HashMap<String, String>,
66 state_path: Vec<String>,
67}
68
69impl<E: ActionExecutor> PlaybookRunner<E> {
70 pub fn new(playbook: Playbook, executor: E) -> Self {
72 let initial = playbook.machine.initial.clone();
73 let pb_executor = PlaybookExecutor::new(playbook.clone(), executor);
74
75 Self {
76 playbook,
77 executor: pb_executor,
78 variables: HashMap::new(),
79 state_path: vec![initial],
80 }
81 }
82
83 pub fn run(&mut self) -> PlaybookRunResult {
85 let start = Instant::now();
86 let mut step_results = Vec::new();
87 let mut passed = true;
88 let mut error_msg: Option<String> = None;
89
90 let steps = self.playbook.playbook.clone().unwrap_or_default();
92
93 if let Err(e) = self.run_setup(&steps.setup) {
95 error_msg = Some(format!("Setup failed: {}", e));
96 passed = false;
97 }
98
99 if passed {
101 for step in &steps.steps {
102 match self.run_step(step) {
103 Ok(result) => {
104 if !result.passed {
105 passed = false;
106 error_msg = result.error.clone();
107 }
108 step_results.push(result);
109 if !passed {
110 break;
111 }
112 }
113 Err(e) => {
114 passed = false;
115 error_msg = Some(e.to_string());
116 step_results.push(StepResult {
117 name: step.name.clone(),
118 passed: false,
119 duration: Duration::ZERO,
120 captured: HashMap::new(),
121 error: Some(e.to_string()),
122 });
123 break;
124 }
125 }
126 }
127 }
128
129 let _ = self.run_teardown(&steps.teardown);
131
132 let assertion_results = self.check_assertions();
134 if assertion_results.iter().any(|a| !a.passed) {
135 passed = false;
136 if error_msg.is_none() {
137 error_msg = Some("Assertions failed".to_string());
138 }
139 }
140
141 PlaybookRunResult {
142 passed,
143 variables: self.variables.clone(),
144 state_path: self.state_path.clone(),
145 step_results,
146 assertion_results,
147 total_time: start.elapsed(),
148 error: error_msg,
149 }
150 }
151
152 fn run_setup(&self, setup: &[PlaybookAction]) -> Result<(), ExecutorError> {
154 for action in setup {
155 self.run_action(action)?;
156 }
157 Ok(())
158 }
159
160 fn run_teardown(&self, teardown: &[PlaybookAction]) -> Result<(), ExecutorError> {
162 for action in teardown {
163 if action.ignore_errors {
164 let _ = self.run_action(action);
165 } else {
166 self.run_action(action)?;
167 }
168 }
169 Ok(())
170 }
171
172 fn run_action(&self, _action: &PlaybookAction) -> Result<(), ExecutorError> {
174 Ok(())
176 }
177
178 fn run_step(&mut self, step: &PlaybookStep) -> Result<StepResult, ExecutorError> {
180 let start = Instant::now();
181 let mut captured = HashMap::new();
182
183 for transition_id in &step.transitions {
185 let transition = self
187 .playbook
188 .machine
189 .transitions
190 .iter()
191 .find(|t| &t.id == transition_id);
192
193 if let Some(t) = transition {
194 if let Some(err) = self.check_forbidden(&t.from, &t.to) {
196 return Ok(StepResult {
197 name: step.name.clone(),
198 passed: false,
199 duration: start.elapsed(),
200 captured,
201 error: Some(err),
202 });
203 }
204
205 self.state_path.push(t.to.clone());
207 }
208 }
209
210 for capture in &step.capture {
212 let value = self.substitute_variables(&capture.from);
214 captured.insert(capture.var.clone(), value.clone());
215 self.variables.insert(capture.var.clone(), value);
216 }
217
218 Ok(StepResult {
219 name: step.name.clone(),
220 passed: true,
221 duration: start.elapsed(),
222 captured,
223 error: None,
224 })
225 }
226
227 fn check_forbidden(&self, from: &str, to: &str) -> Option<String> {
229 for forbidden in &self.playbook.machine.forbidden {
230 if forbidden.from == from && forbidden.to == to {
231 return Some(format!(
232 "Forbidden transition: {} -> {} ({})",
233 from, to, forbidden.reason
234 ));
235 }
236 }
237 None
238 }
239
240 fn substitute_variables(&self, input: &str) -> String {
242 let mut result = input.to_string();
243 for (key, value) in &self.variables {
244 let pattern = format!("${{{}}}", key);
245 result = result.replace(&pattern, value);
246 }
247 result
248 }
249
250 fn check_assertions(&self) -> Vec<AssertionCheckResult> {
252 let mut results = Vec::new();
253
254 if let Some(assertions) = &self.playbook.assertions {
255 if let Some(path) = &assertions.path {
257 results.push(self.check_path_assertion(path));
258 }
259
260 for output in &assertions.output {
262 results.push(self.check_output_assertion(output));
263 }
264 }
265
266 results
267 }
268
269 fn check_path_assertion(&self, path: &PathAssertion) -> AssertionCheckResult {
271 let actual_path: Vec<&str> = self.state_path.iter().map(|s| s.as_str()).collect();
272 let expected_path: Vec<&str> = path.expected.iter().map(|s| s.as_str()).collect();
273
274 if actual_path == expected_path {
275 AssertionCheckResult {
276 description: "Path matches expected sequence".to_string(),
277 passed: true,
278 error: None,
279 }
280 } else {
281 AssertionCheckResult {
282 description: "Path matches expected sequence".to_string(),
283 passed: false,
284 error: Some(format!(
285 "Expected path {:?}, got {:?}",
286 expected_path, actual_path
287 )),
288 }
289 }
290 }
291
292 fn check_output_assertion(&self, output: &OutputAssertion) -> AssertionCheckResult {
294 let value = self.variables.get(&output.var);
295
296 if output.not_empty == Some(true) && value.map_or(true, String::is_empty) {
298 return AssertionCheckResult {
299 description: format!("Variable '{}' is not empty", output.var),
300 passed: false,
301 error: Some(format!("Variable '{}' is empty or undefined", output.var)),
302 };
303 }
304
305 if let Some(pattern) = &output.matches {
307 if let Some(val) = value {
308 if let Ok(re) = regex::Regex::new(pattern) {
309 if !re.is_match(val) {
310 return AssertionCheckResult {
311 description: format!("Variable '{}' matches '{}'", output.var, pattern),
312 passed: false,
313 error: Some(format!(
314 "Value '{}' does not match pattern '{}'",
315 val, pattern
316 )),
317 };
318 }
319 }
320 } else {
321 return AssertionCheckResult {
322 description: format!("Variable '{}' matches '{}'", output.var, pattern),
323 passed: false,
324 error: Some(format!("Variable '{}' is undefined", output.var)),
325 };
326 }
327 }
328
329 if let Some(max) = output.less_than {
331 if let Some(val) = value {
332 if let Ok(num) = val.parse::<i64>() {
333 if num >= max {
334 return AssertionCheckResult {
335 description: format!("Variable '{}' < {}", output.var, max),
336 passed: false,
337 error: Some(format!("{} is not less than {}", num, max)),
338 };
339 }
340 }
341 }
342 }
343
344 if let Some(min) = output.greater_than {
346 if let Some(val) = value {
347 if let Ok(num) = val.parse::<i64>() {
348 if num <= min {
349 return AssertionCheckResult {
350 description: format!("Variable '{}' > {}", output.var, min),
351 passed: false,
352 error: Some(format!("{} is not greater than {}", num, min)),
353 };
354 }
355 }
356 }
357 }
358
359 if let Some(expected) = &output.equals {
361 if value != Some(expected) {
362 return AssertionCheckResult {
363 description: format!("Variable '{}' equals '{}'", output.var, expected),
364 passed: false,
365 error: Some(format!(
366 "Expected '{}', got '{}'",
367 expected,
368 value.map_or("undefined", String::as_str)
369 )),
370 };
371 }
372 }
373
374 AssertionCheckResult {
375 description: format!("Variable '{}' assertion", output.var),
376 passed: true,
377 error: None,
378 }
379 }
380
381 pub fn export_trace_json(&self) -> String {
383 serde_json::json!({
384 "playbook": self.playbook.name,
385 "state_path": self.state_path,
386 "variables": self.variables,
387 })
388 .to_string()
389 }
390}
391
392pub fn to_svg(playbook: &Playbook) -> String {
394 let dot = super::state_machine::to_dot(playbook);
395
396 let mut svg = String::from(
398 r##"<?xml version="1.0" encoding="UTF-8"?>
399<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600">
400 <style>
401 .state { fill: #e0e0e0; stroke: #333; stroke-width: 2; }
402 .state-final { fill: #c8e6c9; }
403 .transition { stroke: #333; stroke-width: 1.5; fill: none; marker-end: url(#arrow); }
404 .label { font-family: sans-serif; font-size: 12px; }
405 </style>
406 <defs>
407 <marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
408 <polygon points="0 0, 10 3.5, 0 7" fill="#333"/>
409 </marker>
410 </defs>
411 <text x="10" y="20" class="label">State Machine: "##,
412 );
413
414 svg.push_str(&playbook.machine.id);
415 svg.push_str("</text>\n");
416
417 let mut y_offset = 100;
419 for (id, state) in &playbook.machine.states {
420 let class = if state.final_state {
421 "state state-final"
422 } else {
423 "state"
424 };
425 svg.push_str(&format!(
426 r#" <ellipse cx="400" cy="{}" rx="60" ry="30" class="{}"/>
427 <text x="400" y="{}" text-anchor="middle" class="label">{}</text>
428"#,
429 y_offset,
430 class,
431 y_offset + 5,
432 id
433 ));
434 y_offset += 100;
435 }
436
437 svg.push_str(&format!(
439 "\n <!-- DOT source:\n{}\n -->\n",
440 dot.lines()
441 .map(|l| format!(" {}", l))
442 .collect::<Vec<_>>()
443 .join("\n")
444 ));
445
446 svg.push_str("</svg>");
447 svg
448}
449
450#[cfg(test)]
451mod tests {
452 use super::*;
453 use crate::playbook::schema::Playbook;
454
455 struct MockExecutor;
456
457 impl ActionExecutor for MockExecutor {
458 fn click(&mut self, _: &str) -> Result<(), ExecutorError> {
459 Ok(())
460 }
461 fn type_text(&mut self, _: &str, _: &str) -> Result<(), ExecutorError> {
462 Ok(())
463 }
464 fn wait(
465 &mut self,
466 _: &crate::playbook::schema::WaitCondition,
467 ) -> Result<(), ExecutorError> {
468 Ok(())
469 }
470 fn navigate(&mut self, _: &str) -> Result<(), ExecutorError> {
471 Ok(())
472 }
473 fn execute_script(&mut self, _: &str) -> Result<String, ExecutorError> {
474 Ok(String::new())
475 }
476 fn screenshot(&mut self, _: &str) -> Result<(), ExecutorError> {
477 Ok(())
478 }
479 fn element_exists(&self, _: &str) -> Result<bool, ExecutorError> {
480 Ok(true)
481 }
482 fn get_text(&self, _: &str) -> Result<String, ExecutorError> {
483 Ok(String::new())
484 }
485 fn get_attribute(&self, _: &str, _: &str) -> Result<String, ExecutorError> {
486 Ok(String::new())
487 }
488 fn get_url(&self) -> Result<String, ExecutorError> {
489 Ok(String::new())
490 }
491 fn evaluate(&self, _: &str) -> Result<bool, ExecutorError> {
492 Ok(true)
493 }
494 }
495
496 #[test]
497 fn test_forbidden_transition_detection() {
498 let yaml = r##"
499version: "1.0"
500name: "Test Playbook"
501machine:
502 id: "test"
503 initial: "start"
504 states:
505 start:
506 id: "start"
507 middle:
508 id: "middle"
509 end:
510 id: "end"
511 final_state: true
512 transitions:
513 - id: "t1"
514 from: "start"
515 to: "middle"
516 event: "go"
517 - id: "t2"
518 from: "middle"
519 to: "end"
520 event: "finish"
521 forbidden:
522 - from: "start"
523 to: "end"
524 reason: "Cannot skip middle state"
525"##;
526 let playbook = Playbook::from_yaml(yaml).expect("parse");
527 let runner = PlaybookRunner::new(playbook, MockExecutor);
528
529 let err = runner.check_forbidden("start", "end");
531 assert!(err.is_some());
532 assert!(err
533 .expect("should have error")
534 .contains("Cannot skip middle state"));
535
536 let ok = runner.check_forbidden("start", "middle");
538 assert!(ok.is_none());
539 }
540
541 #[test]
542 fn test_variable_substitution() {
543 let yaml = r##"
544version: "1.0"
545machine:
546 id: "test"
547 initial: "start"
548 states:
549 start:
550 id: "start"
551 transitions:
552 - id: "t1"
553 from: "start"
554 to: "start"
555 event: "loop"
556"##;
557 let playbook = Playbook::from_yaml(yaml).expect("parse");
558 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
559
560 runner
561 .variables
562 .insert("name".to_string(), "test".to_string());
563 runner
564 .variables
565 .insert("value".to_string(), "123".to_string());
566
567 let result = runner.substitute_variables("Hello ${name}, value is ${value}");
568 assert_eq!(result, "Hello test, value is 123");
569 }
570
571 #[test]
572 fn test_svg_export() {
573 let yaml = r##"
574version: "1.0"
575machine:
576 id: "test_machine"
577 initial: "start"
578 states:
579 start:
580 id: "start"
581 end:
582 id: "end"
583 final_state: true
584 transitions:
585 - id: "t1"
586 from: "start"
587 to: "end"
588 event: "finish"
589"##;
590 let playbook = Playbook::from_yaml(yaml).expect("parse");
591 let svg = to_svg(&playbook);
592
593 assert!(svg.contains("<svg"));
594 assert!(svg.contains("test_machine"));
595 assert!(svg.contains("</svg>"));
596 }
597
598 #[test]
599 fn test_run_empty_playbook() {
600 let yaml = r##"
601version: "1.0"
602machine:
603 id: "test"
604 initial: "start"
605 states:
606 start:
607 id: "start"
608 transitions:
609 - id: "t_loop"
610 from: "start"
611 to: "start"
612 event: "noop"
613"##;
614 let playbook = Playbook::from_yaml(yaml).expect("parse");
615 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
616 let result = runner.run();
617
618 assert!(result.passed);
619 assert!(result.error.is_none());
620 assert_eq!(result.state_path, vec!["start"]);
621 }
622
623 #[test]
624 fn test_run_with_steps_and_transitions() {
625 let yaml = r##"
626version: "1.0"
627machine:
628 id: "test"
629 initial: "start"
630 states:
631 start:
632 id: "start"
633 middle:
634 id: "middle"
635 end:
636 id: "end"
637 final_state: true
638 transitions:
639 - id: "t1"
640 from: "start"
641 to: "middle"
642 event: "go"
643 - id: "t2"
644 from: "middle"
645 to: "end"
646 event: "finish"
647playbook:
648 setup: []
649 steps:
650 - name: "Go to middle"
651 transitions: ["t1"]
652 capture: []
653 - name: "Go to end"
654 transitions: ["t2"]
655 capture: []
656 teardown: []
657"##;
658 let playbook = Playbook::from_yaml(yaml).expect("parse");
659 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
660 let result = runner.run();
661
662 assert!(result.passed);
663 assert_eq!(result.state_path, vec!["start", "middle", "end"]);
664 assert_eq!(result.step_results.len(), 2);
665 }
666
667 #[test]
668 fn test_run_with_variable_capture() {
669 let yaml = r##"
670version: "1.0"
671machine:
672 id: "test"
673 initial: "start"
674 states:
675 start:
676 id: "start"
677 transitions:
678 - id: "t1"
679 from: "start"
680 to: "start"
681 event: "loop"
682playbook:
683 setup: []
684 steps:
685 - name: "Capture step"
686 transitions: ["t1"]
687 capture:
688 - var: "captured_val"
689 from: "test_value"
690 teardown: []
691"##;
692 let playbook = Playbook::from_yaml(yaml).expect("parse");
693 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
694 let result = runner.run();
695
696 assert!(result.passed);
697 assert_eq!(
698 result.variables.get("captured_val"),
699 Some(&"test_value".to_string())
700 );
701 }
702
703 #[test]
704 fn test_run_forbidden_transition_fails() {
705 let yaml = r##"
706version: "1.0"
707machine:
708 id: "test"
709 initial: "start"
710 states:
711 start:
712 id: "start"
713 end:
714 id: "end"
715 final_state: true
716 transitions:
717 - id: "forbidden_t"
718 from: "start"
719 to: "end"
720 event: "skip"
721 forbidden:
722 - from: "start"
723 to: "end"
724 reason: "Cannot skip"
725playbook:
726 setup: []
727 steps:
728 - name: "Try forbidden"
729 transitions: ["forbidden_t"]
730 capture: []
731 teardown: []
732"##;
733 let playbook = Playbook::from_yaml(yaml).expect("parse");
734 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
735 let result = runner.run();
736
737 assert!(!result.passed);
738 assert!(result.step_results[0]
739 .error
740 .as_ref()
741 .expect("should have error")
742 .contains("Forbidden"));
743 }
744
745 #[test]
746 fn test_path_assertion_pass() {
747 let yaml = r##"
748version: "1.0"
749machine:
750 id: "test"
751 initial: "start"
752 states:
753 start:
754 id: "start"
755 end:
756 id: "end"
757 transitions:
758 - id: "t1"
759 from: "start"
760 to: "end"
761 event: "go"
762playbook:
763 setup: []
764 steps:
765 - name: "Go"
766 transitions: ["t1"]
767 capture: []
768 teardown: []
769assertions:
770 path:
771 expected: ["start", "end"]
772 output: []
773"##;
774 let playbook = Playbook::from_yaml(yaml).expect("parse");
775 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
776 let result = runner.run();
777
778 assert!(result.passed);
779 assert!(result.assertion_results.iter().all(|a| a.passed));
780 }
781
782 #[test]
783 fn test_path_assertion_fail() {
784 let yaml = r##"
785version: "1.0"
786machine:
787 id: "test"
788 initial: "start"
789 states:
790 start:
791 id: "start"
792 end:
793 id: "end"
794 transitions:
795 - id: "t_loop"
796 from: "start"
797 to: "start"
798 event: "noop"
799assertions:
800 path:
801 expected: ["start", "end"]
802 output: []
803"##;
804 let playbook = Playbook::from_yaml(yaml).expect("parse");
805 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
806 let result = runner.run();
807
808 assert!(!result.passed);
809 assert!(result.assertion_results.iter().any(|a| !a.passed));
810 }
811
812 #[test]
813 fn test_output_assertion_not_empty() {
814 let yaml = r##"
815version: "1.0"
816machine:
817 id: "test"
818 initial: "start"
819 states:
820 start:
821 id: "start"
822 transitions:
823 - id: "t1"
824 from: "start"
825 to: "start"
826 event: "loop"
827playbook:
828 setup: []
829 steps:
830 - name: "Capture"
831 transitions: ["t1"]
832 capture:
833 - var: "my_var"
834 from: "some_value"
835 teardown: []
836assertions:
837 output:
838 - var: "my_var"
839 not_empty: true
840"##;
841 let playbook = Playbook::from_yaml(yaml).expect("parse");
842 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
843 let result = runner.run();
844
845 assert!(result.passed);
846 }
847
848 #[test]
849 fn test_output_assertion_not_empty_fails() {
850 let yaml = r##"
851version: "1.0"
852machine:
853 id: "test"
854 initial: "start"
855 states:
856 start:
857 id: "start"
858 transitions:
859 - id: "t_loop"
860 from: "start"
861 to: "start"
862 event: "noop"
863assertions:
864 output:
865 - var: "missing_var"
866 not_empty: true
867"##;
868 let playbook = Playbook::from_yaml(yaml).expect("parse");
869 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
870 let result = runner.run();
871
872 assert!(!result.passed);
873 }
874
875 #[test]
876 fn test_output_assertion_matches() {
877 let yaml = r##"
878version: "1.0"
879machine:
880 id: "test"
881 initial: "start"
882 states:
883 start:
884 id: "start"
885 transitions:
886 - id: "t1"
887 from: "start"
888 to: "start"
889 event: "loop"
890playbook:
891 setup: []
892 steps:
893 - name: "Capture"
894 transitions: ["t1"]
895 capture:
896 - var: "email"
897 from: "test@example.com"
898 teardown: []
899assertions:
900 output:
901 - var: "email"
902 matches: ".*@.*\\.com"
903"##;
904 let playbook = Playbook::from_yaml(yaml).expect("parse");
905 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
906 let result = runner.run();
907
908 assert!(result.passed);
909 }
910
911 #[test]
912 fn test_output_assertion_matches_fails() {
913 let yaml = r##"
914version: "1.0"
915machine:
916 id: "test"
917 initial: "start"
918 states:
919 start:
920 id: "start"
921 transitions:
922 - id: "t1"
923 from: "start"
924 to: "start"
925 event: "loop"
926playbook:
927 setup: []
928 steps:
929 - name: "Capture"
930 transitions: ["t1"]
931 capture:
932 - var: "value"
933 from: "abc"
934 teardown: []
935assertions:
936 output:
937 - var: "value"
938 matches: "^[0-9]+$"
939"##;
940 let playbook = Playbook::from_yaml(yaml).expect("parse");
941 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
942 let result = runner.run();
943
944 assert!(!result.passed);
945 }
946
947 #[test]
948 fn test_output_assertion_matches_undefined() {
949 let yaml = r##"
950version: "1.0"
951machine:
952 id: "test"
953 initial: "start"
954 states:
955 start:
956 id: "start"
957 transitions:
958 - id: "t_loop"
959 from: "start"
960 to: "start"
961 event: "noop"
962assertions:
963 output:
964 - var: "undefined_var"
965 matches: ".*"
966"##;
967 let playbook = Playbook::from_yaml(yaml).expect("parse");
968 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
969 let result = runner.run();
970
971 assert!(!result.passed);
972 }
973
974 #[test]
975 fn test_output_assertion_less_than() {
976 let yaml = r##"
977version: "1.0"
978machine:
979 id: "test"
980 initial: "start"
981 states:
982 start:
983 id: "start"
984 transitions:
985 - id: "t1"
986 from: "start"
987 to: "start"
988 event: "loop"
989playbook:
990 setup: []
991 steps:
992 - name: "Capture"
993 transitions: ["t1"]
994 capture:
995 - var: "count"
996 from: "5"
997 teardown: []
998assertions:
999 output:
1000 - var: "count"
1001 less_than: 10
1002"##;
1003 let playbook = Playbook::from_yaml(yaml).expect("parse");
1004 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1005 let result = runner.run();
1006
1007 assert!(result.passed);
1008 }
1009
1010 #[test]
1011 fn test_output_assertion_less_than_fails() {
1012 let yaml = r##"
1013version: "1.0"
1014machine:
1015 id: "test"
1016 initial: "start"
1017 states:
1018 start:
1019 id: "start"
1020 transitions:
1021 - id: "t1"
1022 from: "start"
1023 to: "start"
1024 event: "loop"
1025playbook:
1026 setup: []
1027 steps:
1028 - name: "Capture"
1029 transitions: ["t1"]
1030 capture:
1031 - var: "count"
1032 from: "15"
1033 teardown: []
1034assertions:
1035 output:
1036 - var: "count"
1037 less_than: 10
1038"##;
1039 let playbook = Playbook::from_yaml(yaml).expect("parse");
1040 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1041 let result = runner.run();
1042
1043 assert!(!result.passed);
1044 }
1045
1046 #[test]
1047 fn test_output_assertion_greater_than() {
1048 let yaml = r##"
1049version: "1.0"
1050machine:
1051 id: "test"
1052 initial: "start"
1053 states:
1054 start:
1055 id: "start"
1056 transitions:
1057 - id: "t1"
1058 from: "start"
1059 to: "start"
1060 event: "loop"
1061playbook:
1062 setup: []
1063 steps:
1064 - name: "Capture"
1065 transitions: ["t1"]
1066 capture:
1067 - var: "count"
1068 from: "100"
1069 teardown: []
1070assertions:
1071 output:
1072 - var: "count"
1073 greater_than: 50
1074"##;
1075 let playbook = Playbook::from_yaml(yaml).expect("parse");
1076 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1077 let result = runner.run();
1078
1079 assert!(result.passed);
1080 }
1081
1082 #[test]
1083 fn test_output_assertion_greater_than_fails() {
1084 let yaml = r##"
1085version: "1.0"
1086machine:
1087 id: "test"
1088 initial: "start"
1089 states:
1090 start:
1091 id: "start"
1092 transitions:
1093 - id: "t1"
1094 from: "start"
1095 to: "start"
1096 event: "loop"
1097playbook:
1098 setup: []
1099 steps:
1100 - name: "Capture"
1101 transitions: ["t1"]
1102 capture:
1103 - var: "count"
1104 from: "10"
1105 teardown: []
1106assertions:
1107 output:
1108 - var: "count"
1109 greater_than: 50
1110"##;
1111 let playbook = Playbook::from_yaml(yaml).expect("parse");
1112 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1113 let result = runner.run();
1114
1115 assert!(!result.passed);
1116 }
1117
1118 #[test]
1119 fn test_output_assertion_equals() {
1120 let yaml = r##"
1121version: "1.0"
1122machine:
1123 id: "test"
1124 initial: "start"
1125 states:
1126 start:
1127 id: "start"
1128 transitions:
1129 - id: "t1"
1130 from: "start"
1131 to: "start"
1132 event: "loop"
1133playbook:
1134 setup: []
1135 steps:
1136 - name: "Capture"
1137 transitions: ["t1"]
1138 capture:
1139 - var: "result"
1140 from: "success"
1141 teardown: []
1142assertions:
1143 output:
1144 - var: "result"
1145 equals: "success"
1146"##;
1147 let playbook = Playbook::from_yaml(yaml).expect("parse");
1148 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1149 let result = runner.run();
1150
1151 assert!(result.passed);
1152 }
1153
1154 #[test]
1155 fn test_output_assertion_equals_fails() {
1156 let yaml = r##"
1157version: "1.0"
1158machine:
1159 id: "test"
1160 initial: "start"
1161 states:
1162 start:
1163 id: "start"
1164 transitions:
1165 - id: "t1"
1166 from: "start"
1167 to: "start"
1168 event: "loop"
1169playbook:
1170 setup: []
1171 steps:
1172 - name: "Capture"
1173 transitions: ["t1"]
1174 capture:
1175 - var: "result"
1176 from: "failure"
1177 teardown: []
1178assertions:
1179 output:
1180 - var: "result"
1181 equals: "success"
1182"##;
1183 let playbook = Playbook::from_yaml(yaml).expect("parse");
1184 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1185 let result = runner.run();
1186
1187 assert!(!result.passed);
1188 }
1189
1190 #[test]
1191 fn test_export_trace_json() {
1192 let yaml = r##"
1193version: "1.0"
1194name: "Trace Test"
1195machine:
1196 id: "test"
1197 initial: "start"
1198 states:
1199 start:
1200 id: "start"
1201 end:
1202 id: "end"
1203 transitions:
1204 - id: "t1"
1205 from: "start"
1206 to: "end"
1207 event: "go"
1208playbook:
1209 setup: []
1210 steps:
1211 - name: "Go"
1212 transitions: ["t1"]
1213 capture:
1214 - var: "test_var"
1215 from: "test_value"
1216 teardown: []
1217"##;
1218 let playbook = Playbook::from_yaml(yaml).expect("parse");
1219 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1220 runner.run();
1221
1222 let json = runner.export_trace_json();
1223 assert!(json.contains("Trace Test"));
1224 assert!(json.contains("state_path"));
1225 assert!(json.contains("test_var"));
1226 }
1227
1228 #[test]
1229 fn test_teardown_with_ignore_errors() {
1230 let yaml = r##"
1231version: "1.0"
1232machine:
1233 id: "test"
1234 initial: "start"
1235 states:
1236 start:
1237 id: "start"
1238 transitions:
1239 - id: "t_loop"
1240 from: "start"
1241 to: "start"
1242 event: "noop"
1243playbook:
1244 setup: []
1245 steps: []
1246 teardown:
1247 - action:
1248 wasm: "cleanup"
1249 args: []
1250 ignore_errors: true
1251"##;
1252 let playbook = Playbook::from_yaml(yaml).expect("parse");
1253 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1254 let result = runner.run();
1255
1256 assert!(result.passed);
1257 }
1258
1259 #[test]
1260 fn test_run_step_with_nonexistent_transition() {
1261 let yaml = r##"
1262version: "1.0"
1263machine:
1264 id: "test"
1265 initial: "start"
1266 states:
1267 start:
1268 id: "start"
1269 transitions:
1270 - id: "t_loop"
1271 from: "start"
1272 to: "start"
1273 event: "noop"
1274playbook:
1275 setup: []
1276 steps:
1277 - name: "Bad transition"
1278 transitions: ["nonexistent"]
1279 capture: []
1280 teardown: []
1281"##;
1282 let playbook = Playbook::from_yaml(yaml).expect("parse");
1283 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1284 let result = runner.run();
1285
1286 assert!(result.passed);
1288 }
1289
1290 #[test]
1291 fn test_step_with_multiple_transitions() {
1292 let yaml = r##"
1293version: "1.0"
1294machine:
1295 id: "test"
1296 initial: "a"
1297 states:
1298 a:
1299 id: "a"
1300 b:
1301 id: "b"
1302 c:
1303 id: "c"
1304 final_state: true
1305 transitions:
1306 - id: "t1"
1307 from: "a"
1308 to: "b"
1309 event: "step1"
1310 - id: "t2"
1311 from: "b"
1312 to: "c"
1313 event: "step2"
1314playbook:
1315 setup: []
1316 steps:
1317 - name: "Multi-transition step"
1318 transitions: ["t1", "t2"]
1319 capture: []
1320 teardown: []
1321"##;
1322 let playbook = Playbook::from_yaml(yaml).expect("parse");
1323 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1324 let result = runner.run();
1325
1326 assert!(result.passed);
1327 assert_eq!(result.state_path, vec!["a", "b", "c"]);
1328 }
1329
1330 #[test]
1331 fn test_variable_substitution_with_captured_variables() {
1332 let yaml = r##"
1333version: "1.0"
1334machine:
1335 id: "test"
1336 initial: "start"
1337 states:
1338 start:
1339 id: "start"
1340 next:
1341 id: "next"
1342 transitions:
1343 - id: "t1"
1344 from: "start"
1345 to: "next"
1346 event: "go"
1347playbook:
1348 setup: []
1349 steps:
1350 - name: "First capture"
1351 transitions: ["t1"]
1352 capture:
1353 - var: "prefix"
1354 from: "hello"
1355 - name: "Use captured"
1356 transitions: []
1357 capture:
1358 - var: "message"
1359 from: "${prefix}_world"
1360 teardown: []
1361"##;
1362 let playbook = Playbook::from_yaml(yaml).expect("parse");
1363 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1364 let result = runner.run();
1365
1366 assert!(result.passed);
1367 assert_eq!(result.variables.get("prefix"), Some(&"hello".to_string()));
1368 assert_eq!(
1369 result.variables.get("message"),
1370 Some(&"hello_world".to_string())
1371 );
1372 }
1373
1374 #[test]
1375 fn test_output_assertion_not_empty_with_empty_string() {
1376 let yaml = r##"
1377version: "1.0"
1378machine:
1379 id: "test"
1380 initial: "start"
1381 states:
1382 start:
1383 id: "start"
1384 transitions:
1385 - id: "t1"
1386 from: "start"
1387 to: "start"
1388 event: "loop"
1389playbook:
1390 setup: []
1391 steps:
1392 - name: "Capture empty"
1393 transitions: ["t1"]
1394 capture:
1395 - var: "empty_var"
1396 from: ""
1397 teardown: []
1398assertions:
1399 output:
1400 - var: "empty_var"
1401 not_empty: true
1402"##;
1403 let playbook = Playbook::from_yaml(yaml).expect("parse");
1404 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1405 let result = runner.run();
1406
1407 assert!(!result.passed);
1408 assert!(result.assertion_results.iter().any(|a| !a.passed
1409 && a.error
1410 .as_ref()
1411 .is_some_and(|e| e.contains("empty or undefined"))));
1412 }
1413
1414 #[test]
1415 fn test_output_assertion_less_than_non_numeric() {
1416 let yaml = r##"
1417version: "1.0"
1418machine:
1419 id: "test"
1420 initial: "start"
1421 states:
1422 start:
1423 id: "start"
1424 transitions:
1425 - id: "t1"
1426 from: "start"
1427 to: "start"
1428 event: "loop"
1429playbook:
1430 setup: []
1431 steps:
1432 - name: "Capture non-numeric"
1433 transitions: ["t1"]
1434 capture:
1435 - var: "text_val"
1436 from: "not_a_number"
1437 teardown: []
1438assertions:
1439 output:
1440 - var: "text_val"
1441 less_than: 100
1442"##;
1443 let playbook = Playbook::from_yaml(yaml).expect("parse");
1444 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1445 let result = runner.run();
1446
1447 assert!(result.passed);
1449 }
1450
1451 #[test]
1452 fn test_output_assertion_greater_than_non_numeric() {
1453 let yaml = r##"
1454version: "1.0"
1455machine:
1456 id: "test"
1457 initial: "start"
1458 states:
1459 start:
1460 id: "start"
1461 transitions:
1462 - id: "t1"
1463 from: "start"
1464 to: "start"
1465 event: "loop"
1466playbook:
1467 setup: []
1468 steps:
1469 - name: "Capture non-numeric"
1470 transitions: ["t1"]
1471 capture:
1472 - var: "text_val"
1473 from: "not_a_number"
1474 teardown: []
1475assertions:
1476 output:
1477 - var: "text_val"
1478 greater_than: 0
1479"##;
1480 let playbook = Playbook::from_yaml(yaml).expect("parse");
1481 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1482 let result = runner.run();
1483
1484 assert!(result.passed);
1486 }
1487
1488 #[test]
1489 fn test_output_assertion_equals_undefined() {
1490 let yaml = r##"
1491version: "1.0"
1492machine:
1493 id: "test"
1494 initial: "start"
1495 states:
1496 start:
1497 id: "start"
1498 transitions:
1499 - id: "t_loop"
1500 from: "start"
1501 to: "start"
1502 event: "noop"
1503assertions:
1504 output:
1505 - var: "missing"
1506 equals: "expected"
1507"##;
1508 let playbook = Playbook::from_yaml(yaml).expect("parse");
1509 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1510 let result = runner.run();
1511
1512 assert!(!result.passed);
1513 assert!(result
1514 .assertion_results
1515 .iter()
1516 .any(|a| !a.passed && a.error.as_ref().is_some_and(|e| e.contains("undefined"))));
1517 }
1518
1519 #[test]
1520 fn test_output_assertion_less_than_undefined() {
1521 let yaml = r##"
1522version: "1.0"
1523machine:
1524 id: "test"
1525 initial: "start"
1526 states:
1527 start:
1528 id: "start"
1529 transitions:
1530 - id: "t_loop"
1531 from: "start"
1532 to: "start"
1533 event: "noop"
1534assertions:
1535 output:
1536 - var: "missing"
1537 less_than: 100
1538"##;
1539 let playbook = Playbook::from_yaml(yaml).expect("parse");
1540 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1541 let result = runner.run();
1542
1543 assert!(result.passed);
1545 }
1546
1547 #[test]
1548 fn test_output_assertion_greater_than_undefined() {
1549 let yaml = r##"
1550version: "1.0"
1551machine:
1552 id: "test"
1553 initial: "start"
1554 states:
1555 start:
1556 id: "start"
1557 transitions:
1558 - id: "t_loop"
1559 from: "start"
1560 to: "start"
1561 event: "noop"
1562assertions:
1563 output:
1564 - var: "missing"
1565 greater_than: 0
1566"##;
1567 let playbook = Playbook::from_yaml(yaml).expect("parse");
1568 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1569 let result = runner.run();
1570
1571 assert!(result.passed);
1573 }
1574
1575 #[test]
1576 fn test_teardown_runs_after_step_failure() {
1577 let yaml = r##"
1578version: "1.0"
1579machine:
1580 id: "test"
1581 initial: "start"
1582 states:
1583 start:
1584 id: "start"
1585 end:
1586 id: "end"
1587 transitions:
1588 - id: "forbidden_t"
1589 from: "start"
1590 to: "end"
1591 event: "skip"
1592 forbidden:
1593 - from: "start"
1594 to: "end"
1595 reason: "Cannot skip"
1596playbook:
1597 setup: []
1598 steps:
1599 - name: "Fail with forbidden"
1600 transitions: ["forbidden_t"]
1601 capture: []
1602 teardown:
1603 - action:
1604 wasm: "cleanup"
1605 args: []
1606 ignore_errors: false
1607"##;
1608 let playbook = Playbook::from_yaml(yaml).expect("parse");
1609 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1610 let result = runner.run();
1611
1612 assert!(!result.passed);
1614 }
1615
1616 #[test]
1617 fn test_svg_export_with_final_state() {
1618 let yaml = r##"
1619version: "1.0"
1620machine:
1621 id: "svg_test"
1622 initial: "start"
1623 states:
1624 start:
1625 id: "start"
1626 middle:
1627 id: "middle"
1628 end:
1629 id: "end"
1630 final_state: true
1631 transitions:
1632 - id: "t1"
1633 from: "start"
1634 to: "middle"
1635 event: "go"
1636 - id: "t2"
1637 from: "middle"
1638 to: "end"
1639 event: "finish"
1640"##;
1641 let playbook = Playbook::from_yaml(yaml).expect("parse");
1642 let svg = to_svg(&playbook);
1643
1644 assert!(svg.contains("<svg"));
1645 assert!(svg.contains("svg_test"));
1646 assert!(svg.contains("state-final")); assert!(svg.contains("</svg>"));
1648 assert!(svg.contains("DOT source")); }
1650
1651 #[test]
1652 fn test_no_assertions_section() {
1653 let yaml = r##"
1654version: "1.0"
1655machine:
1656 id: "test"
1657 initial: "start"
1658 states:
1659 start:
1660 id: "start"
1661 transitions:
1662 - id: "t_loop"
1663 from: "start"
1664 to: "start"
1665 event: "noop"
1666"##;
1667 let playbook = Playbook::from_yaml(yaml).expect("parse");
1668 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1669 let result = runner.run();
1670
1671 assert!(result.passed);
1672 assert!(result.assertion_results.is_empty());
1673 }
1674
1675 #[test]
1676 fn test_step_result_fields() {
1677 let yaml = r##"
1678version: "1.0"
1679machine:
1680 id: "test"
1681 initial: "start"
1682 states:
1683 start:
1684 id: "start"
1685 end:
1686 id: "end"
1687 transitions:
1688 - id: "t1"
1689 from: "start"
1690 to: "end"
1691 event: "go"
1692playbook:
1693 setup: []
1694 steps:
1695 - name: "Test Step"
1696 transitions: ["t1"]
1697 capture:
1698 - var: "step_var"
1699 from: "step_value"
1700 teardown: []
1701"##;
1702 let playbook = Playbook::from_yaml(yaml).expect("parse");
1703 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1704 let result = runner.run();
1705
1706 assert!(result.passed);
1707 assert_eq!(result.step_results.len(), 1);
1708 let step = &result.step_results[0];
1709 assert_eq!(step.name, "Test Step");
1710 assert!(step.passed);
1711 assert!(step.error.is_none());
1712 assert_eq!(
1713 step.captured.get("step_var"),
1714 Some(&"step_value".to_string())
1715 );
1716 }
1717
1718 #[test]
1719 fn test_playbook_run_result_fields() {
1720 let yaml = r##"
1721version: "1.0"
1722name: "Result Test Playbook"
1723machine:
1724 id: "test"
1725 initial: "start"
1726 states:
1727 start:
1728 id: "start"
1729 end:
1730 id: "end"
1731 transitions:
1732 - id: "t1"
1733 from: "start"
1734 to: "end"
1735 event: "go"
1736playbook:
1737 setup: []
1738 steps:
1739 - name: "Go"
1740 transitions: ["t1"]
1741 capture:
1742 - var: "test_var"
1743 from: "test_value"
1744 teardown: []
1745assertions:
1746 path:
1747 expected: ["start", "end"]
1748 output:
1749 - var: "test_var"
1750 equals: "test_value"
1751"##;
1752 let playbook = Playbook::from_yaml(yaml).expect("parse");
1753 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1754 let result = runner.run();
1755
1756 assert!(result.passed);
1757 assert!(result.error.is_none());
1758 assert_eq!(result.state_path, vec!["start", "end"]);
1759 assert_eq!(
1760 result.variables.get("test_var"),
1761 Some(&"test_value".to_string())
1762 );
1763 assert!(!result.total_time.is_zero() || result.total_time == std::time::Duration::ZERO);
1764 assert_eq!(result.step_results.len(), 1);
1765 assert_eq!(result.assertion_results.len(), 2); assert!(result.assertion_results.iter().all(|a| a.passed));
1767 }
1768
1769 #[test]
1770 fn test_assertion_result_error_formats() {
1771 let yaml = r##"
1772version: "1.0"
1773machine:
1774 id: "test"
1775 initial: "start"
1776 states:
1777 start:
1778 id: "start"
1779 transitions:
1780 - id: "t_loop"
1781 from: "start"
1782 to: "start"
1783 event: "noop"
1784assertions:
1785 path:
1786 expected: ["start", "wrong", "path"]
1787 output:
1788 - var: "missing"
1789 not_empty: true
1790"##;
1791 let playbook = Playbook::from_yaml(yaml).expect("parse");
1792 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1793 let result = runner.run();
1794
1795 assert!(!result.passed);
1796 assert!(result
1797 .error
1798 .as_ref()
1799 .is_some_and(|e| e.contains("Assertions failed")));
1800
1801 let path_result = result
1803 .assertion_results
1804 .iter()
1805 .find(|a| a.description.contains("Path"));
1806 assert!(path_result.is_some());
1807 let path_err = path_result.and_then(|p| p.error.as_ref());
1808 assert!(path_err.is_some_and(|e| e.contains("Expected path")));
1809 }
1810
1811 #[test]
1812 fn test_less_than_boundary_value() {
1813 let yaml = r##"
1814version: "1.0"
1815machine:
1816 id: "test"
1817 initial: "start"
1818 states:
1819 start:
1820 id: "start"
1821 transitions:
1822 - id: "t1"
1823 from: "start"
1824 to: "start"
1825 event: "loop"
1826playbook:
1827 setup: []
1828 steps:
1829 - name: "Capture"
1830 transitions: ["t1"]
1831 capture:
1832 - var: "count"
1833 from: "10"
1834 teardown: []
1835assertions:
1836 output:
1837 - var: "count"
1838 less_than: 10
1839"##;
1840 let playbook = Playbook::from_yaml(yaml).expect("parse");
1841 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1842 let result = runner.run();
1843
1844 assert!(!result.passed);
1846 }
1847
1848 #[test]
1849 fn test_greater_than_boundary_value() {
1850 let yaml = r##"
1851version: "1.0"
1852machine:
1853 id: "test"
1854 initial: "start"
1855 states:
1856 start:
1857 id: "start"
1858 transitions:
1859 - id: "t1"
1860 from: "start"
1861 to: "start"
1862 event: "loop"
1863playbook:
1864 setup: []
1865 steps:
1866 - name: "Capture"
1867 transitions: ["t1"]
1868 capture:
1869 - var: "count"
1870 from: "50"
1871 teardown: []
1872assertions:
1873 output:
1874 - var: "count"
1875 greater_than: 50
1876"##;
1877 let playbook = Playbook::from_yaml(yaml).expect("parse");
1878 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1879 let result = runner.run();
1880
1881 assert!(!result.passed);
1883 }
1884
1885 #[test]
1886 fn test_multiple_output_assertions_on_same_var() {
1887 let yaml = r##"
1888version: "1.0"
1889machine:
1890 id: "test"
1891 initial: "start"
1892 states:
1893 start:
1894 id: "start"
1895 transitions:
1896 - id: "t1"
1897 from: "start"
1898 to: "start"
1899 event: "loop"
1900playbook:
1901 setup: []
1902 steps:
1903 - name: "Capture"
1904 transitions: ["t1"]
1905 capture:
1906 - var: "count"
1907 from: "50"
1908 teardown: []
1909assertions:
1910 output:
1911 - var: "count"
1912 not_empty: true
1913 - var: "count"
1914 greater_than: 40
1915 - var: "count"
1916 less_than: 60
1917"##;
1918 let playbook = Playbook::from_yaml(yaml).expect("parse");
1919 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1920 let result = runner.run();
1921
1922 assert!(result.passed);
1923 assert_eq!(result.assertion_results.len(), 3);
1924 }
1925
1926 #[test]
1927 fn test_step_fails_early_remaining_steps_skipped() {
1928 let yaml = r##"
1929version: "1.0"
1930machine:
1931 id: "test"
1932 initial: "start"
1933 states:
1934 start:
1935 id: "start"
1936 end:
1937 id: "end"
1938 transitions:
1939 - id: "forbidden_t"
1940 from: "start"
1941 to: "end"
1942 event: "skip"
1943 - id: "t_loop"
1944 from: "start"
1945 to: "start"
1946 event: "loop"
1947 forbidden:
1948 - from: "start"
1949 to: "end"
1950 reason: "Cannot skip"
1951playbook:
1952 setup: []
1953 steps:
1954 - name: "First (fails)"
1955 transitions: ["forbidden_t"]
1956 capture: []
1957 - name: "Second (should be skipped)"
1958 transitions: ["t_loop"]
1959 capture:
1960 - var: "should_not_exist"
1961 from: "value"
1962 teardown: []
1963"##;
1964 let playbook = Playbook::from_yaml(yaml).expect("parse");
1965 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
1966 let result = runner.run();
1967
1968 assert!(!result.passed);
1969 assert_eq!(result.step_results.len(), 1);
1971 assert!(result.variables.get("should_not_exist").is_none());
1973 }
1974
1975 #[test]
1976 fn test_forbidden_check_multiple_forbidden_rules() {
1977 let yaml = r##"
1978version: "1.0"
1979machine:
1980 id: "test"
1981 initial: "start"
1982 states:
1983 start:
1984 id: "start"
1985 middle:
1986 id: "middle"
1987 end:
1988 id: "end"
1989 transitions:
1990 - id: "t1"
1991 from: "start"
1992 to: "middle"
1993 event: "go"
1994 - id: "t2"
1995 from: "middle"
1996 to: "end"
1997 event: "finish"
1998 forbidden:
1999 - from: "start"
2000 to: "end"
2001 reason: "Cannot skip middle from start"
2002 - from: "middle"
2003 to: "start"
2004 reason: "Cannot go backwards"
2005"##;
2006 let playbook = Playbook::from_yaml(yaml).expect("parse");
2007 let runner = PlaybookRunner::new(playbook, MockExecutor);
2008
2009 let err1 = runner.check_forbidden("start", "end");
2011 assert!(err1.is_some());
2012 assert!(err1
2013 .as_ref()
2014 .is_some_and(|e| e.contains("Cannot skip middle from start")));
2015
2016 let err2 = runner.check_forbidden("middle", "start");
2018 assert!(err2.is_some());
2019 assert!(err2
2020 .as_ref()
2021 .is_some_and(|e| e.contains("Cannot go backwards")));
2022
2023 let ok = runner.check_forbidden("start", "middle");
2025 assert!(ok.is_none());
2026 }
2027
2028 #[test]
2029 fn test_substitute_variables_no_match() {
2030 let yaml = r##"
2031version: "1.0"
2032machine:
2033 id: "test"
2034 initial: "start"
2035 states:
2036 start:
2037 id: "start"
2038 transitions:
2039 - id: "t_loop"
2040 from: "start"
2041 to: "start"
2042 event: "noop"
2043"##;
2044 let playbook = Playbook::from_yaml(yaml).expect("parse");
2045 let runner = PlaybookRunner::new(playbook, MockExecutor);
2046
2047 let result = runner.substitute_variables("No ${vars} here ${at_all}");
2049 assert_eq!(result, "No ${vars} here ${at_all}");
2050 }
2051
2052 #[test]
2053 fn test_substitute_variables_partial_match() {
2054 let yaml = r##"
2055version: "1.0"
2056machine:
2057 id: "test"
2058 initial: "start"
2059 states:
2060 start:
2061 id: "start"
2062 transitions:
2063 - id: "t_loop"
2064 from: "start"
2065 to: "start"
2066 event: "noop"
2067"##;
2068 let playbook = Playbook::from_yaml(yaml).expect("parse");
2069 let mut runner = PlaybookRunner::new(playbook, MockExecutor);
2070
2071 runner
2072 .variables
2073 .insert("found".to_string(), "YES".to_string());
2074
2075 let result = runner.substitute_variables("${found} but ${not_found}");
2076 assert_eq!(result, "YES but ${not_found}");
2077 }
2078
2079 #[test]
2080 fn test_assertion_check_result_clone() {
2081 let result = AssertionCheckResult {
2082 description: "Test".to_string(),
2083 passed: true,
2084 error: None,
2085 };
2086 let cloned = result;
2087 assert_eq!(cloned.description, "Test");
2088 assert!(cloned.passed);
2089 assert!(cloned.error.is_none());
2090 }
2091
2092 #[test]
2093 fn test_step_result_clone() {
2094 let result = StepResult {
2095 name: "Test Step".to_string(),
2096 passed: false,
2097 duration: std::time::Duration::from_millis(100),
2098 captured: HashMap::new(),
2099 error: Some("Test error".to_string()),
2100 };
2101 let cloned = result;
2102 assert_eq!(cloned.name, "Test Step");
2103 assert!(!cloned.passed);
2104 assert_eq!(cloned.duration, std::time::Duration::from_millis(100));
2105 assert_eq!(cloned.error, Some("Test error".to_string()));
2106 }
2107}