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::ErrorContext;
18
19/// Parsed command-line options
20///
21/// To parse, see [`TestOptsBuilder`]
22#[derive(Debug, Default)]
23pub struct TestOpts {
24    pub list: bool,
25    pub filters: Vec<String>,
26    pub filter_exact: bool,
27    pub run_ignored: RunIgnored,
28    pub run_tests: bool,
29    pub bench_benchmarks: bool,
30    pub nocapture: bool,
31    pub color: ColorConfig,
32    pub format: OutputFormat,
33    pub test_threads: Option<std::num::NonZeroUsize>,
34    pub skip: Vec<String>,
35    /// Stop at first failing test.
36    /// May run a few more tests due to threading, but will
37    /// abort as soon as possible.
38    pub fail_fast: bool,
39    pub options: Options,
40    pub allowed_unstable: Vec<String>,
41}
42
43/// Whether ignored test should be run or not (see [`TestOpts::run_ignored`])
44#[derive(Copy, Clone, Debug, PartialEq, Eq)]
45pub enum RunIgnored {
46    Yes,
47    No,
48    /// Run only ignored tests
49    Only,
50}
51
52impl Default for RunIgnored {
53    fn default() -> Self {
54        Self::No
55    }
56}
57
58/// Whether should console output be colored or not (see [`TestOpts::color`])
59#[derive(Copy, Clone, Debug)]
60pub enum ColorConfig {
61    AutoColor,
62    AlwaysColor,
63    NeverColor,
64}
65
66impl Default for ColorConfig {
67    fn default() -> Self {
68        Self::AutoColor
69    }
70}
71
72/// Format of the test results output (see [`TestOpts::format`])
73#[derive(Copy, Clone, Debug, PartialEq, Eq)]
74pub enum OutputFormat {
75    /// Verbose output
76    Pretty,
77    /// Quiet output
78    Terse,
79    /// JSON output
80    Json,
81}
82
83impl Default for OutputFormat {
84    fn default() -> Self {
85        Self::Pretty
86    }
87}
88
89/// Options for the test run defined by the caller (instead of CLI arguments) (see
90/// [`TestOpts::options`])
91///
92/// In case we want to add other options as well, just add them in this struct.
93#[derive(Copy, Clone, Debug, Default)]
94pub struct Options {
95    pub display_output: bool,
96    pub panic_abort: bool,
97}
98
99pub const UNSTABLE_OPTIONS: &str = "unstable-options";
100
101pub const OPTIONS_HELP: &str = r#"
102Options:
103        --include-ignored 
104                        Run ignored and not ignored tests
105        --ignored       Run only ignored tests
106        --test          Run tests and not benchmarks
107        --bench         Run benchmarks instead of tests
108        --list          List all tests and benchmarks
109        --nocapture     don't capture stdout/stderr of each task, allow
110                        printing directly
111        --test-threads n_threads
112                        Number of threads used for running tests in parallel
113        --skip FILTER   Skip tests whose names contain FILTER (this flag can
114                        be used multiple times)
115    -q, --quiet         Display one character per test instead of one line.
116                        Alias to --format=terse
117        --exact         Exactly match filters rather than by substring
118        --color auto|always|never
119                        Configure coloring of output:
120                        auto = colorize if stdout is a tty and tests are run
121                        on serially (default);
122                        always = always colorize output;
123                        never = never colorize output;
124        --format pretty|terse|json
125                        Configure formatting of output:
126                        pretty = Print verbose output;
127                        terse = Display one character per test;
128                        json = Output a json document;
129        --show-output   Show captured stdout of successful tests
130    -Z unstable-options Enable nightly-only flags:
131                        unstable-options = Allow use of experimental features
132        --report-time   Show execution time of each test.
133                        Threshold values for colorized output can be
134                        configured via
135                        `RUST_TEST_TIME_UNIT`, `RUST_TEST_TIME_INTEGRATION`
136                        and
137                        `RUST_TEST_TIME_DOCTEST` environment variables.
138                        Expected format of environment variable is
139                        `VARIABLE=WARN_TIME,CRITICAL_TIME`.
140                        Durations must be specified in milliseconds, e.g.
141                        `500,2000` means that the warn time
142                        is 0.5 seconds, and the critical time is 2 seconds.
143                        Not available for --format=terse
144        --ensure-time   Treat excess of the test execution time limit as
145                        error.
146                        Threshold values for this option can be configured via
147                        `RUST_TEST_TIME_UNIT`, `RUST_TEST_TIME_INTEGRATION`
148                        and
149                        `RUST_TEST_TIME_DOCTEST` environment variables.
150                        Expected format of environment variable is
151                        `VARIABLE=WARN_TIME,CRITICAL_TIME`.
152                        `CRITICAL_TIME` here means the limit that should not
153                        be exceeded by test.
154"#;
155
156pub const AFTER_HELP: &str = r#"
157The FILTER string is tested against the name of all tests, and only those
158tests whose names contain the filter are run. Multiple filter strings may
159be passed, which will run all tests matching any of the filters.
160
161By default, all tests are run in parallel. This can be altered with the
162--test-threads flag or the RUST_TEST_THREADS environment variable when running
163tests (set it to 1).
164
165All tests have their standard output and standard error captured by default.
166This can be overridden with the --nocapture flag or setting RUST_TEST_NOCAPTURE
167environment variable to a value other than "0". Logging is not captured by default.
168
169Test Attributes:
170
171    `#[test]`        - Indicates a function is a test to be run. This function
172                       takes no arguments.
173    `#[bench]`       - Indicates a function is a benchmark to be run. This
174                       function takes one argument (test::Bencher).
175    `#[should_panic]` - This function (also labeled with `#[test]`) will only pass if
176                        the code causes a panic (an assertion failure or panic!)
177                        A message may be provided, which the failure string must
178                        contain: #[should_panic(expected = "foo")].
179    `#[ignore]`       - When applied to a function which is already attributed as a
180                        test, then the test runner will ignore these tests during
181                        normal test runs. Running with --ignored or --include-ignored will run
182                        these tests.
183"#;
184
185/// Intermediate CLI parser state for [`TestOpts`]
186///
187/// See [`TestOptsBuilder::parse_next`]
188#[derive(Debug, Default)]
189pub struct TestOptsBuilder {
190    opts: TestOpts,
191    quiet: bool,
192    format: Option<OutputFormat>,
193    include_ignored: bool,
194    ignored: bool,
195}
196
197impl TestOptsBuilder {
198    pub fn new() -> Self {
199        Default::default()
200    }
201
202    /// Check if `arg` is relevant to [`TestOpts`]
203    pub fn parse_next<'a>(
204        &mut self,
205        parser: &mut lexarg::Parser<'a>,
206        arg: Arg<'a>,
207    ) -> Result<Option<Arg<'a>>, ErrorContext<'a>> {
208        use lexarg::prelude::*;
209
210        match arg {
211            Long("include-ignored") => {
212                self.include_ignored = true;
213            }
214            Long("ignored") => self.ignored = true,
215            Long("test") => {
216                self.opts.run_tests = true;
217            }
218            Long("bench") => {
219                self.opts.bench_benchmarks = true;
220            }
221            Long("list") => {
222                self.opts.list = true;
223            }
224            Long("nocapture") => {
225                self.opts.nocapture = true;
226            }
227            Long("test-threads") => {
228                let test_threads = parser
229                    .next_flag_value()
230                    .ok_or_missing(Value(std::ffi::OsStr::new("NUM")))
231                    .parse()
232                    .within(arg)?;
233                self.opts.test_threads = Some(test_threads);
234            }
235            Long("skip") => {
236                let filter = parser
237                    .next_flag_value()
238                    .ok_or_missing(Value(std::ffi::OsStr::new("NAME")))
239                    .string("NAME")
240                    .within(arg)?;
241                self.opts.skip.push(filter.to_owned());
242            }
243            Long("exact") => {
244                self.opts.filter_exact = true;
245            }
246            Long("color") => {
247                let color = parser
248                    .next_flag_value()
249                    .ok_or_missing(Value(std::ffi::OsStr::new("WHEN")))
250                    .one_of(&["auto", "always", "never"])
251                    .within(arg)?;
252                self.opts.color = match color {
253                    "auto" => ColorConfig::AutoColor,
254                    "always" => ColorConfig::AlwaysColor,
255                    "never" => ColorConfig::NeverColor,
256                    _ => unreachable!("`one_of` should prevent this"),
257                };
258            }
259            Short("q") | Long("quiet") => {
260                self.format = None;
261                self.quiet = true;
262            }
263            Long("format") => {
264                self.quiet = false;
265                let format = parser
266                    .next_flag_value()
267                    .ok_or_missing(Value(std::ffi::OsStr::new("FORMAT")))
268                    .one_of(&["pretty", "terse", "json"])
269                    .within(arg)?;
270                self.format = Some(match format {
271                    "pretty" => OutputFormat::Pretty,
272                    "terse" => OutputFormat::Terse,
273                    "json" => OutputFormat::Json,
274                    _ => unreachable!("`one_of` should prevent this"),
275                });
276            }
277            Long("show-output") => {
278                self.opts.options.display_output = true;
279            }
280            Short("Z") => {
281                let feature = parser
282                    .next_flag_value()
283                    .ok_or_missing(Value(std::ffi::OsStr::new("FEATURE")))
284                    .string("FEATURE")
285                    .within(arg)?;
286                if !is_nightly() {
287                    return Err(ErrorContext::msg("expected nightly compiler").unexpected(arg));
288                }
289                // Don't validate `feature` as other parsers might provide values
290                self.opts.allowed_unstable.push(feature.to_owned());
291            }
292            Value(filter) => {
293                let filter = filter.string("FILTER")?;
294                self.opts.filters.push(filter.to_owned());
295            }
296            _ => {
297                return Ok(Some(arg));
298            }
299        }
300        Ok(None)
301    }
302
303    /// Finish parsing, resolving to [`TestOpts`]
304    pub fn finish(mut self) -> Result<TestOpts, ErrorContext<'static>> {
305        let allow_unstable_options = self
306            .opts
307            .allowed_unstable
308            .iter()
309            .any(|f| f == UNSTABLE_OPTIONS);
310
311        if !self.opts.nocapture {
312            self.opts.nocapture = match std::env::var("RUST_TEST_NOCAPTURE") {
313                Ok(val) => &val != "0",
314                Err(_) => false,
315            };
316        }
317
318        if self.format.is_some() && !allow_unstable_options {
319            return Err(ErrorContext::msg(
320                "`--format` requires `-Zunstable-options`",
321            ));
322        }
323        if let Some(format) = self.format {
324            self.opts.format = format;
325        } else if self.quiet {
326            self.opts.format = OutputFormat::Terse;
327        }
328
329        self.opts.run_tests |= !self.opts.bench_benchmarks;
330
331        self.opts.run_ignored = match (self.include_ignored, self.ignored) {
332            (true, true) => {
333                return Err(ErrorContext::msg(
334                    "`--include-ignored` and `--ignored` are mutually exclusive",
335                ))
336            }
337            (true, false) => RunIgnored::Yes,
338            (false, true) => RunIgnored::Only,
339            (false, false) => RunIgnored::No,
340        };
341
342        if self.opts.test_threads.is_none() {
343            if let Ok(value) = std::env::var("RUST_TEST_THREADS") {
344                self.opts.test_threads =
345                    Some(value.parse::<std::num::NonZeroUsize>().map_err(|_e| {
346                        ErrorContext::msg(format!(
347                            "RUST_TEST_THREADS is `{value}`, should be a positive integer."
348                        ))
349                    })?);
350            }
351        }
352
353        let opts = self.opts;
354        Ok(opts)
355    }
356}
357
358// FIXME: Copied from librustc_ast until linkage errors are resolved. Issue #47566
359fn is_nightly() -> bool {
360    // Whether this is a feature-staged build, i.e., on the beta or stable channel
361    let disable_unstable_features = option_env!("CFG_DISABLE_UNSTABLE_FEATURES")
362        .map(|s| s != "0")
363        .unwrap_or(false);
364    // Whether we should enable unstable features for bootstrapping
365    let bootstrap = std::env::var("RUSTC_BOOTSTRAP").is_ok();
366
367    bootstrap || !disable_unstable_features
368}
369
370#[doc = include_str!("../README.md")]
371#[cfg(doctest)]
372pub struct ReadmeDoctests;