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 = "ipc", hide = true)]
126 pub ipc: Option<String>,
127
128 #[arg(long = "spawn-workers", hide = true)]
130 pub spawn_workers: bool,
131}
132
133impl Arguments {
134 pub fn from_args() -> Self {
140 let mut result: Self = Parser::parse();
141 if result.shuffle && result.shuffle_seed.is_none() {
142 result.shuffle_seed = Some(rand::random());
144 result.shuffle = false;
145 }
146 result
147 }
148
149 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 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 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 } else {
309 self.spawn_workers = true;
311
312 if self.test_threads().get() > 1 {
313 if execution.has_dependencies() {
317 if execution.remaining() > 1 {
318 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); }
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#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
336pub enum ColorSetting {
337 #[default]
339 Auto,
340
341 Always,
343
344 Never,
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
350pub enum UnstableFlags {
351 UnstableOptions,
353}
354
355#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
357pub enum FormatSetting {
358 #[default]
360 Pretty,
361
362 Terse,
364
365 Json,
367
368 Junit,
370}
371
372#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
376pub struct TimeThreshold {
377 pub warn: Duration,
378 pub critical: Duration,
379}
380
381impl TimeThreshold {
382 pub fn new(warn: Duration, critical: Duration) -> Self {
384 Self { warn, critical }
385 }
386
387 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}