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 test_threads: Option<usize>, |
25 | pub skip: Vec<String>, |
26 | pub time_options: Option<TestTimeOptions>, |
27 | pub options: Options, |
28 | } |
29 | |
30 | impl 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. |
41 | pub type OptRes = Result<TestOpts, String>; |
42 | /// Result of parsing the option part. |
43 | type OptPartRes<T> = Result<T, String>; |
44 | |
45 | fn 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 | |
150 | fn usage(binary: &str, options: &getopts::Options) { |
151 | let message = format!("Usage: {} [OPTIONS] [FILTERS...]" , binary); |
152 | println!( |
153 | r#" {usage} |
154 | |
155 | The FILTERS string is tested against the name of all tests, and only those |
156 | tests whose names contain the filter are run. Multiple filter strings may |
157 | be passed, which will run all tests matching any of the filters. |
158 | |
159 | By 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 |
161 | tests (set it to 1). |
162 | |
163 | All tests have their standard output and standard error captured by default. |
164 | This can be overridden with the --nocapture flag or setting RUST_TEST_NOCAPTURE |
165 | environment variable to a value other than "0". Logging is not captured by default. |
166 | |
167 | Test 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. |
189 | pub fn parse_opts(args: &[String]) -> Option<OptRes> { |
190 | // Parse matches. |
191 | let opts: Options = optgroups(); |
192 | let args: &[String] = args.get(1..).unwrap_or(default: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: Result = parse_opts_impl(matches); |
207 | |
208 | Some(opts_result) |
209 | } |
210 | |
211 | // Gets the option value and checks if unstable features are enabled. |
212 | macro_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`. |
228 | fn 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 |
278 | fn is_nightly() -> bool { |
279 | // Whether this is a feature-staged build, i.e., on the beta or stable channel |
280 | let disable_unstable_features: bool = option_env!("CFG_DISABLE_UNSTABLE_FEATURES" ).is_some(); |
281 | // Whether we should enable unstable features for bootstrapping |
282 | let bootstrap: bool = env::var(key:"RUSTC_BOOTSTRAP" ).is_ok(); |
283 | |
284 | bootstrap || !disable_unstable_features |
285 | } |
286 | |
287 | // Gets the CLI options associated with `report-time` feature. |
288 | fn get_time_options( |
289 | matches: &getopts::Matches, |
290 | allow_unstable: bool, |
291 | ) -> OptPartRes<Option<TestTimeOptions>> { |
292 | let report_time: bool = unstable_optflag!(matches, allow_unstable, "report-time" ); |
293 | let colored_opt_str = matches.opt_str(nm:"report-time" ); |
294 | let mut report_time_colored: bool = report_time && colored_opt_str == Some("colored" .into()); |
295 | let ensure_test_time: bool = 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: Option = 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(error_on_excess:ensure_test_time, report_time_colored)) |
304 | } else { |
305 | None |
306 | }; |
307 | |
308 | Ok(options) |
309 | } |
310 | |
311 | fn get_test_threads(matches: &getopts::Matches) -> OptPartRes<Option<usize>> { |
312 | let test_threads: Option = match matches.opt_str(nm:"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: usize) => 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 | |
330 | fn get_format( |
331 | matches: &getopts::Matches, |
332 | quiet: bool, |
333 | allow_unstable: bool, |
334 | ) -> OptPartRes<OutputFormat> { |
335 | let format: OutputFormat = match matches.opt_str(nm:"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: &str) => { |
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 | |
358 | fn get_color_config(matches: &getopts::Matches) -> OptPartRes<ColorConfig> { |
359 | let color: ColorConfig = match matches.opt_str(nm:"color" ).as_deref() { |
360 | Some("auto" ) | None => ColorConfig::AutoColor, |
361 | Some("always" ) => ColorConfig::AlwaysColor, |
362 | Some("never" ) => ColorConfig::NeverColor, |
363 | |
364 | Some(v: &str) => { |
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 | |
376 | fn get_nocapture(matches: &getopts::Matches) -> OptPartRes<bool> { |
377 | let mut nocapture: bool = matches.opt_present(nm:"nocapture" ); |
378 | if !nocapture { |
379 | nocapture = match env::var(key:"RUST_TEST_NOCAPTURE" ) { |
380 | Ok(val: String) => &val != "0" , |
381 | Err(_) => false, |
382 | }; |
383 | } |
384 | |
385 | Ok(nocapture) |
386 | } |
387 | |
388 | fn get_run_ignored(matches: &getopts::Matches, include_ignored: bool) -> OptPartRes<RunIgnored> { |
389 | let run_ignored: RunIgnored = match (include_ignored, matches.opt_present(nm:"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 | |
401 | fn get_allow_unstable(matches: &getopts::Matches) -> OptPartRes<bool> { |
402 | let mut allow_unstable: bool = false; |
403 | |
404 | if let Some(opt) = matches.opt_str(nm:"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 | |
422 | fn get_log_file(matches: &getopts::Matches) -> OptPartRes<Option<PathBuf>> { |
423 | let logfile: Option = matches.opt_str(nm:"logfile" ).map(|s| PathBuf::from(&s)); |
424 | |
425 | Ok(logfile) |
426 | } |
427 | |