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}