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