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}