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        tokio::time::pause();
113
114        let features = match parse_features(&self.features_path) {
115            Ok(f) => f,
116            Err(e) => {
117                eprintln!("{} Failed to parse features: {}", "Error:".red().bold(), e);
118                std::process::exit(1);
119            }
120        };
121
122        self.hooks.run_before_all().await;
123
124        let mut all_results = Vec::new();
125        let mut total_passed = 0;
126        let mut total_failed = 0;
127
128        for feature in features {
129            let result = self.run_feature(feature).await;
130            total_passed += result.scenarios_passed();
131            total_failed += result.scenarios_failed();
132            all_results.push(result);
133        }
134
135        self.hooks.run_after_all().await;
136
137        println!();
138        let total_scenarios = total_passed + total_failed;
139        let total_steps_passed: usize = all_results.iter().map(|r| r.total_steps_passed()).sum();
140        let total_steps_failed: usize = all_results.iter().map(|r| r.total_steps_failed()).sum();
141        let total_steps = total_steps_passed + total_steps_failed;
142
143        if total_failed == 0 {
144            println!(
145                "{} {} ({} passed)",
146                format!("{} scenarios", total_scenarios).green(),
147                "✓".green(),
148                total_passed
149            );
150        } else {
151            println!(
152                "{} ({} passed, {} failed)",
153                format!("{} scenarios", total_scenarios).yellow(),
154                total_passed,
155                total_failed
156            );
157        }
158
159        println!(
160            "{} ({} passed, {} failed)",
161            format!("{} steps", total_steps),
162            total_steps_passed,
163            total_steps_failed
164        );
165
166        if total_failed > 0 {
167            std::process::exit(1);
168        }
169    }
170
171    async fn run_feature(&self, feature: Feature) -> FeatureResult {
172        let start = Instant::now();
173        println!("\n{} {}", "Feature:".bold(), feature.name);
174
175        let mut scenario_results = Vec::new();
176
177        for scenario in feature.scenarios {
178            let result = self
179                .run_scenario(&scenario, &feature.env, &feature.containers)
180                .await;
181            scenario_results.push(result);
182        }
183
184        FeatureResult {
185            name: feature.name,
186            scenarios: scenario_results,
187            duration: start.elapsed(),
188        }
189    }
190
191    async fn run_scenario(
192        &self,
193        scenario: &Scenario,
194        env: &HashMap<String, String>,
195        containers: &HashMap<String, String>,
196    ) -> ScenarioResult {
197        let start = Instant::now();
198
199        let mut world = match W::new().await {
200            Ok(w) => w,
201            Err(e) => {
202                println!(
203                    "  {} {} (world init failed: {})",
204                    "✗".red(),
205                    scenario.name,
206                    e
207                );
208                return ScenarioResult {
209                    name: scenario.name.clone(),
210                    steps: vec![],
211                    duration: start.elapsed(),
212                };
213            }
214        };
215
216        self.hooks.run_before_scenario(&mut world).await;
217
218        let mut ctx = ExprContext::new();
219        ctx.env = env.clone();
220
221        for (name, _image) in containers {
222            ctx.containers.insert(
223                name.clone(),
224                ContainerInfo {
225                    url: format!("{}://localhost:5432", name),
226                    host: "localhost".to_string(),
227                    port: 5432,
228                },
229            );
230        }
231
232        let mut step_results = Vec::new();
233        let mut should_skip = false;
234
235        for step in &scenario.steps {
236            if should_skip {
237                step_results.push((step.name.clone(), StepResult::Skipped));
238                continue;
239            }
240
241            self.hooks.run_before_step(&mut world, step).await;
242
243            let result = self.run_step(&mut world, step, &mut ctx).await;
244
245            self.hooks.run_after_step(&mut world, step, &result).await;
246
247            if result.is_failed() && !step.continue_on_error {
248                should_skip = true;
249            }
250
251            step_results.push((step.name.clone(), result));
252        }
253
254        self.hooks.run_after_scenario(&mut world).await;
255
256        let duration = start.elapsed();
257        let all_passed = step_results.iter().all(|(_, r)| r.is_passed());
258
259        if all_passed {
260            println!(
261                "  {} {} ({:?})",
262                "✓".green(),
263                scenario.name,
264                duration
265            );
266        } else {
267            println!(
268                "  {} {} ({:?})",
269                "✗".red(),
270                scenario.name,
271                duration
272            );
273        }
274
275        for (name, result) in &step_results {
276            match result {
277                StepResult::Passed(_) => {
278                    println!("    {} {}", "✓".green(), name);
279                }
280                StepResult::Failed(_, msg) => {
281                    println!("    {} {}", "✗".red(), name);
282                    println!("      {}: {}", "Error".red(), msg);
283                }
284                StepResult::Skipped => {
285                    println!("    {} {} (skipped)", "○".dimmed(), name);
286                }
287            }
288        }
289
290        ScenarioResult {
291            name: scenario.name.clone(),
292            steps: step_results,
293            duration,
294        }
295    }
296
297    async fn run_step(
298        &self,
299        world: &mut W,
300        step: &Step,
301        ctx: &mut ExprContext,
302    ) -> StepResult {
303        let start = Instant::now();
304
305        for assertion in &step.pre_assert {
306            match evaluate_assertion(assertion, ctx) {
307                Ok(true) => {}
308                Ok(false) => {
309                    return StepResult::Failed(
310                        start.elapsed(),
311                        format!("Pre-assertion failed: {}", assertion),
312                    );
313                }
314                Err(e) => {
315                    return StepResult::Failed(
316                        start.elapsed(),
317                        format!("Pre-assertion error: {}", e),
318                    );
319                }
320            }
321        }
322
323        let step_fn = match self.steps.get(&step.uses) {
324            Some(f) => f,
325            None => {
326                return StepResult::Failed(
327                    start.elapsed(),
328                    format!("Step not found: {}", step.uses),
329                );
330            }
331        };
332
333        let evaluated_args = match step
334            .with
335            .iter()
336            .map(|(k, v)| evaluate_value(v, ctx).map(|ev| (k.clone(), ev)))
337            .collect::<Result<HashMap<_, _>>>()
338        {
339            Ok(args) => args,
340            Err(e) => {
341                return StepResult::Failed(
342                    start.elapsed(),
343                    format!("Args evaluation failed: {}", e),
344                );
345            }
346        };
347
348        let world_any: &mut dyn Any = world;
349        let outputs = match step_fn(world_any, evaluated_args).await {
350            Ok(outputs) => outputs,
351            Err(e) => return StepResult::Failed(start.elapsed(), e.to_string()),
352        };
353
354        if let Some(id) = &step.id {
355            ctx.steps.insert(id.clone(), outputs.clone());
356        }
357
358        if !step.post_assert.is_empty() {
359            let assert_ctx = ctx.with_outputs(outputs);
360
361            for assertion in &step.post_assert {
362                match evaluate_assertion(assertion, &assert_ctx) {
363                    Ok(true) => {}
364                    Ok(false) => {
365                        return StepResult::Failed(
366                            start.elapsed(),
367                            format!("Post-assertion failed: {}", assertion),
368                        );
369                    }
370                    Err(e) => {
371                        return StepResult::Failed(
372                            start.elapsed(),
373                            format!("Post-assertion error: {}", e),
374                        );
375                    }
376                }
377            }
378        }
379
380        StepResult::Passed(start.elapsed())
381    }
382}
383
384impl<W: World + 'static> Default for RustActions<W> {
385    fn default() -> Self {
386        Self::new()
387    }
388}