cucumber/writer/
fail_on_skipped.rs

1// Copyright (c) 2018-2025  Brendan Molloy <brendan@bbqsrc.net>,
2//                          Ilya Solovyiov <ilya.solovyiov@gmail.com>,
3//                          Kai Ren <tyranron@gmail.com>
4//
5// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8// option. This file may not be copied, modified, or distributed
9// except according to those terms.
10
11//! [`Writer`]-wrapper for transforming [`Skipped`] [`Step`]s into [`Failed`].
12//!
13//! [`Failed`]: event::Step::Failed
14//! [`Skipped`]: event::Step::Skipped
15//! [`Step`]: gherkin::Step
16
17use derive_more::with_trait::Deref;
18
19use crate::{
20    Event, World, Writer,
21    event::{self, Source},
22    parser, writer,
23};
24
25/// [`Writer`]-wrapper for transforming [`Skipped`] [`Step`]s into [`Failed`].
26///
27/// [`Failed`]: event::Step::Failed
28/// [`Skipped`]: event::Step::Skipped
29/// [`Step`]: gherkin::Step
30#[derive(Clone, Copy, Debug, Deref)]
31pub struct FailOnSkipped<W, F = SkipFn> {
32    /// Original [`Writer`] to pass transformed event into.
33    #[deref]
34    writer: W,
35
36    /// [`Fn`] to determine whether [`Skipped`] test should be considered as
37    /// [`Failed`] or not.
38    ///
39    /// [`Failed`]: event::Step::Failed
40    /// [`Skipped`]: event::Step::Skipped
41    should_fail: F,
42}
43
44/// Alias for a [`fn`] used to determine whether [`Skipped`] test should be
45/// considered as [`Failed`] or not.
46///
47/// [`Failed`]: event::Step::Failed
48/// [`Skipped`]: event::Step::Skipped
49pub type SkipFn =
50    fn(&gherkin::Feature, Option<&gherkin::Rule>, &gherkin::Scenario) -> bool;
51
52impl<W, Wr, F> Writer<W> for FailOnSkipped<Wr, F>
53where
54    W: World,
55    F: Fn(
56        &gherkin::Feature,
57        Option<&gherkin::Rule>,
58        &gherkin::Scenario,
59    ) -> bool,
60    Wr: Writer<W>,
61{
62    type Cli = Wr::Cli;
63
64    async fn handle_event(
65        &mut self,
66        event: parser::Result<Event<event::Cucumber<W>>>,
67        cli: &Self::Cli,
68    ) {
69        use event::{
70            Cucumber, Feature, RetryableScenario, Rule, Scenario, Step,
71            StepError::NotFound,
72        };
73
74        let map_failed = |f: &Source<_>, r: &Option<_>, sc: &Source<_>| {
75            if (self.should_fail)(f, r.as_deref(), sc) {
76                Step::Failed(None, None, None, NotFound)
77            } else {
78                Step::Skipped
79            }
80        };
81        let map_failed_bg =
82            |f: Source<_>, r: Option<_>, sc: Source<_>, st: _, ret| {
83                let ev = map_failed(&f, &r, &sc);
84                let ev = Scenario::Background(st, ev).with_retries(ret);
85                Cucumber::scenario(f, r, sc, ev)
86            };
87        let map_failed_step =
88            |f: Source<_>, r: Option<_>, sc: Source<_>, st: _, ret| {
89                let ev = map_failed(&f, &r, &sc);
90                let ev = Scenario::Step(st, ev).with_retries(ret);
91                Cucumber::scenario(f, r, sc, ev)
92            };
93
94        let event = event.map(|outer| {
95            outer.map(|ev| match ev {
96                Cucumber::Feature(
97                    f,
98                    Feature::Rule(
99                        r,
100                        Rule::Scenario(
101                            sc,
102                            RetryableScenario {
103                                event: Scenario::Background(st, Step::Skipped),
104                                retries,
105                            },
106                        ),
107                    ),
108                ) => map_failed_bg(f, Some(r), sc, st, retries),
109                Cucumber::Feature(
110                    f,
111                    Feature::Scenario(
112                        sc,
113                        RetryableScenario {
114                            event: Scenario::Background(st, Step::Skipped),
115                            retries,
116                        },
117                    ),
118                ) => map_failed_bg(f, None, sc, st, retries),
119                Cucumber::Feature(
120                    f,
121                    Feature::Rule(
122                        r,
123                        Rule::Scenario(
124                            sc,
125                            RetryableScenario {
126                                event: Scenario::Step(st, Step::Skipped),
127                                retries,
128                            },
129                        ),
130                    ),
131                ) => map_failed_step(f, Some(r), sc, st, retries),
132                Cucumber::Feature(
133                    f,
134                    Feature::Scenario(
135                        sc,
136                        RetryableScenario {
137                            event: Scenario::Step(st, Step::Skipped),
138                            retries,
139                        },
140                        ..,
141                    ),
142                ) => map_failed_step(f, None, sc, st, retries),
143                Cucumber::Started
144                | Cucumber::Feature(..)
145                | Cucumber::ParsingFinished { .. }
146                | Cucumber::Finished => ev,
147            })
148        });
149
150        self.writer.handle_event(event, cli).await;
151    }
152}
153
154#[warn(clippy::missing_trait_methods)]
155impl<W, Wr, Val, F> writer::Arbitrary<W, Val> for FailOnSkipped<Wr, F>
156where
157    W: World,
158    Self: Writer<W>,
159    Wr: writer::Arbitrary<W, Val>,
160{
161    async fn write(&mut self, val: Val) {
162        self.writer.write(val).await;
163    }
164}
165
166#[warn(clippy::missing_trait_methods)]
167impl<W, Wr, F> writer::Stats<W> for FailOnSkipped<Wr, F>
168where
169    Wr: writer::Stats<W>,
170    Self: Writer<W>,
171{
172    fn passed_steps(&self) -> usize {
173        self.writer.passed_steps()
174    }
175
176    fn skipped_steps(&self) -> usize {
177        self.writer.skipped_steps()
178    }
179
180    fn failed_steps(&self) -> usize {
181        self.writer.failed_steps()
182    }
183
184    fn retried_steps(&self) -> usize {
185        self.writer.retried_steps()
186    }
187
188    fn parsing_errors(&self) -> usize {
189        self.writer.parsing_errors()
190    }
191
192    fn hook_errors(&self) -> usize {
193        self.writer.hook_errors()
194    }
195
196    fn execution_has_failed(&self) -> bool {
197        self.writer.execution_has_failed()
198    }
199}
200
201#[warn(clippy::missing_trait_methods)]
202impl<Wr: writer::Normalized, F> writer::Normalized for FailOnSkipped<Wr, F> {}
203
204impl<Writer> From<Writer> for FailOnSkipped<Writer> {
205    fn from(writer: Writer) -> Self {
206        Self {
207            writer,
208            should_fail: |feat, rule, sc| {
209                !sc.tags
210                    .iter()
211                    .chain(rule.iter().flat_map(|r| &r.tags))
212                    .chain(&feat.tags)
213                    .any(|t| t == "allow.skipped")
214            },
215        }
216    }
217}
218
219impl<Writer> FailOnSkipped<Writer> {
220    /// Wraps the given [`Writer`] in a new [`FailOnSkipped`] one.
221    #[must_use]
222    pub fn new(writer: Writer) -> Self {
223        Self::from(writer)
224    }
225
226    /// Wraps the given [`Writer`] in a new [`FailOnSkipped`] one with the given
227    /// `predicate` indicating when a [`Skipped`] [`Step`] is considered
228    /// [`Failed`].
229    ///
230    /// [`Failed`]: event::Step::Failed
231    /// [`Skipped`]: event::Step::Skipped
232    /// [`Step`]: gherkin::Step
233    #[must_use]
234    pub const fn with<P>(
235        writer: Writer,
236        predicate: P,
237    ) -> FailOnSkipped<Writer, P>
238    where
239        P: Fn(
240            &gherkin::Feature,
241            Option<&gherkin::Rule>,
242            &gherkin::Scenario,
243        ) -> bool,
244    {
245        FailOnSkipped { writer, should_fail: predicate }
246    }
247
248    /// Returns the original [`Writer`], wrapped by this [`FailOnSkipped`] one.
249    #[must_use]
250    pub fn inner_writer(&self) -> &Writer {
251        &self.writer
252    }
253}