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")]
862 show_progress: Option<ShowProgressOpt>,
863
864 #[arg(long, env = "NEXTEST_HIDE_PROGRESS_BAR", value_parser = BoolishValueParser::new())]
866 hide_progress_bar: bool,
867
868 #[arg(long, env = "NEXTEST_NO_OUTPUT_INDENT", value_parser = BoolishValueParser::new())]
877 no_output_indent: bool,
878
879 #[arg(long, env = "NEXTEST_NO_INPUT_HANDLER", value_parser = BoolishValueParser::new())]
884 pub(super) no_input_handler: bool,
885
886 #[arg(
888 long,
889 name = "message-format",
890 value_enum,
891 value_name = "FORMAT",
892 env = "NEXTEST_MESSAGE_FORMAT"
893 )]
894 pub(super) message_format: Option<MessageFormat>,
895
896 #[arg(
901 long,
902 requires = "message-format",
903 value_name = "VERSION",
904 env = "NEXTEST_MESSAGE_FORMAT_VERSION"
905 )]
906 pub(super) message_format_version: Option<String>,
907}
908
909impl ReporterOpts {
910 pub(super) fn to_builder(
911 &self,
912 no_run: bool,
913 no_capture: bool,
914 should_colorize: bool,
915 ) -> ReporterBuilder {
916 if no_run && no_capture {
920 warn!("ignoring --no-capture because --no-run is specified");
921 }
922
923 let reasons = no_run_no_capture_reasons(no_run, no_capture);
924
925 if self.failure_output.is_some() {
926 if let Some(reasons) = reasons {
927 warn!("ignoring --failure-output because {}", reasons);
928 }
929 }
930 if self.success_output.is_some() {
931 if let Some(reasons) = reasons {
932 warn!("ignoring --success-output because {}", reasons);
933 }
934 }
935 if self.status_level.is_some() && no_run {
936 warn!("ignoring --status-level because --no-run is specified");
937 }
938 if self.final_status_level.is_some() && no_run {
939 warn!("ignoring --final-status-level because --no-run is specified");
940 }
941 if self.message_format.is_some() && no_run {
942 warn!("ignoring --message-format because --no-run is specified");
943 }
944 if self.message_format_version.is_some() && no_run {
945 warn!("ignoring --message-format-version because --no-run is specified");
946 }
947
948 let show_progress = match (self.show_progress, self.hide_progress_bar) {
949 (Some(show_progress), true) => {
950 warn!("ignoring --hide-progress-bar because --show-progress is specified");
951 show_progress
952 }
953 (Some(show_progress), false) => show_progress,
954 (None, true) => ShowProgressOpt::None,
955 (None, false) => ShowProgressOpt::default(),
956 };
957
958 let mut builder = ReporterBuilder::default();
961 builder.set_no_capture(no_capture);
962 builder.set_colorize(should_colorize);
963
964 if let Some(ShowProgressOpt::Only) = self.show_progress {
965 builder.set_status_level(StatusLevel::Slow);
969 builder.set_final_status_level(FinalStatusLevel::None);
970 }
971 if let Some(failure_output) = self.failure_output {
972 builder.set_failure_output(failure_output.into());
973 }
974 if let Some(success_output) = self.success_output {
975 builder.set_success_output(success_output.into());
976 }
977 if let Some(status_level) = self.status_level {
978 builder.set_status_level(status_level.into());
979 }
980 if let Some(final_status_level) = self.final_status_level {
981 builder.set_final_status_level(final_status_level.into());
982 }
983 builder.set_show_progress(show_progress.into());
984 builder.set_no_output_indent(self.no_output_indent);
985 builder
986 }
987}
988
989#[derive(Clone, Copy, Debug, ValueEnum)]
990enum TestOutputDisplayOpt {
991 Immediate,
992 ImmediateFinal,
993 Final,
994 Never,
995}
996
997impl From<TestOutputDisplayOpt> for TestOutputDisplay {
998 fn from(opt: TestOutputDisplayOpt) -> Self {
999 match opt {
1000 TestOutputDisplayOpt::Immediate => TestOutputDisplay::Immediate,
1001 TestOutputDisplayOpt::ImmediateFinal => TestOutputDisplay::ImmediateFinal,
1002 TestOutputDisplayOpt::Final => TestOutputDisplay::Final,
1003 TestOutputDisplayOpt::Never => TestOutputDisplay::Never,
1004 }
1005 }
1006}
1007
1008#[derive(Clone, Copy, Debug, ValueEnum)]
1009enum StatusLevelOpt {
1010 None,
1011 Fail,
1012 Retry,
1013 Slow,
1014 Leak,
1015 Pass,
1016 Skip,
1017 All,
1018}
1019
1020impl From<StatusLevelOpt> for StatusLevel {
1021 fn from(opt: StatusLevelOpt) -> Self {
1022 match opt {
1023 StatusLevelOpt::None => StatusLevel::None,
1024 StatusLevelOpt::Fail => StatusLevel::Fail,
1025 StatusLevelOpt::Retry => StatusLevel::Retry,
1026 StatusLevelOpt::Slow => StatusLevel::Slow,
1027 StatusLevelOpt::Leak => StatusLevel::Leak,
1028 StatusLevelOpt::Pass => StatusLevel::Pass,
1029 StatusLevelOpt::Skip => StatusLevel::Skip,
1030 StatusLevelOpt::All => StatusLevel::All,
1031 }
1032 }
1033}
1034
1035#[derive(Clone, Copy, Debug, ValueEnum)]
1036enum FinalStatusLevelOpt {
1037 None,
1038 Fail,
1039 #[clap(alias = "retry")]
1040 Flaky,
1041 Slow,
1042 Skip,
1043 Pass,
1044 All,
1045}
1046
1047impl From<FinalStatusLevelOpt> for FinalStatusLevel {
1048 fn from(opt: FinalStatusLevelOpt) -> Self {
1049 match opt {
1050 FinalStatusLevelOpt::None => FinalStatusLevel::None,
1051 FinalStatusLevelOpt::Fail => FinalStatusLevel::Fail,
1052 FinalStatusLevelOpt::Flaky => FinalStatusLevel::Flaky,
1053 FinalStatusLevelOpt::Slow => FinalStatusLevel::Slow,
1054 FinalStatusLevelOpt::Skip => FinalStatusLevel::Skip,
1055 FinalStatusLevelOpt::Pass => FinalStatusLevel::Pass,
1056 FinalStatusLevelOpt::All => FinalStatusLevel::All,
1057 }
1058 }
1059}
1060
1061#[derive(Default, Clone, Copy, Debug, ValueEnum)]
1062enum ShowProgressOpt {
1063 #[default]
1066 Auto,
1067
1068 None,
1070
1071 Bar,
1073
1074 Counter,
1076
1077 Running,
1079
1080 Only,
1083}
1084
1085impl From<ShowProgressOpt> for ShowProgress {
1086 fn from(opt: ShowProgressOpt) -> Self {
1087 match opt {
1088 ShowProgressOpt::Auto => ShowProgress::Auto,
1089 ShowProgressOpt::None => ShowProgress::None,
1090 ShowProgressOpt::Bar => ShowProgress::Bar,
1091 ShowProgressOpt::Counter => ShowProgress::Counter,
1092 ShowProgressOpt::Running => ShowProgress::Running,
1093 ShowProgressOpt::Only => ShowProgress::Running,
1094 }
1095 }
1096}
1097
1098#[derive(Debug, clap::Parser)]
1103#[command(
1104 version = crate::version::short(),
1105 long_version = crate::version::long(),
1106 bin_name = "cargo",
1107 styles = crate::output::clap_styles::style(),
1108 max_term_width = 100,
1109)]
1110pub struct CargoNextestApp {
1111 #[clap(subcommand)]
1112 subcommand: NextestSubcommand,
1113}
1114
1115impl CargoNextestApp {
1116 pub fn init_output(&self) -> OutputContext {
1118 match &self.subcommand {
1119 NextestSubcommand::Nextest(args) => args.common.output.init(),
1120 NextestSubcommand::Ntr(args) => args.common.output.init(),
1121 #[cfg(unix)]
1122 NextestSubcommand::DoubleSpawn(_) => OutputContext::color_never_init(),
1124 }
1125 }
1126
1127 pub fn exec(
1129 self,
1130 cli_args: Vec<String>,
1131 output: OutputContext,
1132 output_writer: &mut crate::output::OutputWriter,
1133 ) -> Result<i32> {
1134 if let Err(err) = nextest_runner::usdt::register_probes() {
1135 tracing::warn!("failed to register USDT probes: {}", err);
1136 }
1137
1138 match self.subcommand {
1139 NextestSubcommand::Nextest(app) => app.exec(cli_args, output, output_writer),
1140 NextestSubcommand::Ntr(opts) => opts.exec(cli_args, output, output_writer),
1141 #[cfg(unix)]
1142 NextestSubcommand::DoubleSpawn(opts) => opts.exec(output),
1143 }
1144 }
1145}
1146
1147#[derive(Debug, Subcommand)]
1148enum NextestSubcommand {
1149 Nextest(Box<AppOpts>),
1151 Ntr(Box<NtrOpts>),
1153 #[cfg(unix)]
1155 #[command(name = nextest_runner::double_spawn::DoubleSpawnInfo::SUBCOMMAND_NAME, hide = true)]
1156 DoubleSpawn(crate::double_spawn::DoubleSpawnOpts),
1157}
1158
1159#[derive(Debug, Args)]
1160#[clap(
1161 version = crate::version::short(),
1162 long_version = crate::version::long(),
1163 display_name = "cargo-nextest",
1164)]
1165pub(super) struct AppOpts {
1166 #[clap(flatten)]
1167 common: CommonOpts,
1168
1169 #[clap(subcommand)]
1170 command: Command,
1171}
1172
1173impl AppOpts {
1174 fn exec(
1178 self,
1179 cli_args: Vec<String>,
1180 output: OutputContext,
1181 output_writer: &mut crate::output::OutputWriter,
1182 ) -> Result<i32> {
1183 match self.command {
1184 Command::List {
1185 cargo_options,
1186 build_filter,
1187 message_format,
1188 list_type,
1189 reuse_build,
1190 } => {
1191 let base = super::execution::BaseApp::new(
1192 output,
1193 reuse_build,
1194 cargo_options,
1195 self.common.config_opts,
1196 self.common.manifest_path,
1197 output_writer,
1198 )?;
1199 let app = super::execution::App::new(base, build_filter)?;
1200 app.exec_list(message_format, list_type, output_writer)?;
1201 Ok(0)
1202 }
1203 Command::Run(run_opts) => {
1204 let base = super::execution::BaseApp::new(
1205 output,
1206 run_opts.reuse_build,
1207 run_opts.cargo_options,
1208 self.common.config_opts,
1209 self.common.manifest_path,
1210 output_writer,
1211 )?;
1212 let app = super::execution::App::new(base, run_opts.build_filter)?;
1213 app.exec_run(
1214 run_opts.no_capture,
1215 &run_opts.runner_opts,
1216 &run_opts.reporter_opts,
1217 cli_args,
1218 output_writer,
1219 )?;
1220 Ok(0)
1221 }
1222 Command::Archive {
1223 cargo_options,
1224 archive_file,
1225 archive_format,
1226 archive_build_filter,
1227 zstd_level,
1228 } => {
1229 let app = super::execution::BaseApp::new(
1230 output,
1231 ReuseBuildOpts::default(),
1232 cargo_options,
1233 self.common.config_opts,
1234 self.common.manifest_path,
1235 output_writer,
1236 )?;
1237
1238 let app = super::execution::ArchiveApp::new(app, archive_build_filter)?;
1239 app.exec_archive(&archive_file, archive_format, zstd_level, output_writer)?;
1240 Ok(0)
1241 }
1242 Command::ShowConfig { command } => command.exec(
1243 self.common.manifest_path,
1244 self.common.config_opts,
1245 output,
1246 output_writer,
1247 ),
1248 Command::Self_ { command } => command.exec(self.common.output),
1249 Command::Debug { command } => command.exec(self.common.output),
1250 }
1251 }
1252}
1253
1254#[derive(Debug, Args)]
1255struct NtrOpts {
1256 #[clap(flatten)]
1257 common: CommonOpts,
1258
1259 #[clap(flatten)]
1260 run_opts: RunOpts,
1261}
1262
1263impl NtrOpts {
1264 fn exec(
1265 self,
1266 cli_args: Vec<String>,
1267 output: OutputContext,
1268 output_writer: &mut crate::output::OutputWriter,
1269 ) -> Result<i32> {
1270 let base = super::execution::BaseApp::new(
1271 output,
1272 self.run_opts.reuse_build,
1273 self.run_opts.cargo_options,
1274 self.common.config_opts,
1275 self.common.manifest_path,
1276 output_writer,
1277 )?;
1278 let app = super::execution::App::new(base, self.run_opts.build_filter)?;
1279 app.exec_run(
1280 self.run_opts.no_capture,
1281 &self.run_opts.runner_opts,
1282 &self.run_opts.reporter_opts,
1283 cli_args,
1284 output_writer,
1285 )
1286 }
1287}
1288
1289#[cfg(test)]
1290mod tests {
1291 use super::*;
1292 use clap::Parser;
1293
1294 #[test]
1295 fn test_argument_parsing() {
1296 use clap::error::ErrorKind::{self, *};
1297
1298 let valid: &[&'static str] = &[
1299 "cargo nextest list",
1303 "cargo nextest run",
1304 "cargo nextest list --list-type binaries-only",
1308 "cargo nextest list --list-type full",
1309 "cargo nextest list --message-format json-pretty",
1310 "cargo nextest run --failure-output never",
1311 "cargo nextest run --success-output=immediate",
1312 "cargo nextest run --status-level=all",
1313 "cargo nextest run --no-capture",
1314 "cargo nextest run --nocapture",
1315 "cargo nextest run --no-run",
1316 "cargo nextest run --final-status-level flaky",
1317 "cargo nextest run --max-fail 3",
1318 "cargo nextest run --max-fail=all",
1319 "cargo nextest run --final-status-level retry",
1321 "NEXTEST_HIDE_PROGRESS_BAR=1 cargo nextest run",
1322 "NEXTEST_HIDE_PROGRESS_BAR=true cargo nextest run",
1323 "cargo nextest run --no-run -j8",
1327 "cargo nextest run --no-run --retries 3",
1328 "NEXTEST_TEST_THREADS=8 cargo nextest run --no-run",
1329 "cargo nextest run --no-run --success-output never",
1330 "NEXTEST_SUCCESS_OUTPUT=never cargo nextest run --no-run",
1331 "cargo nextest run --no-run --failure-output immediate",
1332 "NEXTEST_FAILURE_OUTPUT=immediate cargo nextest run --no-run",
1333 "cargo nextest run --no-run --status-level pass",
1334 "NEXTEST_STATUS_LEVEL=pass cargo nextest run --no-run",
1335 "cargo nextest run --no-run --final-status-level skip",
1336 "NEXTEST_FINAL_STATUS_LEVEL=skip cargo nextest run --no-run",
1337 "cargo nextest run --no-capture --test-threads=24",
1341 "NEXTEST_NO_CAPTURE=1 cargo nextest run --test-threads=24",
1342 "cargo nextest run --no-capture --failure-output=never",
1343 "NEXTEST_NO_CAPTURE=1 cargo nextest run --failure-output=never",
1344 "cargo nextest run --no-capture --success-output=final",
1345 "NEXTEST_SUCCESS_OUTPUT=final cargo nextest run --no-capture",
1346 "cargo nextest list --lib --bins",
1350 "cargo nextest run --ignore-rust-version --unit-graph",
1351 "cargo nextest list --binaries-metadata=foo",
1355 "cargo nextest run --binaries-metadata=foo --target-dir-remap=bar",
1356 "cargo nextest list --cargo-metadata path",
1357 "cargo nextest run --cargo-metadata=path --workspace-remap remapped-path",
1358 "cargo nextest archive --archive-file my-archive.tar.zst --zstd-level -1",
1359 "cargo nextest archive --archive-file my-archive.foo --archive-format tar-zst",
1360 "cargo nextest archive --archive-file my-archive.foo --archive-format tar-zstd",
1361 "cargo nextest list --archive-file my-archive.tar.zst",
1362 "cargo nextest list --archive-file my-archive.tar.zst --archive-format tar-zst",
1363 "cargo nextest list --archive-file my-archive.tar.zst --extract-to my-path",
1364 "cargo nextest list --archive-file my-archive.tar.zst --extract-to my-path --extract-overwrite",
1365 "cargo nextest list --archive-file my-archive.tar.zst --persist-extract-tempdir",
1366 "cargo nextest list --archive-file my-archive.tar.zst --workspace-remap foo",
1367 "cargo nextest list --archive-file my-archive.tar.zst --config target.'cfg(all())'.runner=\"my-runner\"",
1368 "cargo nextest list -E deps(foo)",
1372 "cargo nextest run --filterset 'test(bar)' --package=my-package test-filter",
1373 "cargo nextest run --filter-expr 'test(bar)' --package=my-package test-filter",
1374 "cargo nextest list -E 'deps(foo)' --ignore-default-filter",
1375 "cargo nextest run --stress-count 4",
1379 "cargo nextest run --stress-count infinite",
1380 "cargo nextest run --stress-duration 60m",
1381 "cargo nextest run --stress-duration 24h",
1382 "cargo nextest run -- --a an arbitrary arg",
1386 "cargo nextest run --jobs -3",
1388 "cargo nextest run --jobs 3",
1389 "cargo nextest run --build-jobs -1",
1391 "cargo nextest run --build-jobs 1",
1392 ];
1393
1394 let invalid: &[(&'static str, ErrorKind)] = &[
1395 ("cargo nextest run --no-run --fail-fast", ArgumentConflict),
1399 (
1400 "cargo nextest run --no-run --no-fail-fast",
1401 ArgumentConflict,
1402 ),
1403 ("cargo nextest run --no-run --max-fail=3", ArgumentConflict),
1404 (
1408 "cargo nextest run --max-fail=3 --no-fail-fast",
1409 ArgumentConflict,
1410 ),
1411 (
1415 "cargo nextest run --manifest-path foo --cargo-metadata bar",
1418 ArgumentConflict,
1419 ),
1420 (
1421 "cargo nextest run --binaries-metadata=foo --lib",
1422 ArgumentConflict,
1423 ),
1424 (
1428 "cargo nextest run --workspace-remap foo",
1429 MissingRequiredArgument,
1430 ),
1431 (
1435 "cargo nextest run --target-dir-remap bar",
1436 MissingRequiredArgument,
1437 ),
1438 (
1442 "cargo nextest run --archive-format tar-zst",
1443 MissingRequiredArgument,
1444 ),
1445 (
1446 "cargo nextest run --archive-file foo --archive-format no",
1447 InvalidValue,
1448 ),
1449 (
1450 "cargo nextest run --extract-to foo",
1451 MissingRequiredArgument,
1452 ),
1453 (
1454 "cargo nextest run --archive-file foo --extract-overwrite",
1455 MissingRequiredArgument,
1456 ),
1457 (
1458 "cargo nextest run --extract-to foo --extract-overwrite",
1459 MissingRequiredArgument,
1460 ),
1461 (
1462 "cargo nextest run --persist-extract-tempdir",
1463 MissingRequiredArgument,
1464 ),
1465 (
1466 "cargo nextest run --archive-file foo --extract-to bar --persist-extract-tempdir",
1467 ArgumentConflict,
1468 ),
1469 (
1470 "cargo nextest run --archive-file foo --cargo-metadata bar",
1471 ArgumentConflict,
1472 ),
1473 (
1474 "cargo nextest run --archive-file foo --binaries-metadata bar",
1475 ArgumentConflict,
1476 ),
1477 (
1478 "cargo nextest run --archive-file foo --target-dir-remap bar",
1479 ArgumentConflict,
1480 ),
1481 ("cargo nextest run --jobs 0", ValueValidation),
1483 ("cargo nextest run --jobs -twenty", UnknownArgument),
1485 ("cargo nextest run --build-jobs -inf1", UnknownArgument),
1486 ("cargo nextest run --stress-count 0", ValueValidation),
1488 ("cargo nextest run --stress-duration 0m", ValueValidation),
1490 ];
1491
1492 for (k, _) in std::env::vars() {
1494 if k.starts_with("NEXTEST_") {
1495 unsafe { std::env::remove_var(k) };
1498 }
1499 }
1500
1501 for valid_args in valid {
1502 let cmd = shell_words::split(valid_args).expect("valid command line");
1503 let env_vars: Vec<_> = cmd
1505 .iter()
1506 .take_while(|arg| arg.contains('='))
1507 .cloned()
1508 .collect();
1509
1510 let mut env_keys = Vec::with_capacity(env_vars.len());
1511 for k_v in &env_vars {
1512 let (k, v) = k_v.split_once('=').expect("valid env var");
1513 unsafe { std::env::set_var(k, v) };
1516 env_keys.push(k);
1517 }
1518
1519 let cmd = cmd.iter().skip(env_vars.len());
1520
1521 if let Err(error) = CargoNextestApp::try_parse_from(cmd) {
1522 panic!("{valid_args} should have successfully parsed, but didn't: {error}");
1523 }
1524
1525 for &k in &env_keys {
1528 unsafe { std::env::remove_var(k) };
1531 }
1532 }
1533
1534 for &(invalid_args, kind) in invalid {
1535 match CargoNextestApp::try_parse_from(
1536 shell_words::split(invalid_args).expect("valid command"),
1537 ) {
1538 Ok(_) => {
1539 panic!("{invalid_args} should have errored out but successfully parsed");
1540 }
1541 Err(error) => {
1542 let actual_kind = error.kind();
1543 if kind != actual_kind {
1544 panic!(
1545 "{invalid_args} should error with kind {kind:?}, but actual kind was {actual_kind:?}",
1546 );
1547 }
1548 }
1549 }
1550 }
1551 }
1552
1553 #[derive(Debug, clap::Parser)]
1554 struct TestCli {
1555 #[structopt(flatten)]
1556 build_filter: TestBuildFilter,
1557 }
1558
1559 #[test]
1560 fn test_test_binary_argument_parsing() {
1561 fn get_test_filter_builder(cmd: &str) -> Result<TestFilterBuilder> {
1562 let app = TestCli::try_parse_from(shell_words::split(cmd).expect("valid command line"))
1563 .unwrap_or_else(|_| panic!("{cmd} should have successfully parsed"));
1564 app.build_filter.make_test_filter_builder(vec![])
1565 }
1566
1567 let valid = &[
1568 ("foo -- str1", "foo str1"),
1572 ("foo -- str2 str3", "foo str2 str3"),
1573 ("foo -- --ignored", "foo --run-ignored only"),
1577 ("foo -- --ignored", "foo --run-ignored ignored-only"),
1578 ("foo -- --include-ignored", "foo --run-ignored all"),
1579 (
1583 "foo -- --ignored -- str --- --ignored",
1584 "foo --run-ignored ignored-only str -- -- --- --ignored",
1585 ),
1586 ("foo -- -- str1 str2 --", "foo str1 str2 -- -- --"),
1587 ];
1588 let skip_exact = &[
1589 ("foo -- --skip my-pattern --skip your-pattern", {
1593 let mut patterns = TestFilterPatterns::default();
1594 patterns.add_skip_pattern("my-pattern".to_owned());
1595 patterns.add_skip_pattern("your-pattern".to_owned());
1596 patterns
1597 }),
1598 ("foo -- pattern1 --skip my-pattern --skip your-pattern", {
1599 let mut patterns = TestFilterPatterns::default();
1600 patterns.add_substring_pattern("pattern1".to_owned());
1601 patterns.add_skip_pattern("my-pattern".to_owned());
1602 patterns.add_skip_pattern("your-pattern".to_owned());
1603 patterns
1604 }),
1605 (
1609 "foo -- --skip my-pattern --skip your-pattern exact1 --exact pattern2",
1610 {
1611 let mut patterns = TestFilterPatterns::default();
1612 patterns.add_skip_exact_pattern("my-pattern".to_owned());
1613 patterns.add_skip_exact_pattern("your-pattern".to_owned());
1614 patterns.add_exact_pattern("exact1".to_owned());
1615 patterns.add_exact_pattern("pattern2".to_owned());
1616 patterns
1617 },
1618 ),
1619 ];
1620 let invalid = &[
1621 ("foo -- --include-ignored --include-ignored", "duplicated"),
1625 ("foo -- --ignored --ignored", "duplicated"),
1626 ("foo -- --exact --exact", "duplicated"),
1627 ("foo -- --ignored --include-ignored", "mutually exclusive"),
1631 ("foo --run-ignored all -- --ignored", "mutually exclusive"),
1632 ("foo -- --skip", "missing required argument"),
1636 ("foo -- --bar", "unsupported"),
1640 ];
1641
1642 for (a, b) in valid {
1643 let a_str = format!(
1644 "{:?}",
1645 get_test_filter_builder(a).unwrap_or_else(|_| panic!("failed to parse {a}"))
1646 );
1647 let b_str = format!(
1648 "{:?}",
1649 get_test_filter_builder(b).unwrap_or_else(|_| panic!("failed to parse {b}"))
1650 );
1651 assert_eq!(a_str, b_str);
1652 }
1653
1654 for (args, patterns) in skip_exact {
1655 let builder =
1656 get_test_filter_builder(args).unwrap_or_else(|_| panic!("failed to parse {args}"));
1657
1658 let builder2 =
1659 TestFilterBuilder::new(RunIgnored::Default, None, patterns.clone(), Vec::new())
1660 .unwrap_or_else(|_| panic!("failed to build TestFilterBuilder"));
1661
1662 assert_eq!(builder, builder2, "{args} matches expected");
1663 }
1664
1665 for (s, r) in invalid {
1666 let res = get_test_filter_builder(s);
1667 if let Err(ExpectedError::TestBinaryArgsParseError { reason, .. }) = &res {
1668 assert_eq!(reason, r);
1669 } else {
1670 panic!(
1671 "{s} should have errored out with TestBinaryArgsParseError, actual: {res:?}",
1672 );
1673 }
1674 }
1675 }
1676}