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 runner::{
38 DebuggerCommand, Interceptor, StressCondition, StressCount, TestRunnerBuilder,
39 TracerCommand,
40 },
41 test_filter::{FilterBound, RunIgnored, TestFilterBuilder, TestFilterPatterns},
42 test_output::CaptureStrategy,
43};
44use std::{collections::BTreeSet, io::Cursor, sync::Arc, time::Duration};
45use tracing::{debug, warn};
46
47#[derive(Debug, Args)]
49pub(super) struct CommonOpts {
50 #[arg(
52 long,
53 global = true,
54 value_name = "PATH",
55 help_heading = "Manifest options"
56 )]
57 pub(super) manifest_path: Option<Utf8PathBuf>,
58
59 #[clap(flatten)]
60 pub(super) output: crate::output::OutputOpts,
61
62 #[clap(flatten)]
63 pub(super) config_opts: ConfigOpts,
64}
65
66#[derive(Debug, Args)]
67#[command(next_help_heading = "Config options")]
68pub(super) struct ConfigOpts {
69 #[arg(long, global = true, value_name = "PATH")]
71 pub config_file: Option<Utf8PathBuf>,
72
73 #[arg(long = "tool-config-file", global = true, value_name = "TOOL:ABS_PATH")]
87 pub tool_config_files: Vec<ToolConfigFile>,
88
89 #[arg(long, global = true)]
94 pub override_version_check: bool,
95
96 #[arg(
102 long,
103 short = 'P',
104 env = "NEXTEST_PROFILE",
105 global = true,
106 help_heading = "Config options"
107 )]
108 pub(super) profile: Option<String>,
109}
110
111impl ConfigOpts {
112 pub(super) fn make_version_only_config(
114 &self,
115 workspace_root: &Utf8Path,
116 ) -> Result<VersionOnlyConfig> {
117 VersionOnlyConfig::from_sources(
118 workspace_root,
119 self.config_file.as_deref(),
120 &self.tool_config_files,
121 )
122 .map_err(ExpectedError::config_parse_error)
123 }
124
125 pub(super) fn make_config(
127 &self,
128 workspace_root: &Utf8Path,
129 pcx: &ParseContext<'_>,
130 experimental: &BTreeSet<ConfigExperimental>,
131 ) -> Result<NextestConfig> {
132 NextestConfig::from_sources(
133 workspace_root,
134 pcx,
135 self.config_file.as_deref(),
136 &self.tool_config_files,
137 experimental,
138 )
139 .map_err(ExpectedError::config_parse_error)
140 }
141}
142
143#[derive(Debug, Subcommand)]
144pub(super) enum Command {
145 List(Box<ListOpts>),
156 #[command(visible_alias = "r")]
163 Run(Box<RunOpts>),
164 Archive(Box<ArchiveOpts>),
172 ShowConfig {
179 #[clap(subcommand)]
180 command: super::commands::ShowConfigCommand,
181 },
182 #[clap(name = "self")]
184 Self_ {
185 #[clap(subcommand)]
186 command: super::commands::SelfCommand,
187 },
188 #[clap(hide = true)]
193 Debug {
194 #[clap(subcommand)]
195 command: super::commands::DebugCommand,
196 },
197}
198
199#[derive(Debug, Args)]
200pub(super) struct ArchiveOpts {
201 #[clap(flatten)]
202 pub(super) cargo_options: CargoOptions,
203
204 #[arg(
206 long,
207 name = "archive-file",
208 help_heading = "Archive options",
209 value_name = "PATH"
210 )]
211 pub(super) archive_file: Utf8PathBuf,
212
213 #[arg(
218 long,
219 value_enum,
220 help_heading = "Archive options",
221 value_name = "FORMAT",
222 default_value_t
223 )]
224 pub(super) archive_format: ArchiveFormatOpt,
225
226 #[clap(flatten)]
227 pub(super) archive_build_filter: ArchiveBuildFilter,
228
229 #[arg(
231 long,
232 help_heading = "Archive options",
233 value_name = "LEVEL",
234 default_value_t = 0,
235 allow_negative_numbers = true
236 )]
237 pub(super) zstd_level: i32,
238 }
240
241#[derive(Debug, Args)]
242pub(super) struct ListOpts {
243 #[clap(flatten)]
244 pub(super) cargo_options: CargoOptions,
245
246 #[clap(flatten)]
247 pub(super) build_filter: TestBuildFilter,
248
249 #[arg(
251 short = 'T',
252 long,
253 value_enum,
254 default_value_t,
255 help_heading = "Output options",
256 value_name = "FMT"
257 )]
258 pub(super) message_format: MessageFormatOpts,
259
260 #[arg(
262 long,
263 value_enum,
264 default_value_t,
265 help_heading = "Output options",
266 value_name = "TYPE"
267 )]
268 pub(super) list_type: ListType,
269
270 #[clap(flatten)]
271 pub(super) reuse_build: ReuseBuildOpts,
272}
273
274#[derive(Debug, Args)]
275pub(super) struct RunOpts {
276 #[clap(flatten)]
277 pub(super) cargo_options: CargoOptions,
278
279 #[clap(flatten)]
280 pub(super) build_filter: TestBuildFilter,
281
282 #[clap(flatten)]
283 pub(super) runner_opts: TestRunnerOpts,
284
285 #[arg(
287 long,
288 name = "no-capture",
289 alias = "nocapture",
290 help_heading = "Runner options",
291 display_order = 100
292 )]
293 pub(super) no_capture: bool,
294
295 #[clap(flatten)]
296 pub(super) reporter_opts: ReporterOpts,
297
298 #[clap(flatten)]
299 pub(super) reuse_build: ReuseBuildOpts,
300}
301
302#[derive(Copy, Clone, Debug, ValueEnum, Default)]
303pub(crate) enum PlatformFilterOpts {
304 Target,
305 Host,
306 #[default]
307 Any,
308}
309
310impl From<PlatformFilterOpts> for Option<BuildPlatform> {
311 fn from(opt: PlatformFilterOpts) -> Self {
312 match opt {
313 PlatformFilterOpts::Target => Some(BuildPlatform::Target),
314 PlatformFilterOpts::Host => Some(BuildPlatform::Host),
315 PlatformFilterOpts::Any => None,
316 }
317 }
318}
319
320#[derive(Copy, Clone, Debug, ValueEnum, Default)]
321pub(super) enum ListType {
322 #[default]
323 Full,
324 BinariesOnly,
325}
326
327#[derive(Copy, Clone, Debug, ValueEnum, Default)]
328pub(super) enum MessageFormatOpts {
329 #[default]
330 Human,
331 Json,
332 JsonPretty,
333}
334
335impl MessageFormatOpts {
336 pub(super) fn to_output_format(self, verbose: bool) -> OutputFormat {
337 match self {
338 Self::Human => OutputFormat::Human { verbose },
339 Self::Json => OutputFormat::Serializable(SerializableFormat::Json),
340 Self::JsonPretty => OutputFormat::Serializable(SerializableFormat::JsonPretty),
341 }
342 }
343}
344
345#[derive(Debug, Args)]
346#[command(next_help_heading = "Filter options")]
347pub(super) struct TestBuildFilter {
348 #[arg(long, value_enum, value_name = "WHICH")]
350 run_ignored: Option<RunIgnoredOpt>,
351
352 #[arg(long)]
354 partition: Option<PartitionerBuilder>,
355
356 #[arg(
360 long,
361 hide_short_help = true,
362 value_enum,
363 value_name = "PLATFORM",
364 default_value_t
365 )]
366 pub(crate) platform_filter: PlatformFilterOpts,
367
368 #[arg(
370 long,
371 alias = "filter-expr",
372 short = 'E',
373 value_name = "EXPR",
374 action(ArgAction::Append)
375 )]
376 pub(super) filterset: Vec<String>,
377
378 #[arg(long)]
385 ignore_default_filter: bool,
386
387 #[arg(help_heading = None, name = "FILTERS")]
389 pre_double_dash_filters: Vec<String>,
390
391 #[arg(help_heading = None, value_name = "FILTERS_AND_ARGS", last = true)]
399 filters: Vec<String>,
400}
401
402impl TestBuildFilter {
403 #[expect(clippy::too_many_arguments)]
404 pub(super) fn compute_test_list<'g>(
405 &self,
406 ctx: &TestExecuteContext<'_>,
407 graph: &'g PackageGraph,
408 workspace_root: Utf8PathBuf,
409 binary_list: Arc<BinaryList>,
410 test_filter_builder: TestFilterBuilder,
411 env: EnvironmentMap,
412 profile: &EvaluatableProfile<'_>,
413 reuse_build: &ReuseBuildInfo,
414 ) -> Result<TestList<'g>> {
415 let path_mapper = make_path_mapper(
416 reuse_build,
417 graph,
418 &binary_list.rust_build_meta.target_directory,
419 )?;
420
421 let rust_build_meta = binary_list.rust_build_meta.map_paths(&path_mapper);
422 let test_artifacts = RustTestArtifact::from_binary_list(
423 graph,
424 binary_list,
425 &rust_build_meta,
426 &path_mapper,
427 self.platform_filter.into(),
428 )?;
429 TestList::new(
430 ctx,
431 test_artifacts,
432 rust_build_meta,
433 &test_filter_builder,
434 workspace_root,
435 env,
436 profile,
437 if self.ignore_default_filter {
438 FilterBound::All
439 } else {
440 FilterBound::DefaultSet
441 },
442 get_num_cpus(),
444 )
445 .map_err(|err| ExpectedError::CreateTestListError { err })
446 }
447
448 pub(super) fn make_test_filter_builder(
449 &self,
450 filter_exprs: Vec<nextest_filtering::Filterset>,
451 ) -> Result<TestFilterBuilder> {
452 let mut run_ignored = self.run_ignored.map(Into::into);
454 let mut patterns = TestFilterPatterns::new(self.pre_double_dash_filters.clone());
455 self.merge_test_binary_args(&mut run_ignored, &mut patterns)?;
456
457 Ok(TestFilterBuilder::new(
458 run_ignored.unwrap_or_default(),
459 self.partition.clone(),
460 patterns,
461 filter_exprs,
462 )?)
463 }
464
465 fn merge_test_binary_args(
466 &self,
467 run_ignored: &mut Option<RunIgnored>,
468 patterns: &mut TestFilterPatterns,
469 ) -> Result<()> {
470 let mut is_exact = false;
473 for arg in &self.filters {
474 if arg == "--" {
475 break;
476 }
477 if arg == "--exact" {
478 if is_exact {
479 return Err(ExpectedError::test_binary_args_parse_error(
480 "duplicated",
481 vec![arg.clone()],
482 ));
483 }
484 is_exact = true;
485 }
486 }
487
488 let mut ignore_filters = Vec::new();
489 let mut read_trailing_filters = false;
490
491 let mut unsupported_args = Vec::new();
492
493 let mut it = self.filters.iter();
494 while let Some(arg) = it.next() {
495 if read_trailing_filters || !arg.starts_with('-') {
496 if is_exact {
497 patterns.add_exact_pattern(arg.clone());
498 } else {
499 patterns.add_substring_pattern(arg.clone());
500 }
501 } else if arg == "--include-ignored" {
502 ignore_filters.push((arg.clone(), RunIgnored::All));
503 } else if arg == "--ignored" {
504 ignore_filters.push((arg.clone(), RunIgnored::Only));
505 } else if arg == "--" {
506 read_trailing_filters = true;
507 } else if arg == "--skip" {
508 let skip_arg = it.next().ok_or_else(|| {
509 ExpectedError::test_binary_args_parse_error(
510 "missing required argument",
511 vec![arg.clone()],
512 )
513 })?;
514
515 if is_exact {
516 patterns.add_skip_exact_pattern(skip_arg.clone());
517 } else {
518 patterns.add_skip_pattern(skip_arg.clone());
519 }
520 } else if arg == "--exact" {
521 } else {
523 unsupported_args.push(arg.clone());
524 }
525 }
526
527 for (s, f) in ignore_filters {
528 if let Some(run_ignored) = run_ignored {
529 if *run_ignored != f {
530 return Err(ExpectedError::test_binary_args_parse_error(
531 "mutually exclusive",
532 vec![s],
533 ));
534 } else {
535 return Err(ExpectedError::test_binary_args_parse_error(
536 "duplicated",
537 vec![s],
538 ));
539 }
540 } else {
541 *run_ignored = Some(f);
542 }
543 }
544
545 if !unsupported_args.is_empty() {
546 return Err(ExpectedError::test_binary_args_parse_error(
547 "unsupported",
548 unsupported_args,
549 ));
550 }
551
552 Ok(())
553 }
554}
555
556#[derive(Debug, Args)]
557#[command(next_help_heading = "Filter options")]
558pub(super) struct ArchiveBuildFilter {
559 #[arg(long, short = 'E', value_name = "EXPR", action(ArgAction::Append))]
563 pub(super) filterset: Vec<String>,
564}
565
566#[derive(Copy, Clone, Debug, ValueEnum)]
567enum RunIgnoredOpt {
568 Default,
570
571 #[clap(alias = "ignored-only")]
573 Only,
574
575 All,
577}
578
579impl From<RunIgnoredOpt> for RunIgnored {
580 fn from(opt: RunIgnoredOpt) -> Self {
581 match opt {
582 RunIgnoredOpt::Default => RunIgnored::Default,
583 RunIgnoredOpt::Only => RunIgnored::Only,
584 RunIgnoredOpt::All => RunIgnored::All,
585 }
586 }
587}
588
589impl CargoOptions {
590 pub(super) fn compute_binary_list(
591 &self,
592 graph: &PackageGraph,
593 manifest_path: Option<&Utf8Path>,
594 output: OutputContext,
595 build_platforms: BuildPlatforms,
596 ) -> Result<BinaryList> {
597 let mut cargo_cli = CargoCli::new("test", manifest_path, output);
600
601 cargo_cli.add_args(["--no-run", "--message-format", "json-render-diagnostics"]);
603 cargo_cli.add_options(self);
604
605 let expression = cargo_cli.to_expression();
606 let output = expression
607 .stdout_capture()
608 .unchecked()
609 .run()
610 .map_err(|err| ExpectedError::build_exec_failed(cargo_cli.all_args(), err))?;
611 if !output.status.success() {
612 return Err(ExpectedError::build_failed(
613 cargo_cli.all_args(),
614 output.status.code(),
615 ));
616 }
617
618 let test_binaries =
619 BinaryList::from_messages(Cursor::new(output.stdout), graph, build_platforms)?;
620 Ok(test_binaries)
621 }
622}
623
624#[derive(Debug, Default, Args)]
626#[command(next_help_heading = "Runner options")]
627pub struct TestRunnerOpts {
628 #[arg(long, name = "no-run")]
630 pub(super) no_run: bool,
631
632 #[arg(
635 long,
636 short = 'j',
637 visible_alias = "jobs",
638 value_name = "N",
639 env = "NEXTEST_TEST_THREADS",
640 allow_negative_numbers = true
641 )]
642 test_threads: Option<TestThreads>,
643
644 #[arg(long, env = "NEXTEST_RETRIES", value_name = "N")]
646 retries: Option<u32>,
647
648 #[arg(
650 long,
651 visible_alias = "ff",
652 name = "fail-fast",
653 conflicts_with = "no-run"
659 )]
660 fail_fast: bool,
661
662 #[arg(
664 long,
665 visible_alias = "nff",
666 name = "no-fail-fast",
667 conflicts_with = "no-run",
668 overrides_with = "fail-fast"
669 )]
670 no_fail_fast: bool,
671
672 #[arg(
680 long,
681 name = "max-fail",
682 value_name = "N[:MODE]",
683 conflicts_with_all = &["no-run", "fail-fast", "no-fail-fast"],
684 )]
685 max_fail: Option<MaxFail>,
686
687 #[clap(flatten)]
689 pub(super) interceptor: InterceptorOpt,
690
691 #[arg(long, value_enum, value_name = "ACTION", env = "NEXTEST_NO_TESTS")]
693 pub(super) no_tests: Option<NoTestsBehavior>,
694
695 #[clap(flatten)]
697 pub(super) stress: StressOptions,
698}
699
700#[derive(Debug, Default, Args)]
701#[group(id = "interceptor", multiple = false)]
702pub(super) struct InterceptorOpt {
703 #[arg(long, value_name = "DEBUGGER", conflicts_with_all = ["stress_condition", "no-run"])]
713 pub(super) debugger: Option<DebuggerCommand>,
714
715 #[arg(long, value_name = "TRACER", conflicts_with_all = ["stress_condition", "no-run"])]
726 pub(super) tracer: Option<TracerCommand>,
727}
728
729impl InterceptorOpt {
730 pub(super) fn is_active(&self) -> bool {
732 self.debugger.is_some() || self.tracer.is_some()
733 }
734
735 pub(super) fn to_interceptor(&self) -> Interceptor {
737 match (&self.debugger, &self.tracer) {
738 (Some(debugger), None) => Interceptor::Debugger(debugger.clone()),
739 (None, Some(tracer)) => Interceptor::Tracer(tracer.clone()),
740 (None, None) => Interceptor::None,
741 (Some(_), Some(_)) => {
742 unreachable!("clap group ensures debugger and tracer are mutually exclusive")
743 }
744 }
745 }
746}
747
748#[derive(Clone, Copy, Debug, ValueEnum)]
749pub(super) enum NoTestsBehavior {
750 Pass,
752
753 Warn,
755
756 #[clap(alias = "error")]
758 Fail,
759}
760
761impl TestRunnerOpts {
762 pub(super) fn to_builder(&self, cap_strat: CaptureStrategy) -> Option<TestRunnerBuilder> {
763 if self.test_threads.is_some()
767 && let Some(reasons) =
768 no_run_no_capture_reasons(self.no_run, cap_strat == CaptureStrategy::None)
769 {
770 warn!("ignoring --test-threads because {reasons}");
771 }
772
773 if self.retries.is_some() && self.no_run {
774 warn!("ignoring --retries because --no-run is specified");
775 }
776 if self.no_tests.is_some() && self.no_run {
777 warn!("ignoring --no-tests because --no-run is specified");
778 }
779
780 if self.no_run {
783 return None;
784 }
785
786 let mut builder = TestRunnerBuilder::default();
787 builder.set_capture_strategy(cap_strat);
788 if let Some(retries) = self.retries {
789 builder.set_retries(RetryPolicy::new_without_delay(retries));
790 }
791
792 if let Some(max_fail) = self.max_fail {
793 builder.set_max_fail(max_fail);
794 debug!(max_fail = ?max_fail, "set max fail");
795 } else if self.no_fail_fast {
796 builder.set_max_fail(MaxFail::from_fail_fast(false));
797 debug!("set max fail via from_fail_fast(false)");
798 } else if self.fail_fast {
799 builder.set_max_fail(MaxFail::from_fail_fast(true));
800 debug!("set max fail via from_fail_fast(true)");
801 }
802
803 if let Some(test_threads) = self.test_threads {
804 builder.set_test_threads(test_threads);
805 }
806
807 if let Some(condition) = self.stress.condition.as_ref() {
808 builder.set_stress_condition(condition.stress_condition());
809 }
810
811 builder.set_interceptor(self.interceptor.to_interceptor());
812
813 Some(builder)
814 }
815}
816
817fn no_run_no_capture_reasons(no_run: bool, no_capture: bool) -> Option<&'static str> {
818 match (no_run, no_capture) {
819 (true, true) => Some("--no-run and --no-capture are specified"),
820 (true, false) => Some("--no-run is specified"),
821 (false, true) => Some("--no-capture is specified"),
822 (false, false) => None,
823 }
824}
825
826#[derive(Clone, Copy, Debug, ValueEnum)]
827pub(super) enum IgnoreOverridesOpt {
828 Retries,
829 All,
830}
831
832#[derive(Clone, Copy, Debug, ValueEnum, Default)]
833pub(super) enum MessageFormat {
834 #[default]
836 Human,
837 LibtestJson,
839 LibtestJsonPlus,
842}
843
844#[derive(Debug, Default, Args)]
845#[command(next_help_heading = "Stress testing options")]
846pub(super) struct StressOptions {
847 #[clap(flatten)]
849 pub(super) condition: Option<StressConditionOpt>,
850 }
852
853#[derive(Clone, Debug, Default, Args)]
854#[group(id = "stress_condition", multiple = false)]
855pub(super) struct StressConditionOpt {
856 #[arg(long, value_name = "COUNT")]
858 stress_count: Option<StressCount>,
859
860 #[arg(long, value_name = "DURATION", value_parser = non_zero_duration)]
862 stress_duration: Option<Duration>,
863}
864
865impl StressConditionOpt {
866 fn stress_condition(&self) -> StressCondition {
867 if let Some(count) = self.stress_count {
868 StressCondition::Count(count)
869 } else if let Some(duration) = self.stress_duration {
870 StressCondition::Duration(duration)
871 } else {
872 unreachable!(
873 "if StressOptions::condition is Some, \
874 one of these should be set"
875 )
876 }
877 }
878}
879
880fn non_zero_duration(input: &str) -> std::result::Result<Duration, String> {
881 let duration = humantime::parse_duration(input).map_err(|error| error.to_string())?;
882 if duration.is_zero() {
883 Err("duration must be non-zero".to_string())
884 } else {
885 Ok(duration)
886 }
887}
888
889#[derive(Debug, Default, Args)]
890#[command(next_help_heading = "Reporter options")]
891pub(super) struct ReporterOpts {
892 #[arg(long, value_enum, value_name = "WHEN", env = "NEXTEST_FAILURE_OUTPUT")]
894 failure_output: Option<TestOutputDisplayOpt>,
895
896 #[arg(long, value_enum, value_name = "WHEN", env = "NEXTEST_SUCCESS_OUTPUT")]
898 success_output: Option<TestOutputDisplayOpt>,
899
900 #[arg(long, value_enum, value_name = "LEVEL", env = "NEXTEST_STATUS_LEVEL")]
903 status_level: Option<StatusLevelOpt>,
904
905 #[arg(
907 long,
908 value_enum,
909 value_name = "LEVEL",
910 env = "NEXTEST_FINAL_STATUS_LEVEL"
911 )]
912 final_status_level: Option<FinalStatusLevelOpt>,
913
914 #[arg(long, env = "NEXTEST_SHOW_PROGRESS")]
916 show_progress: Option<ShowProgressOpt>,
917
918 #[arg(long, env = "NEXTEST_HIDE_PROGRESS_BAR", value_parser = BoolishValueParser::new())]
920 hide_progress_bar: bool,
921
922 #[arg(long, env = "NEXTEST_NO_OUTPUT_INDENT", value_parser = BoolishValueParser::new())]
931 no_output_indent: bool,
932
933 #[arg(long, env = "NEXTEST_NO_INPUT_HANDLER", value_parser = BoolishValueParser::new())]
938 pub(super) no_input_handler: bool,
939
940 #[arg(
948 long = "max-progress-running",
949 value_name = "N",
950 env = "NEXTEST_MAX_PROGRESS_RUNNING",
951 default_value = "8"
952 )]
953 max_progress_running: MaxProgressRunning,
954
955 #[arg(
957 long,
958 name = "message-format",
959 value_enum,
960 value_name = "FORMAT",
961 env = "NEXTEST_MESSAGE_FORMAT"
962 )]
963 pub(super) message_format: Option<MessageFormat>,
964
965 #[arg(
970 long,
971 requires = "message-format",
972 value_name = "VERSION",
973 env = "NEXTEST_MESSAGE_FORMAT_VERSION"
974 )]
975 pub(super) message_format_version: Option<String>,
976}
977
978impl ReporterOpts {
979 pub(super) fn to_builder(
980 &self,
981 no_run: bool,
982 no_capture: bool,
983 should_colorize: bool,
984 ) -> ReporterBuilder {
985 if no_run && no_capture {
989 warn!("ignoring --no-capture because --no-run is specified");
990 }
991
992 let reasons = no_run_no_capture_reasons(no_run, no_capture);
993
994 if self.failure_output.is_some()
995 && let Some(reasons) = reasons
996 {
997 warn!("ignoring --failure-output because {}", reasons);
998 }
999 if self.success_output.is_some()
1000 && let Some(reasons) = reasons
1001 {
1002 warn!("ignoring --success-output because {}", reasons);
1003 }
1004 if self.status_level.is_some() && no_run {
1005 warn!("ignoring --status-level because --no-run is specified");
1006 }
1007 if self.final_status_level.is_some() && no_run {
1008 warn!("ignoring --final-status-level because --no-run is specified");
1009 }
1010 if self.message_format.is_some() && no_run {
1011 warn!("ignoring --message-format because --no-run is specified");
1012 }
1013 if self.message_format_version.is_some() && no_run {
1014 warn!("ignoring --message-format-version because --no-run is specified");
1015 }
1016
1017 let show_progress = match (self.show_progress, self.hide_progress_bar) {
1018 (Some(show_progress), true) => {
1019 warn!("ignoring --hide-progress-bar because --show-progress is specified");
1020 show_progress
1021 }
1022 (Some(show_progress), false) => show_progress,
1023 (None, true) => ShowProgressOpt::None,
1024 (None, false) => ShowProgressOpt::default(),
1025 };
1026
1027 let mut builder = ReporterBuilder::default();
1030 builder.set_no_capture(no_capture);
1031 builder.set_colorize(should_colorize);
1032
1033 if let Some(ShowProgressOpt::Only) = self.show_progress {
1034 builder.set_status_level(StatusLevel::Slow);
1038 builder.set_final_status_level(FinalStatusLevel::None);
1039 }
1040 if let Some(failure_output) = self.failure_output {
1041 builder.set_failure_output(failure_output.into());
1042 }
1043 if let Some(success_output) = self.success_output {
1044 builder.set_success_output(success_output.into());
1045 }
1046 if let Some(status_level) = self.status_level {
1047 builder.set_status_level(status_level.into());
1048 }
1049 if let Some(final_status_level) = self.final_status_level {
1050 builder.set_final_status_level(final_status_level.into());
1051 }
1052 builder.set_show_progress(show_progress.into());
1053 builder.set_no_output_indent(self.no_output_indent);
1054 builder.set_max_progress_running(self.max_progress_running);
1055 builder
1056 }
1057}
1058
1059#[derive(Clone, Copy, Debug, ValueEnum)]
1060enum TestOutputDisplayOpt {
1061 Immediate,
1062 ImmediateFinal,
1063 Final,
1064 Never,
1065}
1066
1067impl From<TestOutputDisplayOpt> for TestOutputDisplay {
1068 fn from(opt: TestOutputDisplayOpt) -> Self {
1069 match opt {
1070 TestOutputDisplayOpt::Immediate => TestOutputDisplay::Immediate,
1071 TestOutputDisplayOpt::ImmediateFinal => TestOutputDisplay::ImmediateFinal,
1072 TestOutputDisplayOpt::Final => TestOutputDisplay::Final,
1073 TestOutputDisplayOpt::Never => TestOutputDisplay::Never,
1074 }
1075 }
1076}
1077
1078#[derive(Clone, Copy, Debug, ValueEnum)]
1079enum StatusLevelOpt {
1080 None,
1081 Fail,
1082 Retry,
1083 Slow,
1084 Leak,
1085 Pass,
1086 Skip,
1087 All,
1088}
1089
1090impl From<StatusLevelOpt> for StatusLevel {
1091 fn from(opt: StatusLevelOpt) -> Self {
1092 match opt {
1093 StatusLevelOpt::None => StatusLevel::None,
1094 StatusLevelOpt::Fail => StatusLevel::Fail,
1095 StatusLevelOpt::Retry => StatusLevel::Retry,
1096 StatusLevelOpt::Slow => StatusLevel::Slow,
1097 StatusLevelOpt::Leak => StatusLevel::Leak,
1098 StatusLevelOpt::Pass => StatusLevel::Pass,
1099 StatusLevelOpt::Skip => StatusLevel::Skip,
1100 StatusLevelOpt::All => StatusLevel::All,
1101 }
1102 }
1103}
1104
1105#[derive(Clone, Copy, Debug, ValueEnum)]
1106enum FinalStatusLevelOpt {
1107 None,
1108 Fail,
1109 #[clap(alias = "retry")]
1110 Flaky,
1111 Slow,
1112 Skip,
1113 Pass,
1114 All,
1115}
1116
1117impl From<FinalStatusLevelOpt> for FinalStatusLevel {
1118 fn from(opt: FinalStatusLevelOpt) -> Self {
1119 match opt {
1120 FinalStatusLevelOpt::None => FinalStatusLevel::None,
1121 FinalStatusLevelOpt::Fail => FinalStatusLevel::Fail,
1122 FinalStatusLevelOpt::Flaky => FinalStatusLevel::Flaky,
1123 FinalStatusLevelOpt::Slow => FinalStatusLevel::Slow,
1124 FinalStatusLevelOpt::Skip => FinalStatusLevel::Skip,
1125 FinalStatusLevelOpt::Pass => FinalStatusLevel::Pass,
1126 FinalStatusLevelOpt::All => FinalStatusLevel::All,
1127 }
1128 }
1129}
1130
1131#[derive(Default, Clone, Copy, Debug, ValueEnum)]
1132enum ShowProgressOpt {
1133 #[default]
1136 Auto,
1137
1138 None,
1140
1141 #[clap(alias = "running")]
1144 Bar,
1145
1146 Counter,
1148
1149 Only,
1153}
1154
1155impl From<ShowProgressOpt> for ShowProgress {
1156 fn from(opt: ShowProgressOpt) -> Self {
1157 match opt {
1158 ShowProgressOpt::Auto => ShowProgress::Auto,
1159 ShowProgressOpt::None => ShowProgress::None,
1160 ShowProgressOpt::Bar => ShowProgress::Running,
1161 ShowProgressOpt::Counter => ShowProgress::Counter,
1162 ShowProgressOpt::Only => ShowProgress::Running,
1163 }
1164 }
1165}
1166
1167#[derive(Debug, clap::Parser)]
1172#[command(
1173 version = crate::version::short(),
1174 long_version = crate::version::long(),
1175 bin_name = "cargo",
1176 styles = crate::output::clap_styles::style(),
1177 max_term_width = 100,
1178)]
1179pub struct CargoNextestApp {
1180 #[clap(subcommand)]
1181 subcommand: NextestSubcommand,
1182}
1183
1184impl CargoNextestApp {
1185 pub fn init_output(&self) -> OutputContext {
1187 match &self.subcommand {
1188 NextestSubcommand::Nextest(args) => args.common.output.init(),
1189 NextestSubcommand::Ntr(args) => args.common.output.init(),
1190 #[cfg(unix)]
1191 NextestSubcommand::DoubleSpawn(_) => OutputContext::color_never_init(),
1193 }
1194 }
1195
1196 pub fn exec(
1198 self,
1199 cli_args: Vec<String>,
1200 output: OutputContext,
1201 output_writer: &mut crate::output::OutputWriter,
1202 ) -> Result<i32> {
1203 if let Err(err) = nextest_runner::usdt::register_probes() {
1204 tracing::warn!("failed to register USDT probes: {}", err);
1205 }
1206
1207 match self.subcommand {
1208 NextestSubcommand::Nextest(app) => app.exec(cli_args, output, output_writer),
1209 NextestSubcommand::Ntr(opts) => opts.exec(cli_args, output, output_writer),
1210 #[cfg(unix)]
1211 NextestSubcommand::DoubleSpawn(opts) => opts.exec(output),
1212 }
1213 }
1214}
1215
1216#[derive(Debug, Subcommand)]
1217enum NextestSubcommand {
1218 Nextest(Box<AppOpts>),
1220 Ntr(Box<NtrOpts>),
1222 #[cfg(unix)]
1224 #[command(name = nextest_runner::double_spawn::DoubleSpawnInfo::SUBCOMMAND_NAME, hide = true)]
1225 DoubleSpawn(crate::double_spawn::DoubleSpawnOpts),
1226}
1227
1228#[derive(Debug, Args)]
1229#[clap(
1230 version = crate::version::short(),
1231 long_version = crate::version::long(),
1232 display_name = "cargo-nextest",
1233)]
1234pub(super) struct AppOpts {
1235 #[clap(flatten)]
1236 common: CommonOpts,
1237
1238 #[clap(subcommand)]
1239 command: Command,
1240}
1241
1242impl AppOpts {
1243 fn exec(
1247 self,
1248 cli_args: Vec<String>,
1249 output: OutputContext,
1250 output_writer: &mut crate::output::OutputWriter,
1251 ) -> Result<i32> {
1252 match self.command {
1253 Command::List(list_opts) => {
1254 let base = super::execution::BaseApp::new(
1255 output,
1256 list_opts.reuse_build,
1257 list_opts.cargo_options,
1258 self.common.config_opts,
1259 self.common.manifest_path,
1260 output_writer,
1261 )?;
1262 let app = super::execution::App::new(base, list_opts.build_filter)?;
1263 app.exec_list(list_opts.message_format, list_opts.list_type, output_writer)?;
1264 Ok(0)
1265 }
1266 Command::Run(run_opts) => {
1267 let base = super::execution::BaseApp::new(
1268 output,
1269 run_opts.reuse_build,
1270 run_opts.cargo_options,
1271 self.common.config_opts,
1272 self.common.manifest_path,
1273 output_writer,
1274 )?;
1275 let app = super::execution::App::new(base, run_opts.build_filter)?;
1276 app.exec_run(
1277 run_opts.no_capture,
1278 &run_opts.runner_opts,
1279 &run_opts.reporter_opts,
1280 cli_args,
1281 output_writer,
1282 )?;
1283 Ok(0)
1284 }
1285 Command::Archive(archive_opts) => {
1286 let app = super::execution::BaseApp::new(
1287 output,
1288 ReuseBuildOpts::default(),
1289 archive_opts.cargo_options,
1290 self.common.config_opts,
1291 self.common.manifest_path,
1292 output_writer,
1293 )?;
1294
1295 let app =
1296 super::execution::ArchiveApp::new(app, archive_opts.archive_build_filter)?;
1297 app.exec_archive(
1298 &archive_opts.archive_file,
1299 archive_opts.archive_format,
1300 archive_opts.zstd_level,
1301 output_writer,
1302 )?;
1303 Ok(0)
1304 }
1305 Command::ShowConfig { command } => command.exec(
1306 self.common.manifest_path,
1307 self.common.config_opts,
1308 output,
1309 output_writer,
1310 ),
1311 Command::Self_ { command } => command.exec(self.common.output),
1312 Command::Debug { command } => command.exec(self.common.output),
1313 }
1314 }
1315}
1316
1317#[derive(Debug, Args)]
1318struct NtrOpts {
1319 #[clap(flatten)]
1320 common: CommonOpts,
1321
1322 #[clap(flatten)]
1323 run_opts: RunOpts,
1324}
1325
1326impl NtrOpts {
1327 fn exec(
1328 self,
1329 cli_args: Vec<String>,
1330 output: OutputContext,
1331 output_writer: &mut crate::output::OutputWriter,
1332 ) -> Result<i32> {
1333 let base = super::execution::BaseApp::new(
1334 output,
1335 self.run_opts.reuse_build,
1336 self.run_opts.cargo_options,
1337 self.common.config_opts,
1338 self.common.manifest_path,
1339 output_writer,
1340 )?;
1341 let app = super::execution::App::new(base, self.run_opts.build_filter)?;
1342 app.exec_run(
1343 self.run_opts.no_capture,
1344 &self.run_opts.runner_opts,
1345 &self.run_opts.reporter_opts,
1346 cli_args,
1347 output_writer,
1348 )
1349 }
1350}
1351
1352#[cfg(test)]
1353mod tests {
1354 use super::*;
1355 use clap::Parser;
1356
1357 #[test]
1358 fn test_argument_parsing() {
1359 use clap::error::ErrorKind::{self, *};
1360
1361 let valid: &[&'static str] = &[
1362 "cargo nextest list",
1366 "cargo nextest run",
1367 "cargo nextest list --list-type binaries-only",
1371 "cargo nextest list --list-type full",
1372 "cargo nextest list --message-format json-pretty",
1373 "cargo nextest run --failure-output never",
1374 "cargo nextest run --success-output=immediate",
1375 "cargo nextest run --status-level=all",
1376 "cargo nextest run --no-capture",
1377 "cargo nextest run --nocapture",
1378 "cargo nextest run --no-run",
1379 "cargo nextest run --final-status-level flaky",
1380 "cargo nextest run --max-fail 3",
1381 "cargo nextest run --max-fail=all",
1382 "cargo nextest run --final-status-level retry",
1384 "NEXTEST_HIDE_PROGRESS_BAR=1 cargo nextest run",
1385 "NEXTEST_HIDE_PROGRESS_BAR=true cargo nextest run",
1386 "cargo nextest run --no-run -j8",
1390 "cargo nextest run --no-run --retries 3",
1391 "NEXTEST_TEST_THREADS=8 cargo nextest run --no-run",
1392 "cargo nextest run --no-run --success-output never",
1393 "NEXTEST_SUCCESS_OUTPUT=never cargo nextest run --no-run",
1394 "cargo nextest run --no-run --failure-output immediate",
1395 "NEXTEST_FAILURE_OUTPUT=immediate cargo nextest run --no-run",
1396 "cargo nextest run --no-run --status-level pass",
1397 "NEXTEST_STATUS_LEVEL=pass cargo nextest run --no-run",
1398 "cargo nextest run --no-run --final-status-level skip",
1399 "NEXTEST_FINAL_STATUS_LEVEL=skip cargo nextest run --no-run",
1400 "cargo nextest run --no-capture --test-threads=24",
1404 "NEXTEST_NO_CAPTURE=1 cargo nextest run --test-threads=24",
1405 "cargo nextest run --no-capture --failure-output=never",
1406 "NEXTEST_NO_CAPTURE=1 cargo nextest run --failure-output=never",
1407 "cargo nextest run --no-capture --success-output=final",
1408 "NEXTEST_SUCCESS_OUTPUT=final cargo nextest run --no-capture",
1409 "cargo nextest list --lib --bins",
1413 "cargo nextest run --ignore-rust-version --unit-graph",
1414 "cargo nextest list --binaries-metadata=foo",
1418 "cargo nextest run --binaries-metadata=foo --target-dir-remap=bar",
1419 "cargo nextest list --cargo-metadata path",
1420 "cargo nextest run --cargo-metadata=path --workspace-remap remapped-path",
1421 "cargo nextest archive --archive-file my-archive.tar.zst --zstd-level -1",
1422 "cargo nextest archive --archive-file my-archive.foo --archive-format tar-zst",
1423 "cargo nextest archive --archive-file my-archive.foo --archive-format tar-zstd",
1424 "cargo nextest list --archive-file my-archive.tar.zst",
1425 "cargo nextest list --archive-file my-archive.tar.zst --archive-format tar-zst",
1426 "cargo nextest list --archive-file my-archive.tar.zst --extract-to my-path",
1427 "cargo nextest list --archive-file my-archive.tar.zst --extract-to my-path --extract-overwrite",
1428 "cargo nextest list --archive-file my-archive.tar.zst --persist-extract-tempdir",
1429 "cargo nextest list --archive-file my-archive.tar.zst --workspace-remap foo",
1430 "cargo nextest list --archive-file my-archive.tar.zst --config target.'cfg(all())'.runner=\"my-runner\"",
1431 "cargo nextest list -E deps(foo)",
1435 "cargo nextest run --filterset 'test(bar)' --package=my-package test-filter",
1436 "cargo nextest run --filter-expr 'test(bar)' --package=my-package test-filter",
1437 "cargo nextest list -E 'deps(foo)' --ignore-default-filter",
1438 "cargo nextest run --stress-count 4",
1442 "cargo nextest run --stress-count infinite",
1443 "cargo nextest run --stress-duration 60m",
1444 "cargo nextest run --stress-duration 24h",
1445 "cargo nextest run -- --a an arbitrary arg",
1449 "cargo nextest run --jobs -3",
1451 "cargo nextest run --jobs 3",
1452 "cargo nextest run --build-jobs -1",
1454 "cargo nextest run --build-jobs 1",
1455 ];
1456
1457 let invalid: &[(&'static str, ErrorKind)] = &[
1458 ("cargo nextest run --no-run --fail-fast", ArgumentConflict),
1462 (
1463 "cargo nextest run --no-run --no-fail-fast",
1464 ArgumentConflict,
1465 ),
1466 ("cargo nextest run --no-run --max-fail=3", ArgumentConflict),
1467 (
1471 "cargo nextest run --max-fail=3 --no-fail-fast",
1472 ArgumentConflict,
1473 ),
1474 (
1478 "cargo nextest run --manifest-path foo --cargo-metadata bar",
1481 ArgumentConflict,
1482 ),
1483 (
1484 "cargo nextest run --binaries-metadata=foo --lib",
1485 ArgumentConflict,
1486 ),
1487 (
1491 "cargo nextest run --workspace-remap foo",
1492 MissingRequiredArgument,
1493 ),
1494 (
1498 "cargo nextest run --target-dir-remap bar",
1499 MissingRequiredArgument,
1500 ),
1501 (
1505 "cargo nextest run --archive-format tar-zst",
1506 MissingRequiredArgument,
1507 ),
1508 (
1509 "cargo nextest run --archive-file foo --archive-format no",
1510 InvalidValue,
1511 ),
1512 (
1513 "cargo nextest run --extract-to foo",
1514 MissingRequiredArgument,
1515 ),
1516 (
1517 "cargo nextest run --archive-file foo --extract-overwrite",
1518 MissingRequiredArgument,
1519 ),
1520 (
1521 "cargo nextest run --extract-to foo --extract-overwrite",
1522 MissingRequiredArgument,
1523 ),
1524 (
1525 "cargo nextest run --persist-extract-tempdir",
1526 MissingRequiredArgument,
1527 ),
1528 (
1529 "cargo nextest run --archive-file foo --extract-to bar --persist-extract-tempdir",
1530 ArgumentConflict,
1531 ),
1532 (
1533 "cargo nextest run --archive-file foo --cargo-metadata bar",
1534 ArgumentConflict,
1535 ),
1536 (
1537 "cargo nextest run --archive-file foo --binaries-metadata bar",
1538 ArgumentConflict,
1539 ),
1540 (
1541 "cargo nextest run --archive-file foo --target-dir-remap bar",
1542 ArgumentConflict,
1543 ),
1544 ("cargo nextest run --jobs 0", ValueValidation),
1546 ("cargo nextest run --jobs -twenty", UnknownArgument),
1548 ("cargo nextest run --build-jobs -inf1", UnknownArgument),
1549 ("cargo nextest run --stress-count 0", ValueValidation),
1551 ("cargo nextest run --stress-duration 0m", ValueValidation),
1553 (
1557 "cargo nextest run --debugger gdb --stress-count 4",
1558 ArgumentConflict,
1559 ),
1560 (
1561 "cargo nextest run --debugger gdb --stress-duration 1h",
1562 ArgumentConflict,
1563 ),
1564 (
1565 "cargo nextest run --debugger gdb --no-run",
1566 ArgumentConflict,
1567 ),
1568 ];
1569
1570 for (k, _) in std::env::vars() {
1572 if k.starts_with("NEXTEST_") {
1573 unsafe { std::env::remove_var(k) };
1576 }
1577 }
1578
1579 for valid_args in valid {
1580 let cmd = shell_words::split(valid_args).expect("valid command line");
1581 let env_vars: Vec<_> = cmd
1583 .iter()
1584 .take_while(|arg| arg.contains('='))
1585 .cloned()
1586 .collect();
1587
1588 let mut env_keys = Vec::with_capacity(env_vars.len());
1589 for k_v in &env_vars {
1590 let (k, v) = k_v.split_once('=').expect("valid env var");
1591 unsafe { std::env::set_var(k, v) };
1594 env_keys.push(k);
1595 }
1596
1597 let cmd = cmd.iter().skip(env_vars.len());
1598
1599 if let Err(error) = CargoNextestApp::try_parse_from(cmd) {
1600 panic!("{valid_args} should have successfully parsed, but didn't: {error}");
1601 }
1602
1603 for &k in &env_keys {
1606 unsafe { std::env::remove_var(k) };
1609 }
1610 }
1611
1612 for &(invalid_args, kind) in invalid {
1613 match CargoNextestApp::try_parse_from(
1614 shell_words::split(invalid_args).expect("valid command"),
1615 ) {
1616 Ok(_) => {
1617 panic!("{invalid_args} should have errored out but successfully parsed");
1618 }
1619 Err(error) => {
1620 let actual_kind = error.kind();
1621 if kind != actual_kind {
1622 panic!(
1623 "{invalid_args} should error with kind {kind:?}, but actual kind was {actual_kind:?}",
1624 );
1625 }
1626 }
1627 }
1628 }
1629 }
1630
1631 #[derive(Debug, clap::Parser)]
1632 struct TestCli {
1633 #[structopt(flatten)]
1634 build_filter: TestBuildFilter,
1635 }
1636
1637 #[test]
1638 fn test_test_binary_argument_parsing() {
1639 fn get_test_filter_builder(cmd: &str) -> Result<TestFilterBuilder> {
1640 let app = TestCli::try_parse_from(shell_words::split(cmd).expect("valid command line"))
1641 .unwrap_or_else(|_| panic!("{cmd} should have successfully parsed"));
1642 app.build_filter.make_test_filter_builder(vec![])
1643 }
1644
1645 let valid = &[
1646 ("foo -- str1", "foo str1"),
1650 ("foo -- str2 str3", "foo str2 str3"),
1651 ("foo -- --ignored", "foo --run-ignored only"),
1655 ("foo -- --ignored", "foo --run-ignored ignored-only"),
1656 ("foo -- --include-ignored", "foo --run-ignored all"),
1657 (
1661 "foo -- --ignored -- str --- --ignored",
1662 "foo --run-ignored ignored-only str -- -- --- --ignored",
1663 ),
1664 ("foo -- -- str1 str2 --", "foo str1 str2 -- -- --"),
1665 ];
1666 let skip_exact = &[
1667 ("foo -- --skip my-pattern --skip your-pattern", {
1671 let mut patterns = TestFilterPatterns::default();
1672 patterns.add_skip_pattern("my-pattern".to_owned());
1673 patterns.add_skip_pattern("your-pattern".to_owned());
1674 patterns
1675 }),
1676 ("foo -- pattern1 --skip my-pattern --skip your-pattern", {
1677 let mut patterns = TestFilterPatterns::default();
1678 patterns.add_substring_pattern("pattern1".to_owned());
1679 patterns.add_skip_pattern("my-pattern".to_owned());
1680 patterns.add_skip_pattern("your-pattern".to_owned());
1681 patterns
1682 }),
1683 (
1687 "foo -- --skip my-pattern --skip your-pattern exact1 --exact pattern2",
1688 {
1689 let mut patterns = TestFilterPatterns::default();
1690 patterns.add_skip_exact_pattern("my-pattern".to_owned());
1691 patterns.add_skip_exact_pattern("your-pattern".to_owned());
1692 patterns.add_exact_pattern("exact1".to_owned());
1693 patterns.add_exact_pattern("pattern2".to_owned());
1694 patterns
1695 },
1696 ),
1697 ];
1698 let invalid = &[
1699 ("foo -- --include-ignored --include-ignored", "duplicated"),
1703 ("foo -- --ignored --ignored", "duplicated"),
1704 ("foo -- --exact --exact", "duplicated"),
1705 ("foo -- --ignored --include-ignored", "mutually exclusive"),
1709 ("foo --run-ignored all -- --ignored", "mutually exclusive"),
1710 ("foo -- --skip", "missing required argument"),
1714 ("foo -- --bar", "unsupported"),
1718 ];
1719
1720 for (a, b) in valid {
1721 let a_str = format!(
1722 "{:?}",
1723 get_test_filter_builder(a).unwrap_or_else(|_| panic!("failed to parse {a}"))
1724 );
1725 let b_str = format!(
1726 "{:?}",
1727 get_test_filter_builder(b).unwrap_or_else(|_| panic!("failed to parse {b}"))
1728 );
1729 assert_eq!(a_str, b_str);
1730 }
1731
1732 for (args, patterns) in skip_exact {
1733 let builder =
1734 get_test_filter_builder(args).unwrap_or_else(|_| panic!("failed to parse {args}"));
1735
1736 let builder2 =
1737 TestFilterBuilder::new(RunIgnored::Default, None, patterns.clone(), Vec::new())
1738 .unwrap_or_else(|_| panic!("failed to build TestFilterBuilder"));
1739
1740 assert_eq!(builder, builder2, "{args} matches expected");
1741 }
1742
1743 for (s, r) in invalid {
1744 let res = get_test_filter_builder(s);
1745 if let Err(ExpectedError::TestBinaryArgsParseError { reason, .. }) = &res {
1746 assert_eq!(reason, r);
1747 } else {
1748 panic!(
1749 "{s} should have errored out with TestBinaryArgsParseError, actual: {res:?}",
1750 );
1751 }
1752 }
1753 }
1754}