1use crate::{
7 cargo_config::{TargetTriple, TargetTripleSource},
8 config::{ConfigExperimental, CustomTestGroup, ScriptId, TestGroup},
9 helpers::{display_exited_with, dylib_path_envvar},
10 redact::Redactor,
11 reuse_build::{ArchiveFormat, ArchiveStep},
12 target_runner::PlatformRunnerSource,
13};
14use camino::{FromPathBufError, Utf8Path, Utf8PathBuf};
15use config::ConfigError;
16use indent_write::{fmt::IndentWriter, indentable::Indented};
17use itertools::{Either, Itertools};
18use nextest_filtering::errors::FiltersetParseErrors;
19use nextest_metadata::RustBinaryId;
20use smol_str::SmolStr;
21use std::{
22 borrow::Cow,
23 collections::BTreeSet,
24 env::JoinPathsError,
25 fmt::{self, Write as _},
26 process::ExitStatus,
27 sync::Arc,
28};
29use target_spec_miette::IntoMietteDiagnostic;
30use thiserror::Error;
31
32#[derive(Debug, Error)]
34#[error(
35 "failed to parse nextest config at `{config_file}`{}",
36 provided_by_tool(tool.as_deref())
37)]
38#[non_exhaustive]
39pub struct ConfigParseError {
40 config_file: Utf8PathBuf,
41 tool: Option<String>,
42 #[source]
43 kind: ConfigParseErrorKind,
44}
45
46impl ConfigParseError {
47 pub(crate) fn new(
48 config_file: impl Into<Utf8PathBuf>,
49 tool: Option<&str>,
50 kind: ConfigParseErrorKind,
51 ) -> Self {
52 Self {
53 config_file: config_file.into(),
54 tool: tool.map(|s| s.to_owned()),
55 kind,
56 }
57 }
58
59 pub fn config_file(&self) -> &Utf8Path {
61 &self.config_file
62 }
63
64 pub fn tool(&self) -> Option<&str> {
66 self.tool.as_deref()
67 }
68
69 pub fn kind(&self) -> &ConfigParseErrorKind {
71 &self.kind
72 }
73}
74
75pub fn provided_by_tool(tool: Option<&str>) -> String {
77 match tool {
78 Some(tool) => format!(" provided by tool `{tool}`"),
79 None => String::new(),
80 }
81}
82
83#[derive(Debug, Error)]
87#[non_exhaustive]
88pub enum ConfigParseErrorKind {
89 #[error(transparent)]
91 BuildError(Box<ConfigError>),
92 #[error(transparent)]
93 DeserializeError(Box<serde_path_to_error::Error<ConfigError>>),
95 #[error(transparent)]
97 VersionOnlyReadError(std::io::Error),
98 #[error(transparent)]
100 VersionOnlyDeserializeError(Box<serde_path_to_error::Error<toml::de::Error>>),
101 #[error("error parsing compiled data (destructure this variant for more details)")]
103 CompileErrors(Vec<ConfigCompileError>),
104 #[error("invalid test groups defined: {}\n(test groups cannot start with '@tool:' unless specified by a tool)", .0.iter().join(", "))]
106 InvalidTestGroupsDefined(BTreeSet<CustomTestGroup>),
107 #[error(
109 "invalid test groups defined by tool: {}\n(test groups must start with '@tool:<tool-name>:')", .0.iter().join(", "))]
110 InvalidTestGroupsDefinedByTool(BTreeSet<CustomTestGroup>),
111 #[error("unknown test groups specified by config (destructure this variant for more details)")]
113 UnknownTestGroups {
114 errors: Vec<UnknownTestGroupError>,
116
117 known_groups: BTreeSet<TestGroup>,
119 },
120 #[error("invalid config scripts defined: {}\n(config scripts cannot start with '@tool:' unless specified by a tool)", .0.iter().join(", "))]
122 InvalidConfigScriptsDefined(BTreeSet<ScriptId>),
123 #[error(
125 "invalid config scripts defined by tool: {}\n(config scripts must start with '@tool:<tool-name>:')", .0.iter().join(", "))]
126 InvalidConfigScriptsDefinedByTool(BTreeSet<ScriptId>),
127 #[error(
129 "unknown config scripts specified by config (destructure this variant for more details)"
130 )]
131 UnknownConfigScripts {
132 errors: Vec<UnknownConfigScriptError>,
134
135 known_scripts: BTreeSet<ScriptId>,
137 },
138 #[error("unknown experimental features defined (destructure this variant for more details)")]
140 UnknownExperimentalFeatures {
141 unknown: BTreeSet<String>,
143
144 known: BTreeSet<ConfigExperimental>,
146 },
147 #[error(
151 "tool config file specifies experimental features `{}` \
152 -- only repository config files can do so",
153 .features.iter().join(", "),
154 )]
155 ExperimentalFeaturesInToolConfig {
156 features: BTreeSet<String>,
158 },
159 #[error("experimental feature `{feature}` is used but not enabled")]
161 ExperimentalFeatureNotEnabled {
162 feature: ConfigExperimental,
164 },
165}
166
167#[derive(Debug)]
170#[non_exhaustive]
171pub struct ConfigCompileError {
172 pub profile_name: String,
174
175 pub section: ConfigCompileSection,
177
178 pub kind: ConfigCompileErrorKind,
180}
181
182#[derive(Debug)]
185pub enum ConfigCompileSection {
186 DefaultFilter,
188
189 Override(usize),
191
192 Script(usize),
194}
195
196#[derive(Debug)]
198#[non_exhaustive]
199pub enum ConfigCompileErrorKind {
200 ConstraintsNotSpecified {
202 default_filter_specified: bool,
207 },
208
209 FilterAndDefaultFilterSpecified,
213
214 Parse {
216 host_parse_error: Option<target_spec::Error>,
218
219 target_parse_error: Option<target_spec::Error>,
221
222 filter_parse_errors: Vec<FiltersetParseErrors>,
224 },
225}
226
227impl ConfigCompileErrorKind {
228 pub fn reports(&self) -> impl Iterator<Item = miette::Report> + '_ {
230 match self {
231 Self::ConstraintsNotSpecified {
232 default_filter_specified,
233 } => {
234 let message = if *default_filter_specified {
235 "for override with `default-filter`, `platform` must also be specified"
236 } else {
237 "at least one of `platform` and `filter` must be specified"
238 };
239 Either::Left(std::iter::once(miette::Report::msg(message)))
240 }
241 Self::FilterAndDefaultFilterSpecified => {
242 Either::Left(std::iter::once(miette::Report::msg(
243 "at most one of `filter` and `default-filter` must be specified",
244 )))
245 }
246 Self::Parse {
247 host_parse_error,
248 target_parse_error,
249 filter_parse_errors,
250 } => {
251 let host_parse_report = host_parse_error
252 .as_ref()
253 .map(|error| miette::Report::new_boxed(error.clone().into_diagnostic()));
254 let target_parse_report = target_parse_error
255 .as_ref()
256 .map(|error| miette::Report::new_boxed(error.clone().into_diagnostic()));
257 let filter_parse_reports =
258 filter_parse_errors.iter().flat_map(|filter_parse_errors| {
259 filter_parse_errors.errors.iter().map(|single_error| {
260 miette::Report::new(single_error.clone())
261 .with_source_code(filter_parse_errors.input.to_owned())
262 })
263 });
264
265 Either::Right(
266 host_parse_report
267 .into_iter()
268 .chain(target_parse_report)
269 .chain(filter_parse_reports),
270 )
271 }
272 }
273 }
274}
275
276#[derive(Clone, Debug, Error)]
278#[error("test priority ({priority}) out of range: must be between -100 and 100, both inclusive")]
279pub struct TestPriorityOutOfRange {
280 pub priority: i8,
282}
283
284#[derive(Clone, Debug, Error)]
286pub enum ChildStartError {
287 #[error("error creating temporary path for setup script")]
289 TempPath(#[source] Arc<std::io::Error>),
290
291 #[error("error spawning child process")]
293 Spawn(#[source] Arc<std::io::Error>),
294}
295
296#[derive(Clone, Debug, Error)]
298pub enum SetupScriptOutputError {
299 #[error("error opening environment file `{path}`")]
301 EnvFileOpen {
302 path: Utf8PathBuf,
304
305 #[source]
307 error: Arc<std::io::Error>,
308 },
309
310 #[error("error reading environment file `{path}`")]
312 EnvFileRead {
313 path: Utf8PathBuf,
315
316 #[source]
318 error: Arc<std::io::Error>,
319 },
320
321 #[error("line `{line}` in environment file `{path}` not in KEY=VALUE format")]
323 EnvFileParse {
324 path: Utf8PathBuf,
326 line: String,
328 },
329
330 #[error("key `{key}` begins with `NEXTEST`, which is reserved for internal use")]
332 EnvFileReservedKey {
333 key: String,
335 },
336}
337
338#[derive(Clone, Debug)]
343pub struct ErrorList<T> {
344 description: &'static str,
346 inner: Vec<T>,
348}
349
350impl<T: std::error::Error> ErrorList<T> {
351 pub(crate) fn new<U>(description: &'static str, errors: Vec<U>) -> Option<Self>
352 where
353 T: From<U>,
354 {
355 if errors.is_empty() {
356 None
357 } else {
358 Some(Self {
359 description,
360 inner: errors.into_iter().map(T::from).collect(),
361 })
362 }
363 }
364
365 pub(crate) fn short_message(&self) -> String {
367 let string = self.to_string();
368 match string.lines().next() {
369 Some(first_line) => first_line.trim_end_matches(':').to_string(),
371 None => String::new(),
372 }
373 }
374
375 pub(crate) fn iter(&self) -> impl Iterator<Item = &T> {
376 self.inner.iter()
377 }
378}
379
380impl<T: std::error::Error> fmt::Display for ErrorList<T> {
381 fn fmt(&self, mut f: &mut fmt::Formatter) -> fmt::Result {
382 if self.inner.len() == 1 {
384 return write!(f, "{}", self.inner[0]);
385 }
386
387 writeln!(
389 f,
390 "{} errors occurred {}:",
391 self.inner.len(),
392 self.description,
393 )?;
394 for error in &self.inner {
395 let mut indent = IndentWriter::new_skip_initial(" ", f);
396 writeln!(indent, "* {}", DisplayErrorChain::new(error))?;
397 f = indent.into_inner();
398 }
399 Ok(())
400 }
401}
402
403impl<T: std::error::Error> std::error::Error for ErrorList<T> {
404 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
405 if self.inner.len() == 1 {
406 self.inner[0].source()
407 } else {
408 None
411 }
412 }
413}
414
415pub(crate) struct DisplayErrorChain<E> {
420 error: E,
421 initial_indent: &'static str,
422}
423
424impl<E: std::error::Error> DisplayErrorChain<E> {
425 pub(crate) fn new(error: E) -> Self {
426 Self {
427 error,
428 initial_indent: "",
429 }
430 }
431
432 pub(crate) fn new_with_initial_indent(initial_indent: &'static str, error: E) -> Self {
433 Self {
434 error,
435 initial_indent,
436 }
437 }
438}
439
440impl<E> fmt::Display for DisplayErrorChain<E>
441where
442 E: std::error::Error,
443{
444 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
445 let mut writer = IndentWriter::new(self.initial_indent, f);
446 write!(writer, "{}", self.error)?;
447
448 let Some(mut cause) = self.error.source() else {
449 return Ok(());
450 };
451
452 write!(writer, "\n caused by:")?;
453
454 loop {
455 writeln!(writer)?;
456 let mut indent = IndentWriter::new_skip_initial(" ", writer);
457 write!(indent, " - {}", cause)?;
458
459 let Some(next_cause) = cause.source() else {
460 break Ok(());
461 };
462
463 cause = next_cause;
464 writer = indent.into_inner();
465 }
466 }
467}
468
469#[derive(Clone, Debug, Error)]
471pub enum ChildError {
472 #[error(transparent)]
474 Fd(#[from] ChildFdError),
475
476 #[error(transparent)]
478 SetupScriptOutput(#[from] SetupScriptOutputError),
479}
480
481#[derive(Clone, Debug, Error)]
483pub enum ChildFdError {
484 #[error("error reading standard output")]
486 ReadStdout(#[source] Arc<std::io::Error>),
487
488 #[error("error reading standard error")]
490 ReadStderr(#[source] Arc<std::io::Error>),
491
492 #[error("error reading combined stream")]
494 ReadCombined(#[source] Arc<std::io::Error>),
495
496 #[error("error waiting for child process to exit")]
498 Wait(#[source] Arc<std::io::Error>),
499}
500
501#[derive(Clone, Debug, Eq, PartialEq)]
503#[non_exhaustive]
504pub struct UnknownTestGroupError {
505 pub profile_name: String,
507
508 pub name: TestGroup,
510}
511
512#[derive(Clone, Debug, Eq, PartialEq)]
514#[non_exhaustive]
515pub struct UnknownConfigScriptError {
516 pub profile_name: String,
518
519 pub name: ScriptId,
521}
522
523#[derive(Clone, Debug, Error)]
525#[error("profile `{profile} not found (known profiles: {})`", .all_profiles.join(", "))]
526pub struct ProfileNotFound {
527 profile: String,
528 all_profiles: Vec<String>,
529}
530
531impl ProfileNotFound {
532 pub(crate) fn new(
533 profile: impl Into<String>,
534 all_profiles: impl IntoIterator<Item = impl Into<String>>,
535 ) -> Self {
536 let mut all_profiles: Vec<_> = all_profiles.into_iter().map(|s| s.into()).collect();
537 all_profiles.sort_unstable();
538 Self {
539 profile: profile.into(),
540 all_profiles,
541 }
542 }
543}
544
545#[derive(Clone, Debug, Error, Eq, PartialEq)]
547pub enum InvalidIdentifier {
548 #[error("identifier is empty")]
550 Empty,
551
552 #[error("invalid identifier `{0}`")]
554 InvalidXid(SmolStr),
555
556 #[error("tool identifier not of the form \"@tool:tool-name:identifier\": `{0}`")]
558 ToolIdentifierInvalidFormat(SmolStr),
559
560 #[error("tool identifier has empty component: `{0}`")]
562 ToolComponentEmpty(SmolStr),
563
564 #[error("invalid tool identifier `{0}`")]
566 ToolIdentifierInvalidXid(SmolStr),
567}
568
569#[derive(Clone, Debug, Error)]
571#[error("invalid custom test group name: {0}")]
572pub struct InvalidCustomTestGroupName(pub InvalidIdentifier);
573
574#[derive(Clone, Debug, Error)]
576#[error("invalid configuration script name: {0}")]
577pub struct InvalidConfigScriptName(pub InvalidIdentifier);
578
579#[derive(Clone, Debug, Error)]
581pub enum ToolConfigFileParseError {
582 #[error(
583 "tool-config-file has invalid format: {input}\n(hint: tool configs must be in the format <tool-name>:<path>)"
584 )]
585 InvalidFormat {
587 input: String,
589 },
590
591 #[error("tool-config-file has empty tool name: {input}")]
593 EmptyToolName {
594 input: String,
596 },
597
598 #[error("tool-config-file has empty config file path: {input}")]
600 EmptyConfigFile {
601 input: String,
603 },
604
605 #[error("tool-config-file is not an absolute path: {config_file}")]
607 ConfigFileNotAbsolute {
608 config_file: Utf8PathBuf,
610 },
611}
612
613#[derive(Clone, Debug, Error)]
615#[error(
616 "unrecognized value for max-fail: {input}\n(hint: expected either a positive integer or \"all\")"
617)]
618pub struct MaxFailParseError {
619 pub input: String,
621}
622
623impl MaxFailParseError {
624 pub(crate) fn new(input: impl Into<String>) -> Self {
625 Self {
626 input: input.into(),
627 }
628 }
629}
630
631#[derive(Clone, Debug, Error)]
633#[error(
634 "unrecognized value for test-threads: {input}\n(hint: expected either an integer or \"num-cpus\")"
635)]
636pub struct TestThreadsParseError {
637 pub input: String,
639}
640
641impl TestThreadsParseError {
642 pub(crate) fn new(input: impl Into<String>) -> Self {
643 Self {
644 input: input.into(),
645 }
646 }
647}
648
649#[derive(Clone, Debug, Error)]
652pub struct PartitionerBuilderParseError {
653 expected_format: Option<&'static str>,
654 message: Cow<'static, str>,
655}
656
657impl PartitionerBuilderParseError {
658 pub(crate) fn new(
659 expected_format: Option<&'static str>,
660 message: impl Into<Cow<'static, str>>,
661 ) -> Self {
662 Self {
663 expected_format,
664 message: message.into(),
665 }
666 }
667}
668
669impl fmt::Display for PartitionerBuilderParseError {
670 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
671 match self.expected_format {
672 Some(format) => {
673 write!(
674 f,
675 "partition must be in the format \"{}\":\n{}",
676 format, self.message
677 )
678 }
679 None => write!(f, "{}", self.message),
680 }
681 }
682}
683
684#[derive(Clone, Debug, Error)]
687pub enum TestFilterBuilderError {
688 #[error("error constructing test filters")]
690 Construct {
691 #[from]
693 error: aho_corasick::BuildError,
694 },
695}
696
697#[derive(Debug, Error)]
699pub enum PathMapperConstructError {
700 #[error("{kind} `{input}` failed to canonicalize")]
702 Canonicalization {
703 kind: PathMapperConstructKind,
705
706 input: Utf8PathBuf,
708
709 #[source]
711 err: std::io::Error,
712 },
713 #[error("{kind} `{input}` canonicalized to a non-UTF-8 path")]
715 NonUtf8Path {
716 kind: PathMapperConstructKind,
718
719 input: Utf8PathBuf,
721
722 #[source]
724 err: FromPathBufError,
725 },
726 #[error("{kind} `{canonicalized_path}` is not a directory")]
728 NotADirectory {
729 kind: PathMapperConstructKind,
731
732 input: Utf8PathBuf,
734
735 canonicalized_path: Utf8PathBuf,
737 },
738}
739
740impl PathMapperConstructError {
741 pub fn kind(&self) -> PathMapperConstructKind {
743 match self {
744 Self::Canonicalization { kind, .. }
745 | Self::NonUtf8Path { kind, .. }
746 | Self::NotADirectory { kind, .. } => *kind,
747 }
748 }
749
750 pub fn input(&self) -> &Utf8Path {
752 match self {
753 Self::Canonicalization { input, .. }
754 | Self::NonUtf8Path { input, .. }
755 | Self::NotADirectory { input, .. } => input,
756 }
757 }
758}
759
760#[derive(Copy, Clone, Debug, PartialEq, Eq)]
765pub enum PathMapperConstructKind {
766 WorkspaceRoot,
768
769 TargetDir,
771}
772
773impl fmt::Display for PathMapperConstructKind {
774 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
775 match self {
776 Self::WorkspaceRoot => write!(f, "remapped workspace root"),
777 Self::TargetDir => write!(f, "remapped target directory"),
778 }
779 }
780}
781
782#[derive(Debug, Error)]
784pub enum RustBuildMetaParseError {
785 #[error("error deserializing platform from build metadata")]
787 PlatformDeserializeError(#[from] target_spec::Error),
788
789 #[error("the host platform could not be determined")]
791 DetectBuildTargetError(#[source] target_spec::Error),
792
793 #[error("unsupported features in the build metadata: {message}")]
795 Unsupported {
796 message: String,
798 },
799}
800
801#[derive(Clone, Debug, thiserror::Error)]
804#[error("invalid format version: {input}")]
805pub struct FormatVersionError {
806 pub input: String,
808 #[source]
810 pub error: FormatVersionErrorInner,
811}
812
813#[derive(Clone, Debug, thiserror::Error)]
815pub enum FormatVersionErrorInner {
816 #[error("expected format version in form of `{expected}`")]
818 InvalidFormat {
819 expected: &'static str,
821 },
822 #[error("version component `{which}` could not be parsed as an integer")]
824 InvalidInteger {
825 which: &'static str,
827 #[source]
829 err: std::num::ParseIntError,
830 },
831 #[error("version component `{which}` value {value} is out of range {range:?}")]
833 InvalidValue {
834 which: &'static str,
836 value: u8,
838 range: std::ops::Range<u8>,
840 },
841}
842
843#[derive(Debug, Error)]
846#[non_exhaustive]
847pub enum FromMessagesError {
848 #[error("error reading Cargo JSON messages")]
850 ReadMessages(#[source] std::io::Error),
851
852 #[error("error querying package graph")]
854 PackageGraph(#[source] guppy::Error),
855
856 #[error("missing kind for target {binary_name} in package {package_name}")]
858 MissingTargetKind {
859 package_name: String,
861 binary_name: String,
863 },
864}
865
866#[derive(Debug, Error)]
868#[non_exhaustive]
869pub enum CreateTestListError {
870 #[error(
872 "for `{binary_id}`, current directory `{cwd}` is not a directory\n\
873 (hint: ensure project source is available at this location)"
874 )]
875 CwdIsNotDir {
876 binary_id: RustBinaryId,
878
879 cwd: Utf8PathBuf,
881 },
882
883 #[error(
885 "for `{binary_id}`, running command `{}` failed to execute",
886 shell_words::join(command)
887 )]
888 CommandExecFail {
889 binary_id: RustBinaryId,
891
892 command: Vec<String>,
894
895 #[source]
897 error: std::io::Error,
898 },
899
900 #[error(
902 "for `{binary_id}`, command `{}` {}\n--- stdout:\n{}\n--- stderr:\n{}\n---",
903 shell_words::join(command),
904 display_exited_with(*exit_status),
905 String::from_utf8_lossy(stdout),
906 String::from_utf8_lossy(stderr),
907 )]
908 CommandFail {
909 binary_id: RustBinaryId,
911
912 command: Vec<String>,
914
915 exit_status: ExitStatus,
917
918 stdout: Vec<u8>,
920
921 stderr: Vec<u8>,
923 },
924
925 #[error(
927 "for `{binary_id}`, command `{}` produced non-UTF-8 output:\n--- stdout:\n{}\n--- stderr:\n{}\n---",
928 shell_words::join(command),
929 String::from_utf8_lossy(stdout),
930 String::from_utf8_lossy(stderr)
931 )]
932 CommandNonUtf8 {
933 binary_id: RustBinaryId,
935
936 command: Vec<String>,
938
939 stdout: Vec<u8>,
941
942 stderr: Vec<u8>,
944 },
945
946 #[error("for `{binary_id}`, {message}\nfull output:\n{full_output}")]
948 ParseLine {
949 binary_id: RustBinaryId,
951
952 message: Cow<'static, str>,
954
955 full_output: String,
957 },
958
959 #[error(
961 "error joining dynamic library paths for {}: [{}]",
962 dylib_path_envvar(),
963 itertools::join(.new_paths, ", ")
964 )]
965 DylibJoinPaths {
966 new_paths: Vec<Utf8PathBuf>,
968
969 #[source]
971 error: JoinPathsError,
972 },
973
974 #[error("error creating Tokio runtime")]
976 TokioRuntimeCreate(#[source] std::io::Error),
977}
978
979impl CreateTestListError {
980 pub(crate) fn parse_line(
981 binary_id: RustBinaryId,
982 message: impl Into<Cow<'static, str>>,
983 full_output: impl Into<String>,
984 ) -> Self {
985 Self::ParseLine {
986 binary_id,
987 message: message.into(),
988 full_output: full_output.into(),
989 }
990 }
991
992 pub(crate) fn dylib_join_paths(new_paths: Vec<Utf8PathBuf>, error: JoinPathsError) -> Self {
993 Self::DylibJoinPaths { new_paths, error }
994 }
995}
996
997#[derive(Debug, Error)]
999#[non_exhaustive]
1000pub enum WriteTestListError {
1001 #[error("error writing to output")]
1003 Io(#[source] std::io::Error),
1004
1005 #[error("error serializing to JSON")]
1007 Json(#[source] serde_json::Error),
1008}
1009
1010#[derive(Debug, Error)]
1014pub enum ConfigureHandleInheritanceError {
1015 #[cfg(windows)]
1017 #[error("error configuring handle inheritance")]
1018 WindowsError(#[from] std::io::Error),
1019}
1020
1021#[derive(Debug, Error)]
1023#[non_exhaustive]
1024pub enum TestRunnerBuildError {
1025 #[error("error creating Tokio runtime")]
1027 TokioRuntimeCreate(#[source] std::io::Error),
1028
1029 #[error("error setting up signals")]
1031 SignalHandlerSetupError(#[from] SignalHandlerSetupError),
1032}
1033
1034#[derive(Debug, Error)]
1036pub struct TestRunnerExecuteErrors<E> {
1037 pub report_error: Option<E>,
1039
1040 pub join_errors: Vec<tokio::task::JoinError>,
1043}
1044
1045impl<E: std::error::Error> fmt::Display for TestRunnerExecuteErrors<E> {
1046 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1047 if let Some(report_error) = &self.report_error {
1048 write!(f, "error reporting results: {}", report_error)?;
1049 }
1050
1051 if !self.join_errors.is_empty() {
1052 if self.report_error.is_some() {
1053 write!(f, "; ")?;
1054 }
1055
1056 write!(f, "errors joining tasks: ")?;
1057
1058 for (i, join_error) in self.join_errors.iter().enumerate() {
1059 if i > 0 {
1060 write!(f, ", ")?;
1061 }
1062
1063 write!(f, "{}", join_error)?;
1064 }
1065 }
1066
1067 Ok(())
1068 }
1069}
1070
1071#[derive(Debug, Error)]
1075#[error(
1076 "could not detect archive format from file name `{file_name}` (supported extensions: {})",
1077 supported_extensions()
1078)]
1079pub struct UnknownArchiveFormat {
1080 pub file_name: String,
1082}
1083
1084fn supported_extensions() -> String {
1085 ArchiveFormat::SUPPORTED_FORMATS
1086 .iter()
1087 .map(|(extension, _)| *extension)
1088 .join(", ")
1089}
1090
1091#[derive(Debug, Error)]
1093#[non_exhaustive]
1094pub enum ArchiveCreateError {
1095 #[error("error creating binary list")]
1097 CreateBinaryList(#[source] WriteTestListError),
1098
1099 #[error("extra path `{}` not found", .redactor.redact_path(path))]
1101 MissingExtraPath {
1102 path: Utf8PathBuf,
1104
1105 redactor: Redactor,
1110 },
1111
1112 #[error("while archiving {step}, error writing {} `{path}` to archive", kind_str(*.is_dir))]
1114 InputFileRead {
1115 step: ArchiveStep,
1117
1118 path: Utf8PathBuf,
1120
1121 is_dir: Option<bool>,
1123
1124 #[source]
1126 error: std::io::Error,
1127 },
1128
1129 #[error("error reading directory entry from `{path}")]
1131 DirEntryRead {
1132 path: Utf8PathBuf,
1134
1135 #[source]
1137 error: std::io::Error,
1138 },
1139
1140 #[error("error writing to archive")]
1142 OutputArchiveIo(#[source] std::io::Error),
1143
1144 #[error("error reporting archive status")]
1146 ReporterIo(#[source] std::io::Error),
1147}
1148
1149fn kind_str(is_dir: Option<bool>) -> &'static str {
1150 match is_dir {
1151 Some(true) => "directory",
1152 Some(false) => "file",
1153 None => "path",
1154 }
1155}
1156
1157#[derive(Debug, Error)]
1159pub enum MetadataMaterializeError {
1160 #[error("I/O error reading metadata file `{path}`")]
1162 Read {
1163 path: Utf8PathBuf,
1165
1166 #[source]
1168 error: std::io::Error,
1169 },
1170
1171 #[error("error deserializing metadata file `{path}`")]
1173 Deserialize {
1174 path: Utf8PathBuf,
1176
1177 #[source]
1179 error: serde_json::Error,
1180 },
1181
1182 #[error("error parsing Rust build metadata from `{path}`")]
1184 RustBuildMeta {
1185 path: Utf8PathBuf,
1187
1188 #[source]
1190 error: RustBuildMetaParseError,
1191 },
1192
1193 #[error("error building package graph from `{path}`")]
1195 PackageGraphConstruct {
1196 path: Utf8PathBuf,
1198
1199 #[source]
1201 error: guppy::Error,
1202 },
1203}
1204
1205#[derive(Debug, Error)]
1209#[non_exhaustive]
1210pub enum ArchiveReadError {
1211 #[error("I/O error reading archive")]
1213 Io(#[source] std::io::Error),
1214
1215 #[error("path in archive `{}` wasn't valid UTF-8", String::from_utf8_lossy(.0))]
1217 NonUtf8Path(Vec<u8>),
1218
1219 #[error("path in archive `{0}` doesn't start with `target/`")]
1221 NoTargetPrefix(Utf8PathBuf),
1222
1223 #[error("path in archive `{path}` contains an invalid component `{component}`")]
1225 InvalidComponent {
1226 path: Utf8PathBuf,
1228
1229 component: String,
1231 },
1232
1233 #[error("corrupted archive: checksum read error for path `{path}`")]
1235 ChecksumRead {
1236 path: Utf8PathBuf,
1238
1239 #[source]
1241 error: std::io::Error,
1242 },
1243
1244 #[error("corrupted archive: invalid checksum for path `{path}`")]
1246 InvalidChecksum {
1247 path: Utf8PathBuf,
1249
1250 expected: u32,
1252
1253 actual: u32,
1255 },
1256
1257 #[error("metadata file `{0}` not found in archive")]
1259 MetadataFileNotFound(&'static Utf8Path),
1260
1261 #[error("error deserializing metadata file `{path}` in archive")]
1263 MetadataDeserializeError {
1264 path: &'static Utf8Path,
1266
1267 #[source]
1269 error: serde_json::Error,
1270 },
1271
1272 #[error("error building package graph from `{path}` in archive")]
1274 PackageGraphConstructError {
1275 path: &'static Utf8Path,
1277
1278 #[source]
1280 error: guppy::Error,
1281 },
1282}
1283
1284#[derive(Debug, Error)]
1288#[non_exhaustive]
1289pub enum ArchiveExtractError {
1290 #[error("error creating temporary directory")]
1292 TempDirCreate(#[source] std::io::Error),
1293
1294 #[error("error canonicalizing destination directory `{dir}`")]
1296 DestDirCanonicalization {
1297 dir: Utf8PathBuf,
1299
1300 #[source]
1302 error: std::io::Error,
1303 },
1304
1305 #[error("destination `{0}` already exists")]
1307 DestinationExists(Utf8PathBuf),
1308
1309 #[error("error reading archive")]
1311 Read(#[source] ArchiveReadError),
1312
1313 #[error("error deserializing Rust build metadata")]
1315 RustBuildMeta(#[from] RustBuildMetaParseError),
1316
1317 #[error("error writing file `{path}` to disk")]
1319 WriteFile {
1320 path: Utf8PathBuf,
1322
1323 #[source]
1325 error: std::io::Error,
1326 },
1327
1328 #[error("error reporting extract status")]
1330 ReporterIo(std::io::Error),
1331}
1332
1333#[derive(Debug, Error)]
1335#[non_exhaustive]
1336pub enum WriteEventError {
1337 #[error("error writing to output")]
1339 Io(#[source] std::io::Error),
1340
1341 #[error("error operating on path {file}")]
1343 Fs {
1344 file: Utf8PathBuf,
1346
1347 #[source]
1349 error: std::io::Error,
1350 },
1351
1352 #[error("error writing JUnit output to {file}")]
1354 Junit {
1355 file: Utf8PathBuf,
1357
1358 #[source]
1360 error: quick_junit::SerializeError,
1361 },
1362}
1363
1364#[derive(Debug, Error)]
1367#[non_exhaustive]
1368pub enum CargoConfigError {
1369 #[error("failed to retrieve current directory")]
1371 GetCurrentDir(#[source] std::io::Error),
1372
1373 #[error("current directory is invalid UTF-8")]
1375 CurrentDirInvalidUtf8(#[source] FromPathBufError),
1376
1377 #[error("failed to parse --config argument `{config_str}` as TOML")]
1379 CliConfigParseError {
1380 config_str: String,
1382
1383 #[source]
1385 error: toml_edit::TomlError,
1386 },
1387
1388 #[error("failed to deserialize --config argument `{config_str}` as TOML")]
1390 CliConfigDeError {
1391 config_str: String,
1393
1394 #[source]
1396 error: toml_edit::de::Error,
1397 },
1398
1399 #[error(
1401 "invalid format for --config argument `{config_str}` (should be a dotted key expression)"
1402 )]
1403 InvalidCliConfig {
1404 config_str: String,
1406
1407 #[source]
1409 reason: InvalidCargoCliConfigReason,
1410 },
1411
1412 #[error("non-UTF-8 path encountered")]
1414 NonUtf8Path(#[source] FromPathBufError),
1415
1416 #[error("failed to retrieve the Cargo home directory")]
1418 GetCargoHome(#[source] std::io::Error),
1419
1420 #[error("failed to canonicalize path `{path}")]
1422 FailedPathCanonicalization {
1423 path: Utf8PathBuf,
1425
1426 #[source]
1428 error: std::io::Error,
1429 },
1430
1431 #[error("failed to read config at `{path}`")]
1433 ConfigReadError {
1434 path: Utf8PathBuf,
1436
1437 #[source]
1439 error: std::io::Error,
1440 },
1441
1442 #[error(transparent)]
1444 ConfigParseError(#[from] Box<CargoConfigParseError>),
1445}
1446
1447#[derive(Debug, Error)]
1451#[error("failed to parse config at `{path}`")]
1452pub struct CargoConfigParseError {
1453 pub path: Utf8PathBuf,
1455
1456 #[source]
1458 pub error: toml::de::Error,
1459}
1460
1461#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)]
1465#[non_exhaustive]
1466pub enum InvalidCargoCliConfigReason {
1467 #[error("was not a TOML dotted key expression (such as `build.jobs = 2`)")]
1469 NotDottedKv,
1470
1471 #[error("includes non-whitespace decoration")]
1473 IncludesNonWhitespaceDecoration,
1474
1475 #[error("sets a value to an inline table, which is not accepted")]
1477 SetsValueToInlineTable,
1478
1479 #[error("sets a value to an array of tables, which is not accepted")]
1481 SetsValueToArrayOfTables,
1482
1483 #[error("doesn't provide a value")]
1485 DoesntProvideValue,
1486}
1487
1488#[derive(Debug, Error)]
1490pub enum HostPlatformDetectError {
1491 #[error(
1494 "error spawning `rustc -vV`, and detecting the build \
1495 target failed as well\n\
1496 - rustc spawn error: {}\n\
1497 - build target error: {}\n",
1498 DisplayErrorChain::new_with_initial_indent(" ", error),
1499 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1500 )]
1501 RustcVvSpawnError {
1502 error: std::io::Error,
1504
1505 build_target_error: Box<target_spec::Error>,
1507 },
1508
1509 #[error(
1512 "`rustc -vV` failed with {}, and detecting the \
1513 build target failed as well\n\
1514 - `rustc -vV` stdout:\n{}\n\
1515 - `rustc -vV` stderr:\n{}\n\
1516 - build target error:\n{}\n",
1517 status,
1518 Indented { item: String::from_utf8_lossy(stdout), indent: " " },
1519 Indented { item: String::from_utf8_lossy(stderr), indent: " " },
1520 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1521 )]
1522 RustcVvFailed {
1523 status: ExitStatus,
1525
1526 stdout: Vec<u8>,
1528
1529 stderr: Vec<u8>,
1531
1532 build_target_error: Box<target_spec::Error>,
1534 },
1535
1536 #[error(
1539 "parsing `rustc -vV` output failed, and detecting the build target \
1540 failed as well\n\
1541 - host platform error:\n{}\n\
1542 - build target error:\n{}\n",
1543 DisplayErrorChain::new_with_initial_indent(" ", host_platform_error),
1544 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1545 )]
1546 HostPlatformParseError {
1547 host_platform_error: Box<target_spec::Error>,
1549
1550 build_target_error: Box<target_spec::Error>,
1552 },
1553
1554 #[error("test-only code, so `rustc -vV` was not called; failed to detect build target")]
1557 BuildTargetError {
1558 #[source]
1560 build_target_error: Box<target_spec::Error>,
1561 },
1562}
1563
1564#[derive(Debug, Error)]
1566pub enum TargetTripleError {
1567 #[error(
1569 "environment variable '{}' contained non-UTF-8 data",
1570 TargetTriple::CARGO_BUILD_TARGET_ENV
1571 )]
1572 InvalidEnvironmentVar,
1573
1574 #[error("error deserializing target triple from {source}")]
1576 TargetSpecError {
1577 source: TargetTripleSource,
1579
1580 #[source]
1582 error: target_spec::Error,
1583 },
1584
1585 #[error("target path `{path}` is not a valid file")]
1587 TargetPathReadError {
1588 source: TargetTripleSource,
1590
1591 path: Utf8PathBuf,
1593
1594 #[source]
1596 error: std::io::Error,
1597 },
1598
1599 #[error(
1601 "for custom platform obtained from {source}, \
1602 failed to create temporary directory for custom platform"
1603 )]
1604 CustomPlatformTempDirError {
1605 source: TargetTripleSource,
1607
1608 #[source]
1610 error: std::io::Error,
1611 },
1612
1613 #[error(
1615 "for custom platform obtained from {source}, \
1616 failed to write JSON to temporary path `{path}`"
1617 )]
1618 CustomPlatformWriteError {
1619 source: TargetTripleSource,
1621
1622 path: Utf8PathBuf,
1624
1625 #[source]
1627 error: std::io::Error,
1628 },
1629
1630 #[error(
1632 "for custom platform obtained from {source}, \
1633 failed to close temporary directory `{dir_path}`"
1634 )]
1635 CustomPlatformCloseError {
1636 source: TargetTripleSource,
1638
1639 dir_path: Utf8PathBuf,
1641
1642 #[source]
1644 error: std::io::Error,
1645 },
1646}
1647
1648impl TargetTripleError {
1649 pub fn source_report(&self) -> Option<miette::Report> {
1654 match self {
1655 Self::TargetSpecError { error, .. } => {
1656 Some(miette::Report::new_boxed(error.clone().into_diagnostic()))
1657 }
1658 TargetTripleError::InvalidEnvironmentVar
1660 | TargetTripleError::TargetPathReadError { .. }
1661 | TargetTripleError::CustomPlatformTempDirError { .. }
1662 | TargetTripleError::CustomPlatformWriteError { .. }
1663 | TargetTripleError::CustomPlatformCloseError { .. } => None,
1664 }
1665 }
1666}
1667
1668#[derive(Debug, Error)]
1670pub enum TargetRunnerError {
1671 #[error("environment variable '{0}' contained non-UTF-8 data")]
1673 InvalidEnvironmentVar(String),
1674
1675 #[error("runner '{key}' = '{value}' did not contain a runner binary")]
1678 BinaryNotSpecified {
1679 key: PlatformRunnerSource,
1681
1682 value: String,
1684 },
1685}
1686
1687#[derive(Debug, Error)]
1689#[error("error setting up signal handler")]
1690pub struct SignalHandlerSetupError(#[from] std::io::Error);
1691
1692#[derive(Debug, Error)]
1694pub enum ShowTestGroupsError {
1695 #[error(
1697 "unknown test groups specified: {}\n(known groups: {})",
1698 unknown_groups.iter().join(", "),
1699 known_groups.iter().join(", "),
1700 )]
1701 UnknownGroups {
1702 unknown_groups: BTreeSet<TestGroup>,
1704
1705 known_groups: BTreeSet<TestGroup>,
1707 },
1708}
1709
1710#[cfg(feature = "self-update")]
1711mod self_update_errors {
1712 use super::*;
1713 use mukti_metadata::ReleaseStatus;
1714 use semver::{Version, VersionReq};
1715
1716 #[cfg(feature = "self-update")]
1720 #[derive(Debug, Error)]
1721 #[non_exhaustive]
1722 pub enum UpdateError {
1723 #[error("failed to read release metadata from `{path}`")]
1725 ReadLocalMetadata {
1726 path: Utf8PathBuf,
1728
1729 #[source]
1731 error: std::io::Error,
1732 },
1733
1734 #[error("self-update failed")]
1736 SelfUpdate(#[source] self_update::errors::Error),
1737
1738 #[error("deserializing release metadata failed")]
1740 ReleaseMetadataDe(#[source] serde_json::Error),
1741
1742 #[error("version `{version}` not found (known versions: {})", known_versions(.known))]
1744 VersionNotFound {
1745 version: Version,
1747
1748 known: Vec<(Version, ReleaseStatus)>,
1750 },
1751
1752 #[error("no version found matching requirement `{req}`")]
1754 NoMatchForVersionReq {
1755 req: VersionReq,
1757 },
1758
1759 #[error("project {not_found} not found in release metadata (known projects: {})", known.join(", "))]
1761 MuktiProjectNotFound {
1762 not_found: String,
1764
1765 known: Vec<String>,
1767 },
1768
1769 #[error(
1771 "for version {version}, no release information found for target `{triple}` \
1772 (known targets: {})",
1773 known_triples.iter().join(", ")
1774 )]
1775 NoTargetData {
1776 version: Version,
1778
1779 triple: String,
1781
1782 known_triples: BTreeSet<String>,
1784 },
1785
1786 #[error("the current executable's path could not be determined")]
1788 CurrentExe(#[source] std::io::Error),
1789
1790 #[error("temporary directory could not be created at `{location}`")]
1792 TempDirCreate {
1793 location: Utf8PathBuf,
1795
1796 #[source]
1798 error: std::io::Error,
1799 },
1800
1801 #[error("temporary archive could not be created at `{archive_path}`")]
1803 TempArchiveCreate {
1804 archive_path: Utf8PathBuf,
1806
1807 #[source]
1809 error: std::io::Error,
1810 },
1811
1812 #[error("error writing to temporary archive at `{archive_path}`")]
1814 TempArchiveWrite {
1815 archive_path: Utf8PathBuf,
1817
1818 #[source]
1820 error: std::io::Error,
1821 },
1822
1823 #[error("error reading from temporary archive at `{archive_path}`")]
1825 TempArchiveRead {
1826 archive_path: Utf8PathBuf,
1828
1829 #[source]
1831 error: std::io::Error,
1832 },
1833
1834 #[error("SHA-256 checksum mismatch: expected: {expected}, actual: {actual}")]
1836 ChecksumMismatch {
1837 expected: String,
1839
1840 actual: String,
1842 },
1843
1844 #[error("error renaming `{source}` to `{dest}`")]
1846 FsRename {
1847 source: Utf8PathBuf,
1849
1850 dest: Utf8PathBuf,
1852
1853 #[source]
1855 error: std::io::Error,
1856 },
1857
1858 #[error("cargo-nextest binary updated, but error running `cargo nextest self setup`")]
1860 SelfSetup(#[source] std::io::Error),
1861 }
1862
1863 fn known_versions(versions: &[(Version, ReleaseStatus)]) -> String {
1864 use std::fmt::Write;
1865
1866 const DISPLAY_COUNT: usize = 4;
1868
1869 let display_versions: Vec<_> = versions
1870 .iter()
1871 .filter(|(v, status)| v.pre.is_empty() && *status == ReleaseStatus::Active)
1872 .map(|(v, _)| v.to_string())
1873 .take(DISPLAY_COUNT)
1874 .collect();
1875 let mut display_str = display_versions.join(", ");
1876 if versions.len() > display_versions.len() {
1877 write!(
1878 display_str,
1879 " and {} others",
1880 versions.len() - display_versions.len()
1881 )
1882 .unwrap();
1883 }
1884
1885 display_str
1886 }
1887
1888 #[cfg(feature = "self-update")]
1889 #[derive(Debug, Error)]
1891 pub enum UpdateVersionParseError {
1892 #[error("version string is empty")]
1894 EmptyString,
1895
1896 #[error(
1898 "`{input}` is not a valid semver requirement\n\
1899 (hint: see https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html for the correct format)"
1900 )]
1901 InvalidVersionReq {
1902 input: String,
1904
1905 #[source]
1907 error: semver::Error,
1908 },
1909
1910 #[error("`{input}` is not a valid semver{}", extra_semver_output(.input))]
1912 InvalidVersion {
1913 input: String,
1915
1916 #[source]
1918 error: semver::Error,
1919 },
1920 }
1921
1922 fn extra_semver_output(input: &str) -> String {
1923 if input.parse::<VersionReq>().is_ok() {
1926 format!(
1927 "\n(if you want to specify a semver range, add an explicit qualifier, like ^{input})"
1928 )
1929 } else {
1930 "".to_owned()
1931 }
1932 }
1933}
1934
1935#[cfg(feature = "self-update")]
1936pub use self_update_errors::*;
1937
1938#[cfg(test)]
1939mod tests {
1940 use super::*;
1941
1942 #[test]
1943 fn display_error_chain() {
1944 let err1 = StringError::new("err1", None);
1945
1946 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&err1)), @"err1");
1947
1948 let err2 = StringError::new("err2", Some(err1));
1949 let err3 = StringError::new("err3\nerr3 line 2", Some(err2));
1950
1951 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&err3)), @r"
1952 err3
1953 err3 line 2
1954 caused by:
1955 - err2
1956 - err1
1957 ");
1958 }
1959
1960 #[test]
1961 fn display_error_list() {
1962 let err1 = StringError::new("err1", None);
1963
1964 let error_list =
1965 ErrorList::<StringError>::new("waiting on the water to boil", vec![err1.clone()])
1966 .expect(">= 1 error");
1967 insta::assert_snapshot!(format!("{}", error_list), @"err1");
1968 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @"err1");
1969
1970 let err2 = StringError::new("err2", Some(err1));
1971 let err3 = StringError::new("err3", Some(err2));
1972
1973 let error_list =
1974 ErrorList::<StringError>::new("waiting on flowers to bloom", vec![err3.clone()])
1975 .expect(">= 1 error");
1976 insta::assert_snapshot!(format!("{}", error_list), @"err3");
1977 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @r"
1978 err3
1979 caused by:
1980 - err2
1981 - err1
1982 ");
1983
1984 let err4 = StringError::new("err4", None);
1985 let err5 = StringError::new("err5", Some(err4));
1986 let err6 = StringError::new("err6\nerr6 line 2", Some(err5));
1987
1988 let error_list = ErrorList::<StringError>::new(
1989 "waiting for the heat death of the universe",
1990 vec![err3, err6],
1991 )
1992 .expect(">= 1 error");
1993
1994 insta::assert_snapshot!(format!("{}", error_list), @r"
1995 2 errors occurred waiting for the heat death of the universe:
1996 * err3
1997 caused by:
1998 - err2
1999 - err1
2000 * err6
2001 err6 line 2
2002 caused by:
2003 - err5
2004 - err4
2005 ");
2006 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @r"
2007 2 errors occurred waiting for the heat death of the universe:
2008 * err3
2009 caused by:
2010 - err2
2011 - err1
2012 * err6
2013 err6 line 2
2014 caused by:
2015 - err5
2016 - err4
2017 ");
2018 }
2019
2020 #[derive(Clone, Debug, Error)]
2021 struct StringError {
2022 message: String,
2023 #[source]
2024 source: Option<Box<StringError>>,
2025 }
2026
2027 impl StringError {
2028 fn new(message: impl Into<String>, source: Option<StringError>) -> Self {
2029 Self {
2030 message: message.into(),
2031 source: source.map(Box::new),
2032 }
2033 }
2034 }
2035
2036 impl fmt::Display for StringError {
2037 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2038 write!(f, "{}", self.message)
2039 }
2040 }
2041}