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    #[arg(long, env = "NEXTEST_SHOW_PROGRESS")]
860    show_progress: Option<ShowProgressOpt>,
861
862    /// Do not display the progress bar. Deprecated, use **--show-progress** instead.
863    #[arg(long, env = "NEXTEST_HIDE_PROGRESS_BAR", value_parser = BoolishValueParser::new())]
864    hide_progress_bar: bool,
865
866    /// Do not indent captured test output.
867    ///
868    /// By default, test output produced by **--failure-output** and
869    /// **--success-output** is indented for visual clarity. This flag disables
870    /// that behavior.
871    ///
872    /// This option has no effect with **--no-capture**, since that passes
873    /// through standard output and standard error.
874    #[arg(long, env = "NEXTEST_NO_OUTPUT_INDENT", value_parser = BoolishValueParser::new())]
875    no_output_indent: bool,
876
877    /// Disable handling of input keys from the terminal.
878    ///
879    /// By default, when running a terminal, nextest accepts the `t` key to dump
880    /// test information. This flag disables that behavior.
881    #[arg(long, env = "NEXTEST_NO_INPUT_HANDLER", value_parser = BoolishValueParser::new())]
882    pub(super) no_input_handler: bool,
883
884    /// Format to use for test results (experimental).
885    #[arg(
886        long,
887        name = "message-format",
888        value_enum,
889        value_name = "FORMAT",
890        env = "NEXTEST_MESSAGE_FORMAT"
891    )]
892    pub(super) message_format: Option<MessageFormat>,
893
894    /// Version of structured message-format to use (experimental).
895    ///
896    /// This allows the machine-readable formats to use a stable structure for consistent
897    /// consumption across changes to nextest. If not specified, the latest version is used.
898    #[arg(
899        long,
900        requires = "message-format",
901        value_name = "VERSION",
902        env = "NEXTEST_MESSAGE_FORMAT_VERSION"
903    )]
904    pub(super) message_format_version: Option<String>,
905}
906
907impl ReporterOpts {
908    pub(super) fn to_builder(
909        &self,
910        no_run: bool,
911        no_capture: bool,
912        should_colorize: bool,
913    ) -> ReporterBuilder {
914        // Warn on conflicts between options. This is a warning and not an error
915        // because these options can be specified via environment variables as
916        // well.
917        if no_run && no_capture {
918            warn!("ignoring --no-capture because --no-run is specified");
919        }
920
921        let reasons = no_run_no_capture_reasons(no_run, no_capture);
922
923        if self.failure_output.is_some() {
924            if let Some(reasons) = reasons {
925                warn!("ignoring --failure-output because {}", reasons);
926            }
927        }
928        if self.success_output.is_some() {
929            if let Some(reasons) = reasons {
930                warn!("ignoring --success-output because {}", reasons);
931            }
932        }
933        if self.status_level.is_some() && no_run {
934            warn!("ignoring --status-level because --no-run is specified");
935        }
936        if self.final_status_level.is_some() && no_run {
937            warn!("ignoring --final-status-level because --no-run is specified");
938        }
939        if self.message_format.is_some() && no_run {
940            warn!("ignoring --message-format because --no-run is specified");
941        }
942        if self.message_format_version.is_some() && no_run {
943            warn!("ignoring --message-format-version because --no-run is specified");
944        }
945
946        let show_progress = match (self.show_progress, self.hide_progress_bar) {
947            (Some(show_progress), true) => {
948                warn!("ignoring --hide-progress-bar because --show-progress is specified");
949                show_progress
950            }
951            (Some(show_progress), false) => show_progress,
952            (None, true) => ShowProgressOpt::None,
953            (None, false) => ShowProgressOpt::default(),
954        };
955
956        // ---
957
958        let mut builder = ReporterBuilder::default();
959        builder.set_no_capture(no_capture);
960        builder.set_colorize(should_colorize);
961
962        if let Some(failure_output) = self.failure_output {
963            builder.set_failure_output(failure_output.into());
964        }
965        if let Some(success_output) = self.success_output {
966            builder.set_success_output(success_output.into());
967        }
968        if let Some(status_level) = self.status_level {
969            builder.set_status_level(status_level.into());
970        }
971        if let Some(final_status_level) = self.final_status_level {
972            builder.set_final_status_level(final_status_level.into());
973        }
974        builder.set_show_progress(show_progress.into());
975        builder.set_no_output_indent(self.no_output_indent);
976        builder
977    }
978}
979
980#[derive(Clone, Copy, Debug, ValueEnum)]
981enum TestOutputDisplayOpt {
982    Immediate,
983    ImmediateFinal,
984    Final,
985    Never,
986}
987
988impl From<TestOutputDisplayOpt> for TestOutputDisplay {
989    fn from(opt: TestOutputDisplayOpt) -> Self {
990        match opt {
991            TestOutputDisplayOpt::Immediate => TestOutputDisplay::Immediate,
992            TestOutputDisplayOpt::ImmediateFinal => TestOutputDisplay::ImmediateFinal,
993            TestOutputDisplayOpt::Final => TestOutputDisplay::Final,
994            TestOutputDisplayOpt::Never => TestOutputDisplay::Never,
995        }
996    }
997}
998
999#[derive(Clone, Copy, Debug, ValueEnum)]
1000enum StatusLevelOpt {
1001    None,
1002    Fail,
1003    Retry,
1004    Slow,
1005    Leak,
1006    Pass,
1007    Skip,
1008    All,
1009}
1010
1011impl From<StatusLevelOpt> for StatusLevel {
1012    fn from(opt: StatusLevelOpt) -> Self {
1013        match opt {
1014            StatusLevelOpt::None => StatusLevel::None,
1015            StatusLevelOpt::Fail => StatusLevel::Fail,
1016            StatusLevelOpt::Retry => StatusLevel::Retry,
1017            StatusLevelOpt::Slow => StatusLevel::Slow,
1018            StatusLevelOpt::Leak => StatusLevel::Leak,
1019            StatusLevelOpt::Pass => StatusLevel::Pass,
1020            StatusLevelOpt::Skip => StatusLevel::Skip,
1021            StatusLevelOpt::All => StatusLevel::All,
1022        }
1023    }
1024}
1025
1026#[derive(Clone, Copy, Debug, ValueEnum)]
1027enum FinalStatusLevelOpt {
1028    None,
1029    Fail,
1030    #[clap(alias = "retry")]
1031    Flaky,
1032    Slow,
1033    Skip,
1034    Pass,
1035    All,
1036}
1037
1038impl From<FinalStatusLevelOpt> for FinalStatusLevel {
1039    fn from(opt: FinalStatusLevelOpt) -> Self {
1040        match opt {
1041            FinalStatusLevelOpt::None => FinalStatusLevel::None,
1042            FinalStatusLevelOpt::Fail => FinalStatusLevel::Fail,
1043            FinalStatusLevelOpt::Flaky => FinalStatusLevel::Flaky,
1044            FinalStatusLevelOpt::Slow => FinalStatusLevel::Slow,
1045            FinalStatusLevelOpt::Skip => FinalStatusLevel::Skip,
1046            FinalStatusLevelOpt::Pass => FinalStatusLevel::Pass,
1047            FinalStatusLevelOpt::All => FinalStatusLevel::All,
1048        }
1049    }
1050}
1051
1052#[derive(Default, Clone, Copy, Debug, ValueEnum)]
1053enum ShowProgressOpt {
1054    #[default]
1055    Auto,
1056    None,
1057    Bar,
1058    Counter,
1059}
1060
1061impl From<ShowProgressOpt> for ShowProgress {
1062    fn from(opt: ShowProgressOpt) -> Self {
1063        match opt {
1064            ShowProgressOpt::Auto => ShowProgress::Auto,
1065            ShowProgressOpt::None => ShowProgress::None,
1066            ShowProgressOpt::Bar => ShowProgress::Bar,
1067            ShowProgressOpt::Counter => ShowProgress::Counter,
1068        }
1069    }
1070}
1071
1072/// A next-generation test runner for Rust.
1073///
1074/// This binary should typically be invoked as `cargo nextest` (in which case
1075/// this message will not be seen), not `cargo-nextest`.
1076#[derive(Debug, clap::Parser)]
1077#[command(
1078    version = crate::version::short(),
1079    long_version = crate::version::long(),
1080    bin_name = "cargo",
1081    styles = crate::output::clap_styles::style(),
1082    max_term_width = 100,
1083)]
1084pub struct CargoNextestApp {
1085    #[clap(subcommand)]
1086    subcommand: NextestSubcommand,
1087}
1088
1089impl CargoNextestApp {
1090    /// Initializes the output context.
1091    pub fn init_output(&self) -> OutputContext {
1092        match &self.subcommand {
1093            NextestSubcommand::Nextest(args) => args.common.output.init(),
1094            NextestSubcommand::Ntr(args) => args.common.output.init(),
1095            #[cfg(unix)]
1096            // Double-spawned processes should never use coloring.
1097            NextestSubcommand::DoubleSpawn(_) => OutputContext::color_never_init(),
1098        }
1099    }
1100
1101    /// Executes the app.
1102    pub fn exec(
1103        self,
1104        cli_args: Vec<String>,
1105        output: OutputContext,
1106        output_writer: &mut crate::output::OutputWriter,
1107    ) -> Result<i32> {
1108        if let Err(err) = nextest_runner::usdt::register_probes() {
1109            tracing::warn!("failed to register USDT probes: {}", err);
1110        }
1111
1112        match self.subcommand {
1113            NextestSubcommand::Nextest(app) => app.exec(cli_args, output, output_writer),
1114            NextestSubcommand::Ntr(opts) => opts.exec(cli_args, output, output_writer),
1115            #[cfg(unix)]
1116            NextestSubcommand::DoubleSpawn(opts) => opts.exec(output),
1117        }
1118    }
1119}
1120
1121#[derive(Debug, Subcommand)]
1122enum NextestSubcommand {
1123    /// A next-generation test runner for Rust. <https://nexte.st>
1124    Nextest(Box<AppOpts>),
1125    /// Build and run tests: a shortcut for `cargo nextest run`.
1126    Ntr(Box<NtrOpts>),
1127    /// Private command, used to double-spawn test processes.
1128    #[cfg(unix)]
1129    #[command(name = nextest_runner::double_spawn::DoubleSpawnInfo::SUBCOMMAND_NAME, hide = true)]
1130    DoubleSpawn(crate::double_spawn::DoubleSpawnOpts),
1131}
1132
1133#[derive(Debug, Args)]
1134#[clap(
1135    version = crate::version::short(),
1136    long_version = crate::version::long(),
1137    display_name = "cargo-nextest",
1138)]
1139pub(super) struct AppOpts {
1140    #[clap(flatten)]
1141    common: CommonOpts,
1142
1143    #[clap(subcommand)]
1144    command: Command,
1145}
1146
1147impl AppOpts {
1148    /// Execute the command.
1149    ///
1150    /// Returns the exit code.
1151    fn exec(
1152        self,
1153        cli_args: Vec<String>,
1154        output: OutputContext,
1155        output_writer: &mut crate::output::OutputWriter,
1156    ) -> Result<i32> {
1157        match self.command {
1158            Command::List {
1159                cargo_options,
1160                build_filter,
1161                message_format,
1162                list_type,
1163                reuse_build,
1164            } => {
1165                let base = super::execution::BaseApp::new(
1166                    output,
1167                    reuse_build,
1168                    cargo_options,
1169                    self.common.config_opts,
1170                    self.common.manifest_path,
1171                    output_writer,
1172                )?;
1173                let app = super::execution::App::new(base, build_filter)?;
1174                app.exec_list(message_format, list_type, output_writer)?;
1175                Ok(0)
1176            }
1177            Command::Run(run_opts) => {
1178                let base = super::execution::BaseApp::new(
1179                    output,
1180                    run_opts.reuse_build,
1181                    run_opts.cargo_options,
1182                    self.common.config_opts,
1183                    self.common.manifest_path,
1184                    output_writer,
1185                )?;
1186                let app = super::execution::App::new(base, run_opts.build_filter)?;
1187                app.exec_run(
1188                    run_opts.no_capture,
1189                    &run_opts.runner_opts,
1190                    &run_opts.reporter_opts,
1191                    cli_args,
1192                    output_writer,
1193                )?;
1194                Ok(0)
1195            }
1196            Command::Archive {
1197                cargo_options,
1198                archive_file,
1199                archive_format,
1200                archive_build_filter,
1201                zstd_level,
1202            } => {
1203                let app = super::execution::BaseApp::new(
1204                    output,
1205                    ReuseBuildOpts::default(),
1206                    cargo_options,
1207                    self.common.config_opts,
1208                    self.common.manifest_path,
1209                    output_writer,
1210                )?;
1211
1212                let app = super::execution::ArchiveApp::new(app, archive_build_filter)?;
1213                app.exec_archive(&archive_file, archive_format, zstd_level, output_writer)?;
1214                Ok(0)
1215            }
1216            Command::ShowConfig { command } => command.exec(
1217                self.common.manifest_path,
1218                self.common.config_opts,
1219                output,
1220                output_writer,
1221            ),
1222            Command::Self_ { command } => command.exec(self.common.output),
1223            Command::Debug { command } => command.exec(self.common.output),
1224        }
1225    }
1226}
1227
1228#[derive(Debug, Args)]
1229struct NtrOpts {
1230    #[clap(flatten)]
1231    common: CommonOpts,
1232
1233    #[clap(flatten)]
1234    run_opts: RunOpts,
1235}
1236
1237impl NtrOpts {
1238    fn exec(
1239        self,
1240        cli_args: Vec<String>,
1241        output: OutputContext,
1242        output_writer: &mut crate::output::OutputWriter,
1243    ) -> Result<i32> {
1244        let base = super::execution::BaseApp::new(
1245            output,
1246            self.run_opts.reuse_build,
1247            self.run_opts.cargo_options,
1248            self.common.config_opts,
1249            self.common.manifest_path,
1250            output_writer,
1251        )?;
1252        let app = super::execution::App::new(base, self.run_opts.build_filter)?;
1253        app.exec_run(
1254            self.run_opts.no_capture,
1255            &self.run_opts.runner_opts,
1256            &self.run_opts.reporter_opts,
1257            cli_args,
1258            output_writer,
1259        )
1260    }
1261}
1262
1263#[cfg(test)]
1264mod tests {
1265    use super::*;
1266    use clap::Parser;
1267
1268    #[test]
1269    fn test_argument_parsing() {
1270        use clap::error::ErrorKind::{self, *};
1271
1272        let valid: &[&'static str] = &[
1273            // ---
1274            // Basic commands
1275            // ---
1276            "cargo nextest list",
1277            "cargo nextest run",
1278            // ---
1279            // Commands with arguments
1280            // ---
1281            "cargo nextest list --list-type binaries-only",
1282            "cargo nextest list --list-type full",
1283            "cargo nextest list --message-format json-pretty",
1284            "cargo nextest run --failure-output never",
1285            "cargo nextest run --success-output=immediate",
1286            "cargo nextest run --status-level=all",
1287            "cargo nextest run --no-capture",
1288            "cargo nextest run --nocapture",
1289            "cargo nextest run --no-run",
1290            "cargo nextest run --final-status-level flaky",
1291            "cargo nextest run --max-fail 3",
1292            "cargo nextest run --max-fail=all",
1293            // retry is an alias for flaky -- ensure that it parses
1294            "cargo nextest run --final-status-level retry",
1295            "NEXTEST_HIDE_PROGRESS_BAR=1 cargo nextest run",
1296            "NEXTEST_HIDE_PROGRESS_BAR=true cargo nextest run",
1297            // ---
1298            // --no-run conflicts that produce warnings rather than errors
1299            // ---
1300            "cargo nextest run --no-run -j8",
1301            "cargo nextest run --no-run --retries 3",
1302            "NEXTEST_TEST_THREADS=8 cargo nextest run --no-run",
1303            "cargo nextest run --no-run --success-output never",
1304            "NEXTEST_SUCCESS_OUTPUT=never cargo nextest run --no-run",
1305            "cargo nextest run --no-run --failure-output immediate",
1306            "NEXTEST_FAILURE_OUTPUT=immediate cargo nextest run --no-run",
1307            "cargo nextest run --no-run --status-level pass",
1308            "NEXTEST_STATUS_LEVEL=pass cargo nextest run --no-run",
1309            "cargo nextest run --no-run --final-status-level skip",
1310            "NEXTEST_FINAL_STATUS_LEVEL=skip cargo nextest run --no-run",
1311            // ---
1312            // --no-capture conflicts that produce warnings rather than errors
1313            // ---
1314            "cargo nextest run --no-capture --test-threads=24",
1315            "NEXTEST_NO_CAPTURE=1 cargo nextest run --test-threads=24",
1316            "cargo nextest run --no-capture --failure-output=never",
1317            "NEXTEST_NO_CAPTURE=1 cargo nextest run --failure-output=never",
1318            "cargo nextest run --no-capture --success-output=final",
1319            "NEXTEST_SUCCESS_OUTPUT=final cargo nextest run --no-capture",
1320            // ---
1321            // Cargo options
1322            // ---
1323            "cargo nextest list --lib --bins",
1324            "cargo nextest run --ignore-rust-version --unit-graph",
1325            // ---
1326            // Reuse build options
1327            // ---
1328            "cargo nextest list --binaries-metadata=foo",
1329            "cargo nextest run --binaries-metadata=foo --target-dir-remap=bar",
1330            "cargo nextest list --cargo-metadata path",
1331            "cargo nextest run --cargo-metadata=path --workspace-remap remapped-path",
1332            "cargo nextest archive --archive-file my-archive.tar.zst --zstd-level -1",
1333            "cargo nextest archive --archive-file my-archive.foo --archive-format tar-zst",
1334            "cargo nextest archive --archive-file my-archive.foo --archive-format tar-zstd",
1335            "cargo nextest list --archive-file my-archive.tar.zst",
1336            "cargo nextest list --archive-file my-archive.tar.zst --archive-format tar-zst",
1337            "cargo nextest list --archive-file my-archive.tar.zst --extract-to my-path",
1338            "cargo nextest list --archive-file my-archive.tar.zst --extract-to my-path --extract-overwrite",
1339            "cargo nextest list --archive-file my-archive.tar.zst --persist-extract-tempdir",
1340            "cargo nextest list --archive-file my-archive.tar.zst --workspace-remap foo",
1341            "cargo nextest list --archive-file my-archive.tar.zst --config target.'cfg(all())'.runner=\"my-runner\"",
1342            // ---
1343            // Filtersets
1344            // ---
1345            "cargo nextest list -E deps(foo)",
1346            "cargo nextest run --filterset 'test(bar)' --package=my-package test-filter",
1347            "cargo nextest run --filter-expr 'test(bar)' --package=my-package test-filter",
1348            "cargo nextest list -E 'deps(foo)' --ignore-default-filter",
1349            // ---
1350            // Stress test options
1351            // ---
1352            "cargo nextest run --stress-count 4",
1353            "cargo nextest run --stress-count infinite",
1354            "cargo nextest run --stress-duration 60m",
1355            "cargo nextest run --stress-duration 24h",
1356            // ---
1357            // Test binary arguments
1358            // ---
1359            "cargo nextest run -- --a an arbitrary arg",
1360            // Test negative test threads
1361            "cargo nextest run --jobs -3",
1362            "cargo nextest run --jobs 3",
1363            // Test negative cargo build jobs
1364            "cargo nextest run --build-jobs -1",
1365            "cargo nextest run --build-jobs 1",
1366        ];
1367
1368        let invalid: &[(&'static str, ErrorKind)] = &[
1369            // ---
1370            // --no-run and these options conflict
1371            // ---
1372            ("cargo nextest run --no-run --fail-fast", ArgumentConflict),
1373            (
1374                "cargo nextest run --no-run --no-fail-fast",
1375                ArgumentConflict,
1376            ),
1377            ("cargo nextest run --no-run --max-fail=3", ArgumentConflict),
1378            // ---
1379            // --max-fail and these options conflict
1380            // ---
1381            (
1382                "cargo nextest run --max-fail=3 --no-fail-fast",
1383                ArgumentConflict,
1384            ),
1385            // ---
1386            // Reuse build options conflict with cargo options
1387            // ---
1388            (
1389                // NOTE: cargo nextest --manifest-path foo run --cargo-metadata bar is currently
1390                // accepted. This is a bug: https://github.com/clap-rs/clap/issues/1204
1391                "cargo nextest run --manifest-path foo --cargo-metadata bar",
1392                ArgumentConflict,
1393            ),
1394            (
1395                "cargo nextest run --binaries-metadata=foo --lib",
1396                ArgumentConflict,
1397            ),
1398            // ---
1399            // workspace-remap requires cargo-metadata
1400            // ---
1401            (
1402                "cargo nextest run --workspace-remap foo",
1403                MissingRequiredArgument,
1404            ),
1405            // ---
1406            // target-dir-remap requires binaries-metadata
1407            // ---
1408            (
1409                "cargo nextest run --target-dir-remap bar",
1410                MissingRequiredArgument,
1411            ),
1412            // ---
1413            // Archive options
1414            // ---
1415            (
1416                "cargo nextest run --archive-format tar-zst",
1417                MissingRequiredArgument,
1418            ),
1419            (
1420                "cargo nextest run --archive-file foo --archive-format no",
1421                InvalidValue,
1422            ),
1423            (
1424                "cargo nextest run --extract-to foo",
1425                MissingRequiredArgument,
1426            ),
1427            (
1428                "cargo nextest run --archive-file foo --extract-overwrite",
1429                MissingRequiredArgument,
1430            ),
1431            (
1432                "cargo nextest run --extract-to foo --extract-overwrite",
1433                MissingRequiredArgument,
1434            ),
1435            (
1436                "cargo nextest run --persist-extract-tempdir",
1437                MissingRequiredArgument,
1438            ),
1439            (
1440                "cargo nextest run --archive-file foo --extract-to bar --persist-extract-tempdir",
1441                ArgumentConflict,
1442            ),
1443            (
1444                "cargo nextest run --archive-file foo --cargo-metadata bar",
1445                ArgumentConflict,
1446            ),
1447            (
1448                "cargo nextest run --archive-file foo --binaries-metadata bar",
1449                ArgumentConflict,
1450            ),
1451            (
1452                "cargo nextest run --archive-file foo --target-dir-remap bar",
1453                ArgumentConflict,
1454            ),
1455            // Invalid test threads: 0
1456            ("cargo nextest run --jobs 0", ValueValidation),
1457            // Test threads must be a number
1458            ("cargo nextest run --jobs -twenty", UnknownArgument),
1459            ("cargo nextest run --build-jobs -inf1", UnknownArgument),
1460            // Invalid stress count: 0
1461            ("cargo nextest run --stress-count 0", ValueValidation),
1462            // Invalid stress duration: 0
1463            ("cargo nextest run --stress-duration 0m", ValueValidation),
1464        ];
1465
1466        // Unset all NEXTEST_ env vars because they can conflict with the try_parse_from below.
1467        for (k, _) in std::env::vars() {
1468            if k.starts_with("NEXTEST_") {
1469                // SAFETY:
1470                // https://nexte.st/docs/configuration/env-vars/#altering-the-environment-within-tests
1471                unsafe { std::env::remove_var(k) };
1472            }
1473        }
1474
1475        for valid_args in valid {
1476            let cmd = shell_words::split(valid_args).expect("valid command line");
1477            // Any args in the beginning with an equals sign should be parsed as environment variables.
1478            let env_vars: Vec<_> = cmd
1479                .iter()
1480                .take_while(|arg| arg.contains('='))
1481                .cloned()
1482                .collect();
1483
1484            let mut env_keys = Vec::with_capacity(env_vars.len());
1485            for k_v in &env_vars {
1486                let (k, v) = k_v.split_once('=').expect("valid env var");
1487                // SAFETY:
1488                // https://nexte.st/docs/configuration/env-vars/#altering-the-environment-within-tests
1489                unsafe { std::env::set_var(k, v) };
1490                env_keys.push(k);
1491            }
1492
1493            let cmd = cmd.iter().skip(env_vars.len());
1494
1495            if let Err(error) = CargoNextestApp::try_parse_from(cmd) {
1496                panic!("{valid_args} should have successfully parsed, but didn't: {error}");
1497            }
1498
1499            // Unset any environment variables we set. (Don't really need to preserve the old value
1500            // for now.)
1501            for &k in &env_keys {
1502                // SAFETY:
1503                // https://nexte.st/docs/configuration/env-vars/#altering-the-environment-within-tests
1504                unsafe { std::env::remove_var(k) };
1505            }
1506        }
1507
1508        for &(invalid_args, kind) in invalid {
1509            match CargoNextestApp::try_parse_from(
1510                shell_words::split(invalid_args).expect("valid command"),
1511            ) {
1512                Ok(_) => {
1513                    panic!("{invalid_args} should have errored out but successfully parsed");
1514                }
1515                Err(error) => {
1516                    let actual_kind = error.kind();
1517                    if kind != actual_kind {
1518                        panic!(
1519                            "{invalid_args} should error with kind {kind:?}, but actual kind was {actual_kind:?}",
1520                        );
1521                    }
1522                }
1523            }
1524        }
1525    }
1526
1527    #[derive(Debug, clap::Parser)]
1528    struct TestCli {
1529        #[structopt(flatten)]
1530        build_filter: TestBuildFilter,
1531    }
1532
1533    #[test]
1534    fn test_test_binary_argument_parsing() {
1535        fn get_test_filter_builder(cmd: &str) -> Result<TestFilterBuilder> {
1536            let app = TestCli::try_parse_from(shell_words::split(cmd).expect("valid command line"))
1537                .unwrap_or_else(|_| panic!("{cmd} should have successfully parsed"));
1538            app.build_filter.make_test_filter_builder(vec![])
1539        }
1540
1541        let valid = &[
1542            // ---
1543            // substring filter
1544            // ---
1545            ("foo -- str1", "foo str1"),
1546            ("foo -- str2 str3", "foo str2 str3"),
1547            // ---
1548            // ignored
1549            // ---
1550            ("foo -- --ignored", "foo --run-ignored only"),
1551            ("foo -- --ignored", "foo --run-ignored ignored-only"),
1552            ("foo -- --include-ignored", "foo --run-ignored all"),
1553            // ---
1554            // two escapes
1555            // ---
1556            (
1557                "foo -- --ignored -- str --- --ignored",
1558                "foo --run-ignored ignored-only str -- -- --- --ignored",
1559            ),
1560            ("foo -- -- str1 str2 --", "foo str1 str2 -- -- --"),
1561        ];
1562        let skip_exact = &[
1563            // ---
1564            // skip
1565            // ---
1566            ("foo -- --skip my-pattern --skip your-pattern", {
1567                let mut patterns = TestFilterPatterns::default();
1568                patterns.add_skip_pattern("my-pattern".to_owned());
1569                patterns.add_skip_pattern("your-pattern".to_owned());
1570                patterns
1571            }),
1572            ("foo -- pattern1 --skip my-pattern --skip your-pattern", {
1573                let mut patterns = TestFilterPatterns::default();
1574                patterns.add_substring_pattern("pattern1".to_owned());
1575                patterns.add_skip_pattern("my-pattern".to_owned());
1576                patterns.add_skip_pattern("your-pattern".to_owned());
1577                patterns
1578            }),
1579            // ---
1580            // skip and exact
1581            // ---
1582            (
1583                "foo -- --skip my-pattern --skip your-pattern exact1 --exact pattern2",
1584                {
1585                    let mut patterns = TestFilterPatterns::default();
1586                    patterns.add_skip_exact_pattern("my-pattern".to_owned());
1587                    patterns.add_skip_exact_pattern("your-pattern".to_owned());
1588                    patterns.add_exact_pattern("exact1".to_owned());
1589                    patterns.add_exact_pattern("pattern2".to_owned());
1590                    patterns
1591                },
1592            ),
1593        ];
1594        let invalid = &[
1595            // ---
1596            // duplicated
1597            // ---
1598            ("foo -- --include-ignored --include-ignored", "duplicated"),
1599            ("foo -- --ignored --ignored", "duplicated"),
1600            ("foo -- --exact --exact", "duplicated"),
1601            // ---
1602            // mutually exclusive
1603            // ---
1604            ("foo -- --ignored --include-ignored", "mutually exclusive"),
1605            ("foo --run-ignored all -- --ignored", "mutually exclusive"),
1606            // ---
1607            // missing required argument
1608            // ---
1609            ("foo -- --skip", "missing required argument"),
1610            // ---
1611            // unsupported
1612            // ---
1613            ("foo -- --bar", "unsupported"),
1614        ];
1615
1616        for (a, b) in valid {
1617            let a_str = format!(
1618                "{:?}",
1619                get_test_filter_builder(a).unwrap_or_else(|_| panic!("failed to parse {a}"))
1620            );
1621            let b_str = format!(
1622                "{:?}",
1623                get_test_filter_builder(b).unwrap_or_else(|_| panic!("failed to parse {b}"))
1624            );
1625            assert_eq!(a_str, b_str);
1626        }
1627
1628        for (args, patterns) in skip_exact {
1629            let builder =
1630                get_test_filter_builder(args).unwrap_or_else(|_| panic!("failed to parse {args}"));
1631
1632            let builder2 =
1633                TestFilterBuilder::new(RunIgnored::Default, None, patterns.clone(), Vec::new())
1634                    .unwrap_or_else(|_| panic!("failed to build TestFilterBuilder"));
1635
1636            assert_eq!(builder, builder2, "{args} matches expected");
1637        }
1638
1639        for (s, r) in invalid {
1640            let res = get_test_filter_builder(s);
1641            if let Err(ExpectedError::TestBinaryArgsParseError { reason, .. }) = &res {
1642                assert_eq!(reason, r);
1643            } else {
1644                panic!(
1645                    "{s} should have errored out with TestBinaryArgsParseError, actual: {res:?}",
1646                );
1647            }
1648        }
1649    }
1650}