tramli_plugins/testing/
mod.rs1use tramli::{FlowDefinition, FlowState, TransitionType};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum ScenarioKind {
6 Happy,
7 Error,
8 GuardRejection,
9 Timeout,
10}
11
12#[derive(Debug, Clone)]
14pub struct FlowScenario {
15 pub name: String,
16 pub kind: ScenarioKind,
17 pub steps: Vec<String>,
18}
19
20#[derive(Debug, Clone)]
22pub struct FlowTestPlan {
23 pub scenarios: Vec<FlowScenario>,
24}
25
26pub struct ScenarioTestPlugin;
29
30impl ScenarioTestPlugin {
31 pub fn generate<S: FlowState>(definition: &FlowDefinition<S>) -> FlowTestPlan {
32 let mut scenarios = Vec::new();
33
34 for t in &definition.transitions {
36 let mut steps = Vec::new();
37 steps.push(format!("given flow in {:?}", t.from));
38 match t.transition_type {
39 TransitionType::External => {
40 if let Some(ref g) = t.guard {
41 steps.push(format!("when external data satisfies guard {}", g.name()));
42 }
43 }
44 TransitionType::Auto => {
45 if let Some(ref p) = t.processor {
46 steps.push(format!("when auto processor {} runs", p.name()));
47 }
48 }
49 TransitionType::Branch => {
50 if let Some(ref b) = t.branch {
51 steps.push(format!("when branch {} selects a route", b.name()));
52 }
53 }
54 _ => {}
55 }
56 steps.push(format!("then flow reaches {:?}", t.to));
57 scenarios.push(FlowScenario {
58 name: format!("{:?}_to_{:?}", t.from, t.to),
59 kind: ScenarioKind::Happy,
60 steps,
61 });
62 }
63
64 for (from, to) in &definition.error_transitions {
66 scenarios.push(FlowScenario {
67 name: format!("error_{:?}_to_{:?}", from, to),
68 kind: ScenarioKind::Error,
69 steps: vec![
70 format!("given flow in {:?}", from),
71 "when processor throws an error".to_string(),
72 format!("then flow transitions to {:?} via on_error", to),
73 ],
74 });
75 }
76
77 for (from, routes) in &definition.exception_routes {
79 for route in routes {
80 let label = &route.label;
81 let target = &route.target;
82 scenarios.push(FlowScenario {
83 name: format!("step_error_{:?}_{}_to_{:?}", from, label, target),
84 kind: ScenarioKind::Error,
85 steps: vec![
86 format!("given flow in {:?}", from),
87 format!("when error matching {} is thrown", label),
88 format!("then flow transitions to {:?} via on_step_error", target),
89 ],
90 });
91 }
92 }
93
94 for t in &definition.transitions {
96 if matches!(t.transition_type, TransitionType::External) {
97 if let Some(ref g) = t.guard {
98 let error_target = definition.error_transitions.get(&t.from);
99 scenarios.push(FlowScenario {
100 name: format!("guard_reject_{:?}_{}", t.from, g.name()),
101 kind: ScenarioKind::GuardRejection,
102 steps: vec![
103 format!("given flow in {:?}", t.from),
104 format!("when guard {} rejects {} times", g.name(), definition.max_guard_retries),
105 if let Some(target) = error_target {
106 format!("then flow transitions to {:?} via error", target)
107 } else {
108 "then flow enters TERMINAL_ERROR".to_string()
109 },
110 ],
111 });
112 }
113 }
114 }
115
116 for t in &definition.transitions {
118 if let Some(timeout) = t.timeout {
119 scenarios.push(FlowScenario {
120 name: format!("timeout_{:?}", t.from),
121 kind: ScenarioKind::Timeout,
122 steps: vec![
123 format!("given flow in {:?}", t.from),
124 format!("when per-state timeout of {}ms expires", timeout.as_millis()),
125 "then flow completes as EXPIRED".to_string(),
126 ],
127 });
128 }
129 }
130
131 FlowTestPlan { scenarios }
132 }
133}