cucumber/
cli.rs

1// Copyright (c) 2018-2024  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//! Tools for composing CLI options.
12//!
13//! The main thing in this module is [`Opts`], which compose all the strongly
14//! typed CLI options from [`Parser`], [`Runner`] and [`Writer`], and provide
15//! filtering based on [`Regex`] or [tag expressions][1].
16//!
17//! The idea behind this is that [`Parser`], [`Runner`] and/or [`Writer`] may
18//! want to introduce their own CLI options to allow tweaking themselves, but we
19//! still do want them combine in a single CLI and avoid any boilerplate burden.
20//!
21//! If the implementation doesn't need any CLI options, it may just use the
22//! prepared [`cli::Empty`] stub.
23//!
24//! [`cli::Empty`]: self::Empty
25//! [`Parser`]: crate::Parser
26//! [`Runner`]: crate::Runner
27//! [`Writer`]: crate::Writer
28//! [1]: https://cucumber.io/docs/cucumber/api#tag-expressions
29
30use gherkin::tagexpr::TagOperation;
31use regex::Regex;
32
33use crate::writer::Coloring;
34
35pub use clap::{Args, Parser};
36
37/// Root CLI (command line interface) of a top-level [`Cucumber`] executor.
38///
39/// It combines all the nested CLIs of [`Parser`], [`Runner`] and [`Writer`],
40/// and may be extended with custom CLI options additionally.
41///
42/// # Example
43///
44/// ```rust
45/// # use std::time::Duration;
46/// #
47/// # use cucumber::{cli, World};
48/// # use futures::FutureExt as _;
49/// # use tokio::time;
50/// #
51/// # #[derive(Debug, Default, World)]
52/// # struct MyWorld;
53/// #
54/// # #[tokio::main(flavor = "current_thread")]
55/// # async fn main() {
56/// #[derive(clap::Args)] // also re-exported as `cli::Args`
57/// struct CustomOpts {
58///     /// Additional time to wait in before hook.
59///     #[arg(
60///         long,
61///         value_parser = humantime::parse_duration,
62///     )]
63///     pre_pause: Option<Duration>,
64/// }
65///
66/// let opts = cli::Opts::<_, _, _, CustomOpts>::parsed();
67/// let pre_pause = opts.custom.pre_pause.unwrap_or_default();
68///
69/// MyWorld::cucumber()
70///     .before(move |_, _, _, _| time::sleep(pre_pause).boxed_local())
71///     .with_cli(opts)
72///     .run_and_exit("tests/features/readme")
73///     .await;
74/// # }
75/// ```
76///
77/// [`Cucumber`]: crate::Cucumber
78/// [`Parser`]: crate::Parser
79/// [`Runner`]: crate::Runner
80/// [`Writer`]: crate::Writer
81#[derive(clap::Parser, Clone, Debug, Default)]
82#[command(
83    name = "cucumber",
84    about = "Run the tests, pet a dog!",
85    long_about = "Run the tests, pet a dog!"
86)]
87pub struct Opts<Parser, Runner, Writer, Custom = Empty>
88where
89    Parser: Args,
90    Runner: Args,
91    Writer: Args,
92    Custom: Args,
93{
94    /// Regex to filter scenarios by their name.
95    #[arg(
96        id = "name",
97        long = "name",
98        short = 'n',
99        value_name = "regex",
100        visible_alias = "scenario-name",
101        global = true
102    )]
103    pub re_filter: Option<Regex>,
104
105    /// Tag expression to filter scenarios by.
106    ///
107    /// Note: Tags from Feature, Rule and Scenario are merged together on
108    /// filtering, so be careful about conflicting tags on different levels.
109    #[arg(
110        id = "tags",
111        long = "tags",
112        short = 't',
113        value_name = "tagexpr",
114        conflicts_with = "name",
115        global = true
116    )]
117    pub tags_filter: Option<TagOperation>,
118
119    /// [`Parser`] CLI options.
120    ///
121    /// [`Parser`]: crate::Parser
122    #[command(flatten)]
123    pub parser: Parser,
124
125    /// [`Runner`] CLI options.
126    ///
127    /// [`Runner`]: crate::Runner
128    #[command(flatten)]
129    pub runner: Runner,
130
131    /// [`Writer`] CLI options.
132    ///
133    /// [`Writer`]: crate::Writer
134    #[command(flatten)]
135    pub writer: Writer,
136
137    /// Additional custom CLI options.
138    #[command(flatten)]
139    pub custom: Custom,
140}
141
142impl<Parser, Runner, Writer, Custom> Opts<Parser, Runner, Writer, Custom>
143where
144    Parser: Args,
145    Runner: Args,
146    Writer: Args,
147    Custom: Args,
148{
149    /// Shortcut for [`clap::Parser::parse()`], which doesn't require the trait
150    /// being imported.
151    #[must_use]
152    pub fn parsed() -> Self {
153        <Self as clap::Parser>::parse()
154    }
155}
156
157/// Indication whether a [`Writer`] using CLI options supports colored output.
158///
159/// [`Writer`]: crate::Writer
160pub trait Colored {
161    /// Returns [`Coloring`] indicating whether a [`Writer`] using CLI options
162    /// supports colored output or not.
163    ///
164    /// [`Writer`]: crate::Writer
165    #[must_use]
166    fn coloring(&self) -> Coloring {
167        Coloring::Never
168    }
169}
170
171/// Empty CLI options.
172#[derive(Args, Clone, Copy, Debug, Default)]
173#[group(skip)]
174pub struct Empty;
175
176impl Colored for Empty {}
177
178/// Composes two [`clap::Args`] derivers together.
179///
180/// # Example
181///
182/// This struct is especially useful, when implementing custom [`Writer`]
183/// wrapping another one:
184/// ```rust
185/// # use cucumber::{cli, event, parser, writer, Event, World, Writer};
186/// #
187/// struct CustomWriter<Wr>(Wr);
188///
189/// #[derive(cli::Args)] // re-export of `clap::Args`
190/// struct Cli {
191///     #[arg(long)]
192///     custom_option: Option<String>,
193/// }
194///
195/// impl<W, Wr> Writer<W> for CustomWriter<Wr>
196/// where
197///     W: World,
198///     Wr: Writer<W>,
199/// {
200///     type Cli = cli::Compose<Cli, Wr::Cli>;
201///
202///     async fn handle_event(
203///         &mut self,
204///         ev: parser::Result<Event<event::Cucumber<W>>>,
205///         cli: &Self::Cli,
206///     ) {
207///         // Some custom logic including `cli.left.custom_option`.
208///         // ...
209///         self.0.handle_event(ev, &cli.right).await;
210///     }
211/// }
212///
213/// // Useful blanket impls:
214///
215/// impl cli::Colored for Cli {}
216///
217/// impl<W, Wr, Val> writer::Arbitrary<W, Val> for CustomWriter<Wr>
218/// where
219///     Wr: writer::Arbitrary<W, Val>,
220///     Self: Writer<W>,
221/// {
222///     async fn write(&mut self, val: Val) {
223///         self.0.write(val).await;
224///     }
225/// }
226///
227/// impl<W, Wr> writer::Stats<W> for CustomWriter<Wr>
228/// where
229///     Wr: writer::Stats<W>,
230///     Self: Writer<W>,
231/// {
232///     fn passed_steps(&self) -> usize {
233///         self.0.failed_steps()
234///     }
235///
236///     fn skipped_steps(&self) -> usize {
237///         self.0.failed_steps()
238///     }
239///
240///     fn failed_steps(&self) -> usize {
241///         self.0.failed_steps()
242///     }
243///
244///     fn retried_steps(&self) -> usize {
245///         self.0.retried_steps()
246///     }
247///
248///     fn parsing_errors(&self) -> usize {
249///         self.0.parsing_errors()
250///     }
251///
252///     fn hook_errors(&self) -> usize {
253///         self.0.hook_errors()
254///     }
255/// }
256///
257/// impl<Wr: writer::Normalized> writer::Normalized for CustomWriter<Wr> {}
258///
259/// impl<Wr: writer::NonTransforming> writer::NonTransforming
260///     for CustomWriter<Wr>
261/// {}
262/// ```
263///
264/// [`Writer`]: crate::Writer
265#[derive(Args, Clone, Copy, Debug, Default)]
266#[group(skip)]
267pub struct Compose<L: Args, R: Args> {
268    /// Left [`clap::Args`] deriver.
269    #[command(flatten)]
270    pub left: L,
271
272    /// Right [`clap::Args`] deriver.
273    #[command(flatten)]
274    pub right: R,
275}
276
277impl<L: Args, R: Args> Compose<L, R> {
278    /// Unpacks this [`Compose`] into the underlying CLIs.
279    #[must_use]
280    pub fn into_inner(self) -> (L, R) {
281        let Self { left, right } = self;
282        (left, right)
283    }
284}
285
286#[warn(clippy::missing_trait_methods)]
287impl<L, R> Colored for Compose<L, R>
288where
289    L: Args + Colored,
290    R: Args + Colored,
291{
292    fn coloring(&self) -> Coloring {
293        // Basically, founds "maximum" `Coloring` of CLI options.
294        match (self.left.coloring(), self.right.coloring()) {
295            (Coloring::Always, _) | (_, Coloring::Always) => Coloring::Always,
296            (Coloring::Auto, _) | (_, Coloring::Auto) => Coloring::Auto,
297            (Coloring::Never, Coloring::Never) => Coloring::Never,
298        }
299    }
300}