tester/
cli.rs

1//! Module converting command-line arguments into test configuration.
2
3use std::env;
4use std::path::PathBuf;
5
6use super::helpers::isatty;
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 test_threads: Option<usize>,
25    pub skip: Vec<String>,
26    pub time_options: Option<TestTimeOptions>,
27    pub options: Options,
28}
29
30impl TestOpts {
31    pub fn use_color(&self) -> bool {
32        match self.color {
33            ColorConfig::AutoColor => !self.nocapture && isatty::stdout_isatty(),
34            ColorConfig::AlwaysColor => true,
35            ColorConfig::NeverColor => false,
36        }
37    }
38}
39
40/// Result of parsing the options.
41pub type OptRes = Result<TestOpts, String>;
42/// Result of parsing the option part.
43type OptPartRes<T> = Result<T, String>;
44
45fn optgroups() -> getopts::Options {
46    let mut opts = getopts::Options::new();
47    opts.optflag("", "include-ignored", "Run ignored and not ignored tests")
48        .optflag("", "ignored", "Run only ignored tests")
49        .optflag("", "force-run-in-process", "Forces tests to run in-process when panic=abort")
50        .optflag("", "exclude-should-panic", "Excludes tests marked as should_panic")
51        .optflag("", "test", "Run tests and not benchmarks")
52        .optflag("", "bench", "Run benchmarks instead of tests")
53        .optflag("", "list", "List all tests and benchmarks")
54        .optflag("h", "help", "Display this message (longer with --help)")
55        .optopt(
56            "",
57            "logfile",
58            "Write logs to the specified file instead \
59             of stdout",
60            "PATH",
61        )
62        .optflag(
63            "",
64            "nocapture",
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            "pretty|terse|json",
106        )
107        .optflag("", "show-output", "Show captured stdout of successful tests")
108        .optopt(
109            "Z",
110            "",
111            "Enable nightly-only flags:
112            unstable-options = Allow use of experimental features",
113            "unstable-options",
114        )
115        .optflagopt(
116            "",
117            "report-time",
118            "Show execution time of each test. Available values:
119            plain   = do not colorize the execution time (default);
120            colored = colorize output according to the `color` parameter value;
121
122            Threshold values for colorized output can be configured via
123            `RUST_TEST_TIME_UNIT`, `RUST_TEST_TIME_INTEGRATION` and
124            `RUST_TEST_TIME_DOCTEST` environment variables.
125
126            Expected format of environment variable is `VARIABLE=WARN_TIME,CRITICAL_TIME`.
127            Durations must be specified in milliseconds, e.g. `500,2000` means that the warn time
128            is 0.5 seconds, and the critical time is 2 seconds.
129
130            Not available for --format=terse",
131            "plain|colored",
132        )
133        .optflag(
134            "",
135            "ensure-time",
136            "Treat excess of the test execution time limit as error.
137
138            Threshold values for this option can be configured via
139            `RUST_TEST_TIME_UNIT`, `RUST_TEST_TIME_INTEGRATION` and
140            `RUST_TEST_TIME_DOCTEST` environment variables.
141
142            Expected format of environment variable is `VARIABLE=WARN_TIME,CRITICAL_TIME`.
143
144            `CRITICAL_TIME` here means the limit that should not be exceeded by test.
145            ",
146        );
147    opts
148}
149
150fn usage(binary: &str, options: &getopts::Options) {
151    let message = format!("Usage: {} [OPTIONS] [FILTERS...]", binary);
152    println!(
153        r#"{usage}
154
155The FILTERS string is tested against the name of all tests, and only those
156tests whose names contain the filter are run. Multiple filter strings may
157be passed, which will run all tests matching any of the filters.
158
159By default, all tests are run in parallel. This can be altered with the
160--test-threads flag or the RUST_TEST_THREADS environment variable when running
161tests (set it to 1).
162
163All tests have their standard output and standard error captured by default.
164This can be overridden with the --nocapture flag or setting RUST_TEST_NOCAPTURE
165environment variable to a value other than "0". Logging is not captured by default.
166
167Test Attributes:
168
169    `#[test]`        - Indicates a function is a test to be run. This function
170                       takes no arguments.
171    `#[bench]`       - Indicates a function is a benchmark to be run. This
172                       function takes one argument (test::Bencher).
173    `#[should_panic]` - This function (also labeled with `#[test]`) will only pass if
174                        the code causes a panic (an assertion failure or panic!)
175                        A message may be provided, which the failure string must
176                        contain: #[should_panic(expected = "foo")].
177    `#[ignore]`       - When applied to a function which is already attributed as a
178                        test, then the test runner will ignore these tests during
179                        normal test runs. Running with --ignored or --include-ignored will run
180                        these tests."#,
181        usage = options.usage(&message)
182    );
183}
184
185/// Parses command line arguments into test options.
186/// Returns `None` if help was requested (since we only show help message and don't run tests),
187/// returns `Some(Err(..))` if provided arguments are incorrect,
188/// otherwise creates a `TestOpts` object and returns it.
189pub fn parse_opts(args: &[String]) -> Option<OptRes> {
190    // Parse matches.
191    let opts = optgroups();
192    let args = args.get(1..).unwrap_or(args);
193    let matches = match opts.parse(args) {
194        Ok(m) => m,
195        Err(f) => return Some(Err(f.to_string())),
196    };
197
198    // Check if help was requested.
199    if matches.opt_present("h") {
200        // Show help and do nothing more.
201        usage(&args[0], &opts);
202        return None;
203    }
204
205    // Actually parse the opts.
206    let opts_result = parse_opts_impl(matches);
207
208    Some(opts_result)
209}
210
211// Gets the option value and checks if unstable features are enabled.
212macro_rules! unstable_optflag {
213    ($matches:ident, $allow_unstable:ident, $option_name:literal) => {{
214        let opt = $matches.opt_present($option_name);
215        if !$allow_unstable && opt {
216            return Err(format!(
217                "The \"{}\" flag is only accepted on the nightly compiler with -Z unstable-options",
218                $option_name
219            ));
220        }
221
222        opt
223    }};
224}
225
226// Implementation of `parse_opts` that doesn't care about help message
227// and returns a `Result`.
228fn parse_opts_impl(matches: getopts::Matches) -> OptRes {
229    let allow_unstable = get_allow_unstable(&matches)?;
230
231    // Unstable flags
232    let force_run_in_process = unstable_optflag!(matches, allow_unstable, "force-run-in-process");
233    let exclude_should_panic = unstable_optflag!(matches, allow_unstable, "exclude-should-panic");
234    let include_ignored = unstable_optflag!(matches, allow_unstable, "include-ignored");
235    let time_options = get_time_options(&matches, allow_unstable)?;
236
237    let quiet = matches.opt_present("quiet");
238    let exact = matches.opt_present("exact");
239    let list = matches.opt_present("list");
240    let skip = matches.opt_strs("skip");
241
242    let bench_benchmarks = matches.opt_present("bench");
243    let run_tests = !bench_benchmarks || matches.opt_present("test");
244
245    let logfile = get_log_file(&matches)?;
246    let run_ignored = get_run_ignored(&matches, include_ignored)?;
247    let filters = matches.free.clone();
248    let nocapture = get_nocapture(&matches)?;
249    let test_threads = get_test_threads(&matches)?;
250    let color = get_color_config(&matches)?;
251    let format = get_format(&matches, quiet, allow_unstable)?;
252
253    let options = Options::new().display_output(matches.opt_present("show-output"));
254
255    let test_opts = TestOpts {
256        list,
257        filters,
258        filter_exact: exact,
259        force_run_in_process,
260        exclude_should_panic,
261        run_ignored,
262        run_tests,
263        bench_benchmarks,
264        logfile,
265        nocapture,
266        color,
267        format,
268        test_threads,
269        skip,
270        time_options,
271        options,
272    };
273
274    Ok(test_opts)
275}
276
277// FIXME: Copied from librustc_ast until linkage errors are resolved. Issue #47566
278fn is_nightly() -> bool {
279    // Whether this is a feature-staged build, i.e., on the beta or stable channel
280    let disable_unstable_features = option_env!("CFG_DISABLE_UNSTABLE_FEATURES").is_some();
281    // Whether we should enable unstable features for bootstrapping
282    let bootstrap = env::var("RUSTC_BOOTSTRAP").is_ok();
283
284    bootstrap || !disable_unstable_features
285}
286
287// Gets the CLI options associated with `report-time` feature.
288fn get_time_options(
289    matches: &getopts::Matches,
290    allow_unstable: bool,
291) -> OptPartRes<Option<TestTimeOptions>> {
292    let report_time = unstable_optflag!(matches, allow_unstable, "report-time");
293    let colored_opt_str = matches.opt_str("report-time");
294    let mut report_time_colored = report_time && colored_opt_str == Some("colored".into());
295    let ensure_test_time = unstable_optflag!(matches, allow_unstable, "ensure-time");
296
297    // If `ensure-test-time` option is provided, time output is enforced,
298    // so user won't be confused if any of tests will silently fail.
299    let options = if report_time || ensure_test_time {
300        if ensure_test_time && !report_time {
301            report_time_colored = true;
302        }
303        Some(TestTimeOptions::new_from_env(ensure_test_time, report_time_colored))
304    } else {
305        None
306    };
307
308    Ok(options)
309}
310
311fn get_test_threads(matches: &getopts::Matches) -> OptPartRes<Option<usize>> {
312    let test_threads = match matches.opt_str("test-threads") {
313        Some(n_str) => match n_str.parse::<usize>() {
314            Ok(0) => return Err("argument for --test-threads must not be 0".to_string()),
315            Ok(n) => Some(n),
316            Err(e) => {
317                return Err(format!(
318                    "argument for --test-threads must be a number > 0 \
319                     (error: {})",
320                    e
321                ));
322            }
323        },
324        None => None,
325    };
326
327    Ok(test_threads)
328}
329
330fn get_format(
331    matches: &getopts::Matches,
332    quiet: bool,
333    allow_unstable: bool,
334) -> OptPartRes<OutputFormat> {
335    let format = match matches.opt_str("format").as_deref() {
336        None if quiet => OutputFormat::Terse,
337        Some("pretty") | None => OutputFormat::Pretty,
338        Some("terse") => OutputFormat::Terse,
339        Some("json") => {
340            if !allow_unstable {
341                return Err("The \"json\" format is only accepted on the nightly compiler".into());
342            }
343            OutputFormat::Json
344        }
345
346        Some(v) => {
347            return Err(format!(
348                "argument for --format must be pretty, terse, or json (was \
349                 {})",
350                v
351            ));
352        }
353    };
354
355    Ok(format)
356}
357
358fn get_color_config(matches: &getopts::Matches) -> OptPartRes<ColorConfig> {
359    let color = match matches.opt_str("color").as_deref() {
360        Some("auto") | None => ColorConfig::AutoColor,
361        Some("always") => ColorConfig::AlwaysColor,
362        Some("never") => ColorConfig::NeverColor,
363
364        Some(v) => {
365            return Err(format!(
366                "argument for --color must be auto, always, or never (was \
367                 {})",
368                v
369            ));
370        }
371    };
372
373    Ok(color)
374}
375
376fn get_nocapture(matches: &getopts::Matches) -> OptPartRes<bool> {
377    let mut nocapture = matches.opt_present("nocapture");
378    if !nocapture {
379        nocapture = match env::var("RUST_TEST_NOCAPTURE") {
380            Ok(val) => &val != "0",
381            Err(_) => false,
382        };
383    }
384
385    Ok(nocapture)
386}
387
388fn get_run_ignored(matches: &getopts::Matches, include_ignored: bool) -> OptPartRes<RunIgnored> {
389    let run_ignored = match (include_ignored, matches.opt_present("ignored")) {
390        (true, true) => {
391            return Err("the options --include-ignored and --ignored are mutually exclusive".into());
392        }
393        (true, false) => RunIgnored::Yes,
394        (false, true) => RunIgnored::Only,
395        (false, false) => RunIgnored::No,
396    };
397
398    Ok(run_ignored)
399}
400
401fn get_allow_unstable(matches: &getopts::Matches) -> OptPartRes<bool> {
402    let mut allow_unstable = false;
403
404    if let Some(opt) = matches.opt_str("Z") {
405        if !is_nightly() {
406            return Err("the option `Z` is only accepted on the nightly compiler".into());
407        }
408
409        match &*opt {
410            "unstable-options" => {
411                allow_unstable = true;
412            }
413            _ => {
414                return Err("Unrecognized option to `Z`".into());
415            }
416        }
417    };
418
419    Ok(allow_unstable)
420}
421
422fn get_log_file(matches: &getopts::Matches) -> OptPartRes<Option<PathBuf>> {
423    let logfile = matches.opt_str("logfile").map(|s| PathBuf::from(&s));
424
425    Ok(logfile)
426}