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::{FinalStatusLevel, ReporterBuilder, ShowProgress, StatusLevel, TestOutputDisplay},
33 reuse_build::ReuseBuildInfo,
34 runner::{StressCondition, StressCount, TestRunnerBuilder},
35 test_filter::{FilterBound, RunIgnored, TestFilterBuilder, TestFilterPatterns},
36 test_output::CaptureStrategy,
37};
38use std::{collections::BTreeSet, io::Cursor, sync::Arc, time::Duration};
39use tracing::{debug, warn};
40
41#[derive(Debug, Args)]
43pub(super) struct CommonOpts {
44 #[arg(
46 long,
47 global = true,
48 value_name = "PATH",
49 help_heading = "Manifest options"
50 )]
51 pub(super) manifest_path: Option<Utf8PathBuf>,
52
53 #[clap(flatten)]
54 pub(super) output: crate::output::OutputOpts,
55
56 #[clap(flatten)]
57 pub(super) config_opts: ConfigOpts,
58}
59
60#[derive(Debug, Args)]
61#[command(next_help_heading = "Config options")]
62pub(super) struct ConfigOpts {
63 #[arg(long, global = true, value_name = "PATH")]
65 pub config_file: Option<Utf8PathBuf>,
66
67 #[arg(long = "tool-config-file", global = true, value_name = "TOOL:ABS_PATH")]
81 pub tool_config_files: Vec<ToolConfigFile>,
82
83 #[arg(long, global = true)]
88 pub override_version_check: bool,
89
90 #[arg(
96 long,
97 short = 'P',
98 env = "NEXTEST_PROFILE",
99 global = true,
100 help_heading = "Config options"
101 )]
102 pub(super) profile: Option<String>,
103}
104
105impl ConfigOpts {
106 pub(super) fn make_version_only_config(
108 &self,
109 workspace_root: &Utf8Path,
110 ) -> Result<VersionOnlyConfig> {
111 VersionOnlyConfig::from_sources(
112 workspace_root,
113 self.config_file.as_deref(),
114 &self.tool_config_files,
115 )
116 .map_err(ExpectedError::config_parse_error)
117 }
118
119 pub(super) fn make_config(
121 &self,
122 workspace_root: &Utf8Path,
123 pcx: &ParseContext<'_>,
124 experimental: &BTreeSet<ConfigExperimental>,
125 ) -> Result<NextestConfig> {
126 NextestConfig::from_sources(
127 workspace_root,
128 pcx,
129 self.config_file.as_deref(),
130 &self.tool_config_files,
131 experimental,
132 )
133 .map_err(ExpectedError::config_parse_error)
134 }
135}
136
137#[derive(Debug, Subcommand)]
138pub(super) enum Command {
139 List {
150 #[clap(flatten)]
151 cargo_options: CargoOptions,
152
153 #[clap(flatten)]
154 build_filter: TestBuildFilter,
155
156 #[arg(
158 short = 'T',
159 long,
160 value_enum,
161 default_value_t,
162 help_heading = "Output options",
163 value_name = "FMT"
164 )]
165 message_format: MessageFormatOpts,
166
167 #[arg(
169 long,
170 value_enum,
171 default_value_t,
172 help_heading = "Output options",
173 value_name = "TYPE"
174 )]
175 list_type: ListType,
176
177 #[clap(flatten)]
178 reuse_build: ReuseBuildOpts,
179 },
180 #[command(visible_alias = "r")]
187 Run(RunOpts),
188 Archive {
196 #[clap(flatten)]
197 cargo_options: CargoOptions,
198
199 #[arg(
201 long,
202 name = "archive-file",
203 help_heading = "Archive options",
204 value_name = "PATH"
205 )]
206 archive_file: Utf8PathBuf,
207
208 #[arg(
213 long,
214 value_enum,
215 help_heading = "Archive options",
216 value_name = "FORMAT",
217 default_value_t
218 )]
219 archive_format: ArchiveFormatOpt,
220
221 #[clap(flatten)]
222 archive_build_filter: ArchiveBuildFilter,
223
224 #[arg(
226 long,
227 help_heading = "Archive options",
228 value_name = "LEVEL",
229 default_value_t = 0,
230 allow_negative_numbers = true
231 )]
232 zstd_level: i32,
233 },
235 ShowConfig {
242 #[clap(subcommand)]
243 command: super::commands::ShowConfigCommand,
244 },
245 #[clap(name = "self")]
247 Self_ {
248 #[clap(subcommand)]
249 command: super::commands::SelfCommand,
250 },
251 #[clap(hide = true)]
256 Debug {
257 #[clap(subcommand)]
258 command: super::commands::DebugCommand,
259 },
260}
261
262#[derive(Debug, Args)]
263pub(super) struct RunOpts {
264 #[clap(flatten)]
265 pub(super) cargo_options: CargoOptions,
266
267 #[clap(flatten)]
268 pub(super) build_filter: TestBuildFilter,
269
270 #[clap(flatten)]
271 pub(super) runner_opts: TestRunnerOpts,
272
273 #[arg(
275 long,
276 name = "no-capture",
277 alias = "nocapture",
278 help_heading = "Runner options",
279 display_order = 100
280 )]
281 pub(super) no_capture: bool,
282
283 #[clap(flatten)]
284 pub(super) reporter_opts: ReporterOpts,
285
286 #[clap(flatten)]
287 pub(super) reuse_build: ReuseBuildOpts,
288}
289
290#[derive(Copy, Clone, Debug, ValueEnum)]
291pub(crate) enum PlatformFilterOpts {
292 Target,
293 Host,
294 Any,
295}
296
297impl Default for PlatformFilterOpts {
298 fn default() -> Self {
299 Self::Any
300 }
301}
302
303impl From<PlatformFilterOpts> for Option<BuildPlatform> {
304 fn from(opt: PlatformFilterOpts) -> Self {
305 match opt {
306 PlatformFilterOpts::Target => Some(BuildPlatform::Target),
307 PlatformFilterOpts::Host => Some(BuildPlatform::Host),
308 PlatformFilterOpts::Any => None,
309 }
310 }
311}
312
313#[derive(Copy, Clone, Debug, ValueEnum)]
314pub(super) enum ListType {
315 Full,
316 BinariesOnly,
317}
318
319impl Default for ListType {
320 fn default() -> Self {
321 Self::Full
322 }
323}
324
325#[derive(Copy, Clone, Debug, ValueEnum)]
326pub(super) enum MessageFormatOpts {
327 Human,
328 Json,
329 JsonPretty,
330}
331
332impl MessageFormatOpts {
333 pub(super) fn to_output_format(self, verbose: bool) -> OutputFormat {
334 match self {
335 Self::Human => OutputFormat::Human { verbose },
336 Self::Json => OutputFormat::Serializable(SerializableFormat::Json),
337 Self::JsonPretty => OutputFormat::Serializable(SerializableFormat::JsonPretty),
338 }
339 }
340}
341
342impl Default for MessageFormatOpts {
343 fn default() -> Self {
344 Self::Human
345 }
346}
347
348#[derive(Debug, Args)]
349#[command(next_help_heading = "Filter options")]
350pub(super) struct TestBuildFilter {
351 #[arg(long, value_enum, value_name = "WHICH")]
353 run_ignored: Option<RunIgnoredOpt>,
354
355 #[arg(long)]
357 partition: Option<PartitionerBuilder>,
358
359 #[arg(
363 long,
364 hide_short_help = true,
365 value_enum,
366 value_name = "PLATFORM",
367 default_value_t
368 )]
369 pub(crate) platform_filter: PlatformFilterOpts,
370
371 #[arg(
373 long,
374 alias = "filter-expr",
375 short = 'E',
376 value_name = "EXPR",
377 action(ArgAction::Append)
378 )]
379 pub(super) filterset: Vec<String>,
380
381 #[arg(long)]
388 ignore_default_filter: bool,
389
390 #[arg(help_heading = None, name = "FILTERS")]
392 pre_double_dash_filters: Vec<String>,
393
394 #[arg(help_heading = None, value_name = "FILTERS_AND_ARGS", last = true)]
402 filters: Vec<String>,
403}
404
405impl TestBuildFilter {
406 #[expect(clippy::too_many_arguments)]
407 pub(super) fn compute_test_list<'g>(
408 &self,
409 ctx: &TestExecuteContext<'_>,
410 graph: &'g PackageGraph,
411 workspace_root: Utf8PathBuf,
412 binary_list: Arc<BinaryList>,
413 test_filter_builder: TestFilterBuilder,
414 env: EnvironmentMap,
415 profile: &EvaluatableProfile<'_>,
416 reuse_build: &ReuseBuildInfo,
417 ) -> Result<TestList<'g>> {
418 let path_mapper = make_path_mapper(
419 reuse_build,
420 graph,
421 &binary_list.rust_build_meta.target_directory,
422 )?;
423
424 let rust_build_meta = binary_list.rust_build_meta.map_paths(&path_mapper);
425 let test_artifacts = RustTestArtifact::from_binary_list(
426 graph,
427 binary_list,
428 &rust_build_meta,
429 &path_mapper,
430 self.platform_filter.into(),
431 )?;
432 TestList::new(
433 ctx,
434 test_artifacts,
435 rust_build_meta,
436 &test_filter_builder,
437 workspace_root,
438 env,
439 profile,
440 if self.ignore_default_filter {
441 FilterBound::All
442 } else {
443 FilterBound::DefaultSet
444 },
445 get_num_cpus(),
447 )
448 .map_err(|err| ExpectedError::CreateTestListError { err })
449 }
450
451 pub(super) fn make_test_filter_builder(
452 &self,
453 filter_exprs: Vec<nextest_filtering::Filterset>,
454 ) -> Result<TestFilterBuilder> {
455 let mut run_ignored = self.run_ignored.map(Into::into);
457 let mut patterns = TestFilterPatterns::new(self.pre_double_dash_filters.clone());
458 self.merge_test_binary_args(&mut run_ignored, &mut patterns)?;
459
460 Ok(TestFilterBuilder::new(
461 run_ignored.unwrap_or_default(),
462 self.partition.clone(),
463 patterns,
464 filter_exprs,
465 )?)
466 }
467
468 fn merge_test_binary_args(
469 &self,
470 run_ignored: &mut Option<RunIgnored>,
471 patterns: &mut TestFilterPatterns,
472 ) -> Result<()> {
473 let mut is_exact = false;
476 for arg in &self.filters {
477 if arg == "--" {
478 break;
479 }
480 if arg == "--exact" {
481 if is_exact {
482 return Err(ExpectedError::test_binary_args_parse_error(
483 "duplicated",
484 vec![arg.clone()],
485 ));
486 }
487 is_exact = true;
488 }
489 }
490
491 let mut ignore_filters = Vec::new();
492 let mut read_trailing_filters = false;
493
494 let mut unsupported_args = Vec::new();
495
496 let mut it = self.filters.iter();
497 while let Some(arg) = it.next() {
498 if read_trailing_filters || !arg.starts_with('-') {
499 if is_exact {
500 patterns.add_exact_pattern(arg.clone());
501 } else {
502 patterns.add_substring_pattern(arg.clone());
503 }
504 } else if arg == "--include-ignored" {
505 ignore_filters.push((arg.clone(), RunIgnored::All));
506 } else if arg == "--ignored" {
507 ignore_filters.push((arg.clone(), RunIgnored::Only));
508 } else if arg == "--" {
509 read_trailing_filters = true;
510 } else if arg == "--skip" {
511 let skip_arg = it.next().ok_or_else(|| {
512 ExpectedError::test_binary_args_parse_error(
513 "missing required argument",
514 vec![arg.clone()],
515 )
516 })?;
517
518 if is_exact {
519 patterns.add_skip_exact_pattern(skip_arg.clone());
520 } else {
521 patterns.add_skip_pattern(skip_arg.clone());
522 }
523 } else if arg == "--exact" {
524 } else {
526 unsupported_args.push(arg.clone());
527 }
528 }
529
530 for (s, f) in ignore_filters {
531 if let Some(run_ignored) = run_ignored {
532 if *run_ignored != f {
533 return Err(ExpectedError::test_binary_args_parse_error(
534 "mutually exclusive",
535 vec![s],
536 ));
537 } else {
538 return Err(ExpectedError::test_binary_args_parse_error(
539 "duplicated",
540 vec![s],
541 ));
542 }
543 } else {
544 *run_ignored = Some(f);
545 }
546 }
547
548 if !unsupported_args.is_empty() {
549 return Err(ExpectedError::test_binary_args_parse_error(
550 "unsupported",
551 unsupported_args,
552 ));
553 }
554
555 Ok(())
556 }
557}
558
559#[derive(Debug, Args)]
560#[command(next_help_heading = "Filter options")]
561pub(super) struct ArchiveBuildFilter {
562 #[arg(long, short = 'E', value_name = "EXPR", action(ArgAction::Append))]
566 pub(super) filterset: Vec<String>,
567}
568
569#[derive(Copy, Clone, Debug, ValueEnum)]
570enum RunIgnoredOpt {
571 Default,
573
574 #[clap(alias = "ignored-only")]
576 Only,
577
578 All,
580}
581
582impl From<RunIgnoredOpt> for RunIgnored {
583 fn from(opt: RunIgnoredOpt) -> Self {
584 match opt {
585 RunIgnoredOpt::Default => RunIgnored::Default,
586 RunIgnoredOpt::Only => RunIgnored::Only,
587 RunIgnoredOpt::All => RunIgnored::All,
588 }
589 }
590}
591
592impl CargoOptions {
593 pub(super) fn compute_binary_list(
594 &self,
595 graph: &PackageGraph,
596 manifest_path: Option<&Utf8Path>,
597 output: OutputContext,
598 build_platforms: BuildPlatforms,
599 ) -> Result<BinaryList> {
600 let mut cargo_cli = CargoCli::new("test", manifest_path, output);
603
604 cargo_cli.add_args(["--no-run", "--message-format", "json-render-diagnostics"]);
606 cargo_cli.add_options(self);
607
608 let expression = cargo_cli.to_expression();
609 let output = expression
610 .stdout_capture()
611 .unchecked()
612 .run()
613 .map_err(|err| ExpectedError::build_exec_failed(cargo_cli.all_args(), err))?;
614 if !output.status.success() {
615 return Err(ExpectedError::build_failed(
616 cargo_cli.all_args(),
617 output.status.code(),
618 ));
619 }
620
621 let test_binaries =
622 BinaryList::from_messages(Cursor::new(output.stdout), graph, build_platforms)?;
623 Ok(test_binaries)
624 }
625}
626
627#[derive(Debug, Default, Args)]
629#[command(next_help_heading = "Runner options")]
630pub struct TestRunnerOpts {
631 #[arg(long, name = "no-run")]
633 pub(super) no_run: bool,
634
635 #[arg(
638 long,
639 short = 'j',
640 visible_alias = "jobs",
641 value_name = "N",
642 env = "NEXTEST_TEST_THREADS",
643 allow_negative_numbers = true
644 )]
645 test_threads: Option<TestThreads>,
646
647 #[arg(long, env = "NEXTEST_RETRIES", value_name = "N")]
649 retries: Option<u32>,
650
651 #[arg(
653 long,
654 visible_alias = "ff",
655 name = "fail-fast",
656 conflicts_with = "no-run"
662 )]
663 fail_fast: bool,
664
665 #[arg(
667 long,
668 visible_alias = "nff",
669 name = "no-fail-fast",
670 conflicts_with = "no-run",
671 overrides_with = "fail-fast"
672 )]
673 no_fail_fast: bool,
674
675 #[arg(
677 long,
678 name = "max-fail",
679 value_name = "N",
680 conflicts_with_all = &["no-run", "fail-fast", "no-fail-fast"],
681 )]
682 max_fail: Option<MaxFail>,
683
684 #[arg(long, value_enum, value_name = "ACTION", env = "NEXTEST_NO_TESTS")]
686 pub(super) no_tests: Option<NoTestsBehavior>,
687
688 #[clap(flatten)]
690 stress: StressOptions,
691}
692
693#[derive(Clone, Copy, Debug, ValueEnum)]
694pub(super) enum NoTestsBehavior {
695 Pass,
697
698 Warn,
700
701 #[clap(alias = "error")]
703 Fail,
704}
705
706impl TestRunnerOpts {
707 pub(super) fn to_builder(&self, cap_strat: CaptureStrategy) -> Option<TestRunnerBuilder> {
708 if self.test_threads.is_some() {
712 if let Some(reasons) =
713 no_run_no_capture_reasons(self.no_run, cap_strat == CaptureStrategy::None)
714 {
715 warn!("ignoring --test-threads because {reasons}");
716 }
717 }
718
719 if self.retries.is_some() && self.no_run {
720 warn!("ignoring --retries because --no-run is specified");
721 }
722 if self.no_tests.is_some() && self.no_run {
723 warn!("ignoring --no-tests because --no-run is specified");
724 }
725
726 if self.no_run {
729 return None;
730 }
731
732 let mut builder = TestRunnerBuilder::default();
733 builder.set_capture_strategy(cap_strat);
734 if let Some(retries) = self.retries {
735 builder.set_retries(RetryPolicy::new_without_delay(retries));
736 }
737
738 if let Some(max_fail) = self.max_fail {
739 builder.set_max_fail(max_fail);
740 debug!(max_fail = ?max_fail, "set max fail");
741 } else if self.no_fail_fast {
742 builder.set_max_fail(MaxFail::from_fail_fast(false));
743 debug!("set max fail via from_fail_fast(false)");
744 } else if self.fail_fast {
745 builder.set_max_fail(MaxFail::from_fail_fast(true));
746 debug!("set max fail via from_fail_fast(true)");
747 }
748
749 if let Some(test_threads) = self.test_threads {
750 builder.set_test_threads(test_threads);
751 }
752
753 if let Some(condition) = self.stress.condition.as_ref() {
754 builder.set_stress_condition(condition.stress_condition());
755 }
756
757 Some(builder)
758 }
759}
760
761fn no_run_no_capture_reasons(no_run: bool, no_capture: bool) -> Option<&'static str> {
762 match (no_run, no_capture) {
763 (true, true) => Some("--no-run and --no-capture are specified"),
764 (true, false) => Some("--no-run is specified"),
765 (false, true) => Some("--no-capture is specified"),
766 (false, false) => None,
767 }
768}
769
770#[derive(Clone, Copy, Debug, ValueEnum)]
771pub(super) enum IgnoreOverridesOpt {
772 Retries,
773 All,
774}
775
776#[derive(Clone, Copy, Debug, ValueEnum, Default)]
777pub(super) enum MessageFormat {
778 #[default]
780 Human,
781 LibtestJson,
783 LibtestJsonPlus,
786}
787
788#[derive(Debug, Default, Args)]
789#[command(next_help_heading = "Stress testing options")]
790struct StressOptions {
791 #[clap(flatten)]
793 condition: Option<StressConditionOpt>,
794 }
796
797#[derive(Clone, Debug, Default, Args)]
798#[group(id = "stress_condition", multiple = false)]
799struct StressConditionOpt {
800 #[arg(long, value_name = "COUNT")]
802 stress_count: Option<StressCount>,
803
804 #[arg(long, value_name = "DURATION", value_parser = non_zero_duration)]
806 stress_duration: Option<Duration>,
807}
808
809impl StressConditionOpt {
810 fn stress_condition(&self) -> StressCondition {
811 if let Some(count) = self.stress_count {
812 StressCondition::Count(count)
813 } else if let Some(duration) = self.stress_duration {
814 StressCondition::Duration(duration)
815 } else {
816 unreachable!(
817 "if StressOptions::condition is Some, \
818 one of these should be set"
819 )
820 }
821 }
822}
823
824fn non_zero_duration(input: &str) -> std::result::Result<Duration, String> {
825 let duration = humantime::parse_duration(input).map_err(|error| error.to_string())?;
826 if duration.is_zero() {
827 Err("duration must be non-zero".to_string())
828 } else {
829 Ok(duration)
830 }
831}
832
833#[derive(Debug, Default, Args)]
834#[command(next_help_heading = "Reporter options")]
835pub(super) struct ReporterOpts {
836 #[arg(long, value_enum, value_name = "WHEN", env = "NEXTEST_FAILURE_OUTPUT")]
838 failure_output: Option<TestOutputDisplayOpt>,
839
840 #[arg(long, value_enum, value_name = "WHEN", env = "NEXTEST_SUCCESS_OUTPUT")]
842 success_output: Option<TestOutputDisplayOpt>,
843
844 #[arg(long, value_enum, value_name = "LEVEL", env = "NEXTEST_STATUS_LEVEL")]
847 status_level: Option<StatusLevelOpt>,
848
849 #[arg(
851 long,
852 value_enum,
853 value_name = "LEVEL",
854 env = "NEXTEST_FINAL_STATUS_LEVEL"
855 )]
856 final_status_level: Option<FinalStatusLevelOpt>,
857
858 #[arg(long, env = "NEXTEST_SHOW_PROGRESS")]
860 show_progress: Option<ShowProgressOpt>,
861
862 #[arg(long, env = "NEXTEST_HIDE_PROGRESS_BAR", value_parser = BoolishValueParser::new())]
864 hide_progress_bar: bool,
865
866 #[arg(long, env = "NEXTEST_NO_OUTPUT_INDENT", value_parser = BoolishValueParser::new())]
875 no_output_indent: bool,
876
877 #[arg(long, env = "NEXTEST_NO_INPUT_HANDLER", value_parser = BoolishValueParser::new())]
882 pub(super) no_input_handler: bool,
883
884 #[arg(
886 long,
887 name = "message-format",
888 value_enum,
889 value_name = "FORMAT",
890 env = "NEXTEST_MESSAGE_FORMAT"
891 )]
892 pub(super) message_format: Option<MessageFormat>,
893
894 #[arg(
899 long,
900 requires = "message-format",
901 value_name = "VERSION",
902 env = "NEXTEST_MESSAGE_FORMAT_VERSION"
903 )]
904 pub(super) message_format_version: Option<String>,
905}
906
907impl ReporterOpts {
908 pub(super) fn to_builder(
909 &self,
910 no_run: bool,
911 no_capture: bool,
912 should_colorize: bool,
913 ) -> ReporterBuilder {
914 if no_run && no_capture {
918 warn!("ignoring --no-capture because --no-run is specified");
919 }
920
921 let reasons = no_run_no_capture_reasons(no_run, no_capture);
922
923 if self.failure_output.is_some() {
924 if let Some(reasons) = reasons {
925 warn!("ignoring --failure-output because {}", reasons);
926 }
927 }
928 if self.success_output.is_some() {
929 if let Some(reasons) = reasons {
930 warn!("ignoring --success-output because {}", reasons);
931 }
932 }
933 if self.status_level.is_some() && no_run {
934 warn!("ignoring --status-level because --no-run is specified");
935 }
936 if self.final_status_level.is_some() && no_run {
937 warn!("ignoring --final-status-level because --no-run is specified");
938 }
939 if self.message_format.is_some() && no_run {
940 warn!("ignoring --message-format because --no-run is specified");
941 }
942 if self.message_format_version.is_some() && no_run {
943 warn!("ignoring --message-format-version because --no-run is specified");
944 }
945
946 let show_progress = match (self.show_progress, self.hide_progress_bar) {
947 (Some(show_progress), true) => {
948 warn!("ignoring --hide-progress-bar because --show-progress is specified");
949 show_progress
950 }
951 (Some(show_progress), false) => show_progress,
952 (None, true) => ShowProgressOpt::None,
953 (None, false) => ShowProgressOpt::default(),
954 };
955
956 let mut builder = ReporterBuilder::default();
959 builder.set_no_capture(no_capture);
960 builder.set_colorize(should_colorize);
961
962 if let Some(failure_output) = self.failure_output {
963 builder.set_failure_output(failure_output.into());
964 }
965 if let Some(success_output) = self.success_output {
966 builder.set_success_output(success_output.into());
967 }
968 if let Some(status_level) = self.status_level {
969 builder.set_status_level(status_level.into());
970 }
971 if let Some(final_status_level) = self.final_status_level {
972 builder.set_final_status_level(final_status_level.into());
973 }
974 builder.set_show_progress(show_progress.into());
975 builder.set_no_output_indent(self.no_output_indent);
976 builder
977 }
978}
979
980#[derive(Clone, Copy, Debug, ValueEnum)]
981enum TestOutputDisplayOpt {
982 Immediate,
983 ImmediateFinal,
984 Final,
985 Never,
986}
987
988impl From<TestOutputDisplayOpt> for TestOutputDisplay {
989 fn from(opt: TestOutputDisplayOpt) -> Self {
990 match opt {
991 TestOutputDisplayOpt::Immediate => TestOutputDisplay::Immediate,
992 TestOutputDisplayOpt::ImmediateFinal => TestOutputDisplay::ImmediateFinal,
993 TestOutputDisplayOpt::Final => TestOutputDisplay::Final,
994 TestOutputDisplayOpt::Never => TestOutputDisplay::Never,
995 }
996 }
997}
998
999#[derive(Clone, Copy, Debug, ValueEnum)]
1000enum StatusLevelOpt {
1001 None,
1002 Fail,
1003 Retry,
1004 Slow,
1005 Leak,
1006 Pass,
1007 Skip,
1008 All,
1009}
1010
1011impl From<StatusLevelOpt> for StatusLevel {
1012 fn from(opt: StatusLevelOpt) -> Self {
1013 match opt {
1014 StatusLevelOpt::None => StatusLevel::None,
1015 StatusLevelOpt::Fail => StatusLevel::Fail,
1016 StatusLevelOpt::Retry => StatusLevel::Retry,
1017 StatusLevelOpt::Slow => StatusLevel::Slow,
1018 StatusLevelOpt::Leak => StatusLevel::Leak,
1019 StatusLevelOpt::Pass => StatusLevel::Pass,
1020 StatusLevelOpt::Skip => StatusLevel::Skip,
1021 StatusLevelOpt::All => StatusLevel::All,
1022 }
1023 }
1024}
1025
1026#[derive(Clone, Copy, Debug, ValueEnum)]
1027enum FinalStatusLevelOpt {
1028 None,
1029 Fail,
1030 #[clap(alias = "retry")]
1031 Flaky,
1032 Slow,
1033 Skip,
1034 Pass,
1035 All,
1036}
1037
1038impl From<FinalStatusLevelOpt> for FinalStatusLevel {
1039 fn from(opt: FinalStatusLevelOpt) -> Self {
1040 match opt {
1041 FinalStatusLevelOpt::None => FinalStatusLevel::None,
1042 FinalStatusLevelOpt::Fail => FinalStatusLevel::Fail,
1043 FinalStatusLevelOpt::Flaky => FinalStatusLevel::Flaky,
1044 FinalStatusLevelOpt::Slow => FinalStatusLevel::Slow,
1045 FinalStatusLevelOpt::Skip => FinalStatusLevel::Skip,
1046 FinalStatusLevelOpt::Pass => FinalStatusLevel::Pass,
1047 FinalStatusLevelOpt::All => FinalStatusLevel::All,
1048 }
1049 }
1050}
1051
1052#[derive(Default, Clone, Copy, Debug, ValueEnum)]
1053enum ShowProgressOpt {
1054 #[default]
1055 Auto,
1056 None,
1057 Bar,
1058 Counter,
1059}
1060
1061impl From<ShowProgressOpt> for ShowProgress {
1062 fn from(opt: ShowProgressOpt) -> Self {
1063 match opt {
1064 ShowProgressOpt::Auto => ShowProgress::Auto,
1065 ShowProgressOpt::None => ShowProgress::None,
1066 ShowProgressOpt::Bar => ShowProgress::Bar,
1067 ShowProgressOpt::Counter => ShowProgress::Counter,
1068 }
1069 }
1070}
1071
1072#[derive(Debug, clap::Parser)]
1077#[command(
1078 version = crate::version::short(),
1079 long_version = crate::version::long(),
1080 bin_name = "cargo",
1081 styles = crate::output::clap_styles::style(),
1082 max_term_width = 100,
1083)]
1084pub struct CargoNextestApp {
1085 #[clap(subcommand)]
1086 subcommand: NextestSubcommand,
1087}
1088
1089impl CargoNextestApp {
1090 pub fn init_output(&self) -> OutputContext {
1092 match &self.subcommand {
1093 NextestSubcommand::Nextest(args) => args.common.output.init(),
1094 NextestSubcommand::Ntr(args) => args.common.output.init(),
1095 #[cfg(unix)]
1096 NextestSubcommand::DoubleSpawn(_) => OutputContext::color_never_init(),
1098 }
1099 }
1100
1101 pub fn exec(
1103 self,
1104 cli_args: Vec<String>,
1105 output: OutputContext,
1106 output_writer: &mut crate::output::OutputWriter,
1107 ) -> Result<i32> {
1108 if let Err(err) = nextest_runner::usdt::register_probes() {
1109 tracing::warn!("failed to register USDT probes: {}", err);
1110 }
1111
1112 match self.subcommand {
1113 NextestSubcommand::Nextest(app) => app.exec(cli_args, output, output_writer),
1114 NextestSubcommand::Ntr(opts) => opts.exec(cli_args, output, output_writer),
1115 #[cfg(unix)]
1116 NextestSubcommand::DoubleSpawn(opts) => opts.exec(output),
1117 }
1118 }
1119}
1120
1121#[derive(Debug, Subcommand)]
1122enum NextestSubcommand {
1123 Nextest(Box<AppOpts>),
1125 Ntr(Box<NtrOpts>),
1127 #[cfg(unix)]
1129 #[command(name = nextest_runner::double_spawn::DoubleSpawnInfo::SUBCOMMAND_NAME, hide = true)]
1130 DoubleSpawn(crate::double_spawn::DoubleSpawnOpts),
1131}
1132
1133#[derive(Debug, Args)]
1134#[clap(
1135 version = crate::version::short(),
1136 long_version = crate::version::long(),
1137 display_name = "cargo-nextest",
1138)]
1139pub(super) struct AppOpts {
1140 #[clap(flatten)]
1141 common: CommonOpts,
1142
1143 #[clap(subcommand)]
1144 command: Command,
1145}
1146
1147impl AppOpts {
1148 fn exec(
1152 self,
1153 cli_args: Vec<String>,
1154 output: OutputContext,
1155 output_writer: &mut crate::output::OutputWriter,
1156 ) -> Result<i32> {
1157 match self.command {
1158 Command::List {
1159 cargo_options,
1160 build_filter,
1161 message_format,
1162 list_type,
1163 reuse_build,
1164 } => {
1165 let base = super::execution::BaseApp::new(
1166 output,
1167 reuse_build,
1168 cargo_options,
1169 self.common.config_opts,
1170 self.common.manifest_path,
1171 output_writer,
1172 )?;
1173 let app = super::execution::App::new(base, build_filter)?;
1174 app.exec_list(message_format, list_type, output_writer)?;
1175 Ok(0)
1176 }
1177 Command::Run(run_opts) => {
1178 let base = super::execution::BaseApp::new(
1179 output,
1180 run_opts.reuse_build,
1181 run_opts.cargo_options,
1182 self.common.config_opts,
1183 self.common.manifest_path,
1184 output_writer,
1185 )?;
1186 let app = super::execution::App::new(base, run_opts.build_filter)?;
1187 app.exec_run(
1188 run_opts.no_capture,
1189 &run_opts.runner_opts,
1190 &run_opts.reporter_opts,
1191 cli_args,
1192 output_writer,
1193 )?;
1194 Ok(0)
1195 }
1196 Command::Archive {
1197 cargo_options,
1198 archive_file,
1199 archive_format,
1200 archive_build_filter,
1201 zstd_level,
1202 } => {
1203 let app = super::execution::BaseApp::new(
1204 output,
1205 ReuseBuildOpts::default(),
1206 cargo_options,
1207 self.common.config_opts,
1208 self.common.manifest_path,
1209 output_writer,
1210 )?;
1211
1212 let app = super::execution::ArchiveApp::new(app, archive_build_filter)?;
1213 app.exec_archive(&archive_file, archive_format, zstd_level, output_writer)?;
1214 Ok(0)
1215 }
1216 Command::ShowConfig { command } => command.exec(
1217 self.common.manifest_path,
1218 self.common.config_opts,
1219 output,
1220 output_writer,
1221 ),
1222 Command::Self_ { command } => command.exec(self.common.output),
1223 Command::Debug { command } => command.exec(self.common.output),
1224 }
1225 }
1226}
1227
1228#[derive(Debug, Args)]
1229struct NtrOpts {
1230 #[clap(flatten)]
1231 common: CommonOpts,
1232
1233 #[clap(flatten)]
1234 run_opts: RunOpts,
1235}
1236
1237impl NtrOpts {
1238 fn exec(
1239 self,
1240 cli_args: Vec<String>,
1241 output: OutputContext,
1242 output_writer: &mut crate::output::OutputWriter,
1243 ) -> Result<i32> {
1244 let base = super::execution::BaseApp::new(
1245 output,
1246 self.run_opts.reuse_build,
1247 self.run_opts.cargo_options,
1248 self.common.config_opts,
1249 self.common.manifest_path,
1250 output_writer,
1251 )?;
1252 let app = super::execution::App::new(base, self.run_opts.build_filter)?;
1253 app.exec_run(
1254 self.run_opts.no_capture,
1255 &self.run_opts.runner_opts,
1256 &self.run_opts.reporter_opts,
1257 cli_args,
1258 output_writer,
1259 )
1260 }
1261}
1262
1263#[cfg(test)]
1264mod tests {
1265 use super::*;
1266 use clap::Parser;
1267
1268 #[test]
1269 fn test_argument_parsing() {
1270 use clap::error::ErrorKind::{self, *};
1271
1272 let valid: &[&'static str] = &[
1273 "cargo nextest list",
1277 "cargo nextest run",
1278 "cargo nextest list --list-type binaries-only",
1282 "cargo nextest list --list-type full",
1283 "cargo nextest list --message-format json-pretty",
1284 "cargo nextest run --failure-output never",
1285 "cargo nextest run --success-output=immediate",
1286 "cargo nextest run --status-level=all",
1287 "cargo nextest run --no-capture",
1288 "cargo nextest run --nocapture",
1289 "cargo nextest run --no-run",
1290 "cargo nextest run --final-status-level flaky",
1291 "cargo nextest run --max-fail 3",
1292 "cargo nextest run --max-fail=all",
1293 "cargo nextest run --final-status-level retry",
1295 "NEXTEST_HIDE_PROGRESS_BAR=1 cargo nextest run",
1296 "NEXTEST_HIDE_PROGRESS_BAR=true cargo nextest run",
1297 "cargo nextest run --no-run -j8",
1301 "cargo nextest run --no-run --retries 3",
1302 "NEXTEST_TEST_THREADS=8 cargo nextest run --no-run",
1303 "cargo nextest run --no-run --success-output never",
1304 "NEXTEST_SUCCESS_OUTPUT=never cargo nextest run --no-run",
1305 "cargo nextest run --no-run --failure-output immediate",
1306 "NEXTEST_FAILURE_OUTPUT=immediate cargo nextest run --no-run",
1307 "cargo nextest run --no-run --status-level pass",
1308 "NEXTEST_STATUS_LEVEL=pass cargo nextest run --no-run",
1309 "cargo nextest run --no-run --final-status-level skip",
1310 "NEXTEST_FINAL_STATUS_LEVEL=skip cargo nextest run --no-run",
1311 "cargo nextest run --no-capture --test-threads=24",
1315 "NEXTEST_NO_CAPTURE=1 cargo nextest run --test-threads=24",
1316 "cargo nextest run --no-capture --failure-output=never",
1317 "NEXTEST_NO_CAPTURE=1 cargo nextest run --failure-output=never",
1318 "cargo nextest run --no-capture --success-output=final",
1319 "NEXTEST_SUCCESS_OUTPUT=final cargo nextest run --no-capture",
1320 "cargo nextest list --lib --bins",
1324 "cargo nextest run --ignore-rust-version --unit-graph",
1325 "cargo nextest list --binaries-metadata=foo",
1329 "cargo nextest run --binaries-metadata=foo --target-dir-remap=bar",
1330 "cargo nextest list --cargo-metadata path",
1331 "cargo nextest run --cargo-metadata=path --workspace-remap remapped-path",
1332 "cargo nextest archive --archive-file my-archive.tar.zst --zstd-level -1",
1333 "cargo nextest archive --archive-file my-archive.foo --archive-format tar-zst",
1334 "cargo nextest archive --archive-file my-archive.foo --archive-format tar-zstd",
1335 "cargo nextest list --archive-file my-archive.tar.zst",
1336 "cargo nextest list --archive-file my-archive.tar.zst --archive-format tar-zst",
1337 "cargo nextest list --archive-file my-archive.tar.zst --extract-to my-path",
1338 "cargo nextest list --archive-file my-archive.tar.zst --extract-to my-path --extract-overwrite",
1339 "cargo nextest list --archive-file my-archive.tar.zst --persist-extract-tempdir",
1340 "cargo nextest list --archive-file my-archive.tar.zst --workspace-remap foo",
1341 "cargo nextest list --archive-file my-archive.tar.zst --config target.'cfg(all())'.runner=\"my-runner\"",
1342 "cargo nextest list -E deps(foo)",
1346 "cargo nextest run --filterset 'test(bar)' --package=my-package test-filter",
1347 "cargo nextest run --filter-expr 'test(bar)' --package=my-package test-filter",
1348 "cargo nextest list -E 'deps(foo)' --ignore-default-filter",
1349 "cargo nextest run --stress-count 4",
1353 "cargo nextest run --stress-count infinite",
1354 "cargo nextest run --stress-duration 60m",
1355 "cargo nextest run --stress-duration 24h",
1356 "cargo nextest run -- --a an arbitrary arg",
1360 "cargo nextest run --jobs -3",
1362 "cargo nextest run --jobs 3",
1363 "cargo nextest run --build-jobs -1",
1365 "cargo nextest run --build-jobs 1",
1366 ];
1367
1368 let invalid: &[(&'static str, ErrorKind)] = &[
1369 ("cargo nextest run --no-run --fail-fast", ArgumentConflict),
1373 (
1374 "cargo nextest run --no-run --no-fail-fast",
1375 ArgumentConflict,
1376 ),
1377 ("cargo nextest run --no-run --max-fail=3", ArgumentConflict),
1378 (
1382 "cargo nextest run --max-fail=3 --no-fail-fast",
1383 ArgumentConflict,
1384 ),
1385 (
1389 "cargo nextest run --manifest-path foo --cargo-metadata bar",
1392 ArgumentConflict,
1393 ),
1394 (
1395 "cargo nextest run --binaries-metadata=foo --lib",
1396 ArgumentConflict,
1397 ),
1398 (
1402 "cargo nextest run --workspace-remap foo",
1403 MissingRequiredArgument,
1404 ),
1405 (
1409 "cargo nextest run --target-dir-remap bar",
1410 MissingRequiredArgument,
1411 ),
1412 (
1416 "cargo nextest run --archive-format tar-zst",
1417 MissingRequiredArgument,
1418 ),
1419 (
1420 "cargo nextest run --archive-file foo --archive-format no",
1421 InvalidValue,
1422 ),
1423 (
1424 "cargo nextest run --extract-to foo",
1425 MissingRequiredArgument,
1426 ),
1427 (
1428 "cargo nextest run --archive-file foo --extract-overwrite",
1429 MissingRequiredArgument,
1430 ),
1431 (
1432 "cargo nextest run --extract-to foo --extract-overwrite",
1433 MissingRequiredArgument,
1434 ),
1435 (
1436 "cargo nextest run --persist-extract-tempdir",
1437 MissingRequiredArgument,
1438 ),
1439 (
1440 "cargo nextest run --archive-file foo --extract-to bar --persist-extract-tempdir",
1441 ArgumentConflict,
1442 ),
1443 (
1444 "cargo nextest run --archive-file foo --cargo-metadata bar",
1445 ArgumentConflict,
1446 ),
1447 (
1448 "cargo nextest run --archive-file foo --binaries-metadata bar",
1449 ArgumentConflict,
1450 ),
1451 (
1452 "cargo nextest run --archive-file foo --target-dir-remap bar",
1453 ArgumentConflict,
1454 ),
1455 ("cargo nextest run --jobs 0", ValueValidation),
1457 ("cargo nextest run --jobs -twenty", UnknownArgument),
1459 ("cargo nextest run --build-jobs -inf1", UnknownArgument),
1460 ("cargo nextest run --stress-count 0", ValueValidation),
1462 ("cargo nextest run --stress-duration 0m", ValueValidation),
1464 ];
1465
1466 for (k, _) in std::env::vars() {
1468 if k.starts_with("NEXTEST_") {
1469 unsafe { std::env::remove_var(k) };
1472 }
1473 }
1474
1475 for valid_args in valid {
1476 let cmd = shell_words::split(valid_args).expect("valid command line");
1477 let env_vars: Vec<_> = cmd
1479 .iter()
1480 .take_while(|arg| arg.contains('='))
1481 .cloned()
1482 .collect();
1483
1484 let mut env_keys = Vec::with_capacity(env_vars.len());
1485 for k_v in &env_vars {
1486 let (k, v) = k_v.split_once('=').expect("valid env var");
1487 unsafe { std::env::set_var(k, v) };
1490 env_keys.push(k);
1491 }
1492
1493 let cmd = cmd.iter().skip(env_vars.len());
1494
1495 if let Err(error) = CargoNextestApp::try_parse_from(cmd) {
1496 panic!("{valid_args} should have successfully parsed, but didn't: {error}");
1497 }
1498
1499 for &k in &env_keys {
1502 unsafe { std::env::remove_var(k) };
1505 }
1506 }
1507
1508 for &(invalid_args, kind) in invalid {
1509 match CargoNextestApp::try_parse_from(
1510 shell_words::split(invalid_args).expect("valid command"),
1511 ) {
1512 Ok(_) => {
1513 panic!("{invalid_args} should have errored out but successfully parsed");
1514 }
1515 Err(error) => {
1516 let actual_kind = error.kind();
1517 if kind != actual_kind {
1518 panic!(
1519 "{invalid_args} should error with kind {kind:?}, but actual kind was {actual_kind:?}",
1520 );
1521 }
1522 }
1523 }
1524 }
1525 }
1526
1527 #[derive(Debug, clap::Parser)]
1528 struct TestCli {
1529 #[structopt(flatten)]
1530 build_filter: TestBuildFilter,
1531 }
1532
1533 #[test]
1534 fn test_test_binary_argument_parsing() {
1535 fn get_test_filter_builder(cmd: &str) -> Result<TestFilterBuilder> {
1536 let app = TestCli::try_parse_from(shell_words::split(cmd).expect("valid command line"))
1537 .unwrap_or_else(|_| panic!("{cmd} should have successfully parsed"));
1538 app.build_filter.make_test_filter_builder(vec![])
1539 }
1540
1541 let valid = &[
1542 ("foo -- str1", "foo str1"),
1546 ("foo -- str2 str3", "foo str2 str3"),
1547 ("foo -- --ignored", "foo --run-ignored only"),
1551 ("foo -- --ignored", "foo --run-ignored ignored-only"),
1552 ("foo -- --include-ignored", "foo --run-ignored all"),
1553 (
1557 "foo -- --ignored -- str --- --ignored",
1558 "foo --run-ignored ignored-only str -- -- --- --ignored",
1559 ),
1560 ("foo -- -- str1 str2 --", "foo str1 str2 -- -- --"),
1561 ];
1562 let skip_exact = &[
1563 ("foo -- --skip my-pattern --skip your-pattern", {
1567 let mut patterns = TestFilterPatterns::default();
1568 patterns.add_skip_pattern("my-pattern".to_owned());
1569 patterns.add_skip_pattern("your-pattern".to_owned());
1570 patterns
1571 }),
1572 ("foo -- pattern1 --skip my-pattern --skip your-pattern", {
1573 let mut patterns = TestFilterPatterns::default();
1574 patterns.add_substring_pattern("pattern1".to_owned());
1575 patterns.add_skip_pattern("my-pattern".to_owned());
1576 patterns.add_skip_pattern("your-pattern".to_owned());
1577 patterns
1578 }),
1579 (
1583 "foo -- --skip my-pattern --skip your-pattern exact1 --exact pattern2",
1584 {
1585 let mut patterns = TestFilterPatterns::default();
1586 patterns.add_skip_exact_pattern("my-pattern".to_owned());
1587 patterns.add_skip_exact_pattern("your-pattern".to_owned());
1588 patterns.add_exact_pattern("exact1".to_owned());
1589 patterns.add_exact_pattern("pattern2".to_owned());
1590 patterns
1591 },
1592 ),
1593 ];
1594 let invalid = &[
1595 ("foo -- --include-ignored --include-ignored", "duplicated"),
1599 ("foo -- --ignored --ignored", "duplicated"),
1600 ("foo -- --exact --exact", "duplicated"),
1601 ("foo -- --ignored --include-ignored", "mutually exclusive"),
1605 ("foo --run-ignored all -- --ignored", "mutually exclusive"),
1606 ("foo -- --skip", "missing required argument"),
1610 ("foo -- --bar", "unsupported"),
1614 ];
1615
1616 for (a, b) in valid {
1617 let a_str = format!(
1618 "{:?}",
1619 get_test_filter_builder(a).unwrap_or_else(|_| panic!("failed to parse {a}"))
1620 );
1621 let b_str = format!(
1622 "{:?}",
1623 get_test_filter_builder(b).unwrap_or_else(|_| panic!("failed to parse {b}"))
1624 );
1625 assert_eq!(a_str, b_str);
1626 }
1627
1628 for (args, patterns) in skip_exact {
1629 let builder =
1630 get_test_filter_builder(args).unwrap_or_else(|_| panic!("failed to parse {args}"));
1631
1632 let builder2 =
1633 TestFilterBuilder::new(RunIgnored::Default, None, patterns.clone(), Vec::new())
1634 .unwrap_or_else(|_| panic!("failed to build TestFilterBuilder"));
1635
1636 assert_eq!(builder, builder2, "{args} matches expected");
1637 }
1638
1639 for (s, r) in invalid {
1640 let res = get_test_filter_builder(s);
1641 if let Err(ExpectedError::TestBinaryArgsParseError { reason, .. }) = &res {
1642 assert_eq!(reason, r);
1643 } else {
1644 panic!(
1645 "{s} should have errored out with TestBinaryArgsParseError, actual: {res:?}",
1646 );
1647 }
1648 }
1649 }
1650}