Skip to main content

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: Vec<String>,
121
122    /// Marks the whole run (all the selected tests) as expected to be flaky, to
123    /// be retried on top level a given number of times if needed.
124    #[arg(long = "flaky-run", value_name = "COUNT")]
125    pub flaky_run: Option<usize>,
126
127    /// Run the test suite in worker IPC mode - listening on the given local socket waiting
128    /// for the test runner to connect and send test execution requests. The only stdout/stderr
129    /// output will be the one emitted by the actual test runs so the test runner can capture them.
130    #[arg(long = "ipc", hide = true)]
131    pub ipc: Option<String>,
132
133    /// If true, spawn worker processes in IPC mode and run the tests on those
134    #[arg(long = "spawn-workers", hide = true)]
135    pub spawn_workers: bool,
136
137    /// Zero-based worker index assigned by the parent runner when it spawns
138    /// a child worker process. Available to `PerWorker` constructors via
139    /// [`crate::worker_index`]. Set only on spawned worker subprocesses;
140    /// the top-level parent leaves it unset and observes index `0`.
141    #[arg(long = "worker-index", hide = true)]
142    pub worker_index: Option<usize>,
143}
144
145impl Arguments {
146    /// Parses the global CLI arguments given to the application.
147    ///
148    /// If the parsing fails (due to incorrect CLI args), an error is shown and
149    /// the application exits. If help is requested (`-h` or `--help`), a help
150    /// message is shown and the application exits, too.
151    pub fn from_args() -> Self {
152        let mut result: Self = Parser::parse();
153        if result.shuffle && result.shuffle_seed.is_none() {
154            // Setting a specific shuffle seed so all spawned workers use the same
155            result.shuffle_seed = Some(rand::random());
156            result.shuffle = false;
157        }
158        result
159    }
160
161    /// Renders the arguments as a list of strings that can be passed to a subprocess
162    pub fn to_args(&self) -> Vec<OsString> {
163        let mut result = Vec::new();
164
165        if self.include_ignored {
166            result.push(OsString::from("--include-ignored"));
167        }
168
169        if self.ignored {
170            result.push(OsString::from("--ignored"));
171        }
172
173        if self.exclude_should_panic {
174            result.push(OsString::from("--exclude-should-panic"));
175        }
176
177        if self.test {
178            result.push(OsString::from("--test"));
179        }
180
181        if self.bench {
182            result.push(OsString::from("--bench"));
183        }
184
185        if self.list {
186            result.push(OsString::from("--list"));
187        }
188
189        if let Some(logfile) = &self.logfile {
190            result.push(OsString::from("--logfile"));
191            result.push(OsString::from(logfile));
192        }
193
194        if self.nocapture {
195            result.push(OsString::from("--nocapture"));
196        }
197
198        if let Some(test_threads) = self.test_threads {
199            result.push(OsString::from("--test-threads"));
200            result.push(OsString::from(test_threads.to_string()));
201        }
202
203        for skip in &self.skip {
204            result.push(OsString::from("--skip"));
205            result.push(OsString::from(skip));
206        }
207
208        if self.quiet {
209            result.push(OsString::from("--quiet"));
210        }
211
212        if self.exact {
213            result.push(OsString::from("--exact"));
214        }
215
216        if let Some(color) = self.color {
217            result.push(OsString::from("--color"));
218            match color {
219                ColorSetting::Auto => result.push(OsString::from("auto")),
220                ColorSetting::Always => result.push(OsString::from("always")),
221                ColorSetting::Never => result.push(OsString::from("never")),
222            }
223        }
224
225        if let Some(format) = self.format {
226            result.push(OsString::from("--format"));
227            match format {
228                FormatSetting::Pretty => result.push(OsString::from("pretty")),
229                FormatSetting::Terse => result.push(OsString::from("terse")),
230                FormatSetting::Json => result.push(OsString::from("json")),
231                FormatSetting::Junit => result.push(OsString::from("junit")),
232                FormatSetting::Ctrf => result.push(OsString::from("ctrf")),
233            }
234        }
235
236        if self.show_output {
237            result.push(OsString::from("--show-output"));
238        }
239
240        if let Some(unstable_flags) = &self.unstable_flags {
241            result.push(OsString::from("-Z"));
242            match unstable_flags {
243                UnstableFlags::UnstableOptions => result.push(OsString::from("unstable-options")),
244            }
245        }
246
247        if self.report_time {
248            result.push(OsString::from("--report-time"));
249        }
250
251        if self.ensure_time {
252            result.push(OsString::from("--ensure-time"));
253        }
254
255        if self.shuffle {
256            result.push(OsString::from("--shuffle"));
257        }
258
259        if let Some(shuffle_seed) = &self.shuffle_seed {
260            result.push(OsString::from("--shuffle-seed"));
261            result.push(OsString::from(shuffle_seed.to_string()));
262        }
263
264        if self.show_stats {
265            result.push(OsString::from("--show-stats"));
266        }
267
268        if let Some(flaky_run) = &self.flaky_run {
269            result.push(OsString::from("--flaky-run"));
270            result.push(OsString::from(flaky_run.to_string()));
271        }
272
273        if let Some(ipc) = &self.ipc {
274            result.push(OsString::from("--ipc"));
275            result.push(OsString::from(ipc));
276        }
277
278        if self.spawn_workers {
279            result.push(OsString::from("--spawn-workers"));
280        }
281
282        if let Some(worker_index) = self.worker_index {
283            result.push(OsString::from("--worker-index"));
284            result.push(OsString::from(worker_index.to_string()));
285        }
286
287        for filter in &self.filter {
288            result.push(OsString::from(filter));
289        }
290
291        result
292    }
293
294    pub fn unit_test_threshold(&self) -> TimeThreshold {
295        TimeThreshold::from_env_var("RUST_TEST_TIME_UNIT").unwrap_or(TimeThreshold::new(
296            Duration::from_millis(50),
297            Duration::from_millis(100),
298        ))
299    }
300
301    pub fn integration_test_threshold(&self) -> TimeThreshold {
302        TimeThreshold::from_env_var("RUST_TEST_TIME_INTEGRATION").unwrap_or(TimeThreshold::new(
303            Duration::from_millis(500),
304            Duration::from_millis(1000),
305        ))
306    }
307
308    pub(crate) fn test_threads(&self) -> NonZero<usize> {
309        if self.ipc.is_some() {
310            // When running as an IPC-controlled worker, always use a single
311            // thread (the worker processes tests one at a time on behalf of
312            // the parent).
313            NonZero::new(1).unwrap()
314        } else {
315            self.test_threads
316                .and_then(NonZero::new)
317                .or_else(|| std::thread::available_parallelism().ok())
318                .unwrap_or(NonZero::new(1).unwrap())
319        }
320    }
321
322    /// Returns `true` when this process is the top-level test-suite parent.
323    ///
324    /// The top-level parent is the only place that may materialise
325    /// `Cloneable` and `Hosted` test-dependency owners. IPC worker
326    /// subprocesses (where `--ipc <name>` is set) MUST NOT run those
327    /// constructors — they receive the parent's materialised values /
328    /// descriptors via IPC instead.
329    pub(crate) fn is_top_level_parent(&self) -> bool {
330        self.ipc.is_none()
331    }
332
333    /// Make necessary adjustments to the configuration if needed based on the final execution plan
334    pub(crate) fn finalize_for_execution(
335        &mut self,
336        execution: &TestSuiteExecution,
337        output: Arc<dyn TestRunnerOutput>,
338    ) {
339        let requires_capturing = execution.requires_capturing(!self.nocapture);
340
341        if !requires_capturing || self.ipc.is_some() {
342            // If there is no need to capture the output, there are no restrictions to check and apply
343            // 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
344        } else {
345            // If capture is enabled, we need to spawn at least one worker process
346            self.spawn_workers = true;
347
348            if self.test_threads().get() > 1 {
349                // Parallel + capture requires worker processes. Only `Shared`
350                // dependencies force a single-threaded fallback, because
351                // their materialised value cannot cross the parent/worker
352                // boundary. `PerWorker` deps are materialised independently
353                // in each worker, `Cloneable` deps ship their wire bytes via
354                // IPC, `Hosted` deps materialise their owner once in the
355                // parent and ship a descriptor to each worker — all three
356                // are safe to run in parallel.
357                if execution.has_shared_dependencies() {
358                    if execution.remaining() > 1 {
359                        // If there is only one test, the warning does not make sense
360                        output.warning("Cannot run tests in parallel when tests have shared dependencies and output capturing is on. Using a single thread.");
361                    }
362                    self.test_threads = Some(1); // Falling back to single-threaded execution
363                }
364            }
365        }
366    }
367}
368
369impl<A: Into<OsString> + Clone> FromIterator<A> for Arguments {
370    fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
371        Parser::parse_from(iter)
372    }
373}
374
375/// Possible values for the `--color` option.
376#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
377pub enum ColorSetting {
378    /// Colorize if stdout is a tty and tests are run on serially (default)
379    #[default]
380    Auto,
381
382    /// Always colorize output
383    Always,
384
385    /// Never colorize output
386    Never,
387}
388
389/// Possible values for the `-Z` option
390#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
391pub enum UnstableFlags {
392    /// Allow use of experimental features
393    UnstableOptions,
394}
395
396/// Possible values for the `--format` option.
397#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
398pub enum FormatSetting {
399    /// Print verbose output
400    #[default]
401    Pretty,
402
403    /// Display one character per test
404    Terse,
405
406    /// Output a json document
407    Json,
408
409    /// Output a JUnit document
410    Junit,
411
412    /// Output to Common Test Report Format
413    Ctrf,
414}
415
416/// Structure denoting time limits for test execution.
417///
418/// From https://github.com/rust-lang/rust/blob/master/library/test/src/time.rs
419#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
420pub struct TimeThreshold {
421    pub warn: Duration,
422    pub critical: Duration,
423}
424
425impl TimeThreshold {
426    /// Creates a new `TimeThreshold` instance with provided durations.
427    pub fn new(warn: Duration, critical: Duration) -> Self {
428        Self { warn, critical }
429    }
430
431    /// Attempts to create a `TimeThreshold` instance with values obtained
432    /// from the environment variable, and returns `None` if the variable
433    /// is not set.
434    /// Environment variable format is expected to match `\d+,\d+`.
435    ///
436    /// # Panics
437    ///
438    /// Panics if variable with provided name is set but contains inappropriate
439    /// value.
440    pub fn from_env_var(env_var_name: &str) -> Option<Self> {
441        let durations_str = std::env::var(env_var_name).ok()?;
442        let (warn_str, critical_str) = durations_str.split_once(',').unwrap_or_else(|| {
443            panic!(
444                "Duration variable {env_var_name} expected to have 2 numbers separated by comma, but got {durations_str}"
445            )
446        });
447
448        let parse_u64 = |v| {
449            u64::from_str(v).unwrap_or_else(|_| {
450                panic!(
451                    "Duration value in variable {env_var_name} is expected to be a number, but got {v}"
452                )
453            })
454        };
455
456        let warn = parse_u64(warn_str);
457        let critical = parse_u64(critical_str);
458        if warn > critical {
459            panic!("Test execution warn time should be less or equal to the critical time");
460        }
461
462        Some(Self::new(
463            Duration::from_millis(warn),
464            Duration::from_millis(critical),
465        ))
466    }
467
468    pub fn is_critical(&self, duration: &Duration) -> bool {
469        *duration >= self.critical
470    }
471
472    pub fn is_warn(&self, duration: &Duration) -> bool {
473        *duration >= self.warn
474    }
475}
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480
481    #[test]
482    fn verify_cli() {
483        use clap::CommandFactory;
484        Arguments::command().debug_assert();
485    }
486
487    /// Regression coverage for the parent-only Hosted-owner materialisation
488    /// gating in `sync.rs` / `tokio.rs`. If this assertion ever flips,
489    /// IPC worker subprocesses could start running Hosted owner
490    /// constructors again — duplicating singleton resources (TCP listeners,
491    /// containers, env-based test environments) across every worker.
492    #[test]
493    fn is_top_level_parent_when_ipc_unset() {
494        // Use a single placeholder binary name so clap definitely sees a
495        // well-formed argv with no `--ipc` flag.
496        let mut args: Arguments = Parser::parse_from(["test-bin"]);
497        // Sanity: a freshly parsed Arguments with no overrides has no IPC.
498        assert!(args.ipc.is_none());
499        assert!(
500            args.is_top_level_parent(),
501            "process without --ipc must be treated as top-level parent"
502        );
503        // Even when the parent decides to spawn workers, it is still the
504        // top-level parent itself.
505        args.spawn_workers = true;
506        assert!(args.is_top_level_parent());
507    }
508
509    #[test]
510    fn ipc_worker_is_not_top_level_parent() {
511        let mut args: Arguments = Parser::parse_from(["test-bin"]);
512        args.ipc = Some("test-ipc-socket".to_string());
513        assert!(
514            !args.is_top_level_parent(),
515            "IPC worker subprocesses must NOT be treated as top-level parent — \
516             otherwise they would duplicate Hosted owner construction"
517        );
518        // And `test_threads()` must already collapse to 1 for IPC workers,
519        // independent of the user's `--test-threads` value.
520        args.test_threads = Some(4);
521        assert_eq!(args.test_threads().get(), 1);
522    }
523
524    /// Phase 3.3: regression coverage for the `--worker-index` round trip.
525    /// If `to_args` ever drops the field or `Parser::parse_from` ever fails
526    /// to populate it, spawned workers would silently fall back to index 0
527    /// and lose the per-worker namespace partitioning that PerWorker deps
528    /// like `LastUniqueId` depend on.
529    #[test]
530    fn worker_index_round_trips_through_to_args_and_parse() {
531        let mut args: Arguments = Parser::parse_from(["test-bin"]);
532        args.worker_index = Some(3);
533        let argv = args.to_args();
534        let argv_strings: Vec<String> = argv
535            .iter()
536            .map(|s| s.to_string_lossy().into_owned())
537            .collect();
538        assert!(
539            argv_strings.iter().any(|s| s == "--worker-index"),
540            "to_args() must emit --worker-index when worker_index is Some; \
541             got {argv_strings:?}"
542        );
543
544        // Round-trip back through clap with the binary name prepended.
545        let mut roundtripped_argv: Vec<String> = vec!["test-bin".to_string()];
546        roundtripped_argv.extend(argv_strings);
547        let parsed: Arguments = Parser::parse_from(roundtripped_argv);
548        assert_eq!(parsed.worker_index, Some(3));
549    }
550
551    #[test]
552    fn worker_index_absent_round_trip_stays_none() {
553        let args: Arguments = Parser::parse_from(["test-bin"]);
554        assert!(args.worker_index.is_none());
555        let argv = args.to_args();
556        let argv_strings: Vec<String> = argv
557            .iter()
558            .map(|s| s.to_string_lossy().into_owned())
559            .collect();
560        assert!(
561            !argv_strings.iter().any(|s| s == "--worker-index"),
562            "to_args() must NOT emit --worker-index when worker_index is None; \
563             got {argv_strings:?}"
564        );
565    }
566}