1use 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#[derive(Debug, Args)]
50pub(super) struct CommonOpts {
51 #[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 #[arg(long, global = true, value_name = "PATH")]
72 pub config_file: Option<Utf8PathBuf>,
73
74 #[arg(long = "tool-config-file", global = true, value_name = "TOOL:ABS_PATH")]
88 pub tool_config_files: Vec<ToolConfigFile>,
89
90 #[arg(long, global = true)]
95 pub override_version_check: bool,
96
97 #[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 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 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(Box<ListOpts>),
157 #[command(visible_alias = "r")]
164 Run(Box<RunOpts>),
165 #[command(visible_alias = "b")]
173 Bench(Box<BenchOpts>),
174 Archive(Box<ArchiveOpts>),
182 ShowConfig {
189 #[clap(subcommand)]
190 command: super::commands::ShowConfigCommand,
191 },
192 #[clap(name = "self")]
194 Self_ {
195 #[clap(subcommand)]
196 command: super::commands::SelfCommand,
197 },
198 #[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 #[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 #[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 #[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 }
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 #[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 #[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 #[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 #[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 }
340
341#[derive(Debug, Default, Args)]
343#[command(next_help_heading = "Runner options")]
344pub(super) struct BenchRunnerOpts {
345 #[arg(long, name = "no-run")]
347 pub(super) no_run: bool,
348
349 #[arg(
351 long,
352 visible_alias = "ff",
353 name = "fail-fast",
354 conflicts_with = "no-run"
360 )]
361 fail_fast: bool,
362
363 #[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 #[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 #[arg(long, value_enum, value_name = "ACTION", env = "NEXTEST_NO_TESTS")]
385 pub(super) no_tests: Option<NoTestsBehavior>,
386
387 #[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 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 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#[derive(Debug, Default, Args)]
434#[command(next_help_heading = "Reporter options")]
435pub(super) struct BenchReporterOpts {
436 #[arg(long, env = "NEXTEST_SHOW_PROGRESS")]
441 show_progress: Option<ShowProgressOpt>,
442
443 #[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 #[default]
492 Auto,
493 Human,
495 Oneline,
497 Json,
499 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 #[arg(long, value_enum, value_name = "WHICH")]
526 run_ignored: Option<RunIgnoredOpt>,
527
528 #[arg(long)]
530 partition: Option<PartitionerBuilder>,
531
532 #[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 #[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 #[arg(long)]
561 ignore_default_filter: bool,
562
563 #[arg(help_heading = None, name = "FILTERS")]
565 pre_double_dash_filters: Vec<String>,
566
567 #[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 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 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 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 } 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 #[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 Default,
748
749 #[clap(alias = "ignored-only")]
751 Only,
752
753 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 let mut cargo_cli = CargoCli::new(cargo_command, manifest_path, output);
779
780 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#[derive(Debug, Default, Args)]
805#[command(next_help_heading = "Runner options")]
806pub struct TestRunnerOpts {
807 #[arg(long, name = "no-run")]
809 pub(super) no_run: bool,
810
811 #[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 #[arg(long, env = "NEXTEST_RETRIES", value_name = "N")]
825 retries: Option<u32>,
826
827 #[arg(
829 long,
830 visible_alias = "ff",
831 name = "fail-fast",
832 conflicts_with = "no-run"
838 )]
839 fail_fast: bool,
840
841 #[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 #[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 #[clap(flatten)]
868 pub(super) interceptor: InterceptorOpt,
869
870 #[arg(long, value_enum, value_name = "ACTION", env = "NEXTEST_NO_TESTS")]
872 pub(super) no_tests: Option<NoTestsBehavior>,
873
874 #[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 #[arg(long, value_name = "DEBUGGER", conflicts_with_all = ["stress_condition", "no-run"])]
892 pub(super) debugger: Option<DebuggerCommand>,
893
894 #[arg(long, value_name = "TRACER", conflicts_with_all = ["stress_condition", "no-run"])]
905 pub(super) tracer: Option<TracerCommand>,
906}
907
908impl InterceptorOpt {
909 pub(super) fn is_active(&self) -> bool {
911 self.debugger.is_some() || self.tracer.is_some()
912 }
913
914 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 Pass,
931
932 Warn,
934
935 #[clap(alias = "error")]
937 Fail,
938}
939
940impl TestRunnerOpts {
941 pub(super) fn to_builder(&self, cap_strat: CaptureStrategy) -> Option<TestRunnerBuilder> {
942 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 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 #[default]
1015 Human,
1016 LibtestJson,
1018 LibtestJsonPlus,
1021}
1022
1023#[derive(Debug, Default, Args)]
1024#[command(next_help_heading = "Stress testing options")]
1025pub(super) struct StressOptions {
1026 #[clap(flatten)]
1028 pub(super) condition: Option<StressConditionOpt>,
1029 }
1031
1032#[derive(Clone, Debug, Default, Args)]
1033#[group(id = "stress_condition", multiple = false)]
1034pub(super) struct StressConditionOpt {
1035 #[arg(long, value_name = "COUNT")]
1037 stress_count: Option<StressCount>,
1038
1039 #[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 #[arg(long, value_enum, value_name = "WHEN", env = "NEXTEST_FAILURE_OUTPUT")]
1073 failure_output: Option<TestOutputDisplayOpt>,
1074
1075 #[arg(long, value_enum, value_name = "WHEN", env = "NEXTEST_SUCCESS_OUTPUT")]
1077 success_output: Option<TestOutputDisplayOpt>,
1078
1079 #[arg(long, value_enum, value_name = "LEVEL", env = "NEXTEST_STATUS_LEVEL")]
1082 status_level: Option<StatusLevelOpt>,
1083
1084 #[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 #[arg(long, env = "NEXTEST_SHOW_PROGRESS")]
1095 show_progress: Option<ShowProgressOpt>,
1096
1097 #[arg(long, env = "NEXTEST_HIDE_PROGRESS_BAR", value_parser = BoolishValueParser::new())]
1099 hide_progress_bar: bool,
1100
1101 #[arg(long, env = "NEXTEST_NO_OUTPUT_INDENT", value_parser = BoolishValueParser::new())]
1110 no_output_indent: bool,
1111
1112 #[arg(long, env = "NEXTEST_NO_INPUT_HANDLER", value_parser = BoolishValueParser::new())]
1117 pub(super) no_input_handler: bool,
1118
1119 #[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 #[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 #[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 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 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 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 #[default]
1315 Auto,
1316
1317 None,
1319
1320 #[clap(alias = "running")]
1323 Bar,
1324
1325 Counter,
1327
1328 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#[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 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 NextestSubcommand::DoubleSpawn(_) => OutputContext::color_never_init(),
1372 }
1373 }
1374
1375 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 Nextest(Box<AppOpts>),
1399 Ntr(Box<NtrOpts>),
1401 #[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 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 "cargo nextest list",
1565 "cargo nextest run",
1566 "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 "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 "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 "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 "cargo nextest list --lib --bins",
1616 "cargo nextest run --ignore-rust-version --unit-graph",
1617 "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 "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 "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 "cargo nextest run -- --a an arbitrary arg",
1652 "cargo nextest run --jobs -3",
1654 "cargo nextest run --jobs 3",
1655 "cargo nextest run --build-jobs -1",
1657 "cargo nextest run --build-jobs 1",
1658 "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 ("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 (
1688 "cargo nextest run --max-fail=3 --no-fail-fast",
1689 ArgumentConflict,
1690 ),
1691 (
1695 "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 (
1708 "cargo nextest run --workspace-remap foo",
1709 MissingRequiredArgument,
1710 ),
1711 (
1715 "cargo nextest run --target-dir-remap bar",
1716 MissingRequiredArgument,
1717 ),
1718 (
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 ("cargo nextest run --jobs 0", ValueValidation),
1763 ("cargo nextest run --jobs -twenty", UnknownArgument),
1765 ("cargo nextest run --build-jobs -inf1", UnknownArgument),
1766 ("cargo nextest run --stress-count 0", ValueValidation),
1768 ("cargo nextest run --stress-duration 0m", ValueValidation),
1770 (
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 ("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 ("cargo nextest bench --stress-count 0", ValueValidation),
1819 ("cargo nextest bench --stress-duration 0m", ValueValidation),
1821 ];
1822
1823 for (k, _) in std::env::vars() {
1825 if k.starts_with("NEXTEST_") {
1826 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 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 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 for &k in &env_keys {
1859 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 ("foo -- str1", "foo str1"),
1904 ("foo -- str2 str3", "foo str2 str3"),
1905 ("foo -- --ignored", "foo --run-ignored only"),
1909 ("foo -- --ignored", "foo --run-ignored ignored-only"),
1910 ("foo -- --include-ignored", "foo --run-ignored all"),
1911 (
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 ("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 (
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 ("foo -- --include-ignored --include-ignored", "duplicated"),
1957 ("foo -- --ignored --ignored", "duplicated"),
1958 ("foo -- --exact --exact", "duplicated"),
1959 ("foo -- --ignored --include-ignored", "mutually exclusive"),
1963 ("foo --run-ignored all -- --ignored", "mutually exclusive"),
1964 ("foo -- --skip", "missing required argument"),
1968 ("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}