libtest_lexarg/
lib.rs

1//! libtest-compatible argument parser
2//!
3//! This does not drive parsing but provides [`TestOptsBuilder`] to plug into the parsing,
4//! allowing additional parsers to be integrated.
5//!
6//! ## Example
7//!
8//! ```no_run
9#![doc = include_str!("../examples/libtest-cli.rs")]
10//! ```
11
12#![cfg_attr(docsrs, feature(doc_auto_cfg))]
13#![forbid(unsafe_code)]
14#![warn(missing_debug_implementations, elided_lifetimes_in_paths)]
15
16use lexarg::Arg;
17use lexarg_error::LexError;
18
19/// Parsed command-line options
20///
21/// To parse, see [`TestOptsBuilder`]
22#[derive(Debug, Default)]
23#[non_exhaustive]
24pub struct TestOpts {
25    pub list: bool,
26    pub filters: Vec<String>,
27    pub filter_exact: bool,
28    pub run_ignored: RunIgnored,
29    pub run_tests: bool,
30    pub bench_benchmarks: bool,
31    pub no_capture: bool,
32    pub show_output: bool,
33    pub color: ColorConfig,
34    pub format: OutputFormat,
35    pub test_threads: Option<std::num::NonZeroUsize>,
36    pub skip: Vec<String>,
37    /// Stop at first failing test.
38    /// May run a few more tests due to threading, but will
39    /// abort as soon as possible.
40    pub fail_fast: bool,
41    pub allowed_unstable: Vec<String>,
42}
43
44/// Whether ignored test should be run or not (see [`TestOpts::run_ignored`])
45#[derive(Copy, Clone, Debug, PartialEq, Eq)]
46pub enum RunIgnored {
47    Yes,
48    No,
49    /// Run only ignored tests
50    Only,
51}
52
53impl Default for RunIgnored {
54    fn default() -> Self {
55        Self::No
56    }
57}
58
59/// Whether should console output be colored or not (see [`TestOpts::color`])
60#[derive(Copy, Clone, Debug)]
61pub enum ColorConfig {
62    AutoColor,
63    AlwaysColor,
64    NeverColor,
65}
66
67impl Default for ColorConfig {
68    fn default() -> Self {
69        Self::AutoColor
70    }
71}
72
73/// Format of the test results output (see [`TestOpts::format`])
74#[derive(Copy, Clone, Debug, PartialEq, Eq)]
75pub enum OutputFormat {
76    /// Verbose output
77    Pretty,
78    /// Quiet output
79    Terse,
80    /// JSON output
81    Json,
82}
83
84impl Default for OutputFormat {
85    fn default() -> Self {
86        Self::Pretty
87    }
88}
89
90pub const UNSTABLE_OPTIONS: &str = "unstable-options";
91
92pub const OPTIONS_HELP: &str = r#"
93Arguments:
94      [FILTER]...     Skip tests whose name does not match one of the filters
95
96Options:
97      --fail-fast     Don't start new tests after the first failure
98      --skip FILTER   Skip tests whose names contain FILTER
99                      (this flag can be used multiple times)
100      --exact         Exactly match filters rather than by substring
101      --ignored       Run only ignored tests
102      --include-ignored 
103                      Run ignored and not ignored tests
104      --test          Run tests and not benchmarks
105      --bench         Run benchmarks instead of tests
106      --no-capture    don't capture stdout/stderr of each task,
107                      allow printing directly
108      --show-output   Show captured stdout of successful tests
109      --list          List all tests and benchmarks
110      --test-threads NUM
111                      Number of threads used for running tests in parallel
112                      (default is >1)
113      --format <pretty|terse|json>
114                      Configure formatting of output:
115                      - pretty: Print verbose output
116                      - terse: Display one character per test
117                      - json: Output a json document
118  -q, --quiet         Display one character per test instead of one line
119                      (alias to --format=terse)
120      --color <auto|always|never>
121                      Configure coloring of output:
122                      - auto: detect terminal support (default)
123                      - always: always colorize output
124                      - never: never colorize output
125  -Z FLAG             Enable nightly-only flags:
126                      - unstable-options: Allow use of experimental features
127"#;
128
129pub const AFTER_HELP: &str = r#""#;
130
131/// Intermediate CLI parser state for [`TestOpts`]
132///
133/// See [`TestOptsBuilder::parse_next`]
134#[derive(Debug, Default)]
135pub struct TestOptsBuilder {
136    opts: TestOpts,
137    quiet: bool,
138    format: Option<OutputFormat>,
139    include_ignored: bool,
140    ignored: bool,
141}
142
143impl TestOptsBuilder {
144    pub fn new() -> Self {
145        Default::default()
146    }
147
148    /// Check if `arg` is relevant to [`TestOpts`]
149    pub fn parse_next<'a>(
150        &mut self,
151        parser: &mut lexarg::Parser<'a>,
152        arg: Arg<'a>,
153    ) -> Result<Option<Arg<'a>>, LexError<'a>> {
154        use lexarg::prelude::*;
155
156        match arg {
157            Long("include-ignored") => {
158                self.include_ignored = true;
159            }
160            Long("ignored") => self.ignored = true,
161            Long("test") => {
162                self.opts.run_tests = true;
163            }
164            Long("bench") => {
165                self.opts.bench_benchmarks = true;
166            }
167            Long("list") => {
168                self.opts.list = true;
169            }
170            Long("no-capture") => {
171                self.opts.no_capture = true;
172            }
173            Long("test-threads") => {
174                let test_threads = parser
175                    .next_flag_value()
176                    .ok_or_missing(Value(std::ffi::OsStr::new("NUM")))
177                    .parse()
178                    .within(arg)?;
179                self.opts.test_threads = Some(test_threads);
180            }
181            Long("skip") => {
182                let filter = parser
183                    .next_flag_value()
184                    .ok_or_missing(Value(std::ffi::OsStr::new("NAME")))
185                    .string("NAME")
186                    .within(arg)?;
187                self.opts.skip.push(filter.to_owned());
188            }
189            Long("exact") => {
190                self.opts.filter_exact = true;
191            }
192            Long("fail-fast") => {
193                self.opts.fail_fast = true;
194            }
195            Long("color") => {
196                let color = parser
197                    .next_flag_value()
198                    .ok_or_missing(Value(std::ffi::OsStr::new("WHEN")))
199                    .one_of(&["auto", "always", "never"])
200                    .within(arg)?;
201                self.opts.color = match color {
202                    "auto" => ColorConfig::AutoColor,
203                    "always" => ColorConfig::AlwaysColor,
204                    "never" => ColorConfig::NeverColor,
205                    _ => unreachable!("`one_of` should prevent this"),
206                };
207            }
208            Short("q") | Long("quiet") => {
209                self.format = None;
210                self.quiet = true;
211            }
212            Long("format") => {
213                self.quiet = false;
214                let format = parser
215                    .next_flag_value()
216                    .ok_or_missing(Value(std::ffi::OsStr::new("FORMAT")))
217                    .one_of(&["pretty", "terse", "json"])
218                    .within(arg)?;
219                self.format = Some(match format {
220                    "pretty" => OutputFormat::Pretty,
221                    "terse" => OutputFormat::Terse,
222                    "json" => OutputFormat::Json,
223                    _ => unreachable!("`one_of` should prevent this"),
224                });
225            }
226            Long("show-output") => {
227                self.opts.show_output = true;
228            }
229            Short("Z") => {
230                let feature = parser
231                    .next_flag_value()
232                    .ok_or_missing(Value(std::ffi::OsStr::new("FEATURE")))
233                    .string("FEATURE")
234                    .within(arg)?;
235                if !is_nightly() {
236                    return Err(LexError::msg("expected nightly compiler").unexpected(arg));
237                }
238                // Don't validate `feature` as other parsers might provide values
239                self.opts.allowed_unstable.push(feature.to_owned());
240            }
241            Value(filter) => {
242                let filter = filter.string("FILTER")?;
243                self.opts.filters.push(filter.to_owned());
244            }
245            _ => {
246                return Ok(Some(arg));
247            }
248        }
249        Ok(None)
250    }
251
252    /// Finish parsing, resolving to [`TestOpts`]
253    pub fn finish(mut self) -> Result<TestOpts, LexError<'static>> {
254        let allow_unstable_options = self
255            .opts
256            .allowed_unstable
257            .iter()
258            .any(|f| f == UNSTABLE_OPTIONS);
259
260        if self.format.is_some() && !allow_unstable_options {
261            return Err(LexError::msg("`--format` requires `-Zunstable-options`"));
262        }
263        if let Some(format) = self.format {
264            self.opts.format = format;
265        } else if self.quiet {
266            self.opts.format = OutputFormat::Terse;
267        }
268
269        self.opts.run_tests |= !self.opts.bench_benchmarks;
270
271        self.opts.run_ignored = match (self.include_ignored, self.ignored) {
272            (true, true) => {
273                return Err(LexError::msg(
274                    "`--include-ignored` and `--ignored` are mutually exclusive",
275                ))
276            }
277            (true, false) => RunIgnored::Yes,
278            (false, true) => RunIgnored::Only,
279            (false, false) => RunIgnored::No,
280        };
281
282        let opts = self.opts;
283        Ok(opts)
284    }
285}
286
287// FIXME: Copied from librustc_ast until linkage errors are resolved. Issue #47566
288fn is_nightly() -> bool {
289    // Whether this is a feature-staged build, i.e., on the beta or stable channel
290    let disable_unstable_features = option_env!("CFG_DISABLE_UNSTABLE_FEATURES")
291        .map(|s| s != "0")
292        .unwrap_or(false);
293    // Whether we should enable unstable features for bootstrapping
294    let bootstrap = std::env::var("RUSTC_BOOTSTRAP").is_ok();
295
296    bootstrap || !disable_unstable_features
297}
298
299#[doc = include_str!("../README.md")]
300#[cfg(doctest)]
301pub struct ReadmeDoctests;