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