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#[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 #[arg(long = "include-ignored")]
23 pub include_ignored: bool,
24
25 #[arg(long = "ignored")]
27 pub ignored: bool,
28
29 #[arg(long = "exclude-should-panic")]
31 pub exclude_should_panic: bool,
32
33 #[arg(long = "test", conflicts_with = "bench")]
35 pub test: bool,
36
37 #[arg(long = "bench")]
39 pub bench: bool,
40
41 #[arg(long = "list")]
43 pub list: bool,
44
45 #[arg(long = "logfile", value_name = "PATH")]
47 pub logfile: Option<String>,
48
49 #[arg(long = "nocapture")]
51 pub nocapture: bool,
52
53 #[arg(long = "test-threads")]
55 pub test_threads: Option<usize>,
56
57 #[arg(long = "skip", value_name = "FILTER")]
59 pub skip: Vec<String>,
60
61 #[arg(short = 'q', long = "quiet", conflicts_with = "format")]
64 pub quiet: bool,
65
66 #[arg(long = "exact")]
68 pub exact: bool,
69
70 #[arg(long = "color", value_enum, value_name = "auto|always|never")]
72 pub color: Option<ColorSetting>,
73
74 #[arg(long = "format", value_enum, value_name = "pretty|terse|json|junit")]
76 pub format: Option<FormatSetting>,
77
78 #[arg(long = "show-output")]
80 pub show_output: bool,
81
82 #[arg(short = 'Z')]
84 pub unstable_flags: Option<UnstableFlags>,
85
86 #[arg(long = "report-time")]
94 pub report_time: bool,
95
96 #[arg(long = "ensure-time")]
102 pub ensure_time: bool,
103
104 #[arg(long = "shuffle", conflicts_with = "shuffle_seed")]
106 pub shuffle: bool,
107
108 #[arg(long = "shuffle-seed", value_name = "SEED", conflicts_with = "shuffle")]
110 pub shuffle_seed: Option<u64>,
111
112 #[arg(long = "show-stats")]
114 pub show_stats: bool,
115
116 #[arg(value_name = "FILTER")]
120 pub filter: Vec<String>,
121
122 #[arg(long = "flaky-run", value_name = "COUNT")]
125 pub flaky_run: Option<usize>,
126
127 #[arg(long = "ipc", hide = true)]
131 pub ipc: Option<String>,
132
133 #[arg(long = "spawn-workers", hide = true)]
135 pub spawn_workers: bool,
136
137 #[arg(long = "worker-index", hide = true)]
142 pub worker_index: Option<usize>,
143}
144
145impl Arguments {
146 pub fn from_args() -> Self {
152 let mut result: Self = Parser::parse();
153 if result.shuffle && result.shuffle_seed.is_none() {
154 result.shuffle_seed = Some(rand::random());
156 result.shuffle = false;
157 }
158 result
159 }
160
161 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 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 pub(crate) fn is_top_level_parent(&self) -> bool {
330 self.ipc.is_none()
331 }
332
333 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 } else {
345 self.spawn_workers = true;
347
348 if self.test_threads().get() > 1 {
349 if execution.has_shared_dependencies() {
358 if execution.remaining() > 1 {
359 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); }
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#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
377pub enum ColorSetting {
378 #[default]
380 Auto,
381
382 Always,
384
385 Never,
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
391pub enum UnstableFlags {
392 UnstableOptions,
394}
395
396#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
398pub enum FormatSetting {
399 #[default]
401 Pretty,
402
403 Terse,
405
406 Json,
408
409 Junit,
411
412 Ctrf,
414}
415
416#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
420pub struct TimeThreshold {
421 pub warn: Duration,
422 pub critical: Duration,
423}
424
425impl TimeThreshold {
426 pub fn new(warn: Duration, critical: Duration) -> Self {
428 Self { warn, critical }
429 }
430
431 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 #[test]
493 fn is_top_level_parent_when_ipc_unset() {
494 let mut args: Arguments = Parser::parse_from(["test-bin"]);
497 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 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 args.test_threads = Some(4);
521 assert_eq!(args.test_threads().get(), 1);
522 }
523
524 #[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 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}