test/
cli.rs

1//! Module converting command-line arguments into test configuration.
2
3use std::env;
4use std::io::{self, IsTerminal, Write};
5use std::path::PathBuf;
6
7use super::options::{ColorConfig, Options, OutputFormat, RunIgnored};
8use super::time::TestTimeOptions;
9
10#[derive(Debug)]
11pub struct TestOpts {
12    pub list: bool,
13    pub filters: Vec<String>,
14    pub filter_exact: bool,
15    pub force_run_in_process: bool,
16    pub exclude_should_panic: bool,
17    pub run_ignored: RunIgnored,
18    pub run_tests: bool,
19    pub bench_benchmarks: bool,
20    pub logfile: Option<PathBuf>,
21    pub nocapture: bool,
22    pub color: ColorConfig,
23    pub format: OutputFormat,
24    pub shuffle: bool,
25    pub shuffle_seed: Option<u64>,
26    pub test_threads: Option<usize>,
27    pub skip: Vec<String>,
28    pub time_options: Option<TestTimeOptions>,
29    /// Stop at first failing test.
30    /// May run a few more tests due to threading, but will
31    /// abort as soon as possible.
32    pub fail_fast: bool,
33    pub options: Options,
34}
35
36impl TestOpts {
37    pub fn use_color(&self) -> bool {
38        match self.color {
39            ColorConfig::AutoColor => !self.nocapture && io::stdout().is_terminal(),
40            ColorConfig::AlwaysColor => true,
41            ColorConfig::NeverColor => false,
42        }
43    }
44}
45
46/// Result of parsing the options.
47pub(crate) type OptRes = Result<TestOpts, String>;
48/// Result of parsing the option part.
49type OptPartRes<T> = Result<T, String>;
50
51fn optgroups() -> getopts::Options {
52    let mut opts = getopts::Options::new();
53    opts.optflag("", "include-ignored", "Run ignored and not ignored tests")
54        .optflag("", "ignored", "Run only ignored tests")
55        .optflag("", "force-run-in-process", "Forces tests to run in-process when panic=abort")
56        .optflag("", "exclude-should-panic", "Excludes tests marked as should_panic")
57        .optflag("", "test", "Run tests and not benchmarks")
58        .optflag("", "bench", "Run benchmarks instead of tests")
59        .optflag("", "list", "List all tests and benchmarks")
60        .optflag("h", "help", "Display this message")
61        .optopt("", "logfile", "Write logs to the specified file (deprecated)", "PATH")
62        .optflag(
63            "",
64            "no-capture",
65            "don't capture stdout/stderr of each \
66             task, allow printing directly",
67        )
68        .optopt(
69            "",
70            "test-threads",
71            "Number of threads used for running tests \
72             in parallel",
73            "n_threads",
74        )
75        .optmulti(
76            "",
77            "skip",
78            "Skip tests whose names contain FILTER (this flag can \
79             be used multiple times)",
80            "FILTER",
81        )
82        .optflag(
83            "q",
84            "quiet",
85            "Display one character per test instead of one line. \
86             Alias to --format=terse",
87        )
88        .optflag("", "exact", "Exactly match filters rather than by substring")
89        .optopt(
90            "",
91            "color",
92            "Configure coloring of output:
93            auto   = colorize if stdout is a tty and tests are run on serially (default);
94            always = always colorize output;
95            never  = never colorize output;",
96            "auto|always|never",
97        )
98        .optopt(
99            "",
100            "format",
101            "Configure formatting of output:
102            pretty = Print verbose output;
103            terse  = Display one character per test;
104            json   = Output a json document;
105            junit  = Output a JUnit document",
106            "pretty|terse|json|junit",
107        )
108        .optflag("", "show-output", "Show captured stdout of successful tests")
109        .optopt(
110            "Z",
111            "",
112            "Enable nightly-only flags:
113            unstable-options = Allow use of experimental features",
114            "unstable-options",
115        )
116        .optflag(
117            "",
118            "report-time",
119            "Show execution time of each test.
120
121            Threshold values for colorized output can be configured via
122            `RUST_TEST_TIME_UNIT`, `RUST_TEST_TIME_INTEGRATION` and
123            `RUST_TEST_TIME_DOCTEST` environment variables.
124
125            Expected format of environment variable is `VARIABLE=WARN_TIME,CRITICAL_TIME`.
126            Durations must be specified in milliseconds, e.g. `500,2000` means that the warn time
127            is 0.5 seconds, and the critical time is 2 seconds.
128
129            Not available for --format=terse",
130        )
131        .optflag(
132            "",
133            "ensure-time",
134            "Treat excess of the test execution time limit as error.
135
136            Threshold values for this option can be configured via
137            `RUST_TEST_TIME_UNIT`, `RUST_TEST_TIME_INTEGRATION` and
138            `RUST_TEST_TIME_DOCTEST` environment variables.
139
140            Expected format of environment variable is `VARIABLE=WARN_TIME,CRITICAL_TIME`.
141
142            `CRITICAL_TIME` here means the limit that should not be exceeded by test.
143            ",
144        )
145        .optflag("", "shuffle", "Run tests in random order")
146        .optopt(
147            "",
148            "shuffle-seed",
149            "Run tests in random order; seed the random number generator with SEED",
150            "SEED",
151        );
152    opts
153}
154
155fn usage(binary: &str, options: &getopts::Options) {
156    let message = format!("Usage: {binary} [OPTIONS] [FILTERS...]");
157    println!(
158        r#"{usage}
159
160The FILTER string is tested against the name of all tests, and only those
161tests whose names contain the filter are run. Multiple filter strings may
162be passed, which will run all tests matching any of the filters.
163
164By default, all tests are run in parallel. This can be altered with the
165--test-threads flag when running tests (set it to 1).
166
167By default, the tests are run in alphabetical order. Use --shuffle to run
168the tests in random order. Pass the generated "shuffle seed" to
169--shuffle-seed to run the tests in the same order again. Note that
170--shuffle and --shuffle-seed do not affect whether the tests are run in
171parallel.
172
173All tests have their standard output and standard error captured by default.
174This can be overridden with the --no-capture flag to a value other than "0".
175Logging is not captured by default.
176
177Test Attributes:
178
179    `#[test]`        - Indicates a function is a test to be run. This function
180                       takes no arguments.
181    `#[bench]`       - Indicates a function is a benchmark to be run. This
182                       function takes one argument (test::Bencher).
183    `#[should_panic]` - This function (also labeled with `#[test]`) will only pass if
184                        the code causes a panic (an assertion failure or panic!)
185                        A message may be provided, which the failure string must
186                        contain: #[should_panic(expected = "foo")].
187    `#[ignore]`       - When applied to a function which is already attributed as a
188                        test, then the test runner will ignore these tests during
189                        normal test runs. Running with --ignored or --include-ignored will run
190                        these tests."#,
191        usage = options.usage(&message)
192    );
193}
194
195/// Parses command line arguments into test options.
196/// Returns `None` if help was requested (since we only show help message and don't run tests),
197/// returns `Some(Err(..))` if provided arguments are incorrect,
198/// otherwise creates a `TestOpts` object and returns it.
199pub fn parse_opts(args: &[String]) -> Option<OptRes> {
200    // Parse matches.
201    let mut opts = optgroups();
202    // Flags hidden from `usage`
203    opts.optflag("", "nocapture", "Deprecated, use `--no-capture`");
204
205    let binary = args.first().map(|c| &**c).unwrap_or("...");
206    let args = args.get(1..).unwrap_or(args);
207    let matches = match opts.parse(args) {
208        Ok(m) => m,
209        Err(f) => return Some(Err(f.to_string())),
210    };
211
212    // Check if help was requested.
213    if matches.opt_present("h") {
214        // Show help and do nothing more.
215        usage(binary, &optgroups());
216        return None;
217    }
218
219    // Actually parse the opts.
220    let opts_result = parse_opts_impl(matches);
221
222    Some(opts_result)
223}
224
225// Gets the option value and checks if unstable features are enabled.
226macro_rules! unstable_optflag {
227    ($matches:ident, $allow_unstable:ident, $option_name:literal) => {{
228        let opt = $matches.opt_present($option_name);
229        if !$allow_unstable && opt {
230            return Err(format!(
231                "The \"{}\" flag is only accepted on the nightly compiler with -Z unstable-options",
232                $option_name
233            ));
234        }
235
236        opt
237    }};
238}
239
240// Gets the option value and checks if unstable features are enabled.
241macro_rules! unstable_optopt {
242    ($matches:ident, $allow_unstable:ident, $option_name:literal) => {{
243        let opt = $matches.opt_str($option_name);
244        if !$allow_unstable && opt.is_some() {
245            return Err(format!(
246                "The \"{}\" option is only accepted on the nightly compiler with -Z unstable-options",
247                $option_name
248            ));
249        }
250
251        opt
252    }};
253}
254
255// Implementation of `parse_opts` that doesn't care about help message
256// and returns a `Result`.
257fn parse_opts_impl(matches: getopts::Matches) -> OptRes {
258    let allow_unstable = get_allow_unstable(&matches)?;
259
260    // Unstable flags
261    let force_run_in_process = unstable_optflag!(matches, allow_unstable, "force-run-in-process");
262    let exclude_should_panic = unstable_optflag!(matches, allow_unstable, "exclude-should-panic");
263    let time_options = get_time_options(&matches, allow_unstable)?;
264    let shuffle = get_shuffle(&matches, allow_unstable)?;
265    let shuffle_seed = get_shuffle_seed(&matches, allow_unstable)?;
266
267    let include_ignored = matches.opt_present("include-ignored");
268    let quiet = matches.opt_present("quiet");
269    let exact = matches.opt_present("exact");
270    let list = matches.opt_present("list");
271    let skip = matches.opt_strs("skip");
272
273    let bench_benchmarks = matches.opt_present("bench");
274    let run_tests = !bench_benchmarks || matches.opt_present("test");
275
276    let logfile = get_log_file(&matches)?;
277    let run_ignored = get_run_ignored(&matches, include_ignored)?;
278    let filters = matches.free.clone();
279    let nocapture = get_nocapture(&matches)?;
280    let test_threads = get_test_threads(&matches)?;
281    let color = get_color_config(&matches)?;
282    let format = get_format(&matches, quiet, allow_unstable)?;
283
284    let options = Options::new().display_output(matches.opt_present("show-output"));
285
286    if logfile.is_some() {
287        let _ = write!(io::stderr(), "warning: `--logfile` is deprecated");
288    }
289
290    let test_opts = TestOpts {
291        list,
292        filters,
293        filter_exact: exact,
294        force_run_in_process,
295        exclude_should_panic,
296        run_ignored,
297        run_tests,
298        bench_benchmarks,
299        logfile,
300        nocapture,
301        color,
302        format,
303        shuffle,
304        shuffle_seed,
305        test_threads,
306        skip,
307        time_options,
308        options,
309        fail_fast: false,
310    };
311
312    Ok(test_opts)
313}
314
315// FIXME: Copied from librustc_ast until linkage errors are resolved. Issue #47566
316fn is_nightly() -> bool {
317    // Whether this is a feature-staged build, i.e., on the beta or stable channel
318    let disable_unstable_features =
319        option_env!("CFG_DISABLE_UNSTABLE_FEATURES").map(|s| s != "0").unwrap_or(false);
320    // Whether we should enable unstable features for bootstrapping
321    let bootstrap = env::var("RUSTC_BOOTSTRAP").is_ok();
322
323    bootstrap || !disable_unstable_features
324}
325
326// Gets the CLI options associated with `report-time` feature.
327fn get_time_options(
328    matches: &getopts::Matches,
329    allow_unstable: bool,
330) -> OptPartRes<Option<TestTimeOptions>> {
331    let report_time = unstable_optflag!(matches, allow_unstable, "report-time");
332    let ensure_test_time = unstable_optflag!(matches, allow_unstable, "ensure-time");
333
334    // If `ensure-test-time` option is provided, time output is enforced,
335    // so user won't be confused if any of tests will silently fail.
336    let options = if report_time || ensure_test_time {
337        Some(TestTimeOptions::new_from_env(ensure_test_time))
338    } else {
339        None
340    };
341
342    Ok(options)
343}
344
345fn get_shuffle(matches: &getopts::Matches, allow_unstable: bool) -> OptPartRes<bool> {
346    let mut shuffle = unstable_optflag!(matches, allow_unstable, "shuffle");
347    if !shuffle && allow_unstable {
348        shuffle = match env::var("RUST_TEST_SHUFFLE") {
349            Ok(val) => &val != "0",
350            Err(_) => false,
351        };
352    }
353
354    Ok(shuffle)
355}
356
357fn get_shuffle_seed(matches: &getopts::Matches, allow_unstable: bool) -> OptPartRes<Option<u64>> {
358    let mut shuffle_seed = match unstable_optopt!(matches, allow_unstable, "shuffle-seed") {
359        Some(n_str) => match n_str.parse::<u64>() {
360            Ok(n) => Some(n),
361            Err(e) => {
362                return Err(format!(
363                    "argument for --shuffle-seed must be a number \
364                     (error: {e})"
365                ));
366            }
367        },
368        None => None,
369    };
370
371    if shuffle_seed.is_none() && allow_unstable {
372        shuffle_seed = match env::var("RUST_TEST_SHUFFLE_SEED") {
373            Ok(val) => match val.parse::<u64>() {
374                Ok(n) => Some(n),
375                Err(_) => panic!("RUST_TEST_SHUFFLE_SEED is `{val}`, should be a number."),
376            },
377            Err(_) => None,
378        };
379    }
380
381    Ok(shuffle_seed)
382}
383
384fn get_test_threads(matches: &getopts::Matches) -> OptPartRes<Option<usize>> {
385    let test_threads = match matches.opt_str("test-threads") {
386        Some(n_str) => match n_str.parse::<usize>() {
387            Ok(0) => return Err("argument for --test-threads must not be 0".to_string()),
388            Ok(n) => Some(n),
389            Err(e) => {
390                return Err(format!(
391                    "argument for --test-threads must be a number > 0 \
392                     (error: {e})"
393                ));
394            }
395        },
396        None => None,
397    };
398
399    Ok(test_threads)
400}
401
402fn get_format(
403    matches: &getopts::Matches,
404    quiet: bool,
405    allow_unstable: bool,
406) -> OptPartRes<OutputFormat> {
407    let format = match matches.opt_str("format").as_deref() {
408        None if quiet => OutputFormat::Terse,
409        Some("pretty") | None => OutputFormat::Pretty,
410        Some("terse") => OutputFormat::Terse,
411        Some("json") => {
412            if !allow_unstable {
413                return Err("The \"json\" format is only accepted on the nightly compiler with -Z unstable-options".into());
414            }
415            OutputFormat::Json
416        }
417        Some("junit") => {
418            if !allow_unstable {
419                return Err("The \"junit\" format is only accepted on the nightly compiler with -Z unstable-options".into());
420            }
421            OutputFormat::Junit
422        }
423        Some(v) => {
424            return Err(format!(
425                "argument for --format must be pretty, terse, json or junit (was \
426                 {v})"
427            ));
428        }
429    };
430
431    Ok(format)
432}
433
434fn get_color_config(matches: &getopts::Matches) -> OptPartRes<ColorConfig> {
435    let color = match matches.opt_str("color").as_deref() {
436        Some("auto") | None => ColorConfig::AutoColor,
437        Some("always") => ColorConfig::AlwaysColor,
438        Some("never") => ColorConfig::NeverColor,
439
440        Some(v) => {
441            return Err(format!(
442                "argument for --color must be auto, always, or never (was \
443                 {v})"
444            ));
445        }
446    };
447
448    Ok(color)
449}
450
451fn get_nocapture(matches: &getopts::Matches) -> OptPartRes<bool> {
452    let mut nocapture = matches.opt_present("nocapture") || matches.opt_present("no-capture");
453    if !nocapture {
454        nocapture = match env::var("RUST_TEST_NOCAPTURE") {
455            Ok(val) => &val != "0",
456            Err(_) => false,
457        };
458    }
459
460    Ok(nocapture)
461}
462
463fn get_run_ignored(matches: &getopts::Matches, include_ignored: bool) -> OptPartRes<RunIgnored> {
464    let run_ignored = match (include_ignored, matches.opt_present("ignored")) {
465        (true, true) => {
466            return Err("the options --include-ignored and --ignored are mutually exclusive".into());
467        }
468        (true, false) => RunIgnored::Yes,
469        (false, true) => RunIgnored::Only,
470        (false, false) => RunIgnored::No,
471    };
472
473    Ok(run_ignored)
474}
475
476fn get_allow_unstable(matches: &getopts::Matches) -> OptPartRes<bool> {
477    let mut allow_unstable = false;
478
479    if let Some(opt) = matches.opt_str("Z") {
480        if !is_nightly() {
481            return Err("the option `Z` is only accepted on the nightly compiler".into());
482        }
483
484        match &*opt {
485            "unstable-options" => {
486                allow_unstable = true;
487            }
488            _ => {
489                return Err("Unrecognized option to `Z`".into());
490            }
491        }
492    };
493
494    Ok(allow_unstable)
495}
496
497fn get_log_file(matches: &getopts::Matches) -> OptPartRes<Option<PathBuf>> {
498    let logfile = matches.opt_str("logfile").map(|s| PathBuf::from(&s));
499
500    Ok(logfile)
501}