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}