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: Option<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
138impl Arguments {
139 pub fn from_args() -> Self {
145 let mut result: Self = Parser::parse();
146 if result.shuffle && result.shuffle_seed.is_none() {
147 result.shuffle_seed = Some(rand::random());
149 result.shuffle = false;
150 }
151 result
152 }
153
154 pub fn to_args(&self) -> Vec<OsString> {
156 let mut result = Vec::new();
157
158 if self.include_ignored {
159 result.push(OsString::from("--include-ignored"));
160 }
161
162 if self.ignored {
163 result.push(OsString::from("--ignored"));
164 }
165
166 if self.exclude_should_panic {
167 result.push(OsString::from("--exclude-should-panic"));
168 }
169
170 if self.test {
171 result.push(OsString::from("--test"));
172 }
173
174 if self.bench {
175 result.push(OsString::from("--bench"));
176 }
177
178 if self.list {
179 result.push(OsString::from("--list"));
180 }
181
182 if let Some(logfile) = &self.logfile {
183 result.push(OsString::from("--logfile"));
184 result.push(OsString::from(logfile));
185 }
186
187 if self.nocapture {
188 result.push(OsString::from("--nocapture"));
189 }
190
191 if let Some(test_threads) = self.test_threads {
192 result.push(OsString::from("--test-threads"));
193 result.push(OsString::from(test_threads.to_string()));
194 }
195
196 for skip in &self.skip {
197 result.push(OsString::from("--skip"));
198 result.push(OsString::from(skip));
199 }
200
201 if self.quiet {
202 result.push(OsString::from("--quiet"));
203 }
204
205 if self.exact {
206 result.push(OsString::from("--exact"));
207 }
208
209 if let Some(color) = self.color {
210 result.push(OsString::from("--color"));
211 match color {
212 ColorSetting::Auto => result.push(OsString::from("auto")),
213 ColorSetting::Always => result.push(OsString::from("always")),
214 ColorSetting::Never => result.push(OsString::from("never")),
215 }
216 }
217
218 if let Some(format) = self.format {
219 result.push(OsString::from("--format"));
220 match format {
221 FormatSetting::Pretty => result.push(OsString::from("pretty")),
222 FormatSetting::Terse => result.push(OsString::from("terse")),
223 FormatSetting::Json => result.push(OsString::from("json")),
224 FormatSetting::Junit => result.push(OsString::from("junit")),
225 FormatSetting::Ctrf => result.push(OsString::from("ctrf")),
226 }
227 }
228
229 if self.show_output {
230 result.push(OsString::from("--show-output"));
231 }
232
233 if let Some(unstable_flags) = &self.unstable_flags {
234 result.push(OsString::from("-Z"));
235 match unstable_flags {
236 UnstableFlags::UnstableOptions => result.push(OsString::from("unstable-options")),
237 }
238 }
239
240 if self.report_time {
241 result.push(OsString::from("--report-time"));
242 }
243
244 if self.ensure_time {
245 result.push(OsString::from("--ensure-time"));
246 }
247
248 if self.shuffle {
249 result.push(OsString::from("--shuffle"));
250 }
251
252 if let Some(shuffle_seed) = &self.shuffle_seed {
253 result.push(OsString::from("--shuffle-seed"));
254 result.push(OsString::from(shuffle_seed.to_string()));
255 }
256
257 if self.show_stats {
258 result.push(OsString::from("--show-stats"));
259 }
260
261 if let Some(filter) = &self.filter {
262 result.push(OsString::from(filter));
263 }
264
265 if let Some(flaky_run) = &self.flaky_run {
266 result.push(OsString::from("--flaky-run"));
267 result.push(OsString::from(flaky_run.to_string()));
268 }
269
270 if let Some(ipc) = &self.ipc {
271 result.push(OsString::from("--ipc"));
272 result.push(OsString::from(ipc));
273 }
274
275 if self.spawn_workers {
276 result.push(OsString::from("--spawn-workers"));
277 }
278
279 result
280 }
281
282 pub fn unit_test_threshold(&self) -> TimeThreshold {
283 TimeThreshold::from_env_var("RUST_TEST_TIME_UNIT").unwrap_or(TimeThreshold::new(
284 Duration::from_millis(50),
285 Duration::from_millis(100),
286 ))
287 }
288
289 pub fn integration_test_threshold(&self) -> TimeThreshold {
290 TimeThreshold::from_env_var("RUST_TEST_TIME_INTEGRATION").unwrap_or(TimeThreshold::new(
291 Duration::from_millis(500),
292 Duration::from_millis(1000),
293 ))
294 }
295
296 pub(crate) fn test_threads(&self) -> NonZero<usize> {
297 if self.ipc.is_some() {
298 NonZero::new(1).unwrap()
300 } else {
301 self.test_threads
302 .and_then(NonZero::new)
303 .or_else(|| std::thread::available_parallelism().ok())
304 .unwrap_or(NonZero::new(1).unwrap())
305 }
306 }
307
308 pub(crate) fn finalize_for_execution(
310 &mut self,
311 execution: &TestSuiteExecution,
312 output: Arc<dyn TestRunnerOutput>,
313 ) {
314 let requires_capturing = execution.requires_capturing(!self.nocapture);
315
316 if !requires_capturing || self.ipc.is_some() {
317 } else {
320 self.spawn_workers = true;
322
323 if self.test_threads().get() > 1 {
324 if execution.has_dependencies() {
328 if execution.remaining() > 1 {
329 output.warning("Cannot run tests in parallel when tests have shared dependencies and output capturing is on. Using a single thread.");
331 }
332 self.test_threads = Some(1); }
334 }
335 }
336 }
337}
338
339impl<A: Into<OsString> + Clone> FromIterator<A> for Arguments {
340 fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
341 Parser::parse_from(iter)
342 }
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
347pub enum ColorSetting {
348 #[default]
350 Auto,
351
352 Always,
354
355 Never,
357}
358
359#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
361pub enum UnstableFlags {
362 UnstableOptions,
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
368pub enum FormatSetting {
369 #[default]
371 Pretty,
372
373 Terse,
375
376 Json,
378
379 Junit,
381
382 Ctrf,
384}
385
386#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
390pub struct TimeThreshold {
391 pub warn: Duration,
392 pub critical: Duration,
393}
394
395impl TimeThreshold {
396 pub fn new(warn: Duration, critical: Duration) -> Self {
398 Self { warn, critical }
399 }
400
401 pub fn from_env_var(env_var_name: &str) -> Option<Self> {
411 let durations_str = std::env::var(env_var_name).ok()?;
412 let (warn_str, critical_str) = durations_str.split_once(',').unwrap_or_else(|| {
413 panic!(
414 "Duration variable {env_var_name} expected to have 2 numbers separated by comma, but got {durations_str}"
415 )
416 });
417
418 let parse_u64 = |v| {
419 u64::from_str(v).unwrap_or_else(|_| {
420 panic!(
421 "Duration value in variable {env_var_name} is expected to be a number, but got {v}"
422 )
423 })
424 };
425
426 let warn = parse_u64(warn_str);
427 let critical = parse_u64(critical_str);
428 if warn > critical {
429 panic!("Test execution warn time should be less or equal to the critical time");
430 }
431
432 Some(Self::new(
433 Duration::from_millis(warn),
434 Duration::from_millis(critical),
435 ))
436 }
437
438 pub fn is_critical(&self, duration: &Duration) -> bool {
439 *duration >= self.critical
440 }
441
442 pub fn is_warn(&self, duration: &Duration) -> bool {
443 *duration >= self.warn
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450
451 #[test]
452 fn verify_cli() {
453 use clap::CommandFactory;
454 Arguments::command().debug_assert();
455 }
456}