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((id, feat, rule, scenario_failed, retried)) =
1038 storage.finished_receiver.try_recv()
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 #[expect(clippy::significant_drop_tightening, reason = "false positive")]
2309 async fn get(
2310 &self,
2311 max_concurrent_scenarios: Option<usize>,
2312 ) -> (
2313 Vec<(
2314 ScenarioId,
2315 Source<gherkin::Feature>,
2316 Option<Source<gherkin::Rule>>,
2317 Source<gherkin::Scenario>,
2318 ScenarioType,
2319 Option<RetryOptions>,
2320 )>,
2321 Option<Duration>,
2322 ) {
2323 use RetryOptionsWithDeadline as WithDeadline;
2324 use ScenarioType::{Concurrent, Serial};
2325
2326 if max_concurrent_scenarios == Some(0) {
2327 return (Vec::new(), None);
2328 }
2329
2330 let mut min_dur = None;
2331 let mut drain =
2332 |storage: &mut Vec<(_, _, _, _, Option<WithDeadline>)>,
2333 ty,
2334 count: Option<usize>| {
2335 let mut i = 0;
2336 let drained = storage
2337 .extract_if(.., |(_, _, _, _, ret)| {
2338 if count.as_ref().is_some_and(|c| i >= *c) {
2341 return false;
2342 }
2343
2344 ret.as_ref()
2345 .and_then(WithDeadline::left_until_retry)
2346 .map_or_else(
2347 || {
2348 i += 1;
2349 true
2350 },
2351 |left| {
2352 min_dur = min_dur
2353 .map(|min| cmp::min(min, left))
2354 .or(Some(left));
2355 false
2356 },
2357 )
2358 })
2359 .map(|(id, f, r, s, ret)| {
2360 (id, f, r, s, ty, ret.map(Into::into))
2361 })
2362 .collect::<Vec<_>>();
2363 (!drained.is_empty()).then_some(drained)
2364 };
2365
2366 let mut guard = self.scenarios.lock().await;
2367 let scenarios = guard
2368 .get_mut(&Serial)
2369 .and_then(|storage| drain(storage, Serial, Some(1)))
2370 .or_else(|| {
2371 guard.get_mut(&Concurrent).and_then(|storage| {
2372 drain(storage, Concurrent, max_concurrent_scenarios)
2373 })
2374 })
2375 .unwrap_or_default();
2376
2377 (scenarios, min_dur)
2378 }
2379
2380 fn finish(&self) {
2384 self.finished.store(true, Ordering::SeqCst);
2385 }
2386
2387 async fn is_finished(&self, fail_fast: bool) -> bool {
2394 self.finished.load(Ordering::SeqCst)
2395 && (fail_fast
2396 || self.scenarios.lock().await.values().all(Vec::is_empty))
2397 }
2398}
2399
2400fn coerce_into_info<T: Any + Send + 'static>(val: T) -> Info {
2402 Arc::new(val)
2403}
2404
2405enum ExecutionFailure<World> {
2410 BeforeHookPanicked {
2412 world: Option<World>,
2414
2415 panic_info: Info,
2419
2420 meta: event::Metadata,
2424 },
2425
2426 StepSkipped(Option<World>),
2430
2431 StepPanicked {
2435 world: Option<World>,
2439
2440 step: Source<gherkin::Step>,
2444
2445 captures: Option<CaptureLocations>,
2449
2450 loc: Option<step::Location>,
2455
2456 err: event::StepError,
2461
2462 meta: event::Metadata,
2467
2468 is_background: bool,
2472 },
2473}
2474
2475struct AfterHookEventsMeta {
2479 started: event::Metadata,
2483
2484 finished: event::Metadata,
2488}
2489
2490impl<W> ExecutionFailure<W> {
2491 const fn take_world(&mut self) -> Option<W> {
2493 match self {
2494 Self::BeforeHookPanicked { world, .. }
2495 | Self::StepSkipped(world)
2496 | Self::StepPanicked { world, .. } => world.take(),
2497 }
2498 }
2499
2500 fn get_scenario_finished_event(&self) -> event::ScenarioFinished {
2502 use event::ScenarioFinished::{
2503 BeforeHookFailed, StepFailed, StepSkipped,
2504 };
2505
2506 match self {
2507 Self::BeforeHookPanicked { panic_info, .. } => {
2508 BeforeHookFailed(Arc::clone(panic_info))
2509 }
2510 Self::StepSkipped(_) => StepSkipped,
2511 Self::StepPanicked { captures, loc, err, .. } => {
2512 StepFailed(captures.clone(), *loc, err.clone())
2513 }
2514 }
2515 }
2516}
2517
2518#[cfg(test)]
2519mod retry_options {
2520 use gherkin::GherkinEnv;
2521 use humantime::parse_duration;
2522
2523 use super::{Cli, Duration, Retries, RetryOptions};
2524
2525 mod scenario_tags {
2526 use super::*;
2527
2528 const FEATURE: &str = r"
2530Feature: only scenarios
2531 Scenario: no tags
2532 Given a step
2533
2534 @retry
2535 Scenario: tag
2536 Given a step
2537
2538 @retry(5)
2539 Scenario: tag with explicit value
2540 Given a step
2541
2542 @retry.after(3s)
2543 Scenario: tag with explicit after
2544 Given a step
2545
2546 @retry(5).after(15s)
2547 Scenario: tag with explicit value and after
2548 Given a step
2549";
2550
2551 #[test]
2552 fn empty_cli() {
2553 let cli = Cli {
2554 concurrency: None,
2555 fail_fast: false,
2556 retry: None,
2557 retry_after: None,
2558 retry_tag_filter: None,
2559 };
2560 let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2561 .expect("failed to parse feature");
2562
2563 assert_eq!(
2564 RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
2565 None,
2566 );
2567 assert_eq!(
2568 RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
2569 Some(RetryOptions {
2570 retries: Retries { current: 0, left: 1 },
2571 after: None,
2572 }),
2573 );
2574 assert_eq!(
2575 RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
2576 Some(RetryOptions {
2577 retries: Retries { current: 0, left: 5 },
2578 after: None,
2579 }),
2580 );
2581 assert_eq!(
2582 RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
2583 Some(RetryOptions {
2584 retries: Retries { current: 0, left: 1 },
2585 after: Some(Duration::from_secs(3)),
2586 }),
2587 );
2588 assert_eq!(
2589 RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
2590 Some(RetryOptions {
2591 retries: Retries { current: 0, left: 5 },
2592 after: Some(Duration::from_secs(15)),
2593 }),
2594 );
2595 }
2596
2597 #[test]
2598 fn cli_retries() {
2599 let cli = Cli {
2600 concurrency: None,
2601 fail_fast: false,
2602 retry: Some(7),
2603 retry_after: None,
2604 retry_tag_filter: None,
2605 };
2606 let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2607 .expect("failed to parse feature");
2608
2609 assert_eq!(
2610 RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
2611 Some(RetryOptions {
2612 retries: Retries { current: 0, left: 7 },
2613 after: None,
2614 }),
2615 );
2616 assert_eq!(
2617 RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
2618 Some(RetryOptions {
2619 retries: Retries { current: 0, left: 7 },
2620 after: None,
2621 }),
2622 );
2623 assert_eq!(
2624 RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
2625 Some(RetryOptions {
2626 retries: Retries { current: 0, left: 5 },
2627 after: None,
2628 }),
2629 );
2630 assert_eq!(
2631 RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
2632 Some(RetryOptions {
2633 retries: Retries { current: 0, left: 7 },
2634 after: Some(Duration::from_secs(3)),
2635 }),
2636 );
2637 assert_eq!(
2638 RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
2639 Some(RetryOptions {
2640 retries: Retries { current: 0, left: 5 },
2641 after: Some(Duration::from_secs(15)),
2642 }),
2643 );
2644 }
2645
2646 #[test]
2647 fn cli_retry_after() {
2648 let cli = Cli {
2649 concurrency: None,
2650 fail_fast: false,
2651 retry: Some(7),
2652 retry_after: Some(parse_duration("5s").unwrap()),
2653 retry_tag_filter: None,
2654 };
2655 let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2656 .expect("failed to parse feature");
2657
2658 assert_eq!(
2659 RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
2660 Some(RetryOptions {
2661 retries: Retries { current: 0, left: 7 },
2662 after: Some(Duration::from_secs(5)),
2663 }),
2664 );
2665 assert_eq!(
2666 RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
2667 Some(RetryOptions {
2668 retries: Retries { current: 0, left: 7 },
2669 after: Some(Duration::from_secs(5)),
2670 }),
2671 );
2672 assert_eq!(
2673 RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
2674 Some(RetryOptions {
2675 retries: Retries { current: 0, left: 5 },
2676 after: Some(Duration::from_secs(5)),
2677 }),
2678 );
2679 assert_eq!(
2680 RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
2681 Some(RetryOptions {
2682 retries: Retries { current: 0, left: 7 },
2683 after: Some(Duration::from_secs(3)),
2684 }),
2685 );
2686 assert_eq!(
2687 RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
2688 Some(RetryOptions {
2689 retries: Retries { current: 0, left: 5 },
2690 after: Some(Duration::from_secs(15)),
2691 }),
2692 );
2693 }
2694
2695 #[test]
2696 fn cli_retry_filter() {
2697 let cli = Cli {
2698 concurrency: None,
2699 fail_fast: false,
2700 retry: Some(7),
2701 retry_after: None,
2702 retry_tag_filter: Some("@retry".parse().unwrap()),
2703 };
2704 let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2705 .expect("failed to parse feature");
2706
2707 assert_eq!(
2708 RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
2709 None,
2710 );
2711 assert_eq!(
2712 RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
2713 Some(RetryOptions {
2714 retries: Retries { current: 0, left: 7 },
2715 after: None,
2716 }),
2717 );
2718 assert_eq!(
2719 RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
2720 Some(RetryOptions {
2721 retries: Retries { current: 0, left: 5 },
2722 after: None,
2723 }),
2724 );
2725 assert_eq!(
2726 RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
2727 Some(RetryOptions {
2728 retries: Retries { current: 0, left: 7 },
2729 after: Some(Duration::from_secs(3)),
2730 }),
2731 );
2732 assert_eq!(
2733 RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
2734 Some(RetryOptions {
2735 retries: Retries { current: 0, left: 5 },
2736 after: Some(Duration::from_secs(15)),
2737 }),
2738 );
2739 }
2740
2741 #[test]
2742 fn cli_retry_after_and_filter() {
2743 let cli = Cli {
2744 concurrency: None,
2745 fail_fast: false,
2746 retry: Some(7),
2747 retry_after: Some(parse_duration("5s").unwrap()),
2748 retry_tag_filter: Some("@retry".parse().unwrap()),
2749 };
2750 let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2751 .expect("failed to parse feature");
2752
2753 assert_eq!(
2754 RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
2755 None,
2756 );
2757 assert_eq!(
2758 RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
2759 Some(RetryOptions {
2760 retries: Retries { current: 0, left: 7 },
2761 after: Some(Duration::from_secs(5)),
2762 }),
2763 );
2764 assert_eq!(
2765 RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
2766 Some(RetryOptions {
2767 retries: Retries { current: 0, left: 5 },
2768 after: Some(Duration::from_secs(5)),
2769 }),
2770 );
2771 assert_eq!(
2772 RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
2773 Some(RetryOptions {
2774 retries: Retries { current: 0, left: 7 },
2775 after: Some(Duration::from_secs(3)),
2776 }),
2777 );
2778 assert_eq!(
2779 RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
2780 Some(RetryOptions {
2781 retries: Retries { current: 0, left: 5 },
2782 after: Some(Duration::from_secs(15)),
2783 }),
2784 );
2785 }
2786 }
2787
2788 mod rule_tags {
2789 use super::*;
2790
2791 const FEATURE: &str = r#"
2793Feature: only scenarios
2794 Rule: no tags
2795 Scenario: no tags
2796 Given a step
2797
2798 @retry
2799 Scenario: tag
2800 Given a step
2801
2802 @retry(5)
2803 Scenario: tag with explicit value
2804 Given a step
2805
2806 @retry.after(3s)
2807 Scenario: tag with explicit after
2808 Given a step
2809
2810 @retry(5).after(15s)
2811 Scenario: tag with explicit value and after
2812 Given a step
2813
2814 @retry(3).after(5s)
2815 Rule: retry tag
2816 Scenario: no tags
2817 Given a step
2818
2819 @retry
2820 Scenario: tag
2821 Given a step
2822
2823 @retry(5)
2824 Scenario: tag with explicit value
2825 Given a step
2826
2827 @retry.after(3s)
2828 Scenario: tag with explicit after
2829 Given a step
2830
2831 @retry(5).after(15s)
2832 Scenario: tag with explicit value and after
2833 Given a step
2834"#;
2835
2836 #[test]
2837 fn empty_cli() {
2838 let cli = Cli {
2839 concurrency: None,
2840 fail_fast: false,
2841 retry: None,
2842 retry_after: None,
2843 retry_tag_filter: None,
2844 };
2845 let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2846 .expect("failed to parse feature");
2847
2848 assert_eq!(
2849 RetryOptions::parse_from_tags(
2850 &f,
2851 Some(&f.rules[0]),
2852 &f.rules[0].scenarios[0],
2853 &cli
2854 ),
2855 None,
2856 );
2857 assert_eq!(
2858 RetryOptions::parse_from_tags(
2859 &f,
2860 Some(&f.rules[0]),
2861 &f.rules[0].scenarios[1],
2862 &cli
2863 ),
2864 Some(RetryOptions {
2865 retries: Retries { current: 0, left: 1 },
2866 after: None,
2867 }),
2868 );
2869 assert_eq!(
2870 RetryOptions::parse_from_tags(
2871 &f,
2872 Some(&f.rules[0]),
2873 &f.rules[0].scenarios[2],
2874 &cli
2875 ),
2876 Some(RetryOptions {
2877 retries: Retries { current: 0, left: 5 },
2878 after: None,
2879 }),
2880 );
2881 assert_eq!(
2882 RetryOptions::parse_from_tags(
2883 &f,
2884 Some(&f.rules[0]),
2885 &f.rules[0].scenarios[3],
2886 &cli
2887 ),
2888 Some(RetryOptions {
2889 retries: Retries { current: 0, left: 1 },
2890 after: Some(Duration::from_secs(3)),
2891 }),
2892 );
2893 assert_eq!(
2894 RetryOptions::parse_from_tags(
2895 &f,
2896 Some(&f.rules[0]),
2897 &f.rules[0].scenarios[4],
2898 &cli
2899 ),
2900 Some(RetryOptions {
2901 retries: Retries { current: 0, left: 5 },
2902 after: Some(Duration::from_secs(15)),
2903 }),
2904 );
2905 assert_eq!(
2906 RetryOptions::parse_from_tags(
2907 &f,
2908 Some(&f.rules[1]),
2909 &f.rules[1].scenarios[0],
2910 &cli
2911 ),
2912 Some(RetryOptions {
2913 retries: Retries { current: 0, left: 3 },
2914 after: Some(Duration::from_secs(5)),
2915 }),
2916 );
2917 assert_eq!(
2918 RetryOptions::parse_from_tags(
2919 &f,
2920 Some(&f.rules[1]),
2921 &f.rules[1].scenarios[1],
2922 &cli
2923 ),
2924 Some(RetryOptions {
2925 retries: Retries { current: 0, left: 1 },
2926 after: None,
2927 }),
2928 );
2929 assert_eq!(
2930 RetryOptions::parse_from_tags(
2931 &f,
2932 Some(&f.rules[1]),
2933 &f.rules[1].scenarios[2],
2934 &cli
2935 ),
2936 Some(RetryOptions {
2937 retries: Retries { current: 0, left: 5 },
2938 after: None,
2939 }),
2940 );
2941 assert_eq!(
2942 RetryOptions::parse_from_tags(
2943 &f,
2944 Some(&f.rules[1]),
2945 &f.rules[1].scenarios[3],
2946 &cli
2947 ),
2948 Some(RetryOptions {
2949 retries: Retries { current: 0, left: 1 },
2950 after: Some(Duration::from_secs(3)),
2951 }),
2952 );
2953 assert_eq!(
2954 RetryOptions::parse_from_tags(
2955 &f,
2956 Some(&f.rules[1]),
2957 &f.rules[1].scenarios[4],
2958 &cli
2959 ),
2960 Some(RetryOptions {
2961 retries: Retries { current: 0, left: 5 },
2962 after: Some(Duration::from_secs(15)),
2963 }),
2964 );
2965 }
2966
2967 #[test]
2968 fn cli_retry_after_and_filter() {
2969 let cli = Cli {
2970 concurrency: None,
2971 fail_fast: false,
2972 retry: Some(7),
2973 retry_after: Some(parse_duration("5s").unwrap()),
2974 retry_tag_filter: Some("@retry".parse().unwrap()),
2975 };
2976 let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
2977 .expect("failed to parse feature");
2978
2979 assert_eq!(
2980 RetryOptions::parse_from_tags(
2981 &f,
2982 Some(&f.rules[0]),
2983 &f.rules[0].scenarios[0],
2984 &cli
2985 ),
2986 None,
2987 );
2988 assert_eq!(
2989 RetryOptions::parse_from_tags(
2990 &f,
2991 Some(&f.rules[0]),
2992 &f.rules[0].scenarios[1],
2993 &cli
2994 ),
2995 Some(RetryOptions {
2996 retries: Retries { current: 0, left: 7 },
2997 after: Some(Duration::from_secs(5)),
2998 }),
2999 );
3000 assert_eq!(
3001 RetryOptions::parse_from_tags(
3002 &f,
3003 Some(&f.rules[0]),
3004 &f.rules[0].scenarios[2],
3005 &cli
3006 ),
3007 Some(RetryOptions {
3008 retries: Retries { current: 0, left: 5 },
3009 after: Some(Duration::from_secs(5)),
3010 }),
3011 );
3012 assert_eq!(
3013 RetryOptions::parse_from_tags(
3014 &f,
3015 Some(&f.rules[0]),
3016 &f.rules[0].scenarios[3],
3017 &cli
3018 ),
3019 Some(RetryOptions {
3020 retries: Retries { current: 0, left: 7 },
3021 after: Some(Duration::from_secs(3)),
3022 }),
3023 );
3024 assert_eq!(
3025 RetryOptions::parse_from_tags(
3026 &f,
3027 Some(&f.rules[0]),
3028 &f.rules[0].scenarios[4],
3029 &cli
3030 ),
3031 Some(RetryOptions {
3032 retries: Retries { current: 0, left: 5 },
3033 after: Some(Duration::from_secs(15)),
3034 }),
3035 );
3036 assert_eq!(
3037 RetryOptions::parse_from_tags(
3038 &f,
3039 Some(&f.rules[1]),
3040 &f.rules[1].scenarios[0],
3041 &cli
3042 ),
3043 Some(RetryOptions {
3044 retries: Retries { current: 0, left: 3 },
3045 after: Some(Duration::from_secs(5)),
3046 }),
3047 );
3048 assert_eq!(
3049 RetryOptions::parse_from_tags(
3050 &f,
3051 Some(&f.rules[1]),
3052 &f.rules[1].scenarios[1],
3053 &cli
3054 ),
3055 Some(RetryOptions {
3056 retries: Retries { current: 0, left: 7 },
3057 after: Some(Duration::from_secs(5)),
3058 }),
3059 );
3060 assert_eq!(
3061 RetryOptions::parse_from_tags(
3062 &f,
3063 Some(&f.rules[1]),
3064 &f.rules[1].scenarios[2],
3065 &cli
3066 ),
3067 Some(RetryOptions {
3068 retries: Retries { current: 0, left: 5 },
3069 after: Some(Duration::from_secs(5)),
3070 }),
3071 );
3072 assert_eq!(
3073 RetryOptions::parse_from_tags(
3074 &f,
3075 Some(&f.rules[1]),
3076 &f.rules[1].scenarios[3],
3077 &cli
3078 ),
3079 Some(RetryOptions {
3080 retries: Retries { current: 0, left: 7 },
3081 after: Some(Duration::from_secs(3)),
3082 }),
3083 );
3084 assert_eq!(
3085 RetryOptions::parse_from_tags(
3086 &f,
3087 Some(&f.rules[1]),
3088 &f.rules[1].scenarios[4],
3089 &cli
3090 ),
3091 Some(RetryOptions {
3092 retries: Retries { current: 0, left: 5 },
3093 after: Some(Duration::from_secs(15)),
3094 }),
3095 );
3096 }
3097 }
3098
3099 mod feature_tags {
3100 use super::*;
3101
3102 const FEATURE: &str = r"
3104@retry(8)
3105Feature: only scenarios
3106 Scenario: no tags
3107 Given a step
3108
3109 @retry
3110 Scenario: tag
3111 Given a step
3112
3113 @retry(5)
3114 Scenario: tag with explicit value
3115 Given a step
3116
3117 @retry.after(3s)
3118 Scenario: tag with explicit after
3119 Given a step
3120
3121 @retry(5).after(15s)
3122 Scenario: tag with explicit value and after
3123 Given a step
3124
3125 Rule: no tags
3126 Scenario: no tags
3127 Given a step
3128
3129 @retry
3130 Scenario: tag
3131 Given a step
3132
3133 @retry(5)
3134 Scenario: tag with explicit value
3135 Given a step
3136
3137 @retry.after(3s)
3138 Scenario: tag with explicit after
3139 Given a step
3140
3141 @retry(5).after(15s)
3142 Scenario: tag with explicit value and after
3143 Given a step
3144
3145 @retry(3).after(5s)
3146 Rule: retry tag
3147 Scenario: no tags
3148 Given a step
3149
3150 @retry
3151 Scenario: tag
3152 Given a step
3153
3154 @retry(5)
3155 Scenario: tag with explicit value
3156 Given a step
3157
3158 @retry.after(3s)
3159 Scenario: tag with explicit after
3160 Given a step
3161
3162 @retry(5).after(15s)
3163 Scenario: tag with explicit value and after
3164 Given a step
3165";
3166
3167 #[test]
3168 fn empty_cli() {
3169 let cli = Cli {
3170 concurrency: None,
3171 fail_fast: false,
3172 retry: None,
3173 retry_after: None,
3174 retry_tag_filter: None,
3175 };
3176 let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
3177 .unwrap_or_else(|e| panic!("failed to parse feature: {e}"));
3178
3179 assert_eq!(
3180 RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
3181 Some(RetryOptions {
3182 retries: Retries { current: 0, left: 8 },
3183 after: None,
3184 }),
3185 );
3186 assert_eq!(
3187 RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
3188 Some(RetryOptions {
3189 retries: Retries { current: 0, left: 1 },
3190 after: None,
3191 }),
3192 );
3193 assert_eq!(
3194 RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
3195 Some(RetryOptions {
3196 retries: Retries { current: 0, left: 5 },
3197 after: None,
3198 }),
3199 );
3200 assert_eq!(
3201 RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
3202 Some(RetryOptions {
3203 retries: Retries { current: 0, left: 1 },
3204 after: Some(Duration::from_secs(3)),
3205 }),
3206 );
3207 assert_eq!(
3208 RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
3209 Some(RetryOptions {
3210 retries: Retries { current: 0, left: 5 },
3211 after: Some(Duration::from_secs(15)),
3212 }),
3213 );
3214 assert_eq!(
3215 RetryOptions::parse_from_tags(
3216 &f,
3217 Some(&f.rules[0]),
3218 &f.rules[0].scenarios[0],
3219 &cli
3220 ),
3221 Some(RetryOptions {
3222 retries: Retries { current: 0, left: 8 },
3223 after: None,
3224 }),
3225 );
3226 assert_eq!(
3227 RetryOptions::parse_from_tags(
3228 &f,
3229 Some(&f.rules[0]),
3230 &f.rules[0].scenarios[1],
3231 &cli
3232 ),
3233 Some(RetryOptions {
3234 retries: Retries { current: 0, left: 1 },
3235 after: None,
3236 }),
3237 );
3238 assert_eq!(
3239 RetryOptions::parse_from_tags(
3240 &f,
3241 Some(&f.rules[0]),
3242 &f.rules[0].scenarios[2],
3243 &cli
3244 ),
3245 Some(RetryOptions {
3246 retries: Retries { current: 0, left: 5 },
3247 after: None,
3248 }),
3249 );
3250 assert_eq!(
3251 RetryOptions::parse_from_tags(
3252 &f,
3253 Some(&f.rules[0]),
3254 &f.rules[0].scenarios[3],
3255 &cli
3256 ),
3257 Some(RetryOptions {
3258 retries: Retries { current: 0, left: 1 },
3259 after: Some(Duration::from_secs(3)),
3260 }),
3261 );
3262 assert_eq!(
3263 RetryOptions::parse_from_tags(
3264 &f,
3265 Some(&f.rules[0]),
3266 &f.rules[0].scenarios[4],
3267 &cli
3268 ),
3269 Some(RetryOptions {
3270 retries: Retries { current: 0, left: 5 },
3271 after: Some(Duration::from_secs(15)),
3272 }),
3273 );
3274 assert_eq!(
3275 RetryOptions::parse_from_tags(
3276 &f,
3277 Some(&f.rules[1]),
3278 &f.rules[1].scenarios[0],
3279 &cli
3280 ),
3281 Some(RetryOptions {
3282 retries: Retries { current: 0, left: 3 },
3283 after: Some(Duration::from_secs(5)),
3284 }),
3285 );
3286 assert_eq!(
3287 RetryOptions::parse_from_tags(
3288 &f,
3289 Some(&f.rules[1]),
3290 &f.rules[1].scenarios[1],
3291 &cli
3292 ),
3293 Some(RetryOptions {
3294 retries: Retries { current: 0, left: 1 },
3295 after: None,
3296 }),
3297 );
3298 assert_eq!(
3299 RetryOptions::parse_from_tags(
3300 &f,
3301 Some(&f.rules[1]),
3302 &f.rules[1].scenarios[2],
3303 &cli
3304 ),
3305 Some(RetryOptions {
3306 retries: Retries { current: 0, left: 5 },
3307 after: None,
3308 }),
3309 );
3310 assert_eq!(
3311 RetryOptions::parse_from_tags(
3312 &f,
3313 Some(&f.rules[1]),
3314 &f.rules[1].scenarios[3],
3315 &cli
3316 ),
3317 Some(RetryOptions {
3318 retries: Retries { current: 0, left: 1 },
3319 after: Some(Duration::from_secs(3)),
3320 }),
3321 );
3322 assert_eq!(
3323 RetryOptions::parse_from_tags(
3324 &f,
3325 Some(&f.rules[1]),
3326 &f.rules[1].scenarios[4],
3327 &cli
3328 ),
3329 Some(RetryOptions {
3330 retries: Retries { current: 0, left: 5 },
3331 after: Some(Duration::from_secs(15)),
3332 }),
3333 );
3334 }
3335
3336 #[test]
3337 fn cli_retry_after_and_filter() {
3338 let cli = Cli {
3339 concurrency: None,
3340 fail_fast: false,
3341 retry: Some(7),
3342 retry_after: Some(parse_duration("5s").unwrap()),
3343 retry_tag_filter: Some("@retry".parse().unwrap()),
3344 };
3345 let f = gherkin::Feature::parse(FEATURE, GherkinEnv::default())
3346 .expect("failed to parse feature");
3347
3348 assert_eq!(
3349 RetryOptions::parse_from_tags(&f, None, &f.scenarios[0], &cli),
3350 Some(RetryOptions {
3351 retries: Retries { current: 0, left: 8 },
3352 after: Some(Duration::from_secs(5)),
3353 }),
3354 );
3355 assert_eq!(
3356 RetryOptions::parse_from_tags(&f, None, &f.scenarios[1], &cli),
3357 Some(RetryOptions {
3358 retries: Retries { current: 0, left: 7 },
3359 after: Some(Duration::from_secs(5)),
3360 }),
3361 );
3362 assert_eq!(
3363 RetryOptions::parse_from_tags(&f, None, &f.scenarios[2], &cli),
3364 Some(RetryOptions {
3365 retries: Retries { current: 0, left: 5 },
3366 after: Some(Duration::from_secs(5)),
3367 }),
3368 );
3369 assert_eq!(
3370 RetryOptions::parse_from_tags(&f, None, &f.scenarios[3], &cli),
3371 Some(RetryOptions {
3372 retries: Retries { current: 0, left: 7 },
3373 after: Some(Duration::from_secs(3)),
3374 }),
3375 );
3376 assert_eq!(
3377 RetryOptions::parse_from_tags(&f, None, &f.scenarios[4], &cli),
3378 Some(RetryOptions {
3379 retries: Retries { current: 0, left: 5 },
3380 after: Some(Duration::from_secs(15)),
3381 }),
3382 );
3383 assert_eq!(
3384 RetryOptions::parse_from_tags(
3385 &f,
3386 Some(&f.rules[0]),
3387 &f.rules[0].scenarios[0],
3388 &cli
3389 ),
3390 Some(RetryOptions {
3391 retries: Retries { current: 0, left: 8 },
3392 after: Some(Duration::from_secs(5)),
3393 }),
3394 );
3395 assert_eq!(
3396 RetryOptions::parse_from_tags(
3397 &f,
3398 Some(&f.rules[0]),
3399 &f.rules[0].scenarios[1],
3400 &cli
3401 ),
3402 Some(RetryOptions {
3403 retries: Retries { current: 0, left: 7 },
3404 after: Some(Duration::from_secs(5)),
3405 }),
3406 );
3407 assert_eq!(
3408 RetryOptions::parse_from_tags(
3409 &f,
3410 Some(&f.rules[0]),
3411 &f.rules[0].scenarios[2],
3412 &cli
3413 ),
3414 Some(RetryOptions {
3415 retries: Retries { current: 0, left: 5 },
3416 after: Some(Duration::from_secs(5)),
3417 }),
3418 );
3419 assert_eq!(
3420 RetryOptions::parse_from_tags(
3421 &f,
3422 Some(&f.rules[0]),
3423 &f.rules[0].scenarios[3],
3424 &cli
3425 ),
3426 Some(RetryOptions {
3427 retries: Retries { current: 0, left: 7 },
3428 after: Some(Duration::from_secs(3)),
3429 }),
3430 );
3431 assert_eq!(
3432 RetryOptions::parse_from_tags(
3433 &f,
3434 Some(&f.rules[0]),
3435 &f.rules[0].scenarios[4],
3436 &cli
3437 ),
3438 Some(RetryOptions {
3439 retries: Retries { current: 0, left: 5 },
3440 after: Some(Duration::from_secs(15)),
3441 }),
3442 );
3443 assert_eq!(
3444 RetryOptions::parse_from_tags(
3445 &f,
3446 Some(&f.rules[1]),
3447 &f.rules[1].scenarios[0],
3448 &cli
3449 ),
3450 Some(RetryOptions {
3451 retries: Retries { current: 0, left: 3 },
3452 after: Some(Duration::from_secs(5)),
3453 }),
3454 );
3455 assert_eq!(
3456 RetryOptions::parse_from_tags(
3457 &f,
3458 Some(&f.rules[1]),
3459 &f.rules[1].scenarios[1],
3460 &cli
3461 ),
3462 Some(RetryOptions {
3463 retries: Retries { current: 0, left: 7 },
3464 after: Some(Duration::from_secs(5)),
3465 }),
3466 );
3467 assert_eq!(
3468 RetryOptions::parse_from_tags(
3469 &f,
3470 Some(&f.rules[1]),
3471 &f.rules[1].scenarios[2],
3472 &cli
3473 ),
3474 Some(RetryOptions {
3475 retries: Retries { current: 0, left: 5 },
3476 after: Some(Duration::from_secs(5)),
3477 }),
3478 );
3479 assert_eq!(
3480 RetryOptions::parse_from_tags(
3481 &f,
3482 Some(&f.rules[1]),
3483 &f.rules[1].scenarios[3],
3484 &cli
3485 ),
3486 Some(RetryOptions {
3487 retries: Retries { current: 0, left: 7 },
3488 after: Some(Duration::from_secs(3)),
3489 }),
3490 );
3491 assert_eq!(
3492 RetryOptions::parse_from_tags(
3493 &f,
3494 Some(&f.rules[1]),
3495 &f.rules[1].scenarios[4],
3496 &cli
3497 ),
3498 Some(RetryOptions {
3499 retries: Retries { current: 0, left: 5 },
3500 after: Some(Duration::from_secs(15)),
3501 }),
3502 );
3503 }
3504 }
3505}