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