cargo_nextest/dispatch/
cli.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! CLI argument parsing structures and enums.
5
6use super::clap_error::EarlyArgs;
7use crate::{
8    ExpectedError, Result,
9    cargo_cli::{CargoCli, CargoOptions},
10    output::OutputContext,
11    reuse_build::{ArchiveFormatOpt, ReuseBuildOpts, make_path_mapper},
12};
13use camino::{Utf8Path, Utf8PathBuf};
14use clap::{ArgAction, Args, Subcommand, ValueEnum, builder::BoolishValueParser};
15use guppy::graph::PackageGraph;
16use nextest_filtering::ParseContext;
17use nextest_metadata::BuildPlatform;
18use nextest_runner::{
19    cargo_config::EnvironmentMap,
20    config::{
21        core::{
22            ConfigExperimental, EvaluatableProfile, NextestConfig, ToolConfigFile,
23            VersionOnlyConfig, get_num_cpus,
24        },
25        elements::{MaxFail, RetryPolicy, TestThreads},
26    },
27    list::{
28        BinaryList, OutputFormat, RustTestArtifact, SerializableFormat, TestExecuteContext,
29        TestList,
30    },
31    partition::PartitionerBuilder,
32    platform::BuildPlatforms,
33    reporter::{
34        FinalStatusLevel, MaxProgressRunning, ReporterBuilder, StatusLevel, TestOutputDisplay,
35    },
36    reuse_build::ReuseBuildInfo,
37    run_mode::NextestRunMode,
38    runner::{
39        DebuggerCommand, Interceptor, StressCondition, StressCount, TestRunnerBuilder,
40        TracerCommand,
41    },
42    test_filter::{FilterBound, RunIgnored, TestFilterBuilder, TestFilterPatterns},
43    test_output::CaptureStrategy,
44    user_config::elements::{PagerSetting, PaginateSetting, UiConfig, UiShowProgress},
45};
46use std::{collections::BTreeSet, io::Cursor, sync::Arc, time::Duration};
47use tracing::{debug, warn};
48
49// Options shared between cargo nextest and cargo ntr.
50#[derive(Debug, Args)]
51pub(super) struct CommonOpts {
52    /// Path to Cargo.toml
53    #[arg(
54        long,
55        global = true,
56        value_name = "PATH",
57        help_heading = "Manifest options"
58    )]
59    pub(super) manifest_path: Option<Utf8PathBuf>,
60
61    #[clap(flatten)]
62    pub(super) output: crate::output::OutputOpts,
63
64    #[clap(flatten)]
65    pub(super) config_opts: ConfigOpts,
66}
67
68#[derive(Debug, Args)]
69#[command(next_help_heading = "Config options")]
70pub(super) struct ConfigOpts {
71    /// Config file [default: workspace-root/.config/nextest.toml]
72    #[arg(long, global = true, value_name = "PATH")]
73    pub config_file: Option<Utf8PathBuf>,
74
75    /// Tool-specific config files
76    ///
77    /// Some tools on top of nextest may want to set up their own default configuration but
78    /// prioritize user configuration on top. Use this argument to insert configuration
79    /// that's lower than --config-file in priority but above the default config shipped with
80    /// nextest.
81    ///
82    /// Arguments are specified in the format "tool:abs_path", for example
83    /// "my-tool:/path/to/nextest.toml" (or "my-tool:C:\\path\\to\\nextest.toml" on Windows).
84    /// Paths must be absolute.
85    ///
86    /// This argument may be specified multiple times. Files that come later are lower priority
87    /// than those that come earlier.
88    #[arg(long = "tool-config-file", global = true, value_name = "TOOL:ABS_PATH")]
89    pub tool_config_files: Vec<ToolConfigFile>,
90
91    /// Override checks for the minimum version defined in nextest's config.
92    ///
93    /// Repository and tool-specific configuration files can specify minimum required and
94    /// recommended versions of nextest. This option overrides those checks.
95    #[arg(long, global = true)]
96    pub override_version_check: bool,
97
98    /// The nextest profile to use.
99    ///
100    /// Nextest's configuration supports multiple profiles, which can be used to set up different
101    /// configurations for different purposes. (For example, a configuration for local runs and one
102    /// for CI.) This option selects the profile to use.
103    #[arg(
104        long,
105        short = 'P',
106        env = "NEXTEST_PROFILE",
107        global = true,
108        help_heading = "Config options"
109    )]
110    pub(super) profile: Option<String>,
111}
112
113impl ConfigOpts {
114    /// Creates a nextest version-only config with the given options.
115    pub(super) fn make_version_only_config(
116        &self,
117        workspace_root: &Utf8Path,
118    ) -> Result<VersionOnlyConfig> {
119        VersionOnlyConfig::from_sources(
120            workspace_root,
121            self.config_file.as_deref(),
122            &self.tool_config_files,
123        )
124        .map_err(ExpectedError::config_parse_error)
125    }
126
127    /// Creates a nextest config with the given options.
128    pub(super) fn make_config(
129        &self,
130        workspace_root: &Utf8Path,
131        pcx: &ParseContext<'_>,
132        experimental: &BTreeSet<ConfigExperimental>,
133    ) -> Result<NextestConfig> {
134        NextestConfig::from_sources(
135            workspace_root,
136            pcx,
137            self.config_file.as_deref(),
138            &self.tool_config_files,
139            experimental,
140        )
141        .map_err(ExpectedError::config_parse_error)
142    }
143}
144
145#[derive(Debug, Subcommand)]
146pub(super) enum Command {
147    /// List tests in workspace
148    ///
149    /// This command builds test binaries and queries them for the tests they contain.
150    ///
151    /// Use --verbose to get more information about tests, including test binary paths and skipped
152    /// tests.
153    ///
154    /// Use --message-format json to get machine-readable output.
155    ///
156    /// For more information, see <https://nexte.st/docs/listing>.
157    List(Box<ListOpts>),
158    /// Build and run tests
159    ///
160    /// This command builds test binaries and queries them for the tests they contain,
161    /// then runs each test in parallel.
162    ///
163    /// For more information, see <https://nexte.st/docs/running>.
164    #[command(visible_alias = "r")]
165    Run(Box<RunOpts>),
166    /// Build and run benchmarks (experimental)
167    ///
168    /// This command builds benchmark binaries and queries them for the benchmarks they contain,
169    /// then runs each benchmark **serially**.
170    ///
171    /// This is an experimental feature. To enable it, set the environment variable
172    /// `NEXTEST_EXPERIMENTAL_BENCHMARKS=1`.
173    #[command(visible_alias = "b")]
174    Bench(Box<BenchOpts>),
175    /// Build and archive tests
176    ///
177    /// This command builds test binaries and archives them to a file. The archive can then be
178    /// transferred to another machine, and tests within it can be run with `cargo nextest run
179    /// --archive-file`.
180    ///
181    /// The archive is a tarball compressed with Zstandard (.tar.zst).
182    Archive(Box<ArchiveOpts>),
183    /// Show information about nextest's configuration in this workspace.
184    ///
185    /// This command shows configuration information about nextest, including overrides applied to
186    /// individual tests.
187    ///
188    /// In the future, this will show more information about configurations and overrides.
189    ShowConfig {
190        #[clap(subcommand)]
191        command: super::commands::ShowConfigCommand,
192    },
193    /// Manage the nextest installation
194    #[clap(name = "self")]
195    Self_ {
196        #[clap(subcommand)]
197        command: super::commands::SelfCommand,
198    },
199    /// Debug commands
200    ///
201    /// The commands in this section are for nextest's own developers and those integrating with it
202    /// to debug issues. They are not part of the public API and may change at any time.
203    #[clap(hide = true)]
204    Debug {
205        #[clap(subcommand)]
206        command: super::commands::DebugCommand,
207    },
208}
209
210#[derive(Debug, Args)]
211pub(super) struct ArchiveOpts {
212    #[clap(flatten)]
213    pub(super) cargo_options: CargoOptions,
214
215    /// File to write archive to
216    #[arg(
217        long,
218        name = "archive-file",
219        help_heading = "Archive options",
220        value_name = "PATH"
221    )]
222    pub(super) archive_file: Utf8PathBuf,
223
224    /// Archive format
225    ///
226    /// `auto` uses the file extension to determine the archive format. Currently supported is
227    /// `.tar.zst`.
228    #[arg(
229        long,
230        value_enum,
231        help_heading = "Archive options",
232        value_name = "FORMAT",
233        default_value_t
234    )]
235    pub(super) archive_format: ArchiveFormatOpt,
236
237    #[clap(flatten)]
238    pub(super) archive_build_filter: ArchiveBuildFilter,
239
240    /// Zstandard compression level (-7 to 22, higher is more compressed + slower)
241    #[arg(
242        long,
243        help_heading = "Archive options",
244        value_name = "LEVEL",
245        default_value_t = 0,
246        allow_negative_numbers = true
247    )]
248    pub(super) zstd_level: i32,
249    // ReuseBuildOpts, while it can theoretically work, is way too confusing so skip it.
250}
251
252#[derive(Debug, Args)]
253pub(super) struct ListOpts {
254    #[clap(flatten)]
255    pub(super) cargo_options: CargoOptions,
256
257    #[clap(flatten)]
258    pub(super) build_filter: TestBuildFilter,
259
260    /// Output format
261    #[arg(
262        short = 'T',
263        long,
264        value_enum,
265        default_value_t,
266        help_heading = "Output options",
267        value_name = "FMT"
268    )]
269    pub(super) message_format: MessageFormatOpts,
270
271    /// Type of listing
272    #[arg(
273        long,
274        value_enum,
275        default_value_t,
276        help_heading = "Output options",
277        value_name = "TYPE"
278    )]
279    pub(super) list_type: ListType,
280
281    #[clap(flatten)]
282    pub(super) pager_opts: PagerOpts,
283
284    #[clap(flatten)]
285    pub(super) reuse_build: ReuseBuildOpts,
286}
287
288/// Pager options for list output.
289#[derive(Debug, Default, Args)]
290#[command(next_help_heading = "Pager options")]
291pub(super) struct PagerOpts {
292    /// Disable paging for this invocation.
293    #[arg(long)]
294    no_pager: bool,
295}
296
297impl PagerOpts {
298    /// Returns the effective pager and paginate settings, given the resolved UI
299    /// config.
300    ///
301    /// If `--no-pager` is specified, returns `PaginateSetting::Never`.
302    /// Otherwise, falls back to the resolved config values.
303    pub(super) fn resolve(&self, resolved_ui: &UiConfig) -> (PagerSetting, PaginateSetting) {
304        if self.no_pager {
305            // --no-pager disables paging entirely.
306            return (resolved_ui.pager.clone(), PaginateSetting::Never);
307        }
308
309        // Fall back to resolved config.
310        (resolved_ui.pager.clone(), resolved_ui.paginate)
311    }
312}
313
314#[derive(Debug, Args)]
315pub(super) struct RunOpts {
316    #[clap(flatten)]
317    pub(super) cargo_options: CargoOptions,
318
319    #[clap(flatten)]
320    pub(super) build_filter: TestBuildFilter,
321
322    #[clap(flatten)]
323    pub(super) runner_opts: TestRunnerOpts,
324
325    /// Run tests serially and do not capture output
326    #[arg(
327        long,
328        name = "no-capture",
329        alias = "nocapture",
330        help_heading = "Runner options",
331        display_order = 100
332    )]
333    pub(super) no_capture: bool,
334
335    #[clap(flatten)]
336    pub(super) reporter_opts: ReporterOpts,
337
338    #[clap(flatten)]
339    pub(super) reuse_build: ReuseBuildOpts,
340}
341
342#[derive(Debug, Args)]
343pub(super) struct BenchOpts {
344    #[clap(flatten)]
345    pub(super) cargo_options: CargoOptions,
346
347    #[clap(flatten)]
348    pub(super) build_filter: TestBuildFilter,
349
350    #[clap(flatten)]
351    pub(super) runner_opts: BenchRunnerOpts,
352
353    /// Run benchmarks serially and do not capture output (always enabled).
354    ///
355    /// Benchmarks in nextest always run serially, so this flag is kept only for
356    /// compatibility and has no effect.
357    #[arg(
358        long,
359        name = "no-capture",
360        alias = "nocapture",
361        help_heading = "Runner options",
362        display_order = 100
363    )]
364    pub(super) no_capture: bool,
365
366    #[clap(flatten)]
367    pub(super) reporter_opts: BenchReporterOpts,
368    // Note: no reuse_build for benchmarks since archive extraction is not supported
369}
370
371/// Benchmark runner options.
372#[derive(Debug, Default, Args)]
373#[command(next_help_heading = "Runner options")]
374pub(super) struct BenchRunnerOpts {
375    /// Compile, but don't run benchmarks.
376    #[arg(long, name = "no-run")]
377    pub(super) no_run: bool,
378
379    /// Cancel benchmark run on the first failure.
380    #[arg(
381        long,
382        visible_alias = "ff",
383        name = "fail-fast",
384        // TODO: It would be nice to warn rather than error if fail-fast is used
385        // with no-run, so that this matches the other options like
386        // test-threads. But there seem to be issues with that: clap 4.5 doesn't
387        // appear to like `Option<bool>` very much. With `ArgAction::SetTrue` it
388        // always sets the value to false or true rather than leaving it unset.
389        conflicts_with = "no-run"
390    )]
391    fail_fast: bool,
392
393    /// Run all benchmarks regardless of failure.
394    #[arg(
395        long,
396        visible_alias = "nff",
397        name = "no-fail-fast",
398        conflicts_with = "no-run",
399        overrides_with = "fail-fast"
400    )]
401    no_fail_fast: bool,
402
403    /// Number of benchmarks that can fail before exiting run [possible
404    /// values: integer or "all"]
405    #[arg(
406        long,
407        name = "max-fail",
408        value_name = "N",
409        conflicts_with_all = &["no-run", "fail-fast", "no-fail-fast"],
410    )]
411    max_fail: Option<MaxFail>,
412
413    /// Behavior if there are no benchmarks to run [default: fail]
414    #[arg(long, value_enum, value_name = "ACTION", env = "NEXTEST_NO_TESTS")]
415    pub(super) no_tests: Option<NoTestsBehavior>,
416
417    /// Stress testing options.
418    #[clap(flatten)]
419    pub(super) stress: StressOptions,
420
421    #[clap(flatten)]
422    pub(super) interceptor: InterceptorOpt,
423}
424
425impl BenchRunnerOpts {
426    pub(super) fn to_builder(&self, cap_strat: CaptureStrategy) -> Option<TestRunnerBuilder> {
427        if self.no_tests.is_some() && self.no_run {
428            warn!("ignoring --no-tests because --no-run is specified");
429        }
430
431        // ---
432
433        if self.no_run {
434            return None;
435        }
436        let mut builder = TestRunnerBuilder::default();
437        builder.set_capture_strategy(cap_strat);
438        if let Some(max_fail) = self.max_fail {
439            builder.set_max_fail(max_fail);
440            debug!(max_fail = ?max_fail, "set max fail");
441        } else if self.no_fail_fast {
442            builder.set_max_fail(MaxFail::from_fail_fast(false));
443            debug!("set max fail via from_fail_fast(false)");
444        } else if self.fail_fast {
445            builder.set_max_fail(MaxFail::from_fail_fast(true));
446            debug!("set max fail via from_fail_fast(true)");
447        }
448
449        // Benchmarks always run serially and use 1 test thread.
450        builder.set_test_threads(TestThreads::Count(1));
451
452        if let Some(condition) = self.stress.condition.as_ref() {
453            builder.set_stress_condition(condition.stress_condition());
454        }
455
456        builder.set_interceptor(self.interceptor.to_interceptor());
457
458        Some(builder)
459    }
460}
461
462/// Benchmark reporter options.
463#[derive(Debug, Default, Args)]
464#[command(next_help_heading = "Reporter options")]
465pub(super) struct BenchReporterOpts {
466    /// Show nextest progress in the specified manner.
467    ///
468    /// For benchmarks, the default is "counter" which shows the benchmark index
469    /// (e.g., "(1/10)") but no progress bar.
470    ///
471    /// This can also be set via user config at `~/.config/nextest/config.toml`.
472    /// See <https://nexte.st/docs/user-config>.
473    #[arg(long, env = "NEXTEST_SHOW_PROGRESS")]
474    show_progress: Option<ShowProgressOpt>,
475
476    /// Disable handling of input keys from the terminal.
477    ///
478    /// By default, when running a terminal, nextest accepts the `t` key to dump
479    /// test information. This flag disables that behavior.
480    #[arg(long, env = "NEXTEST_NO_INPUT_HANDLER", value_parser = BoolishValueParser::new())]
481    pub(super) no_input_handler: bool,
482}
483
484impl BenchReporterOpts {
485    pub(super) fn to_builder(
486        &self,
487        should_colorize: bool,
488        resolved_ui: &UiConfig,
489    ) -> ReporterBuilder {
490        let mut builder = ReporterBuilder::default();
491        builder.set_no_capture(true);
492        builder.set_colorize(should_colorize);
493        // Determine show_progress with precedence: CLI/env > resolved config.
494        let ui_show_progress = self
495            .show_progress
496            .map(UiShowProgress::from)
497            .unwrap_or(resolved_ui.show_progress);
498        if ui_show_progress == UiShowProgress::Only {
499            // "only" implies --status-level=slow and --final-status-level=none.
500            builder.set_status_level(StatusLevel::Slow);
501            builder.set_final_status_level(FinalStatusLevel::None);
502        }
503        builder.set_show_progress(ui_show_progress.into());
504        builder
505    }
506}
507
508#[derive(Copy, Clone, Debug, ValueEnum, Default)]
509pub(crate) enum PlatformFilterOpts {
510    Target,
511    Host,
512    #[default]
513    Any,
514}
515
516impl From<PlatformFilterOpts> for Option<BuildPlatform> {
517    fn from(opt: PlatformFilterOpts) -> Self {
518        match opt {
519            PlatformFilterOpts::Target => Some(BuildPlatform::Target),
520            PlatformFilterOpts::Host => Some(BuildPlatform::Host),
521            PlatformFilterOpts::Any => None,
522        }
523    }
524}
525
526#[derive(Copy, Clone, Debug, ValueEnum, Default)]
527pub(super) enum ListType {
528    #[default]
529    Full,
530    BinariesOnly,
531}
532
533#[derive(Copy, Clone, Debug, ValueEnum, Default)]
534pub(super) enum MessageFormatOpts {
535    /// Auto-detect: **human** if stdout is an interactive terminal, **oneline**
536    /// otherwise.
537    #[default]
538    Auto,
539    /// A human-readable output format.
540    Human,
541    /// One test per line.
542    Oneline,
543    /// JSON with no whitespace.
544    Json,
545    /// JSON, prettified.
546    JsonPretty,
547}
548
549impl MessageFormatOpts {
550    pub(super) fn to_output_format(self, verbose: bool, is_terminal: bool) -> OutputFormat {
551        match self {
552            Self::Auto => {
553                if is_terminal {
554                    OutputFormat::Human { verbose }
555                } else {
556                    OutputFormat::Oneline { verbose }
557                }
558            }
559            Self::Human => OutputFormat::Human { verbose },
560            Self::Oneline => OutputFormat::Oneline { verbose },
561            Self::Json => OutputFormat::Serializable(SerializableFormat::Json),
562            Self::JsonPretty => OutputFormat::Serializable(SerializableFormat::JsonPretty),
563        }
564    }
565
566    /// Returns true if this format is human-readable (suitable for paging).
567    ///
568    /// Machine-readable formats (JSON) should not be paged.
569    pub(super) fn is_human_readable(self) -> bool {
570        match self {
571            Self::Auto | Self::Human | Self::Oneline => true,
572            Self::Json | Self::JsonPretty => false,
573        }
574    }
575}
576
577#[derive(Debug, Args)]
578#[command(next_help_heading = "Filter options")]
579pub(super) struct TestBuildFilter {
580    /// Run ignored tests
581    #[arg(long, value_enum, value_name = "WHICH")]
582    run_ignored: Option<RunIgnoredOpt>,
583
584    /// Test partition, e.g. hash:1/2 or count:2/3
585    #[arg(long)]
586    partition: Option<PartitionerBuilder>,
587
588    /// Filter test binaries by build platform (DEPRECATED)
589    ///
590    /// Instead, use -E with 'platform(host)' or 'platform(target)'.
591    #[arg(
592        long,
593        hide_short_help = true,
594        value_enum,
595        value_name = "PLATFORM",
596        default_value_t
597    )]
598    pub(crate) platform_filter: PlatformFilterOpts,
599
600    /// Test filterset (see {n}<https://nexte.st/docs/filtersets>).
601    #[arg(
602        long,
603        alias = "filter-expr",
604        short = 'E',
605        value_name = "EXPR",
606        action(ArgAction::Append)
607    )]
608    pub(super) filterset: Vec<String>,
609
610    /// Ignore the default filter configured in the profile.
611    ///
612    /// By default, all filtersets are intersected with the default filter configured in the
613    /// profile. This flag disables that behavior.
614    ///
615    /// This flag doesn't change the definition of the `default()` filterset.
616    #[arg(long)]
617    ignore_default_filter: bool,
618
619    /// Test name filters.
620    #[arg(help_heading = None, name = "FILTERS")]
621    pre_double_dash_filters: Vec<String>,
622
623    /// Test name filters and emulated test binary arguments.
624    ///
625    /// Supported arguments:
626    ///
627    /// - --ignored:         Only run ignored tests
628    /// - --include-ignored: Run both ignored and non-ignored tests
629    /// - --skip PATTERN:    Skip tests that match the pattern
630    /// - --exact:           Run tests that exactly match patterns after `--`
631    #[arg(help_heading = None, value_name = "FILTERS_AND_ARGS", last = true)]
632    filters: Vec<String>,
633}
634
635impl TestBuildFilter {
636    #[expect(clippy::too_many_arguments)]
637    pub(super) fn compute_test_list<'g>(
638        &self,
639        ctx: &TestExecuteContext<'_>,
640        graph: &'g PackageGraph,
641        workspace_root: Utf8PathBuf,
642        binary_list: Arc<BinaryList>,
643        test_filter_builder: TestFilterBuilder,
644        env: EnvironmentMap,
645        profile: &EvaluatableProfile<'_>,
646        reuse_build: &ReuseBuildInfo,
647    ) -> Result<TestList<'g>> {
648        let path_mapper = make_path_mapper(
649            reuse_build,
650            graph,
651            &binary_list.rust_build_meta.target_directory,
652        )?;
653
654        let rust_build_meta = binary_list.rust_build_meta.map_paths(&path_mapper);
655        let test_artifacts = RustTestArtifact::from_binary_list(
656            graph,
657            binary_list,
658            &rust_build_meta,
659            &path_mapper,
660            self.platform_filter.into(),
661        )?;
662        TestList::new(
663            ctx,
664            test_artifacts,
665            rust_build_meta,
666            &test_filter_builder,
667            workspace_root,
668            env,
669            profile,
670            if self.ignore_default_filter {
671                FilterBound::All
672            } else {
673                FilterBound::DefaultSet
674            },
675            // TODO: do we need to allow customizing this?
676            get_num_cpus(),
677        )
678        .map_err(|err| ExpectedError::CreateTestListError { err })
679    }
680
681    pub(super) fn make_test_filter_builder(
682        &self,
683        mode: NextestRunMode,
684        filter_exprs: Vec<nextest_filtering::Filterset>,
685    ) -> Result<TestFilterBuilder> {
686        // Merge the test binary args into the patterns.
687        let mut run_ignored = self.run_ignored.map(Into::into);
688        let mut patterns = TestFilterPatterns::new(self.pre_double_dash_filters.clone());
689        self.merge_test_binary_args(&mut run_ignored, &mut patterns)?;
690
691        Ok(TestFilterBuilder::new(
692            mode,
693            run_ignored.unwrap_or_default(),
694            self.partition.clone(),
695            patterns,
696            filter_exprs,
697        )?)
698    }
699
700    fn merge_test_binary_args(
701        &self,
702        run_ignored: &mut Option<RunIgnored>,
703        patterns: &mut TestFilterPatterns,
704    ) -> Result<()> {
705        // First scan to see if `--exact` is specified. If so, then everything here will be added to
706        // `--exact`.
707        let mut is_exact = false;
708        for arg in &self.filters {
709            if arg == "--" {
710                break;
711            }
712            if arg == "--exact" {
713                if is_exact {
714                    return Err(ExpectedError::test_binary_args_parse_error(
715                        "duplicated",
716                        vec![arg.clone()],
717                    ));
718                }
719                is_exact = true;
720            }
721        }
722
723        let mut ignore_filters = Vec::new();
724        let mut read_trailing_filters = false;
725
726        let mut unsupported_args = Vec::new();
727
728        let mut it = self.filters.iter();
729        while let Some(arg) = it.next() {
730            if read_trailing_filters || !arg.starts_with('-') {
731                if is_exact {
732                    patterns.add_exact_pattern(arg.clone());
733                } else {
734                    patterns.add_substring_pattern(arg.clone());
735                }
736            } else if arg == "--include-ignored" {
737                ignore_filters.push((arg.clone(), RunIgnored::All));
738            } else if arg == "--ignored" {
739                ignore_filters.push((arg.clone(), RunIgnored::Only));
740            } else if arg == "--" {
741                read_trailing_filters = true;
742            } else if arg == "--skip" {
743                let skip_arg = it.next().ok_or_else(|| {
744                    ExpectedError::test_binary_args_parse_error(
745                        "missing required argument",
746                        vec![arg.clone()],
747                    )
748                })?;
749
750                if is_exact {
751                    patterns.add_skip_exact_pattern(skip_arg.clone());
752                } else {
753                    patterns.add_skip_pattern(skip_arg.clone());
754                }
755            } else if arg == "--exact" {
756                // Already handled above.
757            } else {
758                unsupported_args.push(arg.clone());
759            }
760        }
761
762        for (s, f) in ignore_filters {
763            if let Some(run_ignored) = run_ignored {
764                if *run_ignored != f {
765                    return Err(ExpectedError::test_binary_args_parse_error(
766                        "mutually exclusive",
767                        vec![s],
768                    ));
769                } else {
770                    return Err(ExpectedError::test_binary_args_parse_error(
771                        "duplicated",
772                        vec![s],
773                    ));
774                }
775            } else {
776                *run_ignored = Some(f);
777            }
778        }
779
780        if !unsupported_args.is_empty() {
781            return Err(ExpectedError::test_binary_args_parse_error(
782                "unsupported",
783                unsupported_args,
784            ));
785        }
786
787        Ok(())
788    }
789}
790
791#[derive(Debug, Args)]
792#[command(next_help_heading = "Filter options")]
793pub(super) struct ArchiveBuildFilter {
794    /// Archive filterset (see <https://nexte.st/docs/filtersets>).
795    ///
796    /// This argument does not accept test predicates.
797    #[arg(long, short = 'E', value_name = "EXPR", action(ArgAction::Append))]
798    pub(super) filterset: Vec<String>,
799}
800
801#[derive(Copy, Clone, Debug, ValueEnum)]
802enum RunIgnoredOpt {
803    /// Run non-ignored tests.
804    Default,
805
806    /// Run ignored tests.
807    #[clap(alias = "ignored-only")]
808    Only,
809
810    /// Run both ignored and non-ignored tests.
811    All,
812}
813
814impl From<RunIgnoredOpt> for RunIgnored {
815    fn from(opt: RunIgnoredOpt) -> Self {
816        match opt {
817            RunIgnoredOpt::Default => RunIgnored::Default,
818            RunIgnoredOpt::Only => RunIgnored::Only,
819            RunIgnoredOpt::All => RunIgnored::All,
820        }
821    }
822}
823
824impl CargoOptions {
825    pub(super) fn compute_binary_list(
826        &self,
827        cargo_command: &str,
828        graph: &PackageGraph,
829        manifest_path: Option<&Utf8Path>,
830        output: OutputContext,
831        build_platforms: BuildPlatforms,
832    ) -> Result<BinaryList> {
833        // Don't use the manifest path from the graph to ensure that if the user cd's into a
834        // particular crate and runs cargo nextest, then it behaves identically to cargo test.
835        let mut cargo_cli = CargoCli::new(cargo_command, manifest_path, output);
836
837        // Only build tests in the cargo test invocation, do not run them.
838        cargo_cli.add_args(["--no-run", "--message-format", "json-render-diagnostics"]);
839        cargo_cli.add_options(self);
840
841        let expression = cargo_cli.to_expression();
842        let output = expression
843            .stdout_capture()
844            .unchecked()
845            .run()
846            .map_err(|err| ExpectedError::build_exec_failed(cargo_cli.all_args(), err))?;
847        if !output.status.success() {
848            return Err(ExpectedError::build_failed(
849                cargo_cli.all_args(),
850                output.status.code(),
851            ));
852        }
853
854        let test_binaries =
855            BinaryList::from_messages(Cursor::new(output.stdout), graph, build_platforms)?;
856        Ok(test_binaries)
857    }
858}
859
860/// Test runner options.
861#[derive(Debug, Default, Args)]
862#[command(next_help_heading = "Runner options")]
863pub struct TestRunnerOpts {
864    /// Compile, but don't run tests
865    #[arg(long, name = "no-run")]
866    pub(super) no_run: bool,
867
868    /// Number of tests to run simultaneously [possible values: integer or "num-cpus"]
869    /// [default: from profile]
870    #[arg(
871        long,
872        short = 'j',
873        visible_alias = "jobs",
874        value_name = "N",
875        env = "NEXTEST_TEST_THREADS",
876        allow_negative_numbers = true
877    )]
878    test_threads: Option<TestThreads>,
879
880    /// Number of retries for failing tests [default: from profile]
881    #[arg(long, env = "NEXTEST_RETRIES", value_name = "N")]
882    retries: Option<u32>,
883
884    /// Cancel test run on the first failure
885    #[arg(
886        long,
887        visible_alias = "ff",
888        name = "fail-fast",
889        // TODO: It would be nice to warn rather than error if fail-fast is used
890        // with no-run, so that this matches the other options like
891        // test-threads. But there seem to be issues with that: clap 4.5 doesn't
892        // appear to like `Option<bool>` very much. With `ArgAction::SetTrue` it
893        // always sets the value to false or true rather than leaving it unset.
894        conflicts_with = "no-run"
895    )]
896    fail_fast: bool,
897
898    /// Run all tests regardless of failure
899    #[arg(
900        long,
901        visible_alias = "nff",
902        name = "no-fail-fast",
903        conflicts_with = "no-run",
904        overrides_with = "fail-fast"
905    )]
906    no_fail_fast: bool,
907
908    /// Number of tests that can fail before exiting test run
909    ///
910    /// To control whether currently running tests are waited for or terminated
911    /// immediately, append ':wait' (default) or ':immediate' to the number
912    /// (e.g., '5:immediate').
913    ///
914    /// [possible values: integer, "all", "N:wait", "N:immediate"]
915    #[arg(
916        long,
917        name = "max-fail",
918        value_name = "N[:MODE]",
919        conflicts_with_all = &["no-run", "fail-fast", "no-fail-fast"],
920    )]
921    max_fail: Option<MaxFail>,
922
923    /// Interceptor options (debugger or tracer)
924    #[clap(flatten)]
925    pub(super) interceptor: InterceptorOpt,
926
927    /// Behavior if there are no tests to run [default: fail]
928    #[arg(long, value_enum, value_name = "ACTION", env = "NEXTEST_NO_TESTS")]
929    pub(super) no_tests: Option<NoTestsBehavior>,
930
931    /// Stress testing options
932    #[clap(flatten)]
933    pub(super) stress: StressOptions,
934}
935
936#[derive(Debug, Default, Args)]
937#[group(id = "interceptor", multiple = false)]
938pub(super) struct InterceptorOpt {
939    /// Debug a single test using a text-based or graphical debugger.
940    ///
941    /// Debugger mode automatically:
942    ///
943    /// - disables timeouts
944    /// - disables output capture
945    /// - passes standard input through to the debugger
946    ///
947    /// Example: `--debugger "rust-gdb --args"`
948    #[arg(long, value_name = "DEBUGGER", conflicts_with_all = ["stress_condition", "no-run"])]
949    pub(super) debugger: Option<DebuggerCommand>,
950
951    /// Trace a single test using a syscall tracer like `strace` or `truss`.
952    ///
953    /// Tracer mode automatically:
954    ///
955    /// - disables timeouts
956    /// - disables output capture
957    ///
958    /// Unlike `--debugger`, tracers do not need stdin passthrough or special signal handling.
959    ///
960    /// Example: `--tracer "strace -tt"`
961    #[arg(long, value_name = "TRACER", conflicts_with_all = ["stress_condition", "no-run"])]
962    pub(super) tracer: Option<TracerCommand>,
963}
964
965impl InterceptorOpt {
966    /// Returns true if either a debugger or a tracer is active.
967    pub(super) fn is_active(&self) -> bool {
968        self.debugger.is_some() || self.tracer.is_some()
969    }
970
971    /// Converts to an [`Interceptor`] enum.
972    pub(super) fn to_interceptor(&self) -> Interceptor {
973        match (&self.debugger, &self.tracer) {
974            (Some(debugger), None) => Interceptor::Debugger(debugger.clone()),
975            (None, Some(tracer)) => Interceptor::Tracer(tracer.clone()),
976            (None, None) => Interceptor::None,
977            (Some(_), Some(_)) => {
978                unreachable!("clap group ensures debugger and tracer are mutually exclusive")
979            }
980        }
981    }
982}
983
984#[derive(Clone, Copy, Debug, ValueEnum)]
985pub(super) enum NoTestsBehavior {
986    /// Silently exit with code 0.
987    Pass,
988
989    /// Produce a warning and exit with code 0.
990    Warn,
991
992    /// Produce an error message and exit with code 4.
993    #[clap(alias = "error")]
994    Fail,
995}
996
997impl TestRunnerOpts {
998    pub(super) fn to_builder(&self, cap_strat: CaptureStrategy) -> Option<TestRunnerBuilder> {
999        // Warn on conflicts between options. This is a warning and not an error
1000        // because these options can be specified via environment variables as
1001        // well.
1002        if self.test_threads.is_some()
1003            && let Some(reasons) =
1004                no_run_no_capture_reasons(self.no_run, cap_strat == CaptureStrategy::None)
1005        {
1006            warn!("ignoring --test-threads because {reasons}");
1007        }
1008
1009        if self.retries.is_some() && self.no_run {
1010            warn!("ignoring --retries because --no-run is specified");
1011        }
1012        if self.no_tests.is_some() && self.no_run {
1013            warn!("ignoring --no-tests because --no-run is specified");
1014        }
1015
1016        // ---
1017
1018        if self.no_run {
1019            return None;
1020        }
1021
1022        let mut builder = TestRunnerBuilder::default();
1023        builder.set_capture_strategy(cap_strat);
1024        if let Some(retries) = self.retries {
1025            builder.set_retries(RetryPolicy::new_without_delay(retries));
1026        }
1027
1028        if let Some(max_fail) = self.max_fail {
1029            builder.set_max_fail(max_fail);
1030            debug!(max_fail = ?max_fail, "set max fail");
1031        } else if self.no_fail_fast {
1032            builder.set_max_fail(MaxFail::from_fail_fast(false));
1033            debug!("set max fail via from_fail_fast(false)");
1034        } else if self.fail_fast {
1035            builder.set_max_fail(MaxFail::from_fail_fast(true));
1036            debug!("set max fail via from_fail_fast(true)");
1037        }
1038
1039        if let Some(test_threads) = self.test_threads {
1040            builder.set_test_threads(test_threads);
1041        }
1042
1043        if let Some(condition) = self.stress.condition.as_ref() {
1044            builder.set_stress_condition(condition.stress_condition());
1045        }
1046
1047        builder.set_interceptor(self.interceptor.to_interceptor());
1048
1049        Some(builder)
1050    }
1051}
1052
1053fn no_run_no_capture_reasons(no_run: bool, no_capture: bool) -> Option<&'static str> {
1054    match (no_run, no_capture) {
1055        (true, true) => Some("--no-run and --no-capture are specified"),
1056        (true, false) => Some("--no-run is specified"),
1057        (false, true) => Some("--no-capture is specified"),
1058        (false, false) => None,
1059    }
1060}
1061
1062#[derive(Clone, Copy, Debug, ValueEnum)]
1063pub(super) enum IgnoreOverridesOpt {
1064    Retries,
1065    All,
1066}
1067
1068#[derive(Clone, Copy, Debug, ValueEnum, Default)]
1069pub(super) enum MessageFormat {
1070    /// The default output format.
1071    #[default]
1072    Human,
1073    /// Output test information in the same format as libtest.
1074    LibtestJson,
1075    /// Output test information in the same format as libtest, with a `nextest` subobject that
1076    /// includes additional metadata.
1077    LibtestJsonPlus,
1078}
1079
1080#[derive(Debug, Default, Args)]
1081#[command(next_help_heading = "Stress testing options")]
1082pub(super) struct StressOptions {
1083    /// Stress testing condition.
1084    #[clap(flatten)]
1085    pub(super) condition: Option<StressConditionOpt>,
1086    // TODO: modes other than serial
1087}
1088
1089#[derive(Clone, Debug, Default, Args)]
1090#[group(id = "stress_condition", multiple = false)]
1091pub(super) struct StressConditionOpt {
1092    /// The number of times to run each test, or `infinite` to run indefinitely.
1093    #[arg(long, value_name = "COUNT")]
1094    stress_count: Option<StressCount>,
1095
1096    /// How long to run stress tests until (e.g. 24h).
1097    #[arg(long, value_name = "DURATION", value_parser = non_zero_duration)]
1098    stress_duration: Option<Duration>,
1099}
1100
1101impl StressConditionOpt {
1102    fn stress_condition(&self) -> StressCondition {
1103        if let Some(count) = self.stress_count {
1104            StressCondition::Count(count)
1105        } else if let Some(duration) = self.stress_duration {
1106            StressCondition::Duration(duration)
1107        } else {
1108            unreachable!(
1109                "if StressOptions::condition is Some, \
1110                 one of these should be set"
1111            )
1112        }
1113    }
1114}
1115
1116fn non_zero_duration(input: &str) -> std::result::Result<Duration, String> {
1117    let duration = humantime::parse_duration(input).map_err(|error| error.to_string())?;
1118    if duration.is_zero() {
1119        Err("duration must be non-zero".to_string())
1120    } else {
1121        Ok(duration)
1122    }
1123}
1124
1125#[derive(Debug, Default, Args)]
1126#[command(next_help_heading = "Reporter options")]
1127pub(super) struct ReporterOpts {
1128    /// Output stdout and stderr on failure
1129    #[arg(long, value_enum, value_name = "WHEN", env = "NEXTEST_FAILURE_OUTPUT")]
1130    failure_output: Option<TestOutputDisplayOpt>,
1131
1132    /// Output stdout and stderr on success
1133    #[arg(long, value_enum, value_name = "WHEN", env = "NEXTEST_SUCCESS_OUTPUT")]
1134    success_output: Option<TestOutputDisplayOpt>,
1135
1136    // status_level does not conflict with --no-capture because pass vs skip still makes sense.
1137    /// Test statuses to output
1138    #[arg(long, value_enum, value_name = "LEVEL", env = "NEXTEST_STATUS_LEVEL")]
1139    status_level: Option<StatusLevelOpt>,
1140
1141    /// Test statuses to output at the end of the run.
1142    #[arg(
1143        long,
1144        value_enum,
1145        value_name = "LEVEL",
1146        env = "NEXTEST_FINAL_STATUS_LEVEL"
1147    )]
1148    final_status_level: Option<FinalStatusLevelOpt>,
1149
1150    /// Show nextest progress in the specified manner.
1151    ///
1152    /// This can also be set via user config at `~/.config/nextest/config.toml`.
1153    /// See <https://nexte.st/docs/user-config>.
1154    #[arg(long, env = "NEXTEST_SHOW_PROGRESS")]
1155    show_progress: Option<ShowProgressOpt>,
1156
1157    /// Do not display the progress bar. Deprecated, use **--show-progress** instead.
1158    #[arg(long, env = "NEXTEST_HIDE_PROGRESS_BAR", value_parser = BoolishValueParser::new())]
1159    hide_progress_bar: bool,
1160
1161    /// Do not indent captured test output.
1162    ///
1163    /// By default, test output produced by **--failure-output** and
1164    /// **--success-output** is indented for visual clarity. This flag disables
1165    /// that behavior.
1166    ///
1167    /// This option has no effect with **--no-capture**, since that passes
1168    /// through standard output and standard error.
1169    #[arg(long, env = "NEXTEST_NO_OUTPUT_INDENT", value_parser = BoolishValueParser::new())]
1170    no_output_indent: bool,
1171
1172    /// Disable handling of input keys from the terminal.
1173    ///
1174    /// By default, when running a terminal, nextest accepts the `t` key to dump
1175    /// test information. This flag disables that behavior.
1176    #[arg(long, env = "NEXTEST_NO_INPUT_HANDLER", value_parser = BoolishValueParser::new())]
1177    pub(super) no_input_handler: bool,
1178
1179    /// Maximum number of running tests to display progress for.
1180    ///
1181    /// When more tests are running than this limit, the progress bar will show
1182    /// the first N tests and a summary of remaining tests (e.g. "... and 24
1183    /// more tests running"). Set to **0** to hide running tests, or
1184    /// **infinite** for unlimited. This applies when using
1185    /// `--show-progress=bar` or `--show-progress=only`.
1186    ///
1187    /// This can also be set via user config at `~/.config/nextest/config.toml`.
1188    /// See <https://nexte.st/docs/user-config>.
1189    #[arg(
1190        long = "max-progress-running",
1191        value_name = "N",
1192        env = "NEXTEST_MAX_PROGRESS_RUNNING"
1193    )]
1194    max_progress_running: Option<MaxProgressRunning>,
1195
1196    /// Format to use for test results (experimental).
1197    #[arg(
1198        long,
1199        name = "message-format",
1200        value_enum,
1201        value_name = "FORMAT",
1202        env = "NEXTEST_MESSAGE_FORMAT"
1203    )]
1204    pub(super) message_format: Option<MessageFormat>,
1205
1206    /// Version of structured message-format to use (experimental).
1207    ///
1208    /// This allows the machine-readable formats to use a stable structure for consistent
1209    /// consumption across changes to nextest. If not specified, the latest version is used.
1210    #[arg(
1211        long,
1212        requires = "message-format",
1213        value_name = "VERSION",
1214        env = "NEXTEST_MESSAGE_FORMAT_VERSION"
1215    )]
1216    pub(super) message_format_version: Option<String>,
1217}
1218
1219impl ReporterOpts {
1220    pub(super) fn to_builder(
1221        &self,
1222        no_run: bool,
1223        no_capture: bool,
1224        should_colorize: bool,
1225        resolved_ui: &UiConfig,
1226    ) -> ReporterBuilder {
1227        // Warn on conflicts between options. This is a warning and not an error
1228        // because these options can be specified via environment variables as
1229        // well.
1230        if no_run && no_capture {
1231            warn!("ignoring --no-capture because --no-run is specified");
1232        }
1233
1234        let reasons = no_run_no_capture_reasons(no_run, no_capture);
1235
1236        if self.failure_output.is_some()
1237            && let Some(reasons) = reasons
1238        {
1239            warn!("ignoring --failure-output because {}", reasons);
1240        }
1241        if self.success_output.is_some()
1242            && let Some(reasons) = reasons
1243        {
1244            warn!("ignoring --success-output because {}", reasons);
1245        }
1246        if self.status_level.is_some() && no_run {
1247            warn!("ignoring --status-level because --no-run is specified");
1248        }
1249        if self.final_status_level.is_some() && no_run {
1250            warn!("ignoring --final-status-level because --no-run is specified");
1251        }
1252        if self.message_format.is_some() && no_run {
1253            warn!("ignoring --message-format because --no-run is specified");
1254        }
1255        if self.message_format_version.is_some() && no_run {
1256            warn!("ignoring --message-format-version because --no-run is specified");
1257        }
1258
1259        // Determine show_progress with precedence: CLI/env > resolved config.
1260        // Use UiShowProgress to preserve the "only" variant's special behavior.
1261        let ui_show_progress = match (self.show_progress, self.hide_progress_bar) {
1262            (Some(show_progress), true) => {
1263                warn!("ignoring --hide-progress-bar because --show-progress is specified");
1264                show_progress.into()
1265            }
1266            (Some(show_progress), false) => show_progress.into(),
1267            (None, true) => UiShowProgress::None,
1268            (None, false) => resolved_ui.show_progress,
1269        };
1270
1271        // Determine max_progress_running with precedence: CLI/env > resolved config.
1272        let max_progress_running = self
1273            .max_progress_running
1274            .unwrap_or(resolved_ui.max_progress_running);
1275
1276        // Note: CLI uses --no-output-indent (negative), resolved config uses
1277        // output_indent (positive).
1278        let no_output_indent = self.no_output_indent || !resolved_ui.output_indent;
1279
1280        debug!(
1281            ?ui_show_progress,
1282            ?max_progress_running,
1283            ?no_output_indent,
1284            "resolved reporter UI settings"
1285        );
1286
1287        // ---
1288
1289        let mut builder = ReporterBuilder::default();
1290        builder.set_no_capture(no_capture);
1291        builder.set_colorize(should_colorize);
1292
1293        if ui_show_progress == UiShowProgress::Only {
1294            // "only" implies --status-level=slow and --final-status-level=none.
1295            // But we allow overriding these options explicitly as well.
1296            builder.set_status_level(StatusLevel::Slow);
1297            builder.set_final_status_level(FinalStatusLevel::None);
1298        }
1299        if let Some(failure_output) = self.failure_output {
1300            builder.set_failure_output(failure_output.into());
1301        }
1302        if let Some(success_output) = self.success_output {
1303            builder.set_success_output(success_output.into());
1304        }
1305        if let Some(status_level) = self.status_level {
1306            builder.set_status_level(status_level.into());
1307        }
1308        if let Some(final_status_level) = self.final_status_level {
1309            builder.set_final_status_level(final_status_level.into());
1310        }
1311        builder.set_show_progress(ui_show_progress.into());
1312        builder.set_no_output_indent(no_output_indent);
1313        builder.set_max_progress_running(max_progress_running);
1314        builder
1315    }
1316}
1317
1318#[derive(Clone, Copy, Debug, ValueEnum)]
1319enum TestOutputDisplayOpt {
1320    Immediate,
1321    ImmediateFinal,
1322    Final,
1323    Never,
1324}
1325
1326impl From<TestOutputDisplayOpt> for TestOutputDisplay {
1327    fn from(opt: TestOutputDisplayOpt) -> Self {
1328        match opt {
1329            TestOutputDisplayOpt::Immediate => TestOutputDisplay::Immediate,
1330            TestOutputDisplayOpt::ImmediateFinal => TestOutputDisplay::ImmediateFinal,
1331            TestOutputDisplayOpt::Final => TestOutputDisplay::Final,
1332            TestOutputDisplayOpt::Never => TestOutputDisplay::Never,
1333        }
1334    }
1335}
1336
1337#[derive(Clone, Copy, Debug, ValueEnum)]
1338enum StatusLevelOpt {
1339    None,
1340    Fail,
1341    Retry,
1342    Slow,
1343    Leak,
1344    Pass,
1345    Skip,
1346    All,
1347}
1348
1349impl From<StatusLevelOpt> for StatusLevel {
1350    fn from(opt: StatusLevelOpt) -> Self {
1351        match opt {
1352            StatusLevelOpt::None => StatusLevel::None,
1353            StatusLevelOpt::Fail => StatusLevel::Fail,
1354            StatusLevelOpt::Retry => StatusLevel::Retry,
1355            StatusLevelOpt::Slow => StatusLevel::Slow,
1356            StatusLevelOpt::Leak => StatusLevel::Leak,
1357            StatusLevelOpt::Pass => StatusLevel::Pass,
1358            StatusLevelOpt::Skip => StatusLevel::Skip,
1359            StatusLevelOpt::All => StatusLevel::All,
1360        }
1361    }
1362}
1363
1364#[derive(Clone, Copy, Debug, ValueEnum)]
1365enum FinalStatusLevelOpt {
1366    None,
1367    Fail,
1368    #[clap(alias = "retry")]
1369    Flaky,
1370    Slow,
1371    Skip,
1372    Pass,
1373    All,
1374}
1375
1376impl From<FinalStatusLevelOpt> for FinalStatusLevel {
1377    fn from(opt: FinalStatusLevelOpt) -> Self {
1378        match opt {
1379            FinalStatusLevelOpt::None => FinalStatusLevel::None,
1380            FinalStatusLevelOpt::Fail => FinalStatusLevel::Fail,
1381            FinalStatusLevelOpt::Flaky => FinalStatusLevel::Flaky,
1382            FinalStatusLevelOpt::Slow => FinalStatusLevel::Slow,
1383            FinalStatusLevelOpt::Skip => FinalStatusLevel::Skip,
1384            FinalStatusLevelOpt::Pass => FinalStatusLevel::Pass,
1385            FinalStatusLevelOpt::All => FinalStatusLevel::All,
1386        }
1387    }
1388}
1389
1390#[derive(Default, Clone, Copy, Debug, ValueEnum)]
1391enum ShowProgressOpt {
1392    /// Automatically choose the best progress display based on whether nextest
1393    /// is running in an interactive terminal.
1394    #[default]
1395    Auto,
1396
1397    /// Do not display a progress bar or counter.
1398    None,
1399
1400    /// Display a progress bar with running tests: default for interactive
1401    /// terminals.
1402    #[clap(alias = "running")]
1403    Bar,
1404
1405    /// Display a counter next to each completed test.
1406    Counter,
1407
1408    /// Display a progress bar with running tests, and hide successful test
1409    /// output; equivalent to `--show-progress=running --status-level=slow
1410    /// --final-status-level=none`.
1411    Only,
1412}
1413
1414impl From<ShowProgressOpt> for UiShowProgress {
1415    fn from(opt: ShowProgressOpt) -> Self {
1416        match opt {
1417            ShowProgressOpt::Auto => UiShowProgress::Auto,
1418            ShowProgressOpt::None => UiShowProgress::None,
1419            ShowProgressOpt::Bar => UiShowProgress::Bar,
1420            ShowProgressOpt::Counter => UiShowProgress::Counter,
1421            ShowProgressOpt::Only => UiShowProgress::Only,
1422        }
1423    }
1424}
1425
1426/// A next-generation test runner for Rust.
1427///
1428/// This binary should typically be invoked as `cargo nextest` (in which case
1429/// this message will not be seen), not `cargo-nextest`.
1430#[derive(Debug, clap::Parser)]
1431#[command(
1432    version = crate::version::short(),
1433    long_version = crate::version::long(),
1434    bin_name = "cargo",
1435    styles = crate::output::clap_styles::style(),
1436    max_term_width = 100,
1437)]
1438pub struct CargoNextestApp {
1439    /// Early args (color, no_pager) flattened at root for early extraction.
1440    #[clap(flatten)]
1441    early_args: EarlyArgs,
1442
1443    #[clap(subcommand)]
1444    subcommand: NextestSubcommand,
1445}
1446
1447impl CargoNextestApp {
1448    /// Initializes the output context.
1449    pub fn init_output(&self) -> OutputContext {
1450        match &self.subcommand {
1451            NextestSubcommand::Nextest(args) => args.common.output.init(self.early_args),
1452            NextestSubcommand::Ntr(args) => args.common.output.init(self.early_args),
1453            #[cfg(unix)]
1454            // Double-spawned processes should never use coloring.
1455            NextestSubcommand::DoubleSpawn(_) => OutputContext::color_never_init(),
1456        }
1457    }
1458
1459    /// Executes the app.
1460    pub fn exec(
1461        self,
1462        cli_args: Vec<String>,
1463        output: OutputContext,
1464        output_writer: &mut crate::output::OutputWriter,
1465    ) -> Result<i32> {
1466        if let Err(err) = nextest_runner::usdt::register_probes() {
1467            tracing::warn!("failed to register USDT probes: {}", err);
1468        }
1469
1470        match self.subcommand {
1471            NextestSubcommand::Nextest(app) => app.exec(cli_args, output, output_writer),
1472            NextestSubcommand::Ntr(opts) => opts.exec(cli_args, output, output_writer),
1473            #[cfg(unix)]
1474            NextestSubcommand::DoubleSpawn(opts) => opts.exec(output),
1475        }
1476    }
1477}
1478
1479#[derive(Debug, Subcommand)]
1480enum NextestSubcommand {
1481    /// A next-generation test runner for Rust. <https://nexte.st>
1482    Nextest(Box<AppOpts>),
1483    /// Build and run tests: a shortcut for `cargo nextest run`.
1484    Ntr(Box<NtrOpts>),
1485    /// Private command, used to double-spawn test processes.
1486    #[cfg(unix)]
1487    #[command(name = nextest_runner::double_spawn::DoubleSpawnInfo::SUBCOMMAND_NAME, hide = true)]
1488    DoubleSpawn(crate::double_spawn::DoubleSpawnOpts),
1489}
1490
1491#[derive(Debug, Args)]
1492#[clap(
1493    version = crate::version::short(),
1494    long_version = crate::version::long(),
1495    display_name = "cargo-nextest",
1496)]
1497pub(super) struct AppOpts {
1498    #[clap(flatten)]
1499    common: CommonOpts,
1500
1501    #[clap(subcommand)]
1502    command: Command,
1503}
1504
1505impl AppOpts {
1506    /// Execute the command.
1507    ///
1508    /// Returns the exit code.
1509    fn exec(
1510        self,
1511        cli_args: Vec<String>,
1512        output: OutputContext,
1513        output_writer: &mut crate::output::OutputWriter,
1514    ) -> Result<i32> {
1515        match self.command {
1516            Command::List(list_opts) => {
1517                let base = super::execution::BaseApp::new(
1518                    output,
1519                    list_opts.reuse_build,
1520                    list_opts.cargo_options,
1521                    self.common.config_opts,
1522                    self.common.manifest_path,
1523                    output_writer,
1524                )?;
1525                let app = super::execution::App::new(base, list_opts.build_filter)?;
1526                app.exec_list(
1527                    list_opts.message_format,
1528                    list_opts.list_type,
1529                    &list_opts.pager_opts,
1530                )?;
1531                Ok(0)
1532            }
1533            Command::Run(run_opts) => {
1534                let base = super::execution::BaseApp::new(
1535                    output,
1536                    run_opts.reuse_build,
1537                    run_opts.cargo_options,
1538                    self.common.config_opts,
1539                    self.common.manifest_path,
1540                    output_writer,
1541                )?;
1542                let app = super::execution::App::new(base, run_opts.build_filter)?;
1543                app.exec_run(
1544                    run_opts.no_capture,
1545                    &run_opts.runner_opts,
1546                    &run_opts.reporter_opts,
1547                    cli_args,
1548                    output_writer,
1549                )?;
1550                Ok(0)
1551            }
1552            Command::Bench(bench_opts) => {
1553                let base = super::execution::BaseApp::new(
1554                    output,
1555                    ReuseBuildOpts::default(),
1556                    bench_opts.cargo_options,
1557                    self.common.config_opts,
1558                    self.common.manifest_path,
1559                    output_writer,
1560                )?;
1561                let app = super::execution::App::new(base, bench_opts.build_filter)?;
1562                app.exec_bench(
1563                    &bench_opts.runner_opts,
1564                    &bench_opts.reporter_opts,
1565                    cli_args,
1566                    output_writer,
1567                )?;
1568                Ok(0)
1569            }
1570            Command::Archive(archive_opts) => {
1571                let app = super::execution::BaseApp::new(
1572                    output,
1573                    ReuseBuildOpts::default(),
1574                    archive_opts.cargo_options,
1575                    self.common.config_opts,
1576                    self.common.manifest_path,
1577                    output_writer,
1578                )?;
1579
1580                let app =
1581                    super::execution::ArchiveApp::new(app, archive_opts.archive_build_filter)?;
1582                app.exec_archive(
1583                    &archive_opts.archive_file,
1584                    archive_opts.archive_format,
1585                    archive_opts.zstd_level,
1586                    output_writer,
1587                )?;
1588                Ok(0)
1589            }
1590            Command::ShowConfig { command } => command.exec(
1591                self.common.manifest_path,
1592                self.common.config_opts,
1593                output,
1594                output_writer,
1595            ),
1596            Command::Self_ { command } => command.exec(output),
1597            Command::Debug { command } => command.exec(output),
1598        }
1599    }
1600}
1601
1602#[derive(Debug, Args)]
1603struct NtrOpts {
1604    #[clap(flatten)]
1605    common: CommonOpts,
1606
1607    #[clap(flatten)]
1608    run_opts: RunOpts,
1609}
1610
1611impl NtrOpts {
1612    fn exec(
1613        self,
1614        cli_args: Vec<String>,
1615        output: OutputContext,
1616        output_writer: &mut crate::output::OutputWriter,
1617    ) -> Result<i32> {
1618        let base = super::execution::BaseApp::new(
1619            output,
1620            self.run_opts.reuse_build,
1621            self.run_opts.cargo_options,
1622            self.common.config_opts,
1623            self.common.manifest_path,
1624            output_writer,
1625        )?;
1626        let app = super::execution::App::new(base, self.run_opts.build_filter)?;
1627        app.exec_run(
1628            self.run_opts.no_capture,
1629            &self.run_opts.runner_opts,
1630            &self.run_opts.reporter_opts,
1631            cli_args,
1632            output_writer,
1633        )?;
1634        Ok(0)
1635    }
1636}
1637
1638#[cfg(test)]
1639mod tests {
1640    use super::*;
1641    use clap::Parser;
1642    use nextest_runner::run_mode::NextestRunMode;
1643
1644    #[test]
1645    fn test_argument_parsing() {
1646        use clap::error::ErrorKind::{self, *};
1647
1648        let valid: &[&'static str] = &[
1649            // ---
1650            // Basic commands
1651            // ---
1652            "cargo nextest list",
1653            "cargo nextest run",
1654            // ---
1655            // Commands with arguments
1656            // ---
1657            "cargo nextest list --list-type binaries-only",
1658            "cargo nextest list --list-type full",
1659            "cargo nextest list --message-format json-pretty",
1660            "cargo nextest list --message-format oneline",
1661            "cargo nextest list --message-format auto",
1662            "cargo nextest list -T oneline",
1663            "cargo nextest list -T auto",
1664            "cargo nextest run --failure-output never",
1665            "cargo nextest run --success-output=immediate",
1666            "cargo nextest run --status-level=all",
1667            "cargo nextest run --no-capture",
1668            "cargo nextest run --nocapture",
1669            "cargo nextest run --no-run",
1670            "cargo nextest run --final-status-level flaky",
1671            "cargo nextest run --max-fail 3",
1672            "cargo nextest run --max-fail=all",
1673            // retry is an alias for flaky -- ensure that it parses
1674            "cargo nextest run --final-status-level retry",
1675            "NEXTEST_HIDE_PROGRESS_BAR=1 cargo nextest run",
1676            "NEXTEST_HIDE_PROGRESS_BAR=true cargo nextest run",
1677            // ---
1678            // --no-run conflicts that produce warnings rather than errors
1679            // ---
1680            "cargo nextest run --no-run -j8",
1681            "cargo nextest run --no-run --retries 3",
1682            "NEXTEST_TEST_THREADS=8 cargo nextest run --no-run",
1683            "cargo nextest run --no-run --success-output never",
1684            "NEXTEST_SUCCESS_OUTPUT=never cargo nextest run --no-run",
1685            "cargo nextest run --no-run --failure-output immediate",
1686            "NEXTEST_FAILURE_OUTPUT=immediate cargo nextest run --no-run",
1687            "cargo nextest run --no-run --status-level pass",
1688            "NEXTEST_STATUS_LEVEL=pass cargo nextest run --no-run",
1689            "cargo nextest run --no-run --final-status-level skip",
1690            "NEXTEST_FINAL_STATUS_LEVEL=skip cargo nextest run --no-run",
1691            // ---
1692            // --no-capture conflicts that produce warnings rather than errors
1693            // ---
1694            "cargo nextest run --no-capture --test-threads=24",
1695            "NEXTEST_NO_CAPTURE=1 cargo nextest run --test-threads=24",
1696            "cargo nextest run --no-capture --failure-output=never",
1697            "NEXTEST_NO_CAPTURE=1 cargo nextest run --failure-output=never",
1698            "cargo nextest run --no-capture --success-output=final",
1699            "NEXTEST_SUCCESS_OUTPUT=final cargo nextest run --no-capture",
1700            // ---
1701            // Cargo options
1702            // ---
1703            "cargo nextest list --lib --bins",
1704            "cargo nextest run --ignore-rust-version --unit-graph",
1705            // ---
1706            // Pager options
1707            // ---
1708            "cargo nextest list --no-pager",
1709            "cargo nextest show-config test-groups --no-pager",
1710            // ---
1711            // Reuse build options
1712            // ---
1713            "cargo nextest list --binaries-metadata=foo",
1714            "cargo nextest run --binaries-metadata=foo --target-dir-remap=bar",
1715            "cargo nextest list --cargo-metadata path",
1716            "cargo nextest run --cargo-metadata=path --workspace-remap remapped-path",
1717            "cargo nextest archive --archive-file my-archive.tar.zst --zstd-level -1",
1718            "cargo nextest archive --archive-file my-archive.foo --archive-format tar-zst",
1719            "cargo nextest archive --archive-file my-archive.foo --archive-format tar-zstd",
1720            "cargo nextest list --archive-file my-archive.tar.zst",
1721            "cargo nextest list --archive-file my-archive.tar.zst --archive-format tar-zst",
1722            "cargo nextest list --archive-file my-archive.tar.zst --extract-to my-path",
1723            "cargo nextest list --archive-file my-archive.tar.zst --extract-to my-path --extract-overwrite",
1724            "cargo nextest list --archive-file my-archive.tar.zst --persist-extract-tempdir",
1725            "cargo nextest list --archive-file my-archive.tar.zst --workspace-remap foo",
1726            "cargo nextest list --archive-file my-archive.tar.zst --config target.'cfg(all())'.runner=\"my-runner\"",
1727            // ---
1728            // Filtersets
1729            // ---
1730            "cargo nextest list -E deps(foo)",
1731            "cargo nextest run --filterset 'test(bar)' --package=my-package test-filter",
1732            "cargo nextest run --filter-expr 'test(bar)' --package=my-package test-filter",
1733            "cargo nextest list -E 'deps(foo)' --ignore-default-filter",
1734            // ---
1735            // Stress test options
1736            // ---
1737            "cargo nextest run --stress-count 4",
1738            "cargo nextest run --stress-count infinite",
1739            "cargo nextest run --stress-duration 60m",
1740            "cargo nextest run --stress-duration 24h",
1741            // ---
1742            // Test binary arguments
1743            // ---
1744            "cargo nextest run -- --a an arbitrary arg",
1745            // Test negative test threads
1746            "cargo nextest run --jobs -3",
1747            "cargo nextest run --jobs 3",
1748            // Test negative cargo build jobs
1749            "cargo nextest run --build-jobs -1",
1750            "cargo nextest run --build-jobs 1",
1751            // ---
1752            // Self update options
1753            // ---
1754            "cargo nextest self update",
1755            "cargo nextest self update --beta",
1756            "cargo nextest self update --rc",
1757            "cargo nextest self update --version 0.9.100",
1758            "cargo nextest self update --version latest",
1759            "cargo nextest self update --check",
1760            "cargo nextest self update --beta --check",
1761            "cargo nextest self update --rc --force",
1762            // ---
1763            // Bench command
1764            // ---
1765            "cargo nextest bench",
1766            "cargo nextest bench --no-run",
1767            "cargo nextest bench --fail-fast",
1768            "cargo nextest bench --no-fail-fast",
1769            "cargo nextest bench --max-fail 3",
1770            "cargo nextest bench --max-fail=all",
1771            "cargo nextest bench --stress-count 4",
1772            "cargo nextest bench --stress-count infinite",
1773            "cargo nextest bench --stress-duration 60m",
1774            "cargo nextest bench --debugger gdb",
1775            "cargo nextest bench --tracer strace",
1776        ];
1777
1778        let invalid: &[(&'static str, ErrorKind)] = &[
1779            // ---
1780            // --no-run and these options conflict
1781            // ---
1782            ("cargo nextest run --no-run --fail-fast", ArgumentConflict),
1783            (
1784                "cargo nextest run --no-run --no-fail-fast",
1785                ArgumentConflict,
1786            ),
1787            ("cargo nextest run --no-run --max-fail=3", ArgumentConflict),
1788            // ---
1789            // --max-fail and these options conflict
1790            // ---
1791            (
1792                "cargo nextest run --max-fail=3 --no-fail-fast",
1793                ArgumentConflict,
1794            ),
1795            // ---
1796            // Reuse build options conflict with cargo options
1797            // ---
1798            (
1799                // NOTE: cargo nextest --manifest-path foo run --cargo-metadata bar is currently
1800                // accepted. This is a bug: https://github.com/clap-rs/clap/issues/1204
1801                "cargo nextest run --manifest-path foo --cargo-metadata bar",
1802                ArgumentConflict,
1803            ),
1804            (
1805                "cargo nextest run --binaries-metadata=foo --lib",
1806                ArgumentConflict,
1807            ),
1808            // ---
1809            // workspace-remap requires cargo-metadata
1810            // ---
1811            (
1812                "cargo nextest run --workspace-remap foo",
1813                MissingRequiredArgument,
1814            ),
1815            // ---
1816            // target-dir-remap requires binaries-metadata
1817            // ---
1818            (
1819                "cargo nextest run --target-dir-remap bar",
1820                MissingRequiredArgument,
1821            ),
1822            // ---
1823            // Archive options
1824            // ---
1825            (
1826                "cargo nextest run --archive-format tar-zst",
1827                MissingRequiredArgument,
1828            ),
1829            (
1830                "cargo nextest run --archive-file foo --archive-format no",
1831                InvalidValue,
1832            ),
1833            (
1834                "cargo nextest run --extract-to foo",
1835                MissingRequiredArgument,
1836            ),
1837            (
1838                "cargo nextest run --archive-file foo --extract-overwrite",
1839                MissingRequiredArgument,
1840            ),
1841            (
1842                "cargo nextest run --extract-to foo --extract-overwrite",
1843                MissingRequiredArgument,
1844            ),
1845            (
1846                "cargo nextest run --persist-extract-tempdir",
1847                MissingRequiredArgument,
1848            ),
1849            (
1850                "cargo nextest run --archive-file foo --extract-to bar --persist-extract-tempdir",
1851                ArgumentConflict,
1852            ),
1853            (
1854                "cargo nextest run --archive-file foo --cargo-metadata bar",
1855                ArgumentConflict,
1856            ),
1857            (
1858                "cargo nextest run --archive-file foo --binaries-metadata bar",
1859                ArgumentConflict,
1860            ),
1861            (
1862                "cargo nextest run --archive-file foo --target-dir-remap bar",
1863                ArgumentConflict,
1864            ),
1865            // Invalid test threads: 0
1866            ("cargo nextest run --jobs 0", ValueValidation),
1867            // Test threads must be a number
1868            ("cargo nextest run --jobs -twenty", UnknownArgument),
1869            ("cargo nextest run --build-jobs -inf1", UnknownArgument),
1870            // Invalid stress count: 0
1871            ("cargo nextest run --stress-count 0", ValueValidation),
1872            // Invalid stress duration: 0
1873            ("cargo nextest run --stress-duration 0m", ValueValidation),
1874            // ---
1875            // --debugger conflicts with stress testing and --no-run
1876            // ---
1877            (
1878                "cargo nextest run --debugger gdb --stress-count 4",
1879                ArgumentConflict,
1880            ),
1881            (
1882                "cargo nextest run --debugger gdb --stress-duration 1h",
1883                ArgumentConflict,
1884            ),
1885            (
1886                "cargo nextest run --debugger gdb --no-run",
1887                ArgumentConflict,
1888            ),
1889            // ---
1890            // Bench command conflicts
1891            // ---
1892            ("cargo nextest bench --no-run --fail-fast", ArgumentConflict),
1893            (
1894                "cargo nextest bench --no-run --no-fail-fast",
1895                ArgumentConflict,
1896            ),
1897            (
1898                "cargo nextest bench --no-run --max-fail=3",
1899                ArgumentConflict,
1900            ),
1901            (
1902                "cargo nextest bench --max-fail=3 --no-fail-fast",
1903                ArgumentConflict,
1904            ),
1905            (
1906                "cargo nextest bench --debugger gdb --stress-count 4",
1907                ArgumentConflict,
1908            ),
1909            (
1910                "cargo nextest bench --debugger gdb --stress-duration 1h",
1911                ArgumentConflict,
1912            ),
1913            (
1914                "cargo nextest bench --debugger gdb --no-run",
1915                ArgumentConflict,
1916            ),
1917            (
1918                "cargo nextest bench --tracer strace --stress-count 4",
1919                ArgumentConflict,
1920            ),
1921            // Invalid stress count: 0
1922            ("cargo nextest bench --stress-count 0", ValueValidation),
1923            // Invalid stress duration: 0
1924            ("cargo nextest bench --stress-duration 0m", ValueValidation),
1925            // ---
1926            // Self update option conflicts
1927            // ---
1928            ("cargo nextest self update --beta --rc", ArgumentConflict),
1929            (
1930                "cargo nextest self update --beta --version 0.9.100",
1931                ArgumentConflict,
1932            ),
1933            (
1934                "cargo nextest self update --rc --version 0.9.100",
1935                ArgumentConflict,
1936            ),
1937        ];
1938
1939        // Unset all NEXTEST_ env vars because they can conflict with the try_parse_from below.
1940        for (k, _) in std::env::vars() {
1941            if k.starts_with("NEXTEST_") {
1942                // SAFETY:
1943                // https://nexte.st/docs/configuration/env-vars/#altering-the-environment-within-tests
1944                unsafe { std::env::remove_var(k) };
1945            }
1946        }
1947
1948        for valid_args in valid {
1949            let cmd = shell_words::split(valid_args).expect("valid command line");
1950            // Any args in the beginning with an equals sign should be parsed as environment variables.
1951            let env_vars: Vec<_> = cmd
1952                .iter()
1953                .take_while(|arg| arg.contains('='))
1954                .cloned()
1955                .collect();
1956
1957            let mut env_keys = Vec::with_capacity(env_vars.len());
1958            for k_v in &env_vars {
1959                let (k, v) = k_v.split_once('=').expect("valid env var");
1960                // SAFETY:
1961                // https://nexte.st/docs/configuration/env-vars/#altering-the-environment-within-tests
1962                unsafe { std::env::set_var(k, v) };
1963                env_keys.push(k);
1964            }
1965
1966            let cmd = cmd.iter().skip(env_vars.len());
1967
1968            if let Err(error) = CargoNextestApp::try_parse_from(cmd) {
1969                panic!("{valid_args} should have successfully parsed, but didn't: {error}");
1970            }
1971
1972            // Unset any environment variables we set. (Don't really need to preserve the old value
1973            // for now.)
1974            for &k in &env_keys {
1975                // SAFETY:
1976                // https://nexte.st/docs/configuration/env-vars/#altering-the-environment-within-tests
1977                unsafe { std::env::remove_var(k) };
1978            }
1979        }
1980
1981        for &(invalid_args, kind) in invalid {
1982            match CargoNextestApp::try_parse_from(
1983                shell_words::split(invalid_args).expect("valid command"),
1984            ) {
1985                Ok(_) => {
1986                    panic!("{invalid_args} should have errored out but successfully parsed");
1987                }
1988                Err(error) => {
1989                    let actual_kind = error.kind();
1990                    if kind != actual_kind {
1991                        panic!(
1992                            "{invalid_args} should error with kind {kind:?}, but actual kind was {actual_kind:?}",
1993                        );
1994                    }
1995                }
1996            }
1997        }
1998    }
1999
2000    #[derive(Debug, clap::Parser)]
2001    struct TestCli {
2002        #[structopt(flatten)]
2003        build_filter: TestBuildFilter,
2004    }
2005
2006    #[test]
2007    fn test_test_binary_argument_parsing() {
2008        fn get_test_filter_builder(cmd: &str) -> Result<TestFilterBuilder> {
2009            let app = TestCli::try_parse_from(shell_words::split(cmd).expect("valid command line"))
2010                .unwrap_or_else(|_| panic!("{cmd} should have successfully parsed"));
2011            app.build_filter
2012                .make_test_filter_builder(NextestRunMode::Test, vec![])
2013        }
2014
2015        let valid = &[
2016            // ---
2017            // substring filter
2018            // ---
2019            ("foo -- str1", "foo str1"),
2020            ("foo -- str2 str3", "foo str2 str3"),
2021            // ---
2022            // ignored
2023            // ---
2024            ("foo -- --ignored", "foo --run-ignored only"),
2025            ("foo -- --ignored", "foo --run-ignored ignored-only"),
2026            ("foo -- --include-ignored", "foo --run-ignored all"),
2027            // ---
2028            // two escapes
2029            // ---
2030            (
2031                "foo -- --ignored -- str --- --ignored",
2032                "foo --run-ignored ignored-only str -- -- --- --ignored",
2033            ),
2034            ("foo -- -- str1 str2 --", "foo str1 str2 -- -- --"),
2035        ];
2036        let skip_exact = &[
2037            // ---
2038            // skip
2039            // ---
2040            ("foo -- --skip my-pattern --skip your-pattern", {
2041                let mut patterns = TestFilterPatterns::default();
2042                patterns.add_skip_pattern("my-pattern".to_owned());
2043                patterns.add_skip_pattern("your-pattern".to_owned());
2044                patterns
2045            }),
2046            ("foo -- pattern1 --skip my-pattern --skip your-pattern", {
2047                let mut patterns = TestFilterPatterns::default();
2048                patterns.add_substring_pattern("pattern1".to_owned());
2049                patterns.add_skip_pattern("my-pattern".to_owned());
2050                patterns.add_skip_pattern("your-pattern".to_owned());
2051                patterns
2052            }),
2053            // ---
2054            // skip and exact
2055            // ---
2056            (
2057                "foo -- --skip my-pattern --skip your-pattern exact1 --exact pattern2",
2058                {
2059                    let mut patterns = TestFilterPatterns::default();
2060                    patterns.add_skip_exact_pattern("my-pattern".to_owned());
2061                    patterns.add_skip_exact_pattern("your-pattern".to_owned());
2062                    patterns.add_exact_pattern("exact1".to_owned());
2063                    patterns.add_exact_pattern("pattern2".to_owned());
2064                    patterns
2065                },
2066            ),
2067        ];
2068        let invalid = &[
2069            // ---
2070            // duplicated
2071            // ---
2072            ("foo -- --include-ignored --include-ignored", "duplicated"),
2073            ("foo -- --ignored --ignored", "duplicated"),
2074            ("foo -- --exact --exact", "duplicated"),
2075            // ---
2076            // mutually exclusive
2077            // ---
2078            ("foo -- --ignored --include-ignored", "mutually exclusive"),
2079            ("foo --run-ignored all -- --ignored", "mutually exclusive"),
2080            // ---
2081            // missing required argument
2082            // ---
2083            ("foo -- --skip", "missing required argument"),
2084            // ---
2085            // unsupported
2086            // ---
2087            ("foo -- --bar", "unsupported"),
2088        ];
2089
2090        for (a, b) in valid {
2091            let a_str = format!(
2092                "{:?}",
2093                get_test_filter_builder(a).unwrap_or_else(|_| panic!("failed to parse {a}"))
2094            );
2095            let b_str = format!(
2096                "{:?}",
2097                get_test_filter_builder(b).unwrap_or_else(|_| panic!("failed to parse {b}"))
2098            );
2099            assert_eq!(a_str, b_str);
2100        }
2101
2102        for (args, patterns) in skip_exact {
2103            let builder =
2104                get_test_filter_builder(args).unwrap_or_else(|_| panic!("failed to parse {args}"));
2105
2106            let builder2 = TestFilterBuilder::new(
2107                NextestRunMode::Test,
2108                RunIgnored::Default,
2109                None,
2110                patterns.clone(),
2111                Vec::new(),
2112            )
2113            .unwrap_or_else(|_| panic!("failed to build TestFilterBuilder"));
2114
2115            assert_eq!(builder, builder2, "{args} matches expected");
2116        }
2117
2118        for (s, r) in invalid {
2119            let res = get_test_filter_builder(s);
2120            if let Err(ExpectedError::TestBinaryArgsParseError { reason, .. }) = &res {
2121                assert_eq!(reason, r);
2122            } else {
2123                panic!(
2124                    "{s} should have errored out with TestBinaryArgsParseError, actual: {res:?}",
2125                );
2126            }
2127        }
2128    }
2129}