Skip to main content

tramli_plugins/testing/
mod.rs

1use tramli::{FlowDefinition, FlowState, TransitionType};
2
3/// Scenario kind classification.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum ScenarioKind {
6    Happy,
7    Error,
8    GuardRejection,
9    Timeout,
10}
11
12/// A test scenario step.
13#[derive(Debug, Clone)]
14pub struct FlowScenario {
15    pub name: String,
16    pub kind: ScenarioKind,
17    pub steps: Vec<String>,
18}
19
20/// A test plan containing generated scenarios.
21#[derive(Debug, Clone)]
22pub struct FlowTestPlan {
23    pub scenarios: Vec<FlowScenario>,
24}
25
26/// Scenario test plugin — generates BDD-style scenarios from a flow definition.
27/// Covers happy paths, error transitions, guard rejections, and timeout expiry.
28pub struct ScenarioTestPlugin;
29
30impl ScenarioTestPlugin {
31    pub fn generate<S: FlowState>(definition: &FlowDefinition<S>) -> FlowTestPlan {
32        let mut scenarios = Vec::new();
33
34        // Happy path scenarios from transitions
35        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        // Error path scenarios from error_transitions
65        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        // Exception route scenarios
78        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        // Guard rejection scenarios
95        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        // Timeout scenarios
117        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}