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#"
93Options:
94        --skip FILTER   Skip tests whose names contain FILTER (this flag can
95                        be used multiple times)
96        --exact         Exactly match filters rather than by substring
97        --test          Run tests and not benchmarks
98        --bench         Run benchmarks instead of tests
99        --ignored       Run only ignored tests
100        --include-ignored 
101                        Run ignored and not ignored tests
102        --fail-fast     Don't start new tests after the first failure
103        --no-capture    don't capture stdout/stderr of each task, allow
104                        printing directly
105        --show-output   Show captured stdout of successful tests
106        --test-threads n_threads
107                        Number of threads used for running tests in parallel
108        --color auto|always|never
109                        Configure coloring of output:
110                        auto = colorize if stdout is a tty and tests are run
111                        on serially (default);
112                        always = always colorize output;
113                        never = never colorize output;
114        --format pretty|terse|json
115                        Configure formatting of output:
116                        pretty = Print verbose output;
117                        terse = Display one character per test;
118                        json = Output a json document;
119        --list          List all tests and benchmarks
120    -q, --quiet         Display one character per test instead of one line.
121                        Alias to --format=terse
122    -Z unstable-options Enable nightly-only flags:
123                        unstable-options = Allow use of experimental features
124"#;
125
126pub const AFTER_HELP: &str = r#"
127The FILTER string is tested against the name of all tests, and only those
128tests whose names contain the filter are run. Multiple filter strings may
129be passed, which will run all tests matching any of the filters.
130
131By default, all tests are run in parallel. This can be altered with the
132--test-threads flag when running
133tests (set it to 1).
134
135All tests have their standard output and standard error captured by default.
136This can be overridden with the --no-capture flag to a value other than "0".
137Logging is not captured by default.
138
139Test Attributes:
140
141    `#[test]`        - Indicates a function is a test to be run. This function
142                       takes no arguments.
143    `#[bench]`       - Indicates a function is a benchmark to be run. This
144                       function takes one argument (test::Bencher).
145    `#[should_panic]` - This function (also labeled with `#[test]`) will only pass if
146                        the code causes a panic (an assertion failure or panic!)
147                        A message may be provided, which the failure string must
148                        contain: #[should_panic(expected = "foo")].
149    `#[ignore]`       - When applied to a function which is already attributed as a
150                        test, then the test runner will ignore these tests during
151                        normal test runs. Running with --ignored or --include-ignored will run
152                        these tests.
153"#;
154
155/// Intermediate CLI parser state for [`TestOpts`]
156///
157/// See [`TestOptsBuilder::parse_next`]
158#[derive(Debug, Default)]
159pub struct TestOptsBuilder {
160    opts: TestOpts,
161    quiet: bool,
162    format: Option<OutputFormat>,
163    include_ignored: bool,
164    ignored: bool,
165}
166
167impl TestOptsBuilder {
168    pub fn new() -> Self {
169        Default::default()
170    }
171
172    /// Check if `arg` is relevant to [`TestOpts`]
173    pub fn parse_next<'a>(
174        &mut self,
175        parser: &mut lexarg::Parser<'a>,
176        arg: Arg<'a>,
177    ) -> Result<Option<Arg<'a>>, LexError<'a>> {
178        use lexarg::prelude::*;
179
180        match arg {
181            Long("include-ignored") => {
182                self.include_ignored = true;
183            }
184            Long("ignored") => self.ignored = true,
185            Long("test") => {
186                self.opts.run_tests = true;
187            }
188            Long("bench") => {
189                self.opts.bench_benchmarks = true;
190            }
191            Long("list") => {
192                self.opts.list = true;
193            }
194            Long("no-capture") => {
195                self.opts.no_capture = true;
196            }
197            Long("test-threads") => {
198                let test_threads = parser
199                    .next_flag_value()
200                    .ok_or_missing(Value(std::ffi::OsStr::new("NUM")))
201                    .parse()
202                    .within(arg)?;
203                self.opts.test_threads = Some(test_threads);
204            }
205            Long("skip") => {
206                let filter = parser
207                    .next_flag_value()
208                    .ok_or_missing(Value(std::ffi::OsStr::new("NAME")))
209                    .string("NAME")
210                    .within(arg)?;
211                self.opts.skip.push(filter.to_owned());
212            }
213            Long("exact") => {
214                self.opts.filter_exact = true;
215            }
216            Long("fail-fast") => {
217                self.opts.fail_fast = true;
218            }
219            Long("color") => {
220                let color = parser
221                    .next_flag_value()
222                    .ok_or_missing(Value(std::ffi::OsStr::new("WHEN")))
223                    .one_of(&["auto", "always", "never"])
224                    .within(arg)?;
225                self.opts.color = match color {
226                    "auto" => ColorConfig::AutoColor,
227                    "always" => ColorConfig::AlwaysColor,
228                    "never" => ColorConfig::NeverColor,
229                    _ => unreachable!("`one_of` should prevent this"),
230                };
231            }
232            Short("q") | Long("quiet") => {
233                self.format = None;
234                self.quiet = true;
235            }
236            Long("format") => {
237                self.quiet = false;
238                let format = parser
239                    .next_flag_value()
240                    .ok_or_missing(Value(std::ffi::OsStr::new("FORMAT")))
241                    .one_of(&["pretty", "terse", "json"])
242                    .within(arg)?;
243                self.format = Some(match format {
244                    "pretty" => OutputFormat::Pretty,
245                    "terse" => OutputFormat::Terse,
246                    "json" => OutputFormat::Json,
247                    _ => unreachable!("`one_of` should prevent this"),
248                });
249            }
250            Long("show-output") => {
251                self.opts.show_output = true;
252            }
253            Short("Z") => {
254                let feature = parser
255                    .next_flag_value()
256                    .ok_or_missing(Value(std::ffi::OsStr::new("FEATURE")))
257                    .string("FEATURE")
258                    .within(arg)?;
259                if !is_nightly() {
260                    return Err(LexError::msg("expected nightly compiler").unexpected(arg));
261                }
262                // Don't validate `feature` as other parsers might provide values
263                self.opts.allowed_unstable.push(feature.to_owned());
264            }
265            Value(filter) => {
266                let filter = filter.string("FILTER")?;
267                self.opts.filters.push(filter.to_owned());
268            }
269            _ => {
270                return Ok(Some(arg));
271            }
272        }
273        Ok(None)
274    }
275
276    /// Finish parsing, resolving to [`TestOpts`]
277    pub fn finish(mut self) -> Result<TestOpts, LexError<'static>> {
278        let allow_unstable_options = self
279            .opts
280            .allowed_unstable
281            .iter()
282            .any(|f| f == UNSTABLE_OPTIONS);
283
284        if self.format.is_some() && !allow_unstable_options {
285            return Err(LexError::msg("`--format` requires `-Zunstable-options`"));
286        }
287        if let Some(format) = self.format {
288            self.opts.format = format;
289        } else if self.quiet {
290            self.opts.format = OutputFormat::Terse;
291        }
292
293        self.opts.run_tests |= !self.opts.bench_benchmarks;
294
295        self.opts.run_ignored = match (self.include_ignored, self.ignored) {
296            (true, true) => {
297                return Err(LexError::msg(
298                    "`--include-ignored` and `--ignored` are mutually exclusive",
299                ))
300            }
301            (true, false) => RunIgnored::Yes,
302            (false, true) => RunIgnored::Only,
303            (false, false) => RunIgnored::No,
304        };
305
306        let opts = self.opts;
307        Ok(opts)
308    }
309}
310
311// FIXME: Copied from librustc_ast until linkage errors are resolved. Issue #47566
312fn is_nightly() -> bool {
313    // Whether this is a feature-staged build, i.e., on the beta or stable channel
314    let disable_unstable_features = option_env!("CFG_DISABLE_UNSTABLE_FEATURES")
315        .map(|s| s != "0")
316        .unwrap_or(false);
317    // Whether we should enable unstable features for bootstrapping
318    let bootstrap = std::env::var("RUSTC_BOOTSTRAP").is_ok();
319
320    bootstrap || !disable_unstable_features
321}
322
323#[doc = include_str!("../README.md")]
324#[cfg(doctest)]
325pub struct ReadmeDoctests;