rust_actions/
runner.rs

1use crate::expr::{evaluate_assertion, evaluate_value, ContainerInfo, ExprContext};
2use crate::hooks::HookRegistry;
3use crate::parser::{parse_features, Feature, Scenario, Step};
4use crate::registry::{ErasedStepFn, StepRegistry};
5use crate::world::World;
6use crate::Result;
7use colored::Colorize;
8use std::any::Any;
9use std::collections::HashMap;
10use std::marker::PhantomData;
11use std::path::PathBuf;
12use std::time::{Duration, Instant};
13
14#[derive(Debug, Clone)]
15pub enum StepResult {
16    Passed(Duration),
17    Failed(Duration, String),
18    Skipped,
19}
20
21impl StepResult {
22    pub fn is_passed(&self) -> bool {
23        matches!(self, StepResult::Passed(_))
24    }
25
26    pub fn is_failed(&self) -> bool {
27        matches!(self, StepResult::Failed(_, _))
28    }
29}
30
31#[derive(Debug)]
32pub struct ScenarioResult {
33    pub name: String,
34    pub steps: Vec<(String, StepResult)>,
35    pub duration: Duration,
36}
37
38impl ScenarioResult {
39    pub fn passed(&self) -> bool {
40        self.steps.iter().all(|(_, r)| r.is_passed())
41    }
42
43    pub fn steps_passed(&self) -> usize {
44        self.steps.iter().filter(|(_, r)| r.is_passed()).count()
45    }
46
47    pub fn steps_failed(&self) -> usize {
48        self.steps.iter().filter(|(_, r)| r.is_failed()).count()
49    }
50}
51
52#[derive(Debug)]
53pub struct FeatureResult {
54    pub name: String,
55    pub scenarios: Vec<ScenarioResult>,
56    pub duration: Duration,
57}
58
59impl FeatureResult {
60    pub fn passed(&self) -> bool {
61        self.scenarios.iter().all(|s| s.passed())
62    }
63
64    pub fn scenarios_passed(&self) -> usize {
65        self.scenarios.iter().filter(|s| s.passed()).count()
66    }
67
68    pub fn scenarios_failed(&self) -> usize {
69        self.scenarios.iter().filter(|s| !s.passed()).count()
70    }
71
72    pub fn total_steps_passed(&self) -> usize {
73        self.scenarios.iter().map(|s| s.steps_passed()).sum()
74    }
75
76    pub fn total_steps_failed(&self) -> usize {
77        self.scenarios.iter().map(|s| s.steps_failed()).sum()
78    }
79}
80
81pub struct RustActions<W: World + 'static> {
82    features_path: PathBuf,
83    steps: StepRegistry,
84    hooks: HookRegistry<W>,
85    _phantom: PhantomData<W>,
86}
87
88impl<W: World + 'static> RustActions<W> {
89    pub fn new() -> Self {
90        let mut steps = StepRegistry::new();
91        steps.collect_for::<W>();
92
93        Self {
94            features_path: PathBuf::from("tests/features"),
95            steps,
96            hooks: HookRegistry::new(),
97            _phantom: PhantomData,
98        }
99    }
100
101    pub fn features(mut self, path: impl Into<PathBuf>) -> Self {
102        self.features_path = path.into();
103        self
104    }
105
106    pub fn register_step(mut self, name: impl Into<String>, func: ErasedStepFn) -> Self {
107        self.steps.register(name, func);
108        self
109    }
110
111    pub async fn run(self) {
112
113        let features = match parse_features(&self.features_path) {
114            Ok(f) => f,
115            Err(e) => {
116                eprintln!("{} Failed to parse features: {}", "Error:".red().bold(), e);
117                std::process::exit(1);
118            }
119        };
120
121        self.hooks.run_before_all().await;
122
123        let mut all_results = Vec::new();
124        let mut total_passed = 0;
125        let mut total_failed = 0;
126
127        for feature in features {
128            let result = self.run_feature(feature).await;
129            total_passed += result.scenarios_passed();
130            total_failed += result.scenarios_failed();
131            all_results.push(result);
132        }
133
134        self.hooks.run_after_all().await;
135
136        println!();
137        let total_scenarios = total_passed + total_failed;
138        let total_steps_passed: usize = all_results.iter().map(|r| r.total_steps_passed()).sum();
139        let total_steps_failed: usize = all_results.iter().map(|r| r.total_steps_failed()).sum();
140        let total_steps = total_steps_passed + total_steps_failed;
141
142        if total_failed == 0 {
143            println!(
144                "{} {} ({} passed)",
145                format!("{} scenarios", total_scenarios).green(),
146                "✓".green(),
147                total_passed
148            );
149        } else {
150            println!(
151                "{} ({} passed, {} failed)",
152                format!("{} scenarios", total_scenarios).yellow(),
153                total_passed,
154                total_failed
155            );
156        }
157
158        println!(
159            "{} ({} passed, {} failed)",
160            format!("{} steps", total_steps),
161            total_steps_passed,
162            total_steps_failed
163        );
164
165        if total_failed > 0 {
166            std::process::exit(1);
167        }
168    }
169
170    async fn run_feature(&self, feature: Feature) -> FeatureResult {
171        let start = Instant::now();
172        println!("\n{} {}", "Feature:".bold(), feature.name);
173
174        let mut scenario_results = Vec::new();
175
176        for scenario in feature.scenarios {
177            let result = self
178                .run_scenario(&scenario, &feature.env, &feature.containers)
179                .await;
180            scenario_results.push(result);
181        }
182
183        FeatureResult {
184            name: feature.name,
185            scenarios: scenario_results,
186            duration: start.elapsed(),
187        }
188    }
189
190    async fn run_scenario(
191        &self,
192        scenario: &Scenario,
193        env: &HashMap<String, String>,
194        containers: &HashMap<String, String>,
195    ) -> ScenarioResult {
196        let start = Instant::now();
197
198        let mut world = match W::new().await {
199            Ok(w) => w,
200            Err(e) => {
201                println!(
202                    "  {} {} (world init failed: {})",
203                    "✗".red(),
204                    scenario.name,
205                    e
206                );
207                return ScenarioResult {
208                    name: scenario.name.clone(),
209                    steps: vec![],
210                    duration: start.elapsed(),
211                };
212            }
213        };
214
215        self.hooks.run_before_scenario(&mut world).await;
216
217        let mut ctx = ExprContext::new();
218        ctx.env = env.clone();
219
220        for (name, _image) in containers {
221            ctx.containers.insert(
222                name.clone(),
223                ContainerInfo {
224                    url: format!("{}://localhost:5432", name),
225                    host: "localhost".to_string(),
226                    port: 5432,
227                },
228            );
229        }
230
231        let mut step_results = Vec::new();
232        let mut should_skip = false;
233
234        for step in &scenario.steps {
235            if should_skip {
236                step_results.push((step.name.clone(), StepResult::Skipped));
237                continue;
238            }
239
240            self.hooks.run_before_step(&mut world, step).await;
241
242            let result = self.run_step(&mut world, step, &mut ctx).await;
243
244            self.hooks.run_after_step(&mut world, step, &result).await;
245
246            if result.is_failed() && !step.continue_on_error {
247                should_skip = true;
248            }
249
250            step_results.push((step.name.clone(), result));
251        }
252
253        self.hooks.run_after_scenario(&mut world).await;
254
255        let duration = start.elapsed();
256        let all_passed = step_results.iter().all(|(_, r)| r.is_passed());
257
258        if all_passed {
259            println!(
260                "  {} {} ({:?})",
261                "✓".green(),
262                scenario.name,
263                duration
264            );
265        } else {
266            println!(
267                "  {} {} ({:?})",
268                "✗".red(),
269                scenario.name,
270                duration
271            );
272        }
273
274        for (name, result) in &step_results {
275            match result {
276                StepResult::Passed(_) => {
277                    println!("    {} {}", "✓".green(), name);
278                }
279                StepResult::Failed(_, msg) => {
280                    println!("    {} {}", "✗".red(), name);
281                    println!("      {}: {}", "Error".red(), msg);
282                }
283                StepResult::Skipped => {
284                    println!("    {} {} (skipped)", "○".dimmed(), name);
285                }
286            }
287        }
288
289        ScenarioResult {
290            name: scenario.name.clone(),
291            steps: step_results,
292            duration,
293        }
294    }
295
296    async fn run_step(
297        &self,
298        world: &mut W,
299        step: &Step,
300        ctx: &mut ExprContext,
301    ) -> StepResult {
302        let start = Instant::now();
303
304        for assertion in &step.pre_assert {
305            match evaluate_assertion(assertion, ctx) {
306                Ok(true) => {}
307                Ok(false) => {
308                    return StepResult::Failed(
309                        start.elapsed(),
310                        format!("Pre-assertion failed: {}", assertion),
311                    );
312                }
313                Err(e) => {
314                    return StepResult::Failed(
315                        start.elapsed(),
316                        format!("Pre-assertion error: {}", e),
317                    );
318                }
319            }
320        }
321
322        let step_fn = match self.steps.get(&step.uses) {
323            Some(f) => f,
324            None => {
325                return StepResult::Failed(
326                    start.elapsed(),
327                    format!("Step not found: {}", step.uses),
328                );
329            }
330        };
331
332        let evaluated_args = match step
333            .with
334            .iter()
335            .map(|(k, v)| evaluate_value(v, ctx).map(|ev| (k.clone(), ev)))
336            .collect::<Result<HashMap<_, _>>>()
337        {
338            Ok(args) => args,
339            Err(e) => {
340                return StepResult::Failed(
341                    start.elapsed(),
342                    format!("Args evaluation failed: {}", e),
343                );
344            }
345        };
346
347        let world_any: &mut dyn Any = world;
348        let outputs = match step_fn(world_any, evaluated_args).await {
349            Ok(outputs) => outputs,
350            Err(e) => return StepResult::Failed(start.elapsed(), e.to_string()),
351        };
352
353        if let Some(id) = &step.id {
354            ctx.steps.insert(id.clone(), outputs.clone());
355        }
356
357        if !step.post_assert.is_empty() {
358            let assert_ctx = ctx.with_outputs(outputs);
359
360            for assertion in &step.post_assert {
361                match evaluate_assertion(assertion, &assert_ctx) {
362                    Ok(true) => {}
363                    Ok(false) => {
364                        return StepResult::Failed(
365                            start.elapsed(),
366                            format!("Post-assertion failed: {}", assertion),
367                        );
368                    }
369                    Err(e) => {
370                        return StepResult::Failed(
371                            start.elapsed(),
372                            format!("Post-assertion error: {}", e),
373                        );
374                    }
375                }
376            }
377        }
378
379        StepResult::Passed(start.elapsed())
380    }
381}
382
383impl<W: World + 'static> Default for RustActions<W> {
384    fn default() -> Self {
385        Self::new()
386    }
387}