test_r_core/
args.rs

1use crate::execution::TestSuiteExecution;
2use crate::output::TestRunnerOutput;
3use clap::{Parser, ValueEnum};
4use std::ffi::OsString;
5use std::num::NonZero;
6use std::str::FromStr;
7use std::sync::Arc;
8use std::time::Duration;
9
10/// Command line arguments.
11///
12/// This type represents everything the user can specify via CLI args. The main
13/// method is [`from_args`][Arguments::from_args] which reads the global
14/// `std::env::args()` and parses them into this type.
15#[derive(Parser, Debug, Clone, Default)]
16#[command(
17    help_template = "USAGE: [OPTIONS] [FILTERS...]\n\n{all-args}\n",
18    disable_version_flag = true
19)]
20pub struct Arguments {
21    /// Run ignored and not ignored tests
22    #[arg(long = "include-ignored")]
23    pub include_ignored: bool,
24
25    /// Run only ignored tests
26    #[arg(long = "ignored")]
27    pub ignored: bool,
28
29    /// Excludes tests marked as should_panic
30    #[arg(long = "exclude-should-panic")]
31    pub exclude_should_panic: bool,
32
33    /// Run tests and not benchmarks
34    #[arg(long = "test", conflicts_with = "bench")]
35    pub test: bool,
36
37    /// Run benchmarks instead of tests
38    #[arg(long = "bench")]
39    pub bench: bool,
40
41    /// List all tests and benchmarks
42    #[arg(long = "list")]
43    pub list: bool,
44
45    /// Write logs to the specified file
46    #[arg(long = "logfile", value_name = "PATH")]
47    pub logfile: Option<String>,
48
49    /// don't capture stdout/stderr of each task, allow printing directly
50    #[arg(long = "nocapture")]
51    pub nocapture: bool,
52
53    /// Number of threads used for running tests in parallel
54    #[arg(long = "test-threads")]
55    pub test_threads: Option<usize>,
56
57    /// Skip tests whose names contains FILTER (this flag can be used multiple times)
58    #[arg(long = "skip", value_name = "FILTER")]
59    pub skip: Vec<String>,
60
61    /// Display one character per test instead of one line.
62    /// Alias to `--format=terse`
63    #[arg(short = 'q', long = "quiet", conflicts_with = "format")]
64    pub quiet: bool,
65
66    /// Exactly match filters rather than by substring
67    #[arg(long = "exact")]
68    pub exact: bool,
69
70    /// Configure coloring of output
71    #[arg(long = "color", value_enum, value_name = "auto|always|never")]
72    pub color: Option<ColorSetting>,
73
74    /// Configure formatting of output
75    #[arg(long = "format", value_enum, value_name = "pretty|terse|json|junit")]
76    pub format: Option<FormatSetting>,
77
78    /// Show captured stdout of successful tests
79    #[arg(long = "show-output")]
80    pub show_output: bool,
81
82    /// Enable nightly-only flags
83    #[arg(short = 'Z')]
84    pub unstable_flags: Option<UnstableFlags>,
85
86    /// Show execution time of each test.
87    /// Threshold values for colorized output can be configured via `RUST_TEST_TIME_UNIT`,
88    /// `RUST_TEST_TIME_INTEGRATION` and `RUST_TEST_TIME_DOCTEST` environment variables.
89    /// Expected format of the environment variables is `VARIABLE=WARN_TIME,CRITICAL_TIME`.
90    /// Durations must be specified in milliseconds, e.g. `500,2000` means that the warn time is 0.5
91    /// seconds, and the critical time is 2 seconds.
92    /// Not available for `--format=terse`.
93    #[arg(long = "report-time")]
94    pub report_time: bool,
95
96    /// Treat excess of the test execution time limit as error.
97    /// Threshold values for this option can be configured via `RUST_TEST_TIME_UNIT`,
98    /// `RUST_TEST_TIME_INTEGRATION` and `RUST_TEST_TIME_DOCTEST` environment variables.
99    /// Expected format of the environment variables is `VARIABLE=WARN_TIME,CRITICAL_TIME`.
100    /// `CRITICAL_TIME` here means the limit that should not be exceeded by test.
101    #[arg(long = "ensure-time")]
102    pub ensure_time: bool,
103
104    /// Run tests in random order
105    #[arg(long = "shuffle", conflicts_with = "shuffle_seed")]
106    pub shuffle: bool,
107
108    /// Run tests in random order; seed the random number generator with SEED
109    #[arg(long = "shuffle-seed", value_name = "SEED", conflicts_with = "shuffle")]
110    pub shuffle_seed: Option<u64>,
111
112    /// Show detailed benchmark statistics for each benchmark
113    #[arg(long = "show-stats")]
114    pub show_stats: bool,
115
116    /// The FILTER string is tested against the name of all tests, and only those
117    /// tests whose names contain the filter are run. Multiple filter strings may
118    /// be passed, which will run all tests matching any of the filters.
119    #[arg(value_name = "FILTER")]
120    pub filter: Option<String>,
121
122    /// Run the test suite in worker IPC mode - listening on the given local socket waiting
123    /// for the test runner to connect and send test execution requests. The only stdout/stderr
124    /// output will be the one emitted by the actual test runs so the test runner can capture them.
125    #[arg(long = "ipc", hide = true)]
126    pub ipc: Option<String>,
127
128    /// If true, spawn worker processes in IPC mode and run the tests on those
129    #[arg(long = "spawn-workers", hide = true)]
130    pub spawn_workers: bool,
131}
132
133impl Arguments {
134    /// Parses the global CLI arguments given to the application.
135    ///
136    /// If the parsing fails (due to incorrect CLI args), an error is shown and
137    /// the application exits. If help is requested (`-h` or `--help`), a help
138    /// message is shown and the application exits, too.
139    pub fn from_args() -> Self {
140        let mut result: Self = Parser::parse();
141        if result.shuffle && result.shuffle_seed.is_none() {
142            // Setting a specific shuffle seed so all spawned workers use the same
143            result.shuffle_seed = Some(rand::random());
144            result.shuffle = false;
145        }
146        result
147    }
148
149    /// Renders the arguments as a list of strings that can be passed to a subprocess
150    pub fn to_args(&self) -> Vec<OsString> {
151        let mut result = Vec::new();
152
153        if self.include_ignored {
154            result.push(OsString::from("--include-ignored"));
155        }
156
157        if self.ignored {
158            result.push(OsString::from("--ignored"));
159        }
160
161        if self.exclude_should_panic {
162            result.push(OsString::from("--exclude-should-panic"));
163        }
164
165        if self.test {
166            result.push(OsString::from("--test"));
167        }
168
169        if self.bench {
170            result.push(OsString::from("--bench"));
171        }
172
173        if self.list {
174            result.push(OsString::from("--list"));
175        }
176
177        if let Some(logfile) = &self.logfile {
178            result.push(OsString::from("--logfile"));
179            result.push(OsString::from(logfile));
180        }
181
182        if self.nocapture {
183            result.push(OsString::from("--nocapture"));
184        }
185
186        if let Some(test_threads) = self.test_threads {
187            result.push(OsString::from("--test-threads"));
188            result.push(OsString::from(test_threads.to_string()));
189        }
190
191        for skip in &self.skip {
192            result.push(OsString::from("--skip"));
193            result.push(OsString::from(skip));
194        }
195
196        if self.quiet {
197            result.push(OsString::from("--quiet"));
198        }
199
200        if self.exact {
201            result.push(OsString::from("--exact"));
202        }
203
204        if let Some(color) = self.color {
205            result.push(OsString::from("--color"));
206            match color {
207                ColorSetting::Auto => result.push(OsString::from("auto")),
208                ColorSetting::Always => result.push(OsString::from("always")),
209                ColorSetting::Never => result.push(OsString::from("never")),
210            }
211        }
212
213        if let Some(format) = self.format {
214            result.push(OsString::from("--format"));
215            match format {
216                FormatSetting::Pretty => result.push(OsString::from("pretty")),
217                FormatSetting::Terse => result.push(OsString::from("terse")),
218                FormatSetting::Json => result.push(OsString::from("json")),
219                FormatSetting::Junit => result.push(OsString::from("junit")),
220            }
221        }
222
223        if self.show_output {
224            result.push(OsString::from("--show-output"));
225        }
226
227        if let Some(unstable_flags) = &self.unstable_flags {
228            result.push(OsString::from("-Z"));
229            match unstable_flags {
230                UnstableFlags::UnstableOptions => result.push(OsString::from("unstable-options")),
231            }
232        }
233
234        if self.report_time {
235            result.push(OsString::from("--report-time"));
236        }
237
238        if self.ensure_time {
239            result.push(OsString::from("--ensure-time"));
240        }
241
242        if self.shuffle {
243            result.push(OsString::from("--shuffle"));
244        }
245
246        if let Some(shuffle_seed) = &self.shuffle_seed {
247            result.push(OsString::from("--shuffle-seed"));
248            result.push(OsString::from(shuffle_seed.to_string()));
249        }
250
251        if self.show_stats {
252            result.push(OsString::from("--show-stats"));
253        }
254
255        if let Some(filter) = &self.filter {
256            result.push(OsString::from(filter));
257        }
258
259        if let Some(ipc) = &self.ipc {
260            result.push(OsString::from("--ipc"));
261            result.push(OsString::from(ipc));
262        }
263
264        if self.spawn_workers {
265            result.push(OsString::from("--spawn-workers"));
266        }
267
268        result
269    }
270
271    pub fn unit_test_threshold(&self) -> TimeThreshold {
272        TimeThreshold::from_env_var("RUST_TEST_TIME_UNIT").unwrap_or(TimeThreshold::new(
273            Duration::from_millis(50),
274            Duration::from_millis(100),
275        ))
276    }
277
278    pub fn integration_test_threshold(&self) -> TimeThreshold {
279        TimeThreshold::from_env_var("RUST_TEST_TIME_INTEGRATION").unwrap_or(TimeThreshold::new(
280            Duration::from_millis(500),
281            Duration::from_millis(1000),
282        ))
283    }
284
285    pub(crate) fn test_threads(&self) -> NonZero<usize> {
286        if self.ipc.is_some() {
287            // When running as an IPC-controlled worker, always use a single thread
288            NonZero::new(1).unwrap()
289        } else {
290            self.test_threads
291                .and_then(NonZero::new)
292                .or_else(|| std::thread::available_parallelism().ok())
293                .unwrap_or(NonZero::new(1).unwrap())
294        }
295    }
296
297    /// Make necessary adjustments to the configuration if needed based on the final execution plan
298    pub(crate) fn finalize_for_execution(
299        &mut self,
300        execution: &TestSuiteExecution,
301        output: Arc<dyn TestRunnerOutput>,
302    ) {
303        let requires_capturing = execution.requires_capturing(!self.nocapture);
304
305        if !requires_capturing || self.ipc.is_some() {
306            // If there is no need to capture the output, there are no restrictions to check and apply
307            // If this is an IPC worker, we don't need to do anything either, as the top level test runner already sets the proper arguments
308        } else {
309            // If capture is enabled, we need to spawn at least one worker process
310            self.spawn_workers = true;
311
312            if self.test_threads().get() > 1 {
313                // If tests are executed in parallel, and output needs to be captured, there cannot be any dependencies
314                // because it can only be done through spawned workers
315
316                if execution.has_dependencies() {
317                    if execution.remaining() > 1 {
318                        // If there is only one test, the warning does not make sense
319                        output.warning("Cannot run tests in parallel when tests have shared dependencies and output capturing is on. Using a single thread.");
320                    }
321                    self.test_threads = Some(1); // Falling back to single-threaded execution
322                }
323            }
324        }
325    }
326}
327
328impl<A: Into<OsString> + Clone> FromIterator<A> for Arguments {
329    fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
330        Parser::parse_from(iter)
331    }
332}
333
334/// Possible values for the `--color` option.
335#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
336pub enum ColorSetting {
337    /// Colorize if stdout is a tty and tests are run on serially (default)
338    #[default]
339    Auto,
340
341    /// Always colorize output
342    Always,
343
344    /// Never colorize output
345    Never,
346}
347
348/// Possible values for the `-Z` option
349#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
350pub enum UnstableFlags {
351    /// Allow use of experimental features
352    UnstableOptions,
353}
354
355/// Possible values for the `--format` option.
356#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
357pub enum FormatSetting {
358    /// Print verbose output
359    #[default]
360    Pretty,
361
362    /// Display one character per test
363    Terse,
364
365    /// Output a json document
366    Json,
367
368    /// Output a JUnit document
369    Junit,
370}
371
372/// Structure denoting time limits for test execution.
373///
374/// From https://github.com/rust-lang/rust/blob/master/library/test/src/time.rs
375#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
376pub struct TimeThreshold {
377    pub warn: Duration,
378    pub critical: Duration,
379}
380
381impl TimeThreshold {
382    /// Creates a new `TimeThreshold` instance with provided durations.
383    pub fn new(warn: Duration, critical: Duration) -> Self {
384        Self { warn, critical }
385    }
386
387    /// Attempts to create a `TimeThreshold` instance with values obtained
388    /// from the environment variable, and returns `None` if the variable
389    /// is not set.
390    /// Environment variable format is expected to match `\d+,\d+`.
391    ///
392    /// # Panics
393    ///
394    /// Panics if variable with provided name is set but contains inappropriate
395    /// value.
396    pub fn from_env_var(env_var_name: &str) -> Option<Self> {
397        let durations_str = std::env::var(env_var_name).ok()?;
398        let (warn_str, critical_str) = durations_str.split_once(',').unwrap_or_else(|| {
399            panic!(
400                "Duration variable {env_var_name} expected to have 2 numbers separated by comma, but got {durations_str}"
401            )
402        });
403
404        let parse_u64 = |v| {
405            u64::from_str(v).unwrap_or_else(|_| {
406                panic!(
407                    "Duration value in variable {env_var_name} is expected to be a number, but got {v}"
408                )
409            })
410        };
411
412        let warn = parse_u64(warn_str);
413        let critical = parse_u64(critical_str);
414        if warn > critical {
415            panic!("Test execution warn time should be less or equal to the critical time");
416        }
417
418        Some(Self::new(
419            Duration::from_millis(warn),
420            Duration::from_millis(critical),
421        ))
422    }
423
424    pub fn is_critical(&self, duration: &Duration) -> bool {
425        *duration >= self.critical
426    }
427
428    pub fn is_warn(&self, duration: &Duration) -> bool {
429        *duration >= self.warn
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn verify_cli() {
439        use clap::CommandFactory;
440        Arguments::command().debug_assert();
441    }
442}