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