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