1use std::{
14 any::Any,
15 cmp,
16 collections::HashMap,
17 iter, mem,
18 ops::ControlFlow,
19 panic::{self, AssertUnwindSafe},
20 sync::{
21 Arc,
22 atomic::{AtomicBool, AtomicU64, Ordering},
23 },
24 thread,
25 time::{Duration, Instant},
26};
27
28#[cfg(feature = "tracing")]
29use crossbeam_utils::atomic::AtomicCell;
30use derive_more::with_trait::{Debug, Display, FromStr};
31use futures::{
32 FutureExt as _, Stream, StreamExt as _, TryFutureExt as _,
33 TryStreamExt as _,
34 channel::{mpsc, oneshot},
35 future::{self, Either, LocalBoxFuture},
36 lock::Mutex,
37 pin_mut,
38 stream::{self, LocalBoxStream},
39};
40use gherkin::tagexpr::TagOperation;
41use itertools::Itertools as _;
42use regex::{CaptureLocations, Regex};
43
44#[cfg(feature = "tracing")]
45use crate::tracing::{Collector as TracingCollector, SpanCloseWaiter};
46use crate::{
47 Event, Runner, Step, World,
48 event::{self, HookType, Info, Retries, Source},
49 feature::Ext as _,
50 future::{FutureExt as _, select_with_biased_first},
51 parser, step,
52 tag::Ext as _,
53};
54
55#[derive(Clone, Debug, Default, clap::Args)]
57#[group(skip)]
58pub struct Cli {
59 #[arg(long, short, value_name = "int", global = true)]
62 pub concurrency: Option<usize>,
63
64 #[arg(long, global = true, visible_alias = "ff")]
66 pub fail_fast: bool,
67
68 #[arg(long, value_name = "int", global = true)]
70 pub retry: Option<usize>,
71
72 #[arg(
82 long,
83 value_name = "duration",
84 value_parser = humantime::parse_duration,
85 verbatim_doc_comment,
86 global = true,
87 )]
88 pub retry_after: Option<Duration>,
89
90 #[arg(long, value_name = "tagexpr", global = true)]
92 pub retry_tag_filter: Option<TagOperation>,
93}
94
95#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
100pub enum ScenarioType {
101 Serial,
105
106 Concurrent,
110}
111
112#[derive(Clone, Copy, Debug, Eq, PartialEq)]
116pub struct RetryOptions {
117 pub retries: Retries,
119
120 pub after: Option<Duration>,
122}
123
124impl RetryOptions {
125 #[must_use]
128 pub fn next_try(self) -> Option<Self> {
129 self.retries
130 .next_try()
131 .map(|num| Self { retries: num, after: self.after })
132 }
133
134 #[must_use]
141 pub fn parse_from_tags(
142 feature: &gherkin::Feature,
143 rule: Option<&gherkin::Rule>,
144 scenario: &gherkin::Scenario,
145 cli: &Cli,
146 ) -> Option<Self> {
147 let parse_tags = |tags: &[String]| {
148 tags.iter().find_map(|tag| {
149 tag.strip_prefix("retry").map(|retries| {
150 let (num, rest) = retries
151 .strip_prefix('(')
152 .and_then(|s| {
153 let (num, rest) = s.split_once(')')?;
154 num.parse::<usize>()
155 .ok()
156 .map(|num| (Some(num), rest))
157 })
158 .unwrap_or((None, retries));
159
160 let after = rest.strip_prefix(".after").and_then(|after| {
161 let after = after.strip_prefix('(')?;
162 let (dur, _) = after.split_once(')')?;
163 humantime::parse_duration(dur).ok()
164 });
165
166 (num, after)
167 })
168 })
169 };
170
171 let apply_cli = |options: Option<_>| {
172 let matched = cli.retry_tag_filter.as_ref().map_or_else(
173 || cli.retry.is_some() || cli.retry_after.is_some(),
174 |op| {
175 op.eval(scenario.tags.iter().chain(
176 rule.iter().flat_map(|r| &r.tags).chain(&feature.tags),
177 ))
178 },
179 );
180
181 (options.is_some() || matched).then(|| Self {
182 retries: Retries::initial(
183 options.and_then(|(r, _)| r).or(cli.retry).unwrap_or(1),
184 ),
185 after: options.and_then(|(_, a)| a).or(cli.retry_after),
186 })
187 };
188
189 apply_cli(
190 parse_tags(&scenario.tags)
191 .or_else(|| parse_tags(&rule?.tags))
192 .or_else(|| parse_tags(&feature.tags)),
193 )
194 }
195
196 fn with_deadline(self, now: Instant) -> RetryOptionsWithDeadline {
202 RetryOptionsWithDeadline {
203 retries: self.retries,
204 after: self.after.map(|at| (at, Some(now))),
205 }
206 }
207
208 fn without_deadline(self) -> RetryOptionsWithDeadline {
214 RetryOptionsWithDeadline {
215 retries: self.retries,
216 after: self.after.map(|at| (at, None)),
217 }
218 }
219}
220
221#[derive(Clone, Copy, Debug)]
226pub struct RetryOptionsWithDeadline {
227 pub retries: Retries,
229
230 pub after: Option<(Duration, Option<Instant>)>,
232}
233
234impl From<RetryOptionsWithDeadline> for RetryOptions {
235 fn from(v: RetryOptionsWithDeadline) -> Self {
236 Self { retries: v.retries, after: v.after.map(|(at, _)| at) }
237 }
238}
239
240impl RetryOptionsWithDeadline {
241 fn left_until_retry(&self) -> Option<Duration> {
246 let (dur, instant) = self.after?;
247 dur.checked_sub(instant?.elapsed())
248 }
249}
250
251pub type WhichScenarioFn = fn(
258 &gherkin::Feature,
259 Option<&gherkin::Rule>,
260 &gherkin::Scenario,
261) -> ScenarioType;
262
263pub type RetryOptionsFn = Arc<
268 dyn Fn(
269 &gherkin::Feature,
270 Option<&gherkin::Rule>,
271 &gherkin::Scenario,
272 &Cli,
273 ) -> Option<RetryOptions>,
274>;
275
276pub type BeforeHookFn<World> = for<'a> fn(
281 &'a gherkin::Feature,
282 Option<&'a gherkin::Rule>,
283 &'a gherkin::Scenario,
284 &'a mut World,
285) -> LocalBoxFuture<'a, ()>;
286
287pub type AfterHookFn<World> = for<'a> fn(
292 &'a gherkin::Feature,
293 Option<&'a gherkin::Rule>,
294 &'a gherkin::Scenario,
295 &'a event::ScenarioFinished,
296 Option<&'a mut World>,
297) -> LocalBoxFuture<'a, ()>;
298
299type IsFailed = bool;
303
304type IsRetried = bool;
308
309#[derive(Debug)]
319pub struct Basic<
320 World,
321 F = WhichScenarioFn,
322 Before = BeforeHookFn<World>,
323 After = AfterHookFn<World>,
324> {
325 max_concurrent_scenarios: Option<usize>,
329
330 retries: Option<usize>,
334
335 retry_after: Option<Duration>,
339
340 retry_filter: Option<TagOperation>,
344
345 steps: step::Collection<World>,
349
350 #[debug(ignore)]
357 which_scenario: F,
358
359 #[debug(ignore)]
363 retry_options: RetryOptionsFn,
364
365 #[debug(ignore)]
372 before_hook: Option<Before>,
373
374 #[debug(ignore)]
380 after_hook: Option<After>,
381
382 fail_fast: bool,
384
385 #[cfg(feature = "tracing")]
386 #[debug(ignore)]
388 pub(crate) logs_collector: Arc<AtomicCell<Box<Option<TracingCollector>>>>,
389}
390
391#[cfg(feature = "tracing")]
392const _: () = {
394 assert!(
395 AtomicCell::<Box<Option<TracingCollector>>>::is_lock_free(),
396 "`AtomicCell::<Box<Option<TracingCollector>>>` is not lock-free",
397 );
398};
399
400impl<World, F: Clone, B: Clone, A: Clone> Clone for Basic<World, F, B, A> {
403 fn clone(&self) -> Self {
404 Self {
405 max_concurrent_scenarios: self.max_concurrent_scenarios,
406 retries: self.retries,
407 retry_after: self.retry_after,
408 retry_filter: self.retry_filter.clone(),
409 steps: self.steps.clone(),
410 which_scenario: self.which_scenario.clone(),
411 retry_options: Arc::clone(&self.retry_options),
412 before_hook: self.before_hook.clone(),
413 after_hook: self.after_hook.clone(),
414 fail_fast: self.fail_fast,
415 #[cfg(feature = "tracing")]
416 logs_collector: Arc::clone(&self.logs_collector),
417 }
418 }
419}
420
421impl<World> Default for Basic<World> {
422 fn default() -> Self {
423 let which_scenario: WhichScenarioFn = |feature, rule, scenario| {
424 scenario
425 .tags
426 .iter()
427 .chain(rule.iter().flat_map(|r| &r.tags))
428 .chain(&feature.tags)
429 .find(|tag| *tag == "serial")
430 .map_or(ScenarioType::Concurrent, |_| ScenarioType::Serial)
431 };
432
433 Self {
434 max_concurrent_scenarios: Some(64),
435 retries: None,
436 retry_after: None,
437 retry_filter: None,
438 steps: step::Collection::new(),
439 which_scenario,
440 retry_options: Arc::new(RetryOptions::parse_from_tags),
441 before_hook: None,
442 after_hook: None,
443 fail_fast: false,
444 #[cfg(feature = "tracing")]
445 logs_collector: Arc::new(AtomicCell::new(Box::new(None))),
446 }
447 }
448}
449
450impl<World, Which, Before, After> Basic<World, Which, Before, After> {
451 #[must_use]
456 pub fn max_concurrent_scenarios(
457 mut self,
458 max: impl Into<Option<usize>>,
459 ) -> Self {
460 self.max_concurrent_scenarios = max.into();
461 self
462 }
463
464 #[must_use]
469 pub fn retries(mut self, retries: impl Into<Option<usize>>) -> Self {
470 self.retries = retries.into();
471 self
472 }
473
474 #[must_use]
479 pub fn retry_after(mut self, after: impl Into<Option<Duration>>) -> Self {
480 self.retry_after = after.into();
481 self
482 }
483
484 #[must_use]
489 pub fn retry_filter(
490 mut self,
491 tag_expression: impl Into<Option<TagOperation>>,
492 ) -> Self {
493 self.retry_filter = tag_expression.into();
494 self
495 }
496
497 #[must_use]
507 pub const fn fail_fast(mut self) -> Self {
508 self.fail_fast = true;
509 self
510 }
511
512 #[must_use]
519 pub fn which_scenario<F>(self, func: F) -> Basic<World, F, Before, After>
520 where
521 F: Fn(
522 &gherkin::Feature,
523 Option<&gherkin::Rule>,
524 &gherkin::Scenario,
525 ) -> ScenarioType
526 + 'static,
527 {
528 let Self {
529 max_concurrent_scenarios,
530 retries,
531 retry_after,
532 retry_filter,
533 steps,
534 retry_options,
535 before_hook,
536 after_hook,
537 fail_fast,
538 #[cfg(feature = "tracing")]
539 logs_collector,
540 ..
541 } = self;
542 Basic {
543 max_concurrent_scenarios,
544 retries,
545 retry_after,
546 retry_filter,
547 steps,
548 which_scenario: func,
549 retry_options,
550 before_hook,
551 after_hook,
552 fail_fast,
553 #[cfg(feature = "tracing")]
554 logs_collector,
555 }
556 }
557
558 #[must_use]
562 pub fn retry_options<R>(mut self, func: R) -> Self
563 where
564 R: Fn(
565 &gherkin::Feature,
566 Option<&gherkin::Rule>,
567 &gherkin::Scenario,
568 &Cli,
569 ) -> Option<RetryOptions>
570 + 'static,
571 {
572 self.retry_options = Arc::new(func);
573 self
574 }
575
576 #[must_use]
588 pub fn before<Func>(self, func: Func) -> Basic<World, Which, Func, After>
589 where
590 Func: for<'a> Fn(
591 &'a gherkin::Feature,
592 Option<&'a gherkin::Rule>,
593 &'a gherkin::Scenario,
594 &'a mut World,
595 ) -> LocalBoxFuture<'a, ()>,
596 {
597 let Self {
598 max_concurrent_scenarios,
599 retries,
600 retry_after,
601 retry_filter,
602 steps,
603 which_scenario,
604 retry_options,
605 after_hook,
606 fail_fast,
607 #[cfg(feature = "tracing")]
608 logs_collector,
609 ..
610 } = self;
611 Basic {
612 max_concurrent_scenarios,
613 retries,
614 retry_after,
615 retry_filter,
616 steps,
617 which_scenario,
618 retry_options,
619 before_hook: Some(func),
620 after_hook,
621 fail_fast,
622 #[cfg(feature = "tracing")]
623 logs_collector,
624 }
625 }
626
627 #[must_use]
644 pub fn after<Func>(self, func: Func) -> Basic<World, Which, Before, Func>
645 where
646 Func: for<'a> Fn(
647 &'a gherkin::Feature,
648 Option<&'a gherkin::Rule>,
649 &'a gherkin::Scenario,
650 &'a event::ScenarioFinished,
651 Option<&'a mut World>,
652 ) -> LocalBoxFuture<'a, ()>,
653 {
654 let Self {
655 max_concurrent_scenarios,
656 retries,
657 retry_after,
658 retry_filter,
659 steps,
660 which_scenario,
661 retry_options,
662 before_hook,
663 fail_fast,
664 #[cfg(feature = "tracing")]
665 logs_collector,
666 ..
667 } = self;
668 Basic {
669 max_concurrent_scenarios,
670 retries,
671 retry_after,
672 retry_filter,
673 steps,
674 which_scenario,
675 retry_options,
676 before_hook,
677 after_hook: Some(func),
678 fail_fast,
679 #[cfg(feature = "tracing")]
680 logs_collector,
681 }
682 }
683
684 #[must_use]
688 pub fn steps(mut self, steps: step::Collection<World>) -> Self {
689 self.steps = steps;
690 self
691 }
692
693 #[must_use]
697 pub fn given(mut self, regex: Regex, step: Step<World>) -> Self {
698 self.steps = mem::take(&mut self.steps).given(None, regex, step);
699 self
700 }
701
702 #[must_use]
706 pub fn when(mut self, regex: Regex, step: Step<World>) -> Self {
707 self.steps = mem::take(&mut self.steps).when(None, regex, step);
708 self
709 }
710
711 #[must_use]
715 pub fn then(mut self, regex: Regex, step: Step<World>) -> Self {
716 self.steps = mem::take(&mut self.steps).then(None, regex, step);
717 self
718 }
719}
720
721impl<W, Which, Before, After> Runner<W> for Basic<W, Which, Before, After>
722where
723 W: World,
724 Which: Fn(
725 &gherkin::Feature,
726 Option<&gherkin::Rule>,
727 &gherkin::Scenario,
728 ) -> ScenarioType
729 + 'static,
730 Before: for<'a> Fn(
731 &'a gherkin::Feature,
732 Option<&'a gherkin::Rule>,
733 &'a gherkin::Scenario,
734 &'a mut W,
735 ) -> LocalBoxFuture<'a, ()>
736 + 'static,
737 After: for<'a> Fn(
738 &'a gherkin::Feature,
739 Option<&'a gherkin::Rule>,
740 &'a gherkin::Scenario,
741 &'a event::ScenarioFinished,
742 Option<&'a mut W>,
743 ) -> LocalBoxFuture<'a, ()>
744 + 'static,
745{
746 type Cli = Cli;
747
748 type EventStream =
749 LocalBoxStream<'static, parser::Result<Event<event::Cucumber<W>>>>;
750
751 fn run<S>(self, features: S, mut cli: Cli) -> Self::EventStream
752 where
753 S: Stream<Item = parser::Result<gherkin::Feature>> + 'static,
754 {
755 #[cfg(feature = "tracing")]
756 let logs_collector = *self.logs_collector.swap(Box::new(None));
757 let Self {
758 max_concurrent_scenarios,
759 retries,
760 retry_after,
761 retry_filter,
762 steps,
763 which_scenario,
764 retry_options,
765 before_hook,
766 after_hook,
767 fail_fast,
768 ..
769 } = self;
770
771 cli.retry = cli.retry.or(retries);
772 cli.retry_after = cli.retry_after.or(retry_after);
773 cli.retry_tag_filter = cli.retry_tag_filter.or(retry_filter);
774 let fail_fast = cli.fail_fast || fail_fast;
775 let concurrency = cli.concurrency.or(max_concurrent_scenarios);
776
777 let buffer = Features::default();
778 let (sender, receiver) = mpsc::unbounded();
779
780 let insert = insert_features(
781 buffer.clone(),
782 features,
783 which_scenario,
784 retry_options,
785 sender.clone(),
786 cli,
787 fail_fast,
788 );
789 let execute = execute(
790 buffer,
791 concurrency,
792 steps,
793 sender,
794 before_hook,
795 after_hook,
796 fail_fast,
797 #[cfg(feature = "tracing")]
798 logs_collector,
799 );
800
801 stream::select(
802 receiver.map(Either::Left),
803 future::join(insert, execute).into_stream().map(Either::Right),
804 )
805 .filter_map(async |r| match r {
806 Either::Left(ev) => Some(ev),
807 Either::Right(_) => None,
808 })
809 .boxed_local()
810 }
811}
812
813async fn insert_features<W, S, F>(
817 into: Features,
818 features_stream: S,
819 which_scenario: F,
820 retries: RetryOptionsFn,
821 sender: mpsc::UnboundedSender<parser::Result<Event<event::Cucumber<W>>>>,
822 cli: Cli,
823 fail_fast: bool,
824) where
825 S: Stream<Item = parser::Result<gherkin::Feature>> + 'static,
826 F: Fn(
827 &gherkin::Feature,
828 Option<&gherkin::Rule>,
829 &gherkin::Scenario,
830 ) -> ScenarioType
831 + 'static,
832{
833 let mut features = 0;
834 let mut rules = 0;
835 let mut scenarios = 0;
836 let mut steps = 0;
837 let mut parser_errors = 0;
838
839 pin_mut!(features_stream);
840 while let Some(feat) = features_stream.next().await {
841 match feat {
842 Ok(f) => {
843 features += 1;
844 rules += f.rules.len();
845 scenarios += f.count_scenarios();
846 steps += f.count_steps();
847
848 into.insert(f, &which_scenario, &retries, &cli).await;
849 }
850 Err(e) => {
851 parser_errors += 1;
852
853 if sender.unbounded_send(Err(e)).is_err() || fail_fast {
856 break;
857 }
858 }
859 }
860 }
861
862 drop(sender.unbounded_send(Ok(Event::new(
863 event::Cucumber::ParsingFinished {
864 features,
865 rules,
866 scenarios,
867 steps,
868 parser_errors,
869 },
870 ))));
871
872 into.finish();
873}
874
875#[expect(clippy::too_many_lines, reason = "needs refactoring")]
888#[cfg_attr(
889 feature = "tracing",
890 expect(clippy::too_many_arguments, reason = "needs refactoring")
891)]
892async fn execute<W, Before, After>(
893 features: Features,
894 max_concurrent_scenarios: Option<usize>,
895 collection: step::Collection<W>,
896 event_sender: mpsc::UnboundedSender<
897 parser::Result<Event<event::Cucumber<W>>>,
898 >,
899 before_hook: Option<Before>,
900 after_hook: Option<After>,
901 fail_fast: bool,
902 #[cfg(feature = "tracing")] mut logs_collector: Option<TracingCollector>,
903) where
904 W: World,
905 Before: 'static
906 + for<'a> Fn(
907 &'a gherkin::Feature,
908 Option<&'a gherkin::Rule>,
909 &'a gherkin::Scenario,
910 &'a mut W,
911 ) -> LocalBoxFuture<'a, ()>,
912 After: 'static
913 + for<'a> Fn(
914 &'a gherkin::Feature,
915 Option<&'a gherkin::Rule>,
916 &'a gherkin::Scenario,
917 &'a event::ScenarioFinished,
918 Option<&'a mut W>,
919 ) -> LocalBoxFuture<'a, ()>,
920{
921 let hook = panic::take_hook();
930 panic::set_hook(Box::new(|_| {}));
931
932 let (finished_sender, finished_receiver) = mpsc::unbounded();
933 let mut storage = FinishedRulesAndFeatures::new(finished_receiver);
934 let executor = Executor::new(
935 collection,
936 before_hook,
937 after_hook,
938 event_sender,
939 finished_sender,
940 features.clone(),
941 );
942
943 executor.send_event(event::Cucumber::Started);
944
945 #[cfg(feature = "tracing")]
946 let waiter = logs_collector
947 .as_ref()
948 .map(TracingCollector::scenario_span_event_waiter);
949
950 let mut started_scenarios = ControlFlow::Continue(max_concurrent_scenarios);
951 let mut run_scenarios = stream::FuturesUnordered::new();
952 loop {
953 let (runnable, sleep) = features
954 .get(started_scenarios.continue_value().unwrap_or(Some(0)))
955 .await;
956 if run_scenarios.is_empty() && runnable.is_empty() {
957 if features.is_finished(started_scenarios.is_break()).await {
958 break;
959 }
960
961 if let Some(dur) = sleep {
968 let (sender, receiver) = oneshot::channel();
969 drop(thread::spawn(move || {
970 thread::sleep(dur);
971 sender.send(())
972 }));
973 _ = receiver.await.ok();
974 }
975
976 continue;
977 }
978
979 let started = storage.start_scenarios(&runnable);
980 executor.send_all_events(started);
981
982 {
983 #[cfg(feature = "tracing")]
984 let forward_logs = {
985 if let Some(coll) = logs_collector.as_mut() {
986 coll.start_scenarios(&runnable);
987 }
988 async {
989 loop {
990 while let Some(logs) = logs_collector
991 .as_mut()
992 .and_then(TracingCollector::emitted_logs)
993 {
994 executor.send_all_events(logs);
995 }
996 future::ready(()).then_yield().await;
997 }
998 }
999 };
1000 #[cfg(feature = "tracing")]
1001 pin_mut!(forward_logs);
1002 #[cfg(not(feature = "tracing"))]
1003 let forward_logs = future::pending();
1004
1005 if let ControlFlow::Continue(Some(sc)) = &mut started_scenarios {
1006 *sc -= runnable.len();
1007 }
1008
1009 for (id, f, r, s, ty, retries) in runnable {
1010 run_scenarios.push(
1011 executor
1012 .run_scenario(
1013 id,
1014 f,
1015 r,
1016 s,
1017 ty,
1018 retries,
1019 #[cfg(feature = "tracing")]
1020 waiter.as_ref(),
1021 )
1022 .then_yield(),
1023 );
1024 }
1025
1026 let (finished_scenario, _) =
1027 select_with_biased_first(forward_logs, run_scenarios.next())
1028 .await
1029 .factor_first();
1030 if finished_scenario.is_some()
1031 && let ControlFlow::Continue(Some(sc)) = &mut started_scenarios
1032 {
1033 *sc += 1;
1034 }
1035 }
1036
1037 while let Ok(Some((id, feat, rule, scenario_failed, retried))) =
1038 storage.finished_receiver.try_next()
1039 {
1040 if let Some(rule) = rule
1041 && let Some(f) =
1042 storage.rule_scenario_finished(feat.clone(), rule, retried)
1043 {
1044 executor.send_event(f);
1045 }
1046 if let Some(f) = storage.feature_scenario_finished(feat, retried) {
1047 executor.send_event(f);
1048 }
1049 #[cfg(feature = "tracing")]
1050 {
1051 if let Some(coll) = logs_collector.as_mut() {
1052 coll.finish_scenario(id);
1053 }
1054 }
1055 #[cfg(not(feature = "tracing"))]
1056 let _: ScenarioId = id;
1057
1058 if fail_fast && scenario_failed && !retried {
1059 started_scenarios = ControlFlow::Break(());
1060 }
1061 }
1062 }
1063
1064 executor.send_all_events(storage.finish_all_rules_and_features());
1067
1068 executor.send_event(event::Cucumber::Finished);
1069
1070 panic::set_hook(hook);
1071}
1072
1073struct Executor<W, Before, After> {
1077 collection: step::Collection<W>,
1081
1082 before_hook: Option<Before>,
1089
1090 after_hook: Option<After>,
1095
1096 event_sender:
1101 mpsc::UnboundedSender<parser::Result<Event<event::Cucumber<W>>>>,
1102
1103 finished_sender: FinishedFeaturesSender,
1107
1108 storage: Features,
1112}
1113
1114impl<W: World, Before, After> Executor<W, Before, After>
1115where
1116 Before: 'static
1117 + for<'a> Fn(
1118 &'a gherkin::Feature,
1119 Option<&'a gherkin::Rule>,
1120 &'a gherkin::Scenario,
1121 &'a mut W,
1122 ) -> LocalBoxFuture<'a, ()>,
1123 After: 'static
1124 + for<'a> Fn(
1125 &'a gherkin::Feature,
1126 Option<&'a gherkin::Rule>,
1127 &'a gherkin::Scenario,
1128 &'a event::ScenarioFinished,
1129 Option<&'a mut W>,
1130 ) -> LocalBoxFuture<'a, ()>,
1131{
1132 const fn new(
1134 collection: step::Collection<W>,
1135 before_hook: Option<Before>,
1136 after_hook: Option<After>,
1137 event_sender: mpsc::UnboundedSender<
1138 parser::Result<Event<event::Cucumber<W>>>,
1139 >,
1140 finished_sender: FinishedFeaturesSender,
1141 storage: Features,
1142 ) -> Self {
1143 Self {
1144 collection,
1145 before_hook,
1146 after_hook,
1147 event_sender,
1148 finished_sender,
1149 storage,
1150 }
1151 }
1152
1153 #[expect(clippy::too_many_lines, reason = "needs refactoring")]
1164 #[cfg_attr(
1165 feature = "tracing",
1166 expect(clippy::too_many_arguments, reason = "needs refactoring")
1167 )]
1168 async fn run_scenario(
1169 &self,
1170 id: ScenarioId,
1171 feature: Source<gherkin::Feature>,
1172 rule: Option<Source<gherkin::Rule>>,
1173 scenario: Source<gherkin::Scenario>,
1174 scenario_ty: ScenarioType,
1175 retries: Option<RetryOptions>,
1176 #[cfg(feature = "tracing")] waiter: Option<&SpanCloseWaiter>,
1177 ) {
1178 let retry_num = retries.map(|r| r.retries);
1179 let ok = |e: fn(_) -> event::Scenario<W>| {
1180 let (f, r, s) = (&feature, &rule, &scenario);
1181 move |step| {
1182 let (f, r, s) = (f.clone(), r.clone(), s.clone());
1183 let event = e(step).with_retries(retry_num);
1184 event::Cucumber::scenario(f, r, s, event)
1185 }
1186 };
1187 let ok_capt = |e: fn(_, _, _) -> event::Scenario<W>| {
1188 let (f, r, s) = (&feature, &rule, &scenario);
1189 move |step, cap, loc| {
1190 let (f, r, s) = (f.clone(), r.clone(), s.clone());
1191 let event = e(step, cap, loc).with_retries(retry_num);
1192 event::Cucumber::scenario(f, r, s, event)
1193 }
1194 };
1195
1196 let compose = |started, passed, skipped| {
1197 (ok(started), ok_capt(passed), ok(skipped))
1198 };
1199 let into_bg_step_ev = compose(
1200 event::Scenario::background_step_started,
1201 event::Scenario::background_step_passed,
1202 event::Scenario::background_step_skipped,
1203 );
1204 let into_step_ev = compose(
1205 event::Scenario::step_started,
1206 event::Scenario::step_passed,
1207 event::Scenario::step_skipped,
1208 );
1209
1210 self.send_event(event::Cucumber::scenario(
1211 feature.clone(),
1212 rule.clone(),
1213 scenario.clone(),
1214 event::Scenario::Started.with_retries(retry_num),
1215 ));
1216
1217 let is_failed = async {
1218 let mut result = async {
1219 let before_hook = self
1220 .run_before_hook(
1221 &feature,
1222 rule.as_ref(),
1223 &scenario,
1224 retry_num,
1225 id,
1226 #[cfg(feature = "tracing")]
1227 waiter,
1228 )
1229 .await?;
1230
1231 let feature_background = feature
1232 .background
1233 .as_ref()
1234 .map(|b| b.steps.iter().map(|s| Source::new(s.clone())))
1235 .into_iter()
1236 .flatten();
1237
1238 let feature_background = stream::iter(feature_background)
1239 .map(Ok)
1240 .try_fold(before_hook, |world, bg_step| {
1241 self.run_step(
1242 world,
1243 bg_step,
1244 true,
1245 into_bg_step_ev,
1246 id,
1247 #[cfg(feature = "tracing")]
1248 waiter,
1249 )
1250 .map_ok(Some)
1251 })
1252 .await?;
1253
1254 let rule_background = rule
1255 .as_ref()
1256 .map(|r| {
1257 r.background
1258 .as_ref()
1259 .map(|b| {
1260 b.steps.iter().map(|s| Source::new(s.clone()))
1261 })
1262 .into_iter()
1263 .flatten()
1264 })
1265 .into_iter()
1266 .flatten();
1267
1268 let rule_background = stream::iter(rule_background)
1269 .map(Ok)
1270 .try_fold(feature_background, |world, bg_step| {
1271 self.run_step(
1272 world,
1273 bg_step,
1274 true,
1275 into_bg_step_ev,
1276 id,
1277 #[cfg(feature = "tracing")]
1278 waiter,
1279 )
1280 .map_ok(Some)
1281 })
1282 .await?;
1283
1284 stream::iter(
1285 scenario.steps.iter().map(|s| Source::new(s.clone())),
1286 )
1287 .map(Ok)
1288 .try_fold(rule_background, |world, step| {
1289 self.run_step(
1290 world,
1291 step,
1292 false,
1293 into_step_ev,
1294 id,
1295 #[cfg(feature = "tracing")]
1296 waiter,
1297 )
1298 .map_ok(Some)
1299 })
1300 .await
1301 }
1302 .await;
1303
1304 let (world, scenario_finished_ev) = match &mut result {
1305 Ok(world) => {
1306 (world.take(), event::ScenarioFinished::StepPassed)
1307 }
1308 Err(exec_err) => (
1309 exec_err.take_world(),
1310 exec_err.get_scenario_finished_event(),
1311 ),
1312 };
1313
1314 let (world, after_hook_meta, after_hook_error) = self
1315 .run_after_hook(
1316 world,
1317 &feature,
1318 rule.as_ref(),
1319 &scenario,
1320 scenario_finished_ev,
1321 id,
1322 #[cfg(feature = "tracing")]
1323 waiter,
1324 )
1325 .await
1326 .map_or_else(
1327 |(w, meta, info)| (w.map(Arc::new), Some(meta), Some(info)),
1328 |(w, meta)| (w.map(Arc::new), meta, None),
1329 );
1330
1331 let scenario_failed = match &result {
1332 Ok(_) | Err(ExecutionFailure::StepSkipped(_)) => false,
1333 Err(
1334 ExecutionFailure::BeforeHookPanicked { .. }
1335 | ExecutionFailure::StepPanicked { .. },
1336 ) => true,
1337 };
1338 let is_failed = scenario_failed || after_hook_error.is_some();
1339
1340 if let Some(exec_error) = result.err() {
1341 self.emit_failed_events(
1342 feature.clone(),
1343 rule.clone(),
1344 scenario.clone(),
1345 world.clone(),
1346 exec_error,
1347 retry_num,
1348 );
1349 }
1350
1351 self.emit_after_hook_events(
1352 feature.clone(),
1353 rule.clone(),
1354 scenario.clone(),
1355 world,
1356 after_hook_meta,
1357 after_hook_error,
1358 retry_num,
1359 );
1360
1361 is_failed
1362 };
1363 #[cfg(feature = "tracing")]
1364 let (is_failed, span_id) = {
1365 let span = id.scenario_span();
1366 let span_id = span.id();
1367 let is_failed = tracing::Instrument::instrument(is_failed, span);
1368 (is_failed, span_id)
1369 };
1370 let is_failed = is_failed.then_yield().await;
1371
1372 #[cfg(feature = "tracing")]
1373 if let Some((waiter, span_id)) = waiter.zip(span_id) {
1374 waiter.wait_for_span_close(span_id).then_yield().await;
1375 }
1376
1377 self.send_event(event::Cucumber::scenario(
1378 feature.clone(),
1379 rule.clone(),
1380 scenario.clone(),
1381 event::Scenario::Finished.with_retries(retry_num),
1382 ));
1383
1384 let next_try =
1385 retries.filter(|_| is_failed).and_then(RetryOptions::next_try);
1386 if let Some(next_try) = next_try {
1387 self.storage
1388 .insert_retried_scenario(
1389 feature.clone(),
1390 rule.clone(),
1391 scenario,
1392 scenario_ty,
1393 Some(next_try),
1394 )
1395 .await;
1396 }
1397
1398 self.scenario_finished(
1399 id,
1400 feature,
1401 rule,
1402 is_failed,
1403 next_try.is_some(),
1404 );
1405 }
1406
1407 async fn run_before_hook(
1416 &self,
1417 feature: &Source<gherkin::Feature>,
1418 rule: Option<&Source<gherkin::Rule>>,
1419 scenario: &Source<gherkin::Scenario>,
1420 retries: Option<Retries>,
1421 scenario_id: ScenarioId,
1422 #[cfg(feature = "tracing")] waiter: Option<&SpanCloseWaiter>,
1423 ) -> Result<Option<W>, ExecutionFailure<W>> {
1424 let init_world = async {
1425 AssertUnwindSafe(async { W::new().await })
1426 .catch_unwind()
1427 .then_yield()
1428 .await
1429 .map_err(Info::from)
1430 .and_then(|r| {
1431 r.map_err(|e| {
1432 coerce_into_info(format!(
1433 "failed to initialize World: {e}",
1434 ))
1435 })
1436 })
1437 .map_err(|info| (info, None))
1438 };
1439
1440 if let Some(hook) = self.before_hook.as_ref() {
1441 self.send_event(event::Cucumber::scenario(
1442 feature.clone(),
1443 rule.cloned(),
1444 scenario.clone(),
1445 event::Scenario::hook_started(HookType::Before)
1446 .with_retries(retries),
1447 ));
1448
1449 let fut = init_world.and_then(async |mut world| {
1450 let fut = async {
1451 (hook)(
1452 feature.as_ref(),
1453 rule.as_ref().map(AsRef::as_ref),
1454 scenario.as_ref(),
1455 &mut world,
1456 )
1457 .await;
1458 };
1459 match AssertUnwindSafe(fut).catch_unwind().await {
1460 Ok(()) => Ok(world),
1461 Err(i) => Err((Info::from(i), Some(world))),
1462 }
1463 });
1464
1465 #[cfg(feature = "tracing")]
1466 let (fut, span_id) = {
1467 let span = scenario_id.hook_span(HookType::Before);
1468 let span_id = span.id();
1469 let fut = tracing::Instrument::instrument(fut, span);
1470 (fut, span_id)
1471 };
1472 #[cfg(not(feature = "tracing"))]
1473 let _: ScenarioId = scenario_id;
1474
1475 let result = fut.then_yield().await;
1476
1477 #[cfg(feature = "tracing")]
1478 if let Some((waiter, id)) = waiter.zip(span_id) {
1479 waiter.wait_for_span_close(id).then_yield().await;
1480 }
1481
1482 match result {
1483 Ok(world) => {
1484 self.send_event(event::Cucumber::scenario(
1485 feature.clone(),
1486 rule.cloned(),
1487 scenario.clone(),
1488 event::Scenario::hook_passed(HookType::Before)
1489 .with_retries(retries),
1490 ));
1491 Ok(Some(world))
1492 }
1493 Err((panic_info, world)) => {
1494 Err(ExecutionFailure::BeforeHookPanicked {
1495 world,
1496 panic_info,
1497 meta: event::Metadata::new(()),
1498 })
1499 }
1500 }
1501 } else {
1502 Ok(None)
1503 }
1504 }
1505
1506 async fn run_step<St, Ps, Sk>(
1516 &self,
1517 world_opt: Option<W>,
1518 step: Source<gherkin::Step>,
1519 is_background: bool,
1520 (started, passed, skipped): (St, Ps, Sk),
1521 scenario_id: ScenarioId,
1522 #[cfg(feature = "tracing")] waiter: Option<&SpanCloseWaiter>,
1523 ) -> Result<W, ExecutionFailure<W>>
1524 where
1525 St: FnOnce(Source<gherkin::Step>) -> event::Cucumber<W>,
1526 Ps: FnOnce(
1527 Source<gherkin::Step>,
1528 CaptureLocations,
1529 Option<step::Location>,
1530 ) -> event::Cucumber<W>,
1531 Sk: FnOnce(Source<gherkin::Step>) -> event::Cucumber<W>,
1532 {
1533 self.send_event(started(step.clone()));
1534
1535 let run = async {
1536 let (step_fn, captures, loc, ctx) =
1537 match self.collection.find(&step) {
1538 Ok(Some(f)) => f,
1539 Ok(None) => return Ok((None, None, world_opt)),
1540 Err(e) => {
1541 let e = event::StepError::AmbiguousMatch(e);
1542 return Err((e, None, None, world_opt));
1543 }
1544 };
1545
1546 let mut world = if let Some(w) = world_opt {
1547 w
1548 } else {
1549 match AssertUnwindSafe(async { W::new().await })
1550 .catch_unwind()
1551 .then_yield()
1552 .await
1553 {
1554 Ok(Ok(w)) => w,
1555 Ok(Err(e)) => {
1556 let e = event::StepError::Panic(coerce_into_info(
1557 format!("failed to initialize `World`: {e}"),
1558 ));
1559 return Err((e, None, loc, None));
1560 }
1561 Err(e) => {
1562 let e = event::StepError::Panic(e.into());
1563 return Err((e, None, loc, None));
1564 }
1565 }
1566 };
1567
1568 match AssertUnwindSafe(async { step_fn(&mut world, ctx).await })
1569 .catch_unwind()
1570 .await
1571 {
1572 Ok(()) => Ok((Some(captures), loc, Some(world))),
1573 Err(e) => {
1574 let e = event::StepError::Panic(e.into());
1575 Err((e, Some(captures), loc, Some(world)))
1576 }
1577 }
1578 };
1579
1580 #[cfg(feature = "tracing")]
1581 let (run, span_id) = {
1582 let span = scenario_id.step_span(is_background);
1583 let span_id = span.id();
1584 let run = tracing::Instrument::instrument(run, span);
1585 (run, span_id)
1586 };
1587 let result = run.then_yield().await;
1588
1589 #[cfg(feature = "tracing")]
1590 if let Some((waiter, id)) = waiter.zip(span_id) {
1591 waiter.wait_for_span_close(id).then_yield().await;
1592 }
1593 #[cfg(not(feature = "tracing"))]
1594 let _: ScenarioId = scenario_id;
1595
1596 match result {
1597 Ok((Some(captures), loc, Some(world))) => {
1598 self.send_event(passed(step, captures, loc));
1599 Ok(world)
1600 }
1601 Ok((_, _, world)) => {
1602 self.send_event(skipped(step));
1603 Err(ExecutionFailure::StepSkipped(world))
1604 }
1605 Err((err, captures, loc, world)) => {
1606 Err(ExecutionFailure::StepPanicked {
1607 world,
1608 step,
1609 captures,
1610 loc,
1611 err,
1612 meta: event::Metadata::new(()),
1613 is_background,
1614 })
1615 }
1616 }
1617 }
1618
1619 fn emit_failed_events(
1638 &self,
1639 feature: Source<gherkin::Feature>,
1640 rule: Option<Source<gherkin::Rule>>,
1641 scenario: Source<gherkin::Scenario>,
1642 world: Option<Arc<W>>,
1643 err: ExecutionFailure<W>,
1644 retries: Option<Retries>,
1645 ) {
1646 match err {
1647 ExecutionFailure::StepSkipped(_) => {}
1648 ExecutionFailure::BeforeHookPanicked {
1649 panic_info, meta, ..
1650 } => {
1651 self.send_event_with_meta(
1652 event::Cucumber::scenario(
1653 feature,
1654 rule,
1655 scenario,
1656 event::Scenario::hook_failed(
1657 HookType::Before,
1658 world,
1659 panic_info,
1660 )
1661 .with_retries(retries),
1662 ),
1663 meta,
1664 );
1665 }
1666 ExecutionFailure::StepPanicked {
1667 step,
1668 captures,
1669 loc,
1670 err: error,
1671 meta,
1672 is_background: true,
1673 ..
1674 } => self.send_event_with_meta(
1675 event::Cucumber::scenario(
1676 feature,
1677 rule,
1678 scenario,
1679 event::Scenario::background_step_failed(
1680 step, captures, loc, world, error,
1681 )
1682 .with_retries(retries),
1683 ),
1684 meta,
1685 ),
1686 ExecutionFailure::StepPanicked {
1687 step,
1688 captures,
1689 loc,
1690 err: error,
1691 meta,
1692 is_background: false,
1693 ..
1694 } => self.send_event_with_meta(
1695 event::Cucumber::scenario(
1696 feature,
1697 rule,
1698 scenario,
1699 event::Scenario::step_failed(
1700 step, captures, loc, world, error,
1701 )
1702 .with_retries(retries),
1703 ),
1704 meta,
1705 ),
1706 }
1707 }
1708
1709 #[cfg_attr(
1715 feature = "tracing",
1716 expect(clippy::too_many_arguments, reason = "needs refactoring")
1717 )]
1718 async fn run_after_hook(
1719 &self,
1720 mut world: Option<W>,
1721 feature: &Source<gherkin::Feature>,
1722 rule: Option<&Source<gherkin::Rule>>,
1723 scenario: &Source<gherkin::Scenario>,
1724 ev: event::ScenarioFinished,
1725 scenario_id: ScenarioId,
1726 #[cfg(feature = "tracing")] waiter: Option<&SpanCloseWaiter>,
1727 ) -> Result<
1728 (Option<W>, Option<AfterHookEventsMeta>),
1729 (Option<W>, AfterHookEventsMeta, Info),
1730 > {
1731 if let Some(hook) = self.after_hook.as_ref() {
1732 let fut = async {
1733 (hook)(
1734 feature.as_ref(),
1735 rule.as_ref().map(AsRef::as_ref),
1736 scenario.as_ref(),
1737 &ev,
1738 world.as_mut(),
1739 )
1740 .await;
1741 };
1742
1743 let started = event::Metadata::new(());
1744 let fut = AssertUnwindSafe(fut).catch_unwind();
1745
1746 #[cfg(feature = "tracing")]
1747 let (fut, span_id) = {
1748 let span = scenario_id.hook_span(HookType::After);
1749 let span_id = span.id();
1750 let fut = tracing::Instrument::instrument(fut, span);
1751 (fut, span_id)
1752 };
1753 #[cfg(not(feature = "tracing"))]
1754 let _: ScenarioId = scenario_id;
1755
1756 let res = fut.then_yield().await;
1757
1758 #[cfg(feature = "tracing")]
1759 if let Some((waiter, id)) = waiter.zip(span_id) {
1760 waiter.wait_for_span_close(id).then_yield().await;
1761 }
1762
1763 let finished = event::Metadata::new(());
1764 let meta = AfterHookEventsMeta { started, finished };
1765
1766 match res {
1767 Ok(()) => Ok((world, Some(meta))),
1768 Err(info) => Err((world, meta, info.into())),
1769 }
1770 } else {
1771 Ok((world, None))
1772 }
1773 }
1774
1775 #[expect(clippy::too_many_arguments, reason = "needs refactoring")]
1781 fn emit_after_hook_events(
1782 &self,
1783 feature: Source<gherkin::Feature>,
1784 rule: Option<Source<gherkin::Rule>>,
1785 scenario: Source<gherkin::Scenario>,
1786 world: Option<Arc<W>>,
1787 meta: Option<AfterHookEventsMeta>,
1788 err: Option<Info>,
1789 retries: Option<Retries>,
1790 ) {
1791 debug_assert_eq!(
1792 self.after_hook.is_some(),
1793 meta.is_some(),
1794 "`AfterHookEventsMeta` is not passed, despite `self.after_hook` \
1795 being set",
1796 );
1797
1798 if let Some(meta) = meta {
1799 self.send_event_with_meta(
1800 event::Cucumber::scenario(
1801 feature.clone(),
1802 rule.clone(),
1803 scenario.clone(),
1804 event::Scenario::hook_started(HookType::After)
1805 .with_retries(retries),
1806 ),
1807 meta.started,
1808 );
1809
1810 let ev = if let Some(e) = err {
1811 event::Cucumber::scenario(
1812 feature,
1813 rule,
1814 scenario,
1815 event::Scenario::hook_failed(HookType::After, world, e)
1816 .with_retries(retries),
1817 )
1818 } else {
1819 event::Cucumber::scenario(
1820 feature,
1821 rule,
1822 scenario,
1823 event::Scenario::hook_passed(HookType::After)
1824 .with_retries(retries),
1825 )
1826 };
1827
1828 self.send_event_with_meta(ev, meta.finished);
1829 }
1830 }
1831
1832 fn scenario_finished(
1836 &self,
1837 id: ScenarioId,
1838 feature: Source<gherkin::Feature>,
1839 rule: Option<Source<gherkin::Rule>>,
1840 is_failed: IsFailed,
1841 is_retried: IsRetried,
1842 ) {
1843 drop(
1846 self.finished_sender
1847 .unbounded_send((id, feature, rule, is_failed, is_retried)),
1848 );
1849 }
1850
1851 fn send_event(&self, event: event::Cucumber<W>) {
1855 drop(self.event_sender.unbounded_send(Ok(Event::new(event))));
1858 }
1859
1860 fn send_event_with_meta(
1865 &self,
1866 event: event::Cucumber<W>,
1867 meta: event::Metadata,
1868 ) {
1869 drop(self.event_sender.unbounded_send(Ok(meta.wrap(event))));
1872 }
1873
1874 fn send_all_events(
1878 &self,
1879 events: impl IntoIterator<Item = event::Cucumber<W>>,
1880 ) {
1881 for v in events {
1882 if self.event_sender.unbounded_send(Ok(Event::new(v))).is_err() {
1885 break;
1886 }
1887 }
1888 }
1889}
1890
1891#[derive(Clone, Copy, Debug, Display, Eq, FromStr, Hash, PartialEq)]
1897pub struct ScenarioId(pub(crate) u64);
1898
1899impl ScenarioId {
1900 pub fn new() -> Self {
1902 static ID: AtomicU64 = AtomicU64::new(0);
1904
1905 Self(ID.fetch_add(1, Ordering::Relaxed))
1906 }
1907}
1908
1909impl Default for ScenarioId {
1910 fn default() -> Self {
1911 Self::new()
1912 }
1913}
1914
1915struct FinishedRulesAndFeatures {
1921 features_scenarios_count: HashMap<Source<gherkin::Feature>, usize>,
1926
1927 rule_scenarios_count:
1936 HashMap<(Source<gherkin::Feature>, Source<gherkin::Rule>), usize>,
1937
1938 finished_receiver: FinishedFeaturesReceiver,
1942}
1943
1944type FinishedFeaturesSender = mpsc::UnboundedSender<(
1949 ScenarioId,
1950 Source<gherkin::Feature>,
1951 Option<Source<gherkin::Rule>>,
1952 IsFailed,
1953 IsRetried,
1954)>;
1955
1956type FinishedFeaturesReceiver = mpsc::UnboundedReceiver<(
1961 ScenarioId,
1962 Source<gherkin::Feature>,
1963 Option<Source<gherkin::Rule>>,
1964 IsFailed,
1965 IsRetried,
1966)>;
1967
1968impl FinishedRulesAndFeatures {
1969 fn new(finished_receiver: FinishedFeaturesReceiver) -> Self {
1971 Self {
1972 features_scenarios_count: HashMap::new(),
1973 rule_scenarios_count: HashMap::new(),
1974 finished_receiver,
1975 }
1976 }
1977
1978 fn rule_scenario_finished<W>(
1985 &mut self,
1986 feature: Source<gherkin::Feature>,
1987 rule: Source<gherkin::Rule>,
1988 is_retried: bool,
1989 ) -> Option<event::Cucumber<W>> {
1990 if is_retried {
1991 return None;
1992 }
1993
1994 let finished_scenarios = self
1995 .rule_scenarios_count
1996 .get_mut(&(feature.clone(), rule.clone()))
1997 .unwrap_or_else(|| panic!("no `Rule: {}`", rule.name));
1998 *finished_scenarios += 1;
1999 (rule.scenarios.len() == *finished_scenarios).then(|| {
2000 _ = self
2001 .rule_scenarios_count
2002 .remove(&(feature.clone(), rule.clone()));
2003 event::Cucumber::rule_finished(feature, rule)
2004 })
2005 }
2006
2007 fn feature_scenario_finished<W>(
2014 &mut self,
2015 feature: Source<gherkin::Feature>,
2016 is_retried: bool,
2017 ) -> Option<event::Cucumber<W>> {
2018 if is_retried {
2019 return None;
2020 }
2021
2022 let finished_scenarios = self
2023 .features_scenarios_count
2024 .get_mut(&feature)
2025 .unwrap_or_else(|| panic!("no `Feature: {}`", feature.name));
2026 *finished_scenarios += 1;
2027 let scenarios = feature.count_scenarios();
2028 (scenarios == *finished_scenarios).then(|| {
2029 _ = self.features_scenarios_count.remove(&feature);
2030 event::Cucumber::feature_finished(feature)
2031 })
2032 }
2033
2034 fn finish_all_rules_and_features<W>(
2040 &mut self,
2041 ) -> impl Iterator<Item = event::Cucumber<W>> {
2042 self.rule_scenarios_count
2043 .drain()
2044 .map(|((feat, rule), _)| event::Cucumber::rule_finished(feat, rule))
2045 .chain(
2046 self.features_scenarios_count
2047 .drain()
2048 .map(|(feat, _)| event::Cucumber::feature_finished(feat)),
2049 )
2050 }
2051
2052 fn start_scenarios<W, R>(
2062 &mut self,
2063 runnable: R,
2064 ) -> impl Iterator<Item = event::Cucumber<W>> + use<W, R>
2065 where
2066 R: AsRef<
2067 [(
2068 ScenarioId,
2069 Source<gherkin::Feature>,
2070 Option<Source<gherkin::Rule>>,
2071 Source<gherkin::Scenario>,
2072 ScenarioType,
2073 Option<RetryOptions>,
2074 )],
2075 >,
2076 {
2077 let runnable = runnable.as_ref();
2078
2079 let mut started_features = Vec::new();
2080 for feature in runnable.iter().map(|(_, f, ..)| f.clone()).dedup() {
2081 _ = self
2082 .features_scenarios_count
2083 .entry(feature.clone())
2084 .or_insert_with(|| {
2085 started_features.push(feature);
2086 0
2087 });
2088 }
2089
2090 let mut started_rules = Vec::new();
2091 for (feat, rule) in runnable
2092 .iter()
2093 .filter_map(|(_, feat, rule, _, _, _)| {
2094 rule.clone().map(|r| (feat.clone(), r))
2095 })
2096 .dedup()
2097 {
2098 _ = self
2099 .rule_scenarios_count
2100 .entry((feat.clone(), rule.clone()))
2101 .or_insert_with(|| {
2102 started_rules.push((feat, rule));
2103 0
2104 });
2105 }
2106
2107 started_features
2108 .into_iter()
2109 .map(event::Cucumber::feature_started)
2110 .chain(
2111 started_rules
2112 .into_iter()
2113 .map(|(f, r)| event::Cucumber::rule_started(f, r)),
2114 )
2115 }
2116}
2117
2118type Scenarios = HashMap<
2122 ScenarioType,
2123 Vec<(
2124 ScenarioId,
2125 Source<gherkin::Feature>,
2126 Option<Source<gherkin::Rule>>,
2127 Source<gherkin::Scenario>,
2128 Option<RetryOptionsWithDeadline>,
2129 )>,
2130>;
2131
2132type InsertedScenarios = HashMap<
2134 ScenarioType,
2135 Vec<(
2136 ScenarioId,
2137 Source<gherkin::Feature>,
2138 Option<Source<gherkin::Rule>>,
2139 Source<gherkin::Scenario>,
2140 Option<RetryOptions>,
2141 )>,
2142>;
2143
2144#[derive(Clone, Default)]
2149struct Features {
2150 scenarios: Arc<Mutex<Scenarios>>,
2152
2153 finished: Arc<AtomicBool>,
2157}
2158
2159impl Features {
2160 async fn insert<Which>(
2166 &self,
2167 feature: gherkin::Feature,
2168 which_scenario: &Which,
2169 retry: &RetryOptionsFn,
2170 cli: &Cli,
2171 ) where
2172 Which: Fn(
2173 &gherkin::Feature,
2174 Option<&gherkin::Rule>,
2175 &gherkin::Scenario,
2176 ) -> ScenarioType
2177 + 'static,
2178 {
2179 let feature = Source::new(feature);
2180
2181 let local = feature
2182 .scenarios
2183 .iter()
2184 .map(|s| (None, s))
2185 .chain(feature.rules.iter().flat_map(|r| {
2186 let rule = Some(Source::new(r.clone()));
2187 r.scenarios
2188 .iter()
2189 .map(|s| (rule.clone(), s))
2190 .collect::<Vec<_>>()
2191 }))
2192 .map(|(rule, scenario)| {
2193 let retries = retry(&feature, rule.as_deref(), scenario, cli);
2194 (
2195 ScenarioId::new(),
2196 feature.clone(),
2197 rule,
2198 Source::new(scenario.clone()),
2199 retries,
2200 )
2201 })
2202 .into_group_map_by(|(_, f, r, s, _)| {
2203 which_scenario(f, r.as_ref().map(AsRef::as_ref), s)
2204 });
2205
2206 self.insert_scenarios(local).await;
2207 }
2208
2209 async fn insert_retried_scenario(
2214 &self,
2215 feature: Source<gherkin::Feature>,
2216 rule: Option<Source<gherkin::Rule>>,
2217 scenario: Source<gherkin::Scenario>,
2218 scenario_ty: ScenarioType,
2219 retries: Option<RetryOptions>,
2220 ) {
2221 self.insert_scenarios(
2222 iter::once((
2223 scenario_ty,
2224 vec![(ScenarioId::new(), feature, rule, scenario, retries)],
2225 ))
2226 .collect(),
2227 )
2228 .await;
2229 }
2230
2231 async fn insert_scenarios(&self, scenarios: InsertedScenarios) {
2235 let now = Instant::now();
2236
2237 let mut with_retries = HashMap::<_, Vec<_>>::new();
2238 let mut without_retries: Scenarios = HashMap::new();
2239 #[expect(clippy::iter_over_hash_type, reason = "order doesn't matter")]
2240 for (which, values) in scenarios {
2241 for (id, f, r, s, ret) in values {
2242 match ret {
2243 ret @ (None
2244 | Some(RetryOptions {
2245 retries: Retries { current: 0, .. },
2246 ..
2247 })) => {
2248 let ret = ret.map(RetryOptions::without_deadline);
2251 without_retries
2252 .entry(which)
2253 .or_default()
2254 .push((id, f, r, s, ret));
2255 }
2256 Some(ret) => {
2257 let ret = ret.with_deadline(now);
2258 with_retries
2259 .entry(which)
2260 .or_default()
2261 .push((id, f, r, s, ret));
2262 }
2263 }
2264 }
2265 }
2266
2267 let mut storage = self.scenarios.lock().await;
2268
2269 #[expect(clippy::iter_over_hash_type, reason = "order doesn't matter")]
2270 for (which, values) in with_retries {
2271 let ty_storage = storage.entry(which).or_default();
2272 for (id, f, r, s, ret) in values {
2273 ty_storage.insert(0, (id, f, r, s, Some(ret)));
2274 }
2275 }
2276
2277 if without_retries.contains_key(&ScenarioType::Serial) {
2278 #[expect(
2283 clippy::iter_over_hash_type,
2284 reason = "order doesn't matter"
2285 )]
2286 for (which, mut values) in without_retries {
2287 let old = mem::take(storage.entry(which).or_default());
2288 values.extend(old);
2289 storage.entry(which).or_default().extend(values);
2290 }
2291 } else {
2292 #[expect(
2295 clippy::iter_over_hash_type,
2296 reason = "order doesn't matter"
2297 )]
2298 for (which, values) in without_retries {
2299 storage.entry(which).or_default().extend(values);
2300 }
2301 }
2302 }
2303
2304 async fn get(
2309 &self,
2310 max_concurrent_scenarios: Option<usize>,
2311 ) -> (
2312 Vec<(
2313 ScenarioId,
2314 Source<gherkin::Feature>,
2315 Option<Source<gherkin::Rule>>,
2316 Source<gherkin::Scenario>,
2317 ScenarioType,
2318 Option<RetryOptions>,
2319 )>,
2320 Option<Duration>,
2321 ) {
2322 use RetryOptionsWithDeadline as WithDeadline;
2323 use ScenarioType::{Concurrent, Serial};
2324
2325 if max_concurrent_scenarios == Some(0) {
2326 return (Vec::new(), None);
2327 }
2328
2329 let mut min_dur = None;
2330 let mut drain =
2331 |storage: &mut Vec<(_, _, _, _, Option<WithDeadline>)>,
2332 ty,
2333 count: Option<usize>| {
2334 let mut i = 0;
2335 let drained = storage
2336 .extract_if(.., |(_, _, _, _, ret)| {
2337 if count.filter(|c| i >= *c).is_some() {
2340 return false;
2341 }
2342
2343 ret.as_ref()
2344 .and_then(WithDeadline::left_until_retry)
2345 .map_or_else(
2346 || {
2347 i += 1;
2348 true
2349 },
2350 |left| {
2351 min_dur = min_dur
2352 .map(|min| cmp::min(min, left))
2353 .or(Some(left));
2354 false
2355 },
2356 )
2357 })
2358 .map(|(id, f, r, s, ret)| {
2359 (id, f, r, s, ty, ret.map(Into::into))
2360 })
2361 .collect::<Vec<_>>();
2362 (!drained.is_empty()).then_some(drained)
2363 };
2364
2365 let mut guard = self.scenarios.lock().await;
2366 let scenarios = guard
2367 .get_mut(&Serial)
2368 .and_then(|storage| drain(storage, Serial, Some(1)))
2369 .or_else(|| {
2370 guard.get_mut(&Concurrent).and_then(|storage| {
2371 drain(storage, Concurrent, max_concurrent_scenarios)
2372 })
2373 })
2374 .unwrap_or_default();
2375
2376 (scenarios, min_dur)
2377 }
2378
2379 fn finish(&self) {
2383 self.finished.store(true, Ordering::SeqCst);
2384 }
2385
2386 async fn is_finished(&self, fail_fast: bool) -> bool {
2393 self.finished.load(Ordering::SeqCst)
2394 && (fail_fast
2395 || self.scenarios.lock().await.values().all(Vec::is_empty))
2396 }
2397}
2398
2399fn coerce_into_info<T: Any + Send + 'static>(val: T) -> Info {
2401 Arc::new(val)
2402}
2403
2404enum ExecutionFailure<World> {
2409 BeforeHookPanicked {
2411 world: Option<World>,
2413
2414 panic_info: Info,
2418
2419 meta: event::Metadata,
2423 },
2424
2425 StepSkipped(Option<World>),
2429
2430 StepPanicked {
2434 world: Option<World>,
2438
2439 step: Source<gherkin::Step>,
2443
2444 captures: Option<CaptureLocations>,
2448
2449 loc: Option<step::Location>,
2454
2455 err: event::StepError,
2460
2461 meta: event::Metadata,
2466
2467 is_background: bool,
2471 },
2472}
2473
2474struct AfterHookEventsMeta {
2478 started: event::Metadata,
2482
2483 finished: event::Metadata,
2487}
2488
2489impl<W> ExecutionFailure<W> {
2490 const fn take_world(&mut self) -> Option<W> {
2492 match self {
2493 Self::BeforeHookPanicked { world, .. }
2494 | Self::StepSkipped(world)
2495 | Self::StepPanicked { world, .. } => world.take(),
2496 }
2497 }
2498
2499 fn get_scenario_finished_event(&self) -> event::ScenarioFinished {
2501 use event::ScenarioFinished::{
2502 BeforeHookFailed, StepFailed, StepSkipped,
2503 };
2504
2505 match self {
2506 Self::BeforeHookPanicked { panic_info, .. } => {
2507 BeforeHookFailed(Arc::clone(panic_info))
2508 }
2509 Self::StepSkipped(_) => StepSkipped,
2510 Self::StepPanicked { captures, loc, err, .. } => {
2511 StepFailed(captures.clone(), *loc, err.clone())
2512 }
2513 }
2514 }
2515}
2516
2517#[cfg(test)]
2518mod retry_options {
2519 use gherkin::GherkinEnv;
2520 use humantime::parse_duration;
2521
2522 use super::{Cli, Duration, Retries, RetryOptions};
2523
2524 mod scenario_tags {
2525 use super::*;
2526
2527 const FEATURE: &str = r"
2529Feature: only scenarios
2530 Scenario: no tags
2531 Given a step
2532
2533 @retry
2534 Scenario: tag
2535 Given a step
2536
2537 @retry(5)
2538 Scenario: tag with explicit value
2539 Given a step
2540
2541 @retry.after(3s)
2542 Scenario: tag with explicit after
2543 Given a step
2544
2545 @retry(5).after(15s)
2546 Scenario: tag with explicit value and after
2547 Given a step
2548";
2549
2550 #[test]
2551 fn empty_cli() {
2552 let cli = Cli {
2553 concurrency: None,
2554 fail_fast: false,
2555 retry: None,
2556 retry_after: None,
2557 retry_tag_filter: None,
2558 };
2559 let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2560 .expect("failed to parse feature");
2561
2562 assert_eq!(
2563 RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
2564 None,
2565 );
2566 assert_eq!(
2567 RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
2568 Some(RetryOptions {
2569 retries: Retries { current: 0, left: 1 },
2570 after: None,
2571 }),
2572 );
2573 assert_eq!(
2574 RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
2575 Some(RetryOptions {
2576 retries: Retries { current: 0, left: 5 },
2577 after: None,
2578 }),
2579 );
2580 assert_eq!(
2581 RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
2582 Some(RetryOptions {
2583 retries: Retries { current: 0, left: 1 },
2584 after: Some(Duration::from_secs(3)),
2585 }),
2586 );
2587 assert_eq!(
2588 RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
2589 Some(RetryOptions {
2590 retries: Retries { current: 0, left: 5 },
2591 after: Some(Duration::from_secs(15)),
2592 }),
2593 );
2594 }
2595
2596 #[test]
2597 fn cli_retries() {
2598 let cli = Cli {
2599 concurrency: None,
2600 fail_fast: false,
2601 retry: Some(7),
2602 retry_after: None,
2603 retry_tag_filter: None,
2604 };
2605 let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2606 .expect("failed to parse feature");
2607
2608 assert_eq!(
2609 RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
2610 Some(RetryOptions {
2611 retries: Retries { current: 0, left: 7 },
2612 after: None,
2613 }),
2614 );
2615 assert_eq!(
2616 RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
2617 Some(RetryOptions {
2618 retries: Retries { current: 0, left: 7 },
2619 after: None,
2620 }),
2621 );
2622 assert_eq!(
2623 RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
2624 Some(RetryOptions {
2625 retries: Retries { current: 0, left: 5 },
2626 after: None,
2627 }),
2628 );
2629 assert_eq!(
2630 RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
2631 Some(RetryOptions {
2632 retries: Retries { current: 0, left: 7 },
2633 after: Some(Duration::from_secs(3)),
2634 }),
2635 );
2636 assert_eq!(
2637 RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
2638 Some(RetryOptions {
2639 retries: Retries { current: 0, left: 5 },
2640 after: Some(Duration::from_secs(15)),
2641 }),
2642 );
2643 }
2644
2645 #[test]
2646 fn cli_retry_after() {
2647 let cli = Cli {
2648 concurrency: None,
2649 fail_fast: false,
2650 retry: Some(7),
2651 retry_after: Some(parse_duration("5s").unwrap()),
2652 retry_tag_filter: None,
2653 };
2654 let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2655 .expect("failed to parse feature");
2656
2657 assert_eq!(
2658 RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
2659 Some(RetryOptions {
2660 retries: Retries { current: 0, left: 7 },
2661 after: Some(Duration::from_secs(5)),
2662 }),
2663 );
2664 assert_eq!(
2665 RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
2666 Some(RetryOptions {
2667 retries: Retries { current: 0, left: 7 },
2668 after: Some(Duration::from_secs(5)),
2669 }),
2670 );
2671 assert_eq!(
2672 RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
2673 Some(RetryOptions {
2674 retries: Retries { current: 0, left: 5 },
2675 after: Some(Duration::from_secs(5)),
2676 }),
2677 );
2678 assert_eq!(
2679 RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
2680 Some(RetryOptions {
2681 retries: Retries { current: 0, left: 7 },
2682 after: Some(Duration::from_secs(3)),
2683 }),
2684 );
2685 assert_eq!(
2686 RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
2687 Some(RetryOptions {
2688 retries: Retries { current: 0, left: 5 },
2689 after: Some(Duration::from_secs(15)),
2690 }),
2691 );
2692 }
2693
2694 #[test]
2695 fn cli_retry_filter() {
2696 let cli = Cli {
2697 concurrency: None,
2698 fail_fast: false,
2699 retry: Some(7),
2700 retry_after: None,
2701 retry_tag_filter: Some("@retry".parse().unwrap()),
2702 };
2703 let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2704 .expect("failed to parse feature");
2705
2706 assert_eq!(
2707 RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
2708 None,
2709 );
2710 assert_eq!(
2711 RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
2712 Some(RetryOptions {
2713 retries: Retries { current: 0, left: 7 },
2714 after: None,
2715 }),
2716 );
2717 assert_eq!(
2718 RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
2719 Some(RetryOptions {
2720 retries: Retries { current: 0, left: 5 },
2721 after: None,
2722 }),
2723 );
2724 assert_eq!(
2725 RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
2726 Some(RetryOptions {
2727 retries: Retries { current: 0, left: 7 },
2728 after: Some(Duration::from_secs(3)),
2729 }),
2730 );
2731 assert_eq!(
2732 RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
2733 Some(RetryOptions {
2734 retries: Retries { current: 0, left: 5 },
2735 after: Some(Duration::from_secs(15)),
2736 }),
2737 );
2738 }
2739
2740 #[test]
2741 fn cli_retry_after_and_filter() {
2742 let cli = Cli {
2743 concurrency: None,
2744 fail_fast: false,
2745 retry: Some(7),
2746 retry_after: Some(parse_duration("5s").unwrap()),
2747 retry_tag_filter: Some("@retry".parse().unwrap()),
2748 };
2749 let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2750 .expect("failed to parse feature");
2751
2752 assert_eq!(
2753 RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
2754 None,
2755 );
2756 assert_eq!(
2757 RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
2758 Some(RetryOptions {
2759 retries: Retries { current: 0, left: 7 },
2760 after: Some(Duration::from_secs(5)),
2761 }),
2762 );
2763 assert_eq!(
2764 RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
2765 Some(RetryOptions {
2766 retries: Retries { current: 0, left: 5 },
2767 after: Some(Duration::from_secs(5)),
2768 }),
2769 );
2770 assert_eq!(
2771 RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
2772 Some(RetryOptions {
2773 retries: Retries { current: 0, left: 7 },
2774 after: Some(Duration::from_secs(3)),
2775 }),
2776 );
2777 assert_eq!(
2778 RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
2779 Some(RetryOptions {
2780 retries: Retries { current: 0, left: 5 },
2781 after: Some(Duration::from_secs(15)),
2782 }),
2783 );
2784 }
2785 }
2786
2787 mod rule_tags {
2788 use super::*;
2789
2790 const FEATURE: &str = r#"
2792Feature: only scenarios
2793 Rule: no tags
2794 Scenario: no tags
2795 Given a step
2796
2797 @retry
2798 Scenario: tag
2799 Given a step
2800
2801 @retry(5)
2802 Scenario: tag with explicit value
2803 Given a step
2804
2805 @retry.after(3s)
2806 Scenario: tag with explicit after
2807 Given a step
2808
2809 @retry(5).after(15s)
2810 Scenario: tag with explicit value and after
2811 Given a step
2812
2813 @retry(3).after(5s)
2814 Rule: retry tag
2815 Scenario: no tags
2816 Given a step
2817
2818 @retry
2819 Scenario: tag
2820 Given a step
2821
2822 @retry(5)
2823 Scenario: tag with explicit value
2824 Given a step
2825
2826 @retry.after(3s)
2827 Scenario: tag with explicit after
2828 Given a step
2829
2830 @retry(5).after(15s)
2831 Scenario: tag with explicit value and after
2832 Given a step
2833"#;
2834
2835 #[test]
2836 fn empty_cli() {
2837 let cli = Cli {
2838 concurrency: None,
2839 fail_fast: false,
2840 retry: None,
2841 retry_after: None,
2842 retry_tag_filter: None,
2843 };
2844 let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2845 .expect("failed to parse feature");
2846
2847 assert_eq!(
2848 RetryOptions::parse_from_tags(
2849 &f,
2850 Some(&f.rules[0]),
2851 &f.rules[0].scenarios[0],
2852 &cli
2853 ),
2854 None,
2855 );
2856 assert_eq!(
2857 RetryOptions::parse_from_tags(
2858 &f,
2859 Some(&f.rules[0]),
2860 &f.rules[0].scenarios[1],
2861 &cli
2862 ),
2863 Some(RetryOptions {
2864 retries: Retries { current: 0, left: 1 },
2865 after: None,
2866 }),
2867 );
2868 assert_eq!(
2869 RetryOptions::parse_from_tags(
2870 &f,
2871 Some(&f.rules[0]),
2872 &f.rules[0].scenarios[2],
2873 &cli
2874 ),
2875 Some(RetryOptions {
2876 retries: Retries { current: 0, left: 5 },
2877 after: None,
2878 }),
2879 );
2880 assert_eq!(
2881 RetryOptions::parse_from_tags(
2882 &f,
2883 Some(&f.rules[0]),
2884 &f.rules[0].scenarios[3],
2885 &cli
2886 ),
2887 Some(RetryOptions {
2888 retries: Retries { current: 0, left: 1 },
2889 after: Some(Duration::from_secs(3)),
2890 }),
2891 );
2892 assert_eq!(
2893 RetryOptions::parse_from_tags(
2894 &f,
2895 Some(&f.rules[0]),
2896 &f.rules[0].scenarios[4],
2897 &cli
2898 ),
2899 Some(RetryOptions {
2900 retries: Retries { current: 0, left: 5 },
2901 after: Some(Duration::from_secs(15)),
2902 }),
2903 );
2904 assert_eq!(
2905 RetryOptions::parse_from_tags(
2906 &f,
2907 Some(&f.rules[1]),
2908 &f.rules[1].scenarios[0],
2909 &cli
2910 ),
2911 Some(RetryOptions {
2912 retries: Retries { current: 0, left: 3 },
2913 after: Some(Duration::from_secs(5)),
2914 }),
2915 );
2916 assert_eq!(
2917 RetryOptions::parse_from_tags(
2918 &f,
2919 Some(&f.rules[1]),
2920 &f.rules[1].scenarios[1],
2921 &cli
2922 ),
2923 Some(RetryOptions {
2924 retries: Retries { current: 0, left: 1 },
2925 after: None,
2926 }),
2927 );
2928 assert_eq!(
2929 RetryOptions::parse_from_tags(
2930 &f,
2931 Some(&f.rules[1]),
2932 &f.rules[1].scenarios[2],
2933 &cli
2934 ),
2935 Some(RetryOptions {
2936 retries: Retries { current: 0, left: 5 },
2937 after: None,
2938 }),
2939 );
2940 assert_eq!(
2941 RetryOptions::parse_from_tags(
2942 &f,
2943 Some(&f.rules[1]),
2944 &f.rules[1].scenarios[3],
2945 &cli
2946 ),
2947 Some(RetryOptions {
2948 retries: Retries { current: 0, left: 1 },
2949 after: Some(Duration::from_secs(3)),
2950 }),
2951 );
2952 assert_eq!(
2953 RetryOptions::parse_from_tags(
2954 &f,
2955 Some(&f.rules[1]),
2956 &f.rules[1].scenarios[4],
2957 &cli
2958 ),
2959 Some(RetryOptions {
2960 retries: Retries { current: 0, left: 5 },
2961 after: Some(Duration::from_secs(15)),
2962 }),
2963 );
2964 }
2965
2966 #[test]
2967 fn cli_retry_after_and_filter() {
2968 let cli = Cli {
2969 concurrency: None,
2970 fail_fast: false,
2971 retry: Some(7),
2972 retry_after: Some(parse_duration("5s").unwrap()),
2973 retry_tag_filter: Some("@retry".parse().unwrap()),
2974 };
2975 let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2976 .expect("failed to parse feature");
2977
2978 assert_eq!(
2979 RetryOptions::parse_from_tags(
2980 &f,
2981 Some(&f.rules[0]),
2982 &f.rules[0].scenarios[0],
2983 &cli
2984 ),
2985 None,
2986 );
2987 assert_eq!(
2988 RetryOptions::parse_from_tags(
2989 &f,
2990 Some(&f.rules[0]),
2991 &f.rules[0].scenarios[1],
2992 &cli
2993 ),
2994 Some(RetryOptions {
2995 retries: Retries { current: 0, left: 7 },
2996 after: Some(Duration::from_secs(5)),
2997 }),
2998 );
2999 assert_eq!(
3000 RetryOptions::parse_from_tags(
3001 &f,
3002 Some(&f.rules[0]),
3003 &f.rules[0].scenarios[2],
3004 &cli
3005 ),
3006 Some(RetryOptions {
3007 retries: Retries { current: 0, left: 5 },
3008 after: Some(Duration::from_secs(5)),
3009 }),
3010 );
3011 assert_eq!(
3012 RetryOptions::parse_from_tags(
3013 &f,
3014 Some(&f.rules[0]),
3015 &f.rules[0].scenarios[3],
3016 &cli
3017 ),
3018 Some(RetryOptions {
3019 retries: Retries { current: 0, left: 7 },
3020 after: Some(Duration::from_secs(3)),
3021 }),
3022 );
3023 assert_eq!(
3024 RetryOptions::parse_from_tags(
3025 &f,
3026 Some(&f.rules[0]),
3027 &f.rules[0].scenarios[4],
3028 &cli
3029 ),
3030 Some(RetryOptions {
3031 retries: Retries { current: 0, left: 5 },
3032 after: Some(Duration::from_secs(15)),
3033 }),
3034 );
3035 assert_eq!(
3036 RetryOptions::parse_from_tags(
3037 &f,
3038 Some(&f.rules[1]),
3039 &f.rules[1].scenarios[0],
3040 &cli
3041 ),
3042 Some(RetryOptions {
3043 retries: Retries { current: 0, left: 3 },
3044 after: Some(Duration::from_secs(5)),
3045 }),
3046 );
3047 assert_eq!(
3048 RetryOptions::parse_from_tags(
3049 &f,
3050 Some(&f.rules[1]),
3051 &f.rules[1].scenarios[1],
3052 &cli
3053 ),
3054 Some(RetryOptions {
3055 retries: Retries { current: 0, left: 7 },
3056 after: Some(Duration::from_secs(5)),
3057 }),
3058 );
3059 assert_eq!(
3060 RetryOptions::parse_from_tags(
3061 &f,
3062 Some(&f.rules[1]),
3063 &f.rules[1].scenarios[2],
3064 &cli
3065 ),
3066 Some(RetryOptions {
3067 retries: Retries { current: 0, left: 5 },
3068 after: Some(Duration::from_secs(5)),
3069 }),
3070 );
3071 assert_eq!(
3072 RetryOptions::parse_from_tags(
3073 &f,
3074 Some(&f.rules[1]),
3075 &f.rules[1].scenarios[3],
3076 &cli
3077 ),
3078 Some(RetryOptions {
3079 retries: Retries { current: 0, left: 7 },
3080 after: Some(Duration::from_secs(3)),
3081 }),
3082 );
3083 assert_eq!(
3084 RetryOptions::parse_from_tags(
3085 &f,
3086 Some(&f.rules[1]),
3087 &f.rules[1].scenarios[4],
3088 &cli
3089 ),
3090 Some(RetryOptions {
3091 retries: Retries { current: 0, left: 5 },
3092 after: Some(Duration::from_secs(15)),
3093 }),
3094 );
3095 }
3096 }
3097
3098 mod feature_tags {
3099 use super::*;
3100
3101 const FEATURE: &str = r"
3103@retry(8)
3104Feature: only scenarios
3105 Scenario: no tags
3106 Given a step
3107
3108 @retry
3109 Scenario: tag
3110 Given a step
3111
3112 @retry(5)
3113 Scenario: tag with explicit value
3114 Given a step
3115
3116 @retry.after(3s)
3117 Scenario: tag with explicit after
3118 Given a step
3119
3120 @retry(5).after(15s)
3121 Scenario: tag with explicit value and after
3122 Given a step
3123
3124 Rule: no tags
3125 Scenario: no tags
3126 Given a step
3127
3128 @retry
3129 Scenario: tag
3130 Given a step
3131
3132 @retry(5)
3133 Scenario: tag with explicit value
3134 Given a step
3135
3136 @retry.after(3s)
3137 Scenario: tag with explicit after
3138 Given a step
3139
3140 @retry(5).after(15s)
3141 Scenario: tag with explicit value and after
3142 Given a step
3143
3144 @retry(3).after(5s)
3145 Rule: retry tag
3146 Scenario: no tags
3147 Given a step
3148
3149 @retry
3150 Scenario: tag
3151 Given a step
3152
3153 @retry(5)
3154 Scenario: tag with explicit value
3155 Given a step
3156
3157 @retry.after(3s)
3158 Scenario: tag with explicit after
3159 Given a step
3160
3161 @retry(5).after(15s)
3162 Scenario: tag with explicit value and after
3163 Given a step
3164";
3165
3166 #[test]
3167 fn empty_cli() {
3168 let cli = Cli {
3169 concurrency: None,
3170 fail_fast: false,
3171 retry: None,
3172 retry_after: None,
3173 retry_tag_filter: None,
3174 };
3175 let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
3176 .unwrap_or_else(|e| panic!("failed to parse feature: {e}"));
3177
3178 assert_eq!(
3179 RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
3180 Some(RetryOptions {
3181 retries: Retries { current: 0, left: 8 },
3182 after: None,
3183 }),
3184 );
3185 assert_eq!(
3186 RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
3187 Some(RetryOptions {
3188 retries: Retries { current: 0, left: 1 },
3189 after: None,
3190 }),
3191 );
3192 assert_eq!(
3193 RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
3194 Some(RetryOptions {
3195 retries: Retries { current: 0, left: 5 },
3196 after: None,
3197 }),
3198 );
3199 assert_eq!(
3200 RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
3201 Some(RetryOptions {
3202 retries: Retries { current: 0, left: 1 },
3203 after: Some(Duration::from_secs(3)),
3204 }),
3205 );
3206 assert_eq!(
3207 RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
3208 Some(RetryOptions {
3209 retries: Retries { current: 0, left: 5 },
3210 after: Some(Duration::from_secs(15)),
3211 }),
3212 );
3213 assert_eq!(
3214 RetryOptions::parse_from_tags(
3215 &f,
3216 Some(&f.rules[0]),
3217 &f.rules[0].scenarios[0],
3218 &cli
3219 ),
3220 Some(RetryOptions {
3221 retries: Retries { current: 0, left: 8 },
3222 after: None,
3223 }),
3224 );
3225 assert_eq!(
3226 RetryOptions::parse_from_tags(
3227 &f,
3228 Some(&f.rules[0]),
3229 &f.rules[0].scenarios[1],
3230 &cli
3231 ),
3232 Some(RetryOptions {
3233 retries: Retries { current: 0, left: 1 },
3234 after: None,
3235 }),
3236 );
3237 assert_eq!(
3238 RetryOptions::parse_from_tags(
3239 &f,
3240 Some(&f.rules[0]),
3241 &f.rules[0].scenarios[2],
3242 &cli
3243 ),
3244 Some(RetryOptions {
3245 retries: Retries { current: 0, left: 5 },
3246 after: None,
3247 }),
3248 );
3249 assert_eq!(
3250 RetryOptions::parse_from_tags(
3251 &f,
3252 Some(&f.rules[0]),
3253 &f.rules[0].scenarios[3],
3254 &cli
3255 ),
3256 Some(RetryOptions {
3257 retries: Retries { current: 0, left: 1 },
3258 after: Some(Duration::from_secs(3)),
3259 }),
3260 );
3261 assert_eq!(
3262 RetryOptions::parse_from_tags(
3263 &f,
3264 Some(&f.rules[0]),
3265 &f.rules[0].scenarios[4],
3266 &cli
3267 ),
3268 Some(RetryOptions {
3269 retries: Retries { current: 0, left: 5 },
3270 after: Some(Duration::from_secs(15)),
3271 }),
3272 );
3273 assert_eq!(
3274 RetryOptions::parse_from_tags(
3275 &f,
3276 Some(&f.rules[1]),
3277 &f.rules[1].scenarios[0],
3278 &cli
3279 ),
3280 Some(RetryOptions {
3281 retries: Retries { current: 0, left: 3 },
3282 after: Some(Duration::from_secs(5)),
3283 }),
3284 );
3285 assert_eq!(
3286 RetryOptions::parse_from_tags(
3287 &f,
3288 Some(&f.rules[1]),
3289 &f.rules[1].scenarios[1],
3290 &cli
3291 ),
3292 Some(RetryOptions {
3293 retries: Retries { current: 0, left: 1 },
3294 after: None,
3295 }),
3296 );
3297 assert_eq!(
3298 RetryOptions::parse_from_tags(
3299 &f,
3300 Some(&f.rules[1]),
3301 &f.rules[1].scenarios[2],
3302 &cli
3303 ),
3304 Some(RetryOptions {
3305 retries: Retries { current: 0, left: 5 },
3306 after: None,
3307 }),
3308 );
3309 assert_eq!(
3310 RetryOptions::parse_from_tags(
3311 &f,
3312 Some(&f.rules[1]),
3313 &f.rules[1].scenarios[3],
3314 &cli
3315 ),
3316 Some(RetryOptions {
3317 retries: Retries { current: 0, left: 1 },
3318 after: Some(Duration::from_secs(3)),
3319 }),
3320 );
3321 assert_eq!(
3322 RetryOptions::parse_from_tags(
3323 &f,
3324 Some(&f.rules[1]),
3325 &f.rules[1].scenarios[4],
3326 &cli
3327 ),
3328 Some(RetryOptions {
3329 retries: Retries { current: 0, left: 5 },
3330 after: Some(Duration::from_secs(15)),
3331 }),
3332 );
3333 }
3334
3335 #[test]
3336 fn cli_retry_after_and_filter() {
3337 let cli = Cli {
3338 concurrency: None,
3339 fail_fast: false,
3340 retry: Some(7),
3341 retry_after: Some(parse_duration("5s").unwrap()),
3342 retry_tag_filter: Some("@retry".parse().unwrap()),
3343 };
3344 let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
3345 .expect("failed to parse feature");
3346
3347 assert_eq!(
3348 RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
3349 Some(RetryOptions {
3350 retries: Retries { current: 0, left: 8 },
3351 after: Some(Duration::from_secs(5)),
3352 }),
3353 );
3354 assert_eq!(
3355 RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
3356 Some(RetryOptions {
3357 retries: Retries { current: 0, left: 7 },
3358 after: Some(Duration::from_secs(5)),
3359 }),
3360 );
3361 assert_eq!(
3362 RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
3363 Some(RetryOptions {
3364 retries: Retries { current: 0, left: 5 },
3365 after: Some(Duration::from_secs(5)),
3366 }),
3367 );
3368 assert_eq!(
3369 RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
3370 Some(RetryOptions {
3371 retries: Retries { current: 0, left: 7 },
3372 after: Some(Duration::from_secs(3)),
3373 }),
3374 );
3375 assert_eq!(
3376 RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
3377 Some(RetryOptions {
3378 retries: Retries { current: 0, left: 5 },
3379 after: Some(Duration::from_secs(15)),
3380 }),
3381 );
3382 assert_eq!(
3383 RetryOptions::parse_from_tags(
3384 &f,
3385 Some(&f.rules[0]),
3386 &f.rules[0].scenarios[0],
3387 &cli
3388 ),
3389 Some(RetryOptions {
3390 retries: Retries { current: 0, left: 8 },
3391 after: Some(Duration::from_secs(5)),
3392 }),
3393 );
3394 assert_eq!(
3395 RetryOptions::parse_from_tags(
3396 &f,
3397 Some(&f.rules[0]),
3398 &f.rules[0].scenarios[1],
3399 &cli
3400 ),
3401 Some(RetryOptions {
3402 retries: Retries { current: 0, left: 7 },
3403 after: Some(Duration::from_secs(5)),
3404 }),
3405 );
3406 assert_eq!(
3407 RetryOptions::parse_from_tags(
3408 &f,
3409 Some(&f.rules[0]),
3410 &f.rules[0].scenarios[2],
3411 &cli
3412 ),
3413 Some(RetryOptions {
3414 retries: Retries { current: 0, left: 5 },
3415 after: Some(Duration::from_secs(5)),
3416 }),
3417 );
3418 assert_eq!(
3419 RetryOptions::parse_from_tags(
3420 &f,
3421 Some(&f.rules[0]),
3422 &f.rules[0].scenarios[3],
3423 &cli
3424 ),
3425 Some(RetryOptions {
3426 retries: Retries { current: 0, left: 7 },
3427 after: Some(Duration::from_secs(3)),
3428 }),
3429 );
3430 assert_eq!(
3431 RetryOptions::parse_from_tags(
3432 &f,
3433 Some(&f.rules[0]),
3434 &f.rules[0].scenarios[4],
3435 &cli
3436 ),
3437 Some(RetryOptions {
3438 retries: Retries { current: 0, left: 5 },
3439 after: Some(Duration::from_secs(15)),
3440 }),
3441 );
3442 assert_eq!(
3443 RetryOptions::parse_from_tags(
3444 &f,
3445 Some(&f.rules[1]),
3446 &f.rules[1].scenarios[0],
3447 &cli
3448 ),
3449 Some(RetryOptions {
3450 retries: Retries { current: 0, left: 3 },
3451 after: Some(Duration::from_secs(5)),
3452 }),
3453 );
3454 assert_eq!(
3455 RetryOptions::parse_from_tags(
3456 &f,
3457 Some(&f.rules[1]),
3458 &f.rules[1].scenarios[1],
3459 &cli
3460 ),
3461 Some(RetryOptions {
3462 retries: Retries { current: 0, left: 7 },
3463 after: Some(Duration::from_secs(5)),
3464 }),
3465 );
3466 assert_eq!(
3467 RetryOptions::parse_from_tags(
3468 &f,
3469 Some(&f.rules[1]),
3470 &f.rules[1].scenarios[2],
3471 &cli
3472 ),
3473 Some(RetryOptions {
3474 retries: Retries { current: 0, left: 5 },
3475 after: Some(Duration::from_secs(5)),
3476 }),
3477 );
3478 assert_eq!(
3479 RetryOptions::parse_from_tags(
3480 &f,
3481 Some(&f.rules[1]),
3482 &f.rules[1].scenarios[3],
3483 &cli
3484 ),
3485 Some(RetryOptions {
3486 retries: Retries { current: 0, left: 7 },
3487 after: Some(Duration::from_secs(3)),
3488 }),
3489 );
3490 assert_eq!(
3491 RetryOptions::parse_from_tags(
3492 &f,
3493 Some(&f.rules[1]),
3494 &f.rules[1].scenarios[4],
3495 &cli
3496 ),
3497 Some(RetryOptions {
3498 retries: Retries { current: 0, left: 5 },
3499 after: Some(Duration::from_secs(15)),
3500 }),
3501 );
3502 }
3503 }
3504}