1use crate::{
7 cargo_config::{TargetTriple, TargetTripleSource},
8 config::{
9 core::ConfigExperimental,
10 elements::{CustomTestGroup, TestGroup},
11 scripts::{ProfileScriptType, ScriptId, ScriptType},
12 },
13 helpers::{display_exited_with, dylib_path_envvar},
14 redact::Redactor,
15 reuse_build::{ArchiveFormat, ArchiveStep},
16 target_runner::PlatformRunnerSource,
17};
18use camino::{FromPathBufError, Utf8Path, Utf8PathBuf};
19use config::ConfigError;
20use indent_write::{fmt::IndentWriter, indentable::Indented};
21use itertools::{Either, Itertools};
22use nextest_filtering::errors::FiltersetParseErrors;
23use nextest_metadata::RustBinaryId;
24use smol_str::SmolStr;
25use std::{
26 borrow::Cow,
27 collections::BTreeSet,
28 env::JoinPathsError,
29 fmt::{self, Write as _},
30 process::ExitStatus,
31 sync::Arc,
32};
33use target_spec_miette::IntoMietteDiagnostic;
34use thiserror::Error;
35
36#[derive(Debug, Error)]
38#[error(
39 "failed to parse nextest config at `{config_file}`{}",
40 provided_by_tool(tool.as_deref())
41)]
42#[non_exhaustive]
43pub struct ConfigParseError {
44 config_file: Utf8PathBuf,
45 tool: Option<String>,
46 #[source]
47 kind: ConfigParseErrorKind,
48}
49
50impl ConfigParseError {
51 pub(crate) fn new(
52 config_file: impl Into<Utf8PathBuf>,
53 tool: Option<&str>,
54 kind: ConfigParseErrorKind,
55 ) -> Self {
56 Self {
57 config_file: config_file.into(),
58 tool: tool.map(|s| s.to_owned()),
59 kind,
60 }
61 }
62
63 pub fn config_file(&self) -> &Utf8Path {
65 &self.config_file
66 }
67
68 pub fn tool(&self) -> Option<&str> {
70 self.tool.as_deref()
71 }
72
73 pub fn kind(&self) -> &ConfigParseErrorKind {
75 &self.kind
76 }
77}
78
79pub fn provided_by_tool(tool: Option<&str>) -> String {
81 match tool {
82 Some(tool) => format!(" provided by tool `{tool}`"),
83 None => String::new(),
84 }
85}
86
87#[derive(Debug, Error)]
91#[non_exhaustive]
92pub enum ConfigParseErrorKind {
93 #[error(transparent)]
95 BuildError(Box<ConfigError>),
96 #[error(transparent)]
97 DeserializeError(Box<serde_path_to_error::Error<ConfigError>>),
99 #[error(transparent)]
101 VersionOnlyReadError(std::io::Error),
102 #[error(transparent)]
104 VersionOnlyDeserializeError(Box<serde_path_to_error::Error<toml::de::Error>>),
105 #[error("error parsing compiled data (destructure this variant for more details)")]
107 CompileErrors(Vec<ConfigCompileError>),
108 #[error("invalid test groups defined: {}\n(test groups cannot start with '@tool:' unless specified by a tool)", .0.iter().join(", "))]
110 InvalidTestGroupsDefined(BTreeSet<CustomTestGroup>),
111 #[error(
113 "invalid test groups defined by tool: {}\n(test groups must start with '@tool:<tool-name>:')", .0.iter().join(", "))]
114 InvalidTestGroupsDefinedByTool(BTreeSet<CustomTestGroup>),
115 #[error("unknown test groups specified by config (destructure this variant for more details)")]
117 UnknownTestGroups {
118 errors: Vec<UnknownTestGroupError>,
120
121 known_groups: BTreeSet<TestGroup>,
123 },
124 #[error(
126 "both `[script.*]` and `[scripts.*]` defined\n\
127 (hint: [script.*] will be removed in the future: switch to [scripts.setup.*])"
128 )]
129 BothScriptAndScriptsDefined,
130 #[error("invalid config scripts defined: {}\n(config scripts cannot start with '@tool:' unless specified by a tool)", .0.iter().join(", "))]
132 InvalidConfigScriptsDefined(BTreeSet<ScriptId>),
133 #[error(
135 "invalid config scripts defined by tool: {}\n(config scripts must start with '@tool:<tool-name>:')", .0.iter().join(", "))]
136 InvalidConfigScriptsDefinedByTool(BTreeSet<ScriptId>),
137 #[error(
139 "config script names used more than once: {}\n\
140 (config script names must be unique across all script types)", .0.iter().join(", ")
141 )]
142 DuplicateConfigScriptNames(BTreeSet<ScriptId>),
143 #[error(
145 "errors in profile-specific config scripts (destructure this variant for more details)"
146 )]
147 ProfileScriptErrors {
148 errors: Box<ProfileScriptErrors>,
150
151 known_scripts: BTreeSet<ScriptId>,
153 },
154 #[error("unknown experimental features defined (destructure this variant for more details)")]
156 UnknownExperimentalFeatures {
157 unknown: BTreeSet<String>,
159
160 known: BTreeSet<ConfigExperimental>,
162 },
163 #[error(
167 "tool config file specifies experimental features `{}` \
168 -- only repository config files can do so",
169 .features.iter().join(", "),
170 )]
171 ExperimentalFeaturesInToolConfig {
172 features: BTreeSet<String>,
174 },
175 #[error("experimental features used but not enabled: {}", .missing_features.iter().join(", "))]
177 ExperimentalFeaturesNotEnabled {
178 missing_features: BTreeSet<ConfigExperimental>,
180 },
181 #[error("inheritance error(s) detected: {}", .0.iter().join(", "))]
183 InheritanceErrors(Vec<InheritsError>),
184}
185
186#[derive(Debug)]
189#[non_exhaustive]
190pub struct ConfigCompileError {
191 pub profile_name: String,
193
194 pub section: ConfigCompileSection,
196
197 pub kind: ConfigCompileErrorKind,
199}
200
201#[derive(Debug)]
204pub enum ConfigCompileSection {
205 DefaultFilter,
207
208 Override(usize),
210
211 Script(usize),
213}
214
215#[derive(Debug)]
217#[non_exhaustive]
218pub enum ConfigCompileErrorKind {
219 ConstraintsNotSpecified {
221 default_filter_specified: bool,
226 },
227
228 FilterAndDefaultFilterSpecified,
232
233 Parse {
235 host_parse_error: Option<target_spec::Error>,
237
238 target_parse_error: Option<target_spec::Error>,
240
241 filter_parse_errors: Vec<FiltersetParseErrors>,
243 },
244}
245
246impl ConfigCompileErrorKind {
247 pub fn reports(&self) -> impl Iterator<Item = miette::Report> + '_ {
249 match self {
250 Self::ConstraintsNotSpecified {
251 default_filter_specified,
252 } => {
253 let message = if *default_filter_specified {
254 "for override with `default-filter`, `platform` must also be specified"
255 } else {
256 "at least one of `platform` and `filter` must be specified"
257 };
258 Either::Left(std::iter::once(miette::Report::msg(message)))
259 }
260 Self::FilterAndDefaultFilterSpecified => {
261 Either::Left(std::iter::once(miette::Report::msg(
262 "at most one of `filter` and `default-filter` must be specified",
263 )))
264 }
265 Self::Parse {
266 host_parse_error,
267 target_parse_error,
268 filter_parse_errors,
269 } => {
270 let host_parse_report = host_parse_error
271 .as_ref()
272 .map(|error| miette::Report::new_boxed(error.clone().into_diagnostic()));
273 let target_parse_report = target_parse_error
274 .as_ref()
275 .map(|error| miette::Report::new_boxed(error.clone().into_diagnostic()));
276 let filter_parse_reports =
277 filter_parse_errors.iter().flat_map(|filter_parse_errors| {
278 filter_parse_errors.errors.iter().map(|single_error| {
279 miette::Report::new(single_error.clone())
280 .with_source_code(filter_parse_errors.input.to_owned())
281 })
282 });
283
284 Either::Right(
285 host_parse_report
286 .into_iter()
287 .chain(target_parse_report)
288 .chain(filter_parse_reports),
289 )
290 }
291 }
292 }
293}
294
295#[derive(Clone, Debug, Error)]
297#[error("test priority ({priority}) out of range: must be between -100 and 100, both inclusive")]
298pub struct TestPriorityOutOfRange {
299 pub priority: i8,
301}
302
303#[derive(Clone, Debug, Error)]
305pub enum ChildStartError {
306 #[error("error creating temporary path for setup script")]
308 TempPath(#[source] Arc<std::io::Error>),
309
310 #[error("error spawning child process")]
312 Spawn(#[source] Arc<std::io::Error>),
313}
314
315#[derive(Clone, Debug, Error)]
317pub enum SetupScriptOutputError {
318 #[error("error opening environment file `{path}`")]
320 EnvFileOpen {
321 path: Utf8PathBuf,
323
324 #[source]
326 error: Arc<std::io::Error>,
327 },
328
329 #[error("error reading environment file `{path}`")]
331 EnvFileRead {
332 path: Utf8PathBuf,
334
335 #[source]
337 error: Arc<std::io::Error>,
338 },
339
340 #[error("line `{line}` in environment file `{path}` not in KEY=VALUE format")]
342 EnvFileParse {
343 path: Utf8PathBuf,
345 line: String,
347 },
348
349 #[error("key `{key}` begins with `NEXTEST`, which is reserved for internal use")]
351 EnvFileReservedKey {
352 key: String,
354 },
355}
356
357#[derive(Clone, Debug)]
362pub struct ErrorList<T> {
363 description: &'static str,
365 inner: Vec<T>,
367}
368
369impl<T: std::error::Error> ErrorList<T> {
370 pub(crate) fn new<U>(description: &'static str, errors: Vec<U>) -> Option<Self>
371 where
372 T: From<U>,
373 {
374 if errors.is_empty() {
375 None
376 } else {
377 Some(Self {
378 description,
379 inner: errors.into_iter().map(T::from).collect(),
380 })
381 }
382 }
383
384 pub(crate) fn short_message(&self) -> String {
386 let string = self.to_string();
387 match string.lines().next() {
388 Some(first_line) => first_line.trim_end_matches(':').to_string(),
390 None => String::new(),
391 }
392 }
393
394 pub(crate) fn iter(&self) -> impl Iterator<Item = &T> {
395 self.inner.iter()
396 }
397}
398
399impl<T: std::error::Error> fmt::Display for ErrorList<T> {
400 fn fmt(&self, mut f: &mut fmt::Formatter) -> fmt::Result {
401 if self.inner.len() == 1 {
403 return write!(f, "{}", self.inner[0]);
404 }
405
406 writeln!(
408 f,
409 "{} errors occurred {}:",
410 self.inner.len(),
411 self.description,
412 )?;
413 for error in &self.inner {
414 let mut indent = IndentWriter::new_skip_initial(" ", f);
415 writeln!(indent, "* {}", DisplayErrorChain::new(error))?;
416 f = indent.into_inner();
417 }
418 Ok(())
419 }
420}
421
422impl<T: std::error::Error> std::error::Error for ErrorList<T> {
423 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
424 if self.inner.len() == 1 {
425 self.inner[0].source()
426 } else {
427 None
430 }
431 }
432}
433
434pub(crate) struct DisplayErrorChain<E> {
439 error: E,
440 initial_indent: &'static str,
441}
442
443impl<E: std::error::Error> DisplayErrorChain<E> {
444 pub(crate) fn new(error: E) -> Self {
445 Self {
446 error,
447 initial_indent: "",
448 }
449 }
450
451 pub(crate) fn new_with_initial_indent(initial_indent: &'static str, error: E) -> Self {
452 Self {
453 error,
454 initial_indent,
455 }
456 }
457}
458
459impl<E> fmt::Display for DisplayErrorChain<E>
460where
461 E: std::error::Error,
462{
463 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
464 let mut writer = IndentWriter::new(self.initial_indent, f);
465 write!(writer, "{}", self.error)?;
466
467 let Some(mut cause) = self.error.source() else {
468 return Ok(());
469 };
470
471 write!(writer, "\n caused by:")?;
472
473 loop {
474 writeln!(writer)?;
475 let mut indent = IndentWriter::new_skip_initial(" ", writer);
476 write!(indent, " - {cause}")?;
477
478 let Some(next_cause) = cause.source() else {
479 break Ok(());
480 };
481
482 cause = next_cause;
483 writer = indent.into_inner();
484 }
485 }
486}
487
488#[derive(Clone, Debug, Error)]
490pub enum ChildError {
491 #[error(transparent)]
493 Fd(#[from] ChildFdError),
494
495 #[error(transparent)]
497 SetupScriptOutput(#[from] SetupScriptOutputError),
498}
499
500#[derive(Clone, Debug, Error)]
502pub enum ChildFdError {
503 #[error("error reading standard output")]
505 ReadStdout(#[source] Arc<std::io::Error>),
506
507 #[error("error reading standard error")]
509 ReadStderr(#[source] Arc<std::io::Error>),
510
511 #[error("error reading combined stream")]
513 ReadCombined(#[source] Arc<std::io::Error>),
514
515 #[error("error waiting for child process to exit")]
517 Wait(#[source] Arc<std::io::Error>),
518}
519
520#[derive(Clone, Debug, Eq, PartialEq)]
522#[non_exhaustive]
523pub struct UnknownTestGroupError {
524 pub profile_name: String,
526
527 pub name: TestGroup,
529}
530
531#[derive(Clone, Debug, Eq, PartialEq)]
534pub struct ProfileUnknownScriptError {
535 pub profile_name: String,
537
538 pub name: ScriptId,
540}
541
542#[derive(Clone, Debug, Eq, PartialEq)]
545pub struct ProfileWrongConfigScriptTypeError {
546 pub profile_name: String,
548
549 pub name: ScriptId,
551
552 pub attempted: ProfileScriptType,
554
555 pub actual: ScriptType,
557}
558
559#[derive(Clone, Debug, Eq, PartialEq)]
562pub struct ProfileListScriptUsesRunFiltersError {
563 pub profile_name: String,
565
566 pub name: ScriptId,
568
569 pub script_type: ProfileScriptType,
571
572 pub filters: BTreeSet<String>,
574}
575
576#[derive(Clone, Debug, Default)]
578pub struct ProfileScriptErrors {
579 pub unknown_scripts: Vec<ProfileUnknownScriptError>,
581
582 pub wrong_script_types: Vec<ProfileWrongConfigScriptTypeError>,
584
585 pub list_scripts_using_run_filters: Vec<ProfileListScriptUsesRunFiltersError>,
587}
588
589impl ProfileScriptErrors {
590 pub fn is_empty(&self) -> bool {
592 self.unknown_scripts.is_empty()
593 && self.wrong_script_types.is_empty()
594 && self.list_scripts_using_run_filters.is_empty()
595 }
596}
597
598#[derive(Clone, Debug, Error)]
600#[error("profile `{profile} not found (known profiles: {})`", .all_profiles.join(", "))]
601pub struct ProfileNotFound {
602 profile: String,
603 all_profiles: Vec<String>,
604}
605
606impl ProfileNotFound {
607 pub(crate) fn new(
608 profile: impl Into<String>,
609 all_profiles: impl IntoIterator<Item = impl Into<String>>,
610 ) -> Self {
611 let mut all_profiles: Vec<_> = all_profiles.into_iter().map(|s| s.into()).collect();
612 all_profiles.sort_unstable();
613 Self {
614 profile: profile.into(),
615 all_profiles,
616 }
617 }
618}
619
620#[derive(Clone, Debug, Error, Eq, PartialEq)]
622pub enum InvalidIdentifier {
623 #[error("identifier is empty")]
625 Empty,
626
627 #[error("invalid identifier `{0}`")]
629 InvalidXid(SmolStr),
630
631 #[error("tool identifier not of the form \"@tool:tool-name:identifier\": `{0}`")]
633 ToolIdentifierInvalidFormat(SmolStr),
634
635 #[error("tool identifier has empty component: `{0}`")]
637 ToolComponentEmpty(SmolStr),
638
639 #[error("invalid tool identifier `{0}`")]
641 ToolIdentifierInvalidXid(SmolStr),
642}
643
644#[derive(Clone, Debug, Error)]
646#[error("invalid custom test group name: {0}")]
647pub struct InvalidCustomTestGroupName(pub InvalidIdentifier);
648
649#[derive(Clone, Debug, Error)]
651#[error("invalid configuration script name: {0}")]
652pub struct InvalidConfigScriptName(pub InvalidIdentifier);
653
654#[derive(Clone, Debug, Error)]
656pub enum ToolConfigFileParseError {
657 #[error(
658 "tool-config-file has invalid format: {input}\n(hint: tool configs must be in the format <tool-name>:<path>)"
659 )]
660 InvalidFormat {
662 input: String,
664 },
665
666 #[error("tool-config-file has empty tool name: {input}")]
668 EmptyToolName {
669 input: String,
671 },
672
673 #[error("tool-config-file has empty config file path: {input}")]
675 EmptyConfigFile {
676 input: String,
678 },
679
680 #[error("tool-config-file is not an absolute path: {config_file}")]
682 ConfigFileNotAbsolute {
683 config_file: Utf8PathBuf,
685 },
686}
687
688#[derive(Clone, Debug, Error)]
690#[error("unrecognized value for max-fail: {reason}")]
691pub struct MaxFailParseError {
692 pub reason: String,
694}
695
696impl MaxFailParseError {
697 pub(crate) fn new(reason: impl Into<String>) -> Self {
698 Self {
699 reason: reason.into(),
700 }
701 }
702}
703
704#[derive(Clone, Debug, Error)]
706#[error(
707 "unrecognized value for stress-count: {input}\n\
708 (hint: expected either a positive integer or \"infinite\")"
709)]
710pub struct StressCountParseError {
711 pub input: String,
713}
714
715impl StressCountParseError {
716 pub(crate) fn new(input: impl Into<String>) -> Self {
717 Self {
718 input: input.into(),
719 }
720 }
721}
722
723#[derive(Clone, Debug, Error)]
725#[non_exhaustive]
726pub enum DebuggerCommandParseError {
727 #[error(transparent)]
729 ShellWordsParse(shell_words::ParseError),
730
731 #[error("debugger command cannot be empty")]
733 EmptyCommand,
734}
735
736#[derive(Clone, Debug, Error)]
738#[non_exhaustive]
739pub enum TracerCommandParseError {
740 #[error(transparent)]
742 ShellWordsParse(shell_words::ParseError),
743
744 #[error("tracer command cannot be empty")]
746 EmptyCommand,
747}
748
749#[derive(Clone, Debug, Error)]
751#[error(
752 "unrecognized value for test-threads: {input}\n(hint: expected either an integer or \"num-cpus\")"
753)]
754pub struct TestThreadsParseError {
755 pub input: String,
757}
758
759impl TestThreadsParseError {
760 pub(crate) fn new(input: impl Into<String>) -> Self {
761 Self {
762 input: input.into(),
763 }
764 }
765}
766
767#[derive(Clone, Debug, Error)]
770pub struct PartitionerBuilderParseError {
771 expected_format: Option<&'static str>,
772 message: Cow<'static, str>,
773}
774
775impl PartitionerBuilderParseError {
776 pub(crate) fn new(
777 expected_format: Option<&'static str>,
778 message: impl Into<Cow<'static, str>>,
779 ) -> Self {
780 Self {
781 expected_format,
782 message: message.into(),
783 }
784 }
785}
786
787impl fmt::Display for PartitionerBuilderParseError {
788 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
789 match self.expected_format {
790 Some(format) => {
791 write!(
792 f,
793 "partition must be in the format \"{}\":\n{}",
794 format, self.message
795 )
796 }
797 None => write!(f, "{}", self.message),
798 }
799 }
800}
801
802#[derive(Clone, Debug, Error)]
805pub enum TestFilterBuilderError {
806 #[error("error constructing test filters")]
808 Construct {
809 #[from]
811 error: aho_corasick::BuildError,
812 },
813}
814
815#[derive(Debug, Error)]
817pub enum PathMapperConstructError {
818 #[error("{kind} `{input}` failed to canonicalize")]
820 Canonicalization {
821 kind: PathMapperConstructKind,
823
824 input: Utf8PathBuf,
826
827 #[source]
829 err: std::io::Error,
830 },
831 #[error("{kind} `{input}` canonicalized to a non-UTF-8 path")]
833 NonUtf8Path {
834 kind: PathMapperConstructKind,
836
837 input: Utf8PathBuf,
839
840 #[source]
842 err: FromPathBufError,
843 },
844 #[error("{kind} `{canonicalized_path}` is not a directory")]
846 NotADirectory {
847 kind: PathMapperConstructKind,
849
850 input: Utf8PathBuf,
852
853 canonicalized_path: Utf8PathBuf,
855 },
856}
857
858impl PathMapperConstructError {
859 pub fn kind(&self) -> PathMapperConstructKind {
861 match self {
862 Self::Canonicalization { kind, .. }
863 | Self::NonUtf8Path { kind, .. }
864 | Self::NotADirectory { kind, .. } => *kind,
865 }
866 }
867
868 pub fn input(&self) -> &Utf8Path {
870 match self {
871 Self::Canonicalization { input, .. }
872 | Self::NonUtf8Path { input, .. }
873 | Self::NotADirectory { input, .. } => input,
874 }
875 }
876}
877
878#[derive(Copy, Clone, Debug, PartialEq, Eq)]
883pub enum PathMapperConstructKind {
884 WorkspaceRoot,
886
887 TargetDir,
889}
890
891impl fmt::Display for PathMapperConstructKind {
892 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
893 match self {
894 Self::WorkspaceRoot => write!(f, "remapped workspace root"),
895 Self::TargetDir => write!(f, "remapped target directory"),
896 }
897 }
898}
899
900#[derive(Debug, Error)]
902pub enum RustBuildMetaParseError {
903 #[error("error deserializing platform from build metadata")]
905 PlatformDeserializeError(#[from] target_spec::Error),
906
907 #[error("the host platform could not be determined")]
909 DetectBuildTargetError(#[source] target_spec::Error),
910
911 #[error("unsupported features in the build metadata: {message}")]
913 Unsupported {
914 message: String,
916 },
917}
918
919#[derive(Clone, Debug, thiserror::Error)]
922#[error("invalid format version: {input}")]
923pub struct FormatVersionError {
924 pub input: String,
926 #[source]
928 pub error: FormatVersionErrorInner,
929}
930
931#[derive(Clone, Debug, thiserror::Error)]
933pub enum FormatVersionErrorInner {
934 #[error("expected format version in form of `{expected}`")]
936 InvalidFormat {
937 expected: &'static str,
939 },
940 #[error("version component `{which}` could not be parsed as an integer")]
942 InvalidInteger {
943 which: &'static str,
945 #[source]
947 err: std::num::ParseIntError,
948 },
949 #[error("version component `{which}` value {value} is out of range {range:?}")]
951 InvalidValue {
952 which: &'static str,
954 value: u8,
956 range: std::ops::Range<u8>,
958 },
959}
960
961#[derive(Debug, Error)]
964#[non_exhaustive]
965pub enum FromMessagesError {
966 #[error("error reading Cargo JSON messages")]
968 ReadMessages(#[source] std::io::Error),
969
970 #[error("error querying package graph")]
972 PackageGraph(#[source] guppy::Error),
973
974 #[error("missing kind for target {binary_name} in package {package_name}")]
976 MissingTargetKind {
977 package_name: String,
979 binary_name: String,
981 },
982}
983
984#[derive(Debug, Error)]
986#[non_exhaustive]
987pub enum CreateTestListError {
988 #[error(
990 "for `{binary_id}`, current directory `{cwd}` is not a directory\n\
991 (hint: ensure project source is available at this location)"
992 )]
993 CwdIsNotDir {
994 binary_id: RustBinaryId,
996
997 cwd: Utf8PathBuf,
999 },
1000
1001 #[error(
1003 "for `{binary_id}`, running command `{}` failed to execute",
1004 shell_words::join(command)
1005 )]
1006 CommandExecFail {
1007 binary_id: RustBinaryId,
1009
1010 command: Vec<String>,
1012
1013 #[source]
1015 error: std::io::Error,
1016 },
1017
1018 #[error(
1020 "for `{binary_id}`, command `{}` {}\n--- stdout:\n{}\n--- stderr:\n{}\n---",
1021 shell_words::join(command),
1022 display_exited_with(*exit_status),
1023 String::from_utf8_lossy(stdout),
1024 String::from_utf8_lossy(stderr),
1025 )]
1026 CommandFail {
1027 binary_id: RustBinaryId,
1029
1030 command: Vec<String>,
1032
1033 exit_status: ExitStatus,
1035
1036 stdout: Vec<u8>,
1038
1039 stderr: Vec<u8>,
1041 },
1042
1043 #[error(
1045 "for `{binary_id}`, command `{}` produced non-UTF-8 output:\n--- stdout:\n{}\n--- stderr:\n{}\n---",
1046 shell_words::join(command),
1047 String::from_utf8_lossy(stdout),
1048 String::from_utf8_lossy(stderr)
1049 )]
1050 CommandNonUtf8 {
1051 binary_id: RustBinaryId,
1053
1054 command: Vec<String>,
1056
1057 stdout: Vec<u8>,
1059
1060 stderr: Vec<u8>,
1062 },
1063
1064 #[error("for `{binary_id}`, {message}\nfull output:\n{full_output}")]
1066 ParseLine {
1067 binary_id: RustBinaryId,
1069
1070 message: Cow<'static, str>,
1072
1073 full_output: String,
1075 },
1076
1077 #[error(
1079 "error joining dynamic library paths for {}: [{}]",
1080 dylib_path_envvar(),
1081 itertools::join(.new_paths, ", ")
1082 )]
1083 DylibJoinPaths {
1084 new_paths: Vec<Utf8PathBuf>,
1086
1087 #[source]
1089 error: JoinPathsError,
1090 },
1091
1092 #[error("error creating Tokio runtime")]
1094 TokioRuntimeCreate(#[source] std::io::Error),
1095}
1096
1097impl CreateTestListError {
1098 pub(crate) fn parse_line(
1099 binary_id: RustBinaryId,
1100 message: impl Into<Cow<'static, str>>,
1101 full_output: impl Into<String>,
1102 ) -> Self {
1103 Self::ParseLine {
1104 binary_id,
1105 message: message.into(),
1106 full_output: full_output.into(),
1107 }
1108 }
1109
1110 pub(crate) fn dylib_join_paths(new_paths: Vec<Utf8PathBuf>, error: JoinPathsError) -> Self {
1111 Self::DylibJoinPaths { new_paths, error }
1112 }
1113}
1114
1115#[derive(Debug, Error)]
1117#[non_exhaustive]
1118pub enum WriteTestListError {
1119 #[error("error writing to output")]
1121 Io(#[source] std::io::Error),
1122
1123 #[error("error serializing to JSON")]
1125 Json(#[source] serde_json::Error),
1126}
1127
1128#[derive(Debug, Error)]
1132pub enum ConfigureHandleInheritanceError {
1133 #[cfg(windows)]
1135 #[error("error configuring handle inheritance")]
1136 WindowsError(#[from] std::io::Error),
1137}
1138
1139#[derive(Debug, Error)]
1141#[non_exhaustive]
1142pub enum TestRunnerBuildError {
1143 #[error("error creating Tokio runtime")]
1145 TokioRuntimeCreate(#[source] std::io::Error),
1146
1147 #[error("error setting up signals")]
1149 SignalHandlerSetupError(#[from] SignalHandlerSetupError),
1150}
1151
1152#[derive(Debug, Error)]
1154pub struct TestRunnerExecuteErrors<E> {
1155 pub report_error: Option<E>,
1157
1158 pub join_errors: Vec<tokio::task::JoinError>,
1161}
1162
1163impl<E: std::error::Error> fmt::Display for TestRunnerExecuteErrors<E> {
1164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1165 if let Some(report_error) = &self.report_error {
1166 write!(f, "error reporting results: {report_error}")?;
1167 }
1168
1169 if !self.join_errors.is_empty() {
1170 if self.report_error.is_some() {
1171 write!(f, "; ")?;
1172 }
1173
1174 write!(f, "errors joining tasks: ")?;
1175
1176 for (i, join_error) in self.join_errors.iter().enumerate() {
1177 if i > 0 {
1178 write!(f, ", ")?;
1179 }
1180
1181 write!(f, "{join_error}")?;
1182 }
1183 }
1184
1185 Ok(())
1186 }
1187}
1188
1189#[derive(Debug, Error)]
1193#[error(
1194 "could not detect archive format from file name `{file_name}` (supported extensions: {})",
1195 supported_extensions()
1196)]
1197pub struct UnknownArchiveFormat {
1198 pub file_name: String,
1200}
1201
1202fn supported_extensions() -> String {
1203 ArchiveFormat::SUPPORTED_FORMATS
1204 .iter()
1205 .map(|(extension, _)| *extension)
1206 .join(", ")
1207}
1208
1209#[derive(Debug, Error)]
1211#[non_exhaustive]
1212pub enum ArchiveCreateError {
1213 #[error("error creating binary list")]
1215 CreateBinaryList(#[source] WriteTestListError),
1216
1217 #[error("extra path `{}` not found", .redactor.redact_path(path))]
1219 MissingExtraPath {
1220 path: Utf8PathBuf,
1222
1223 redactor: Redactor,
1228 },
1229
1230 #[error("while archiving {step}, error writing {} `{path}` to archive", kind_str(*.is_dir))]
1232 InputFileRead {
1233 step: ArchiveStep,
1235
1236 path: Utf8PathBuf,
1238
1239 is_dir: Option<bool>,
1241
1242 #[source]
1244 error: std::io::Error,
1245 },
1246
1247 #[error("error reading directory entry from `{path}")]
1249 DirEntryRead {
1250 path: Utf8PathBuf,
1252
1253 #[source]
1255 error: std::io::Error,
1256 },
1257
1258 #[error("error writing to archive")]
1260 OutputArchiveIo(#[source] std::io::Error),
1261
1262 #[error("error reporting archive status")]
1264 ReporterIo(#[source] std::io::Error),
1265}
1266
1267fn kind_str(is_dir: Option<bool>) -> &'static str {
1268 match is_dir {
1269 Some(true) => "directory",
1270 Some(false) => "file",
1271 None => "path",
1272 }
1273}
1274
1275#[derive(Debug, Error)]
1277pub enum MetadataMaterializeError {
1278 #[error("I/O error reading metadata file `{path}`")]
1280 Read {
1281 path: Utf8PathBuf,
1283
1284 #[source]
1286 error: std::io::Error,
1287 },
1288
1289 #[error("error deserializing metadata file `{path}`")]
1291 Deserialize {
1292 path: Utf8PathBuf,
1294
1295 #[source]
1297 error: serde_json::Error,
1298 },
1299
1300 #[error("error parsing Rust build metadata from `{path}`")]
1302 RustBuildMeta {
1303 path: Utf8PathBuf,
1305
1306 #[source]
1308 error: RustBuildMetaParseError,
1309 },
1310
1311 #[error("error building package graph from `{path}`")]
1313 PackageGraphConstruct {
1314 path: Utf8PathBuf,
1316
1317 #[source]
1319 error: guppy::Error,
1320 },
1321}
1322
1323#[derive(Debug, Error)]
1327#[non_exhaustive]
1328pub enum ArchiveReadError {
1329 #[error("I/O error reading archive")]
1331 Io(#[source] std::io::Error),
1332
1333 #[error("path in archive `{}` wasn't valid UTF-8", String::from_utf8_lossy(.0))]
1335 NonUtf8Path(Vec<u8>),
1336
1337 #[error("path in archive `{0}` doesn't start with `target/`")]
1339 NoTargetPrefix(Utf8PathBuf),
1340
1341 #[error("path in archive `{path}` contains an invalid component `{component}`")]
1343 InvalidComponent {
1344 path: Utf8PathBuf,
1346
1347 component: String,
1349 },
1350
1351 #[error("corrupted archive: checksum read error for path `{path}`")]
1353 ChecksumRead {
1354 path: Utf8PathBuf,
1356
1357 #[source]
1359 error: std::io::Error,
1360 },
1361
1362 #[error("corrupted archive: invalid checksum for path `{path}`")]
1364 InvalidChecksum {
1365 path: Utf8PathBuf,
1367
1368 expected: u32,
1370
1371 actual: u32,
1373 },
1374
1375 #[error("metadata file `{0}` not found in archive")]
1377 MetadataFileNotFound(&'static Utf8Path),
1378
1379 #[error("error deserializing metadata file `{path}` in archive")]
1381 MetadataDeserializeError {
1382 path: &'static Utf8Path,
1384
1385 #[source]
1387 error: serde_json::Error,
1388 },
1389
1390 #[error("error building package graph from `{path}` in archive")]
1392 PackageGraphConstructError {
1393 path: &'static Utf8Path,
1395
1396 #[source]
1398 error: guppy::Error,
1399 },
1400}
1401
1402#[derive(Debug, Error)]
1406#[non_exhaustive]
1407pub enum ArchiveExtractError {
1408 #[error("error creating temporary directory")]
1410 TempDirCreate(#[source] std::io::Error),
1411
1412 #[error("error canonicalizing destination directory `{dir}`")]
1414 DestDirCanonicalization {
1415 dir: Utf8PathBuf,
1417
1418 #[source]
1420 error: std::io::Error,
1421 },
1422
1423 #[error("destination `{0}` already exists")]
1425 DestinationExists(Utf8PathBuf),
1426
1427 #[error("error reading archive")]
1429 Read(#[source] ArchiveReadError),
1430
1431 #[error("error deserializing Rust build metadata")]
1433 RustBuildMeta(#[from] RustBuildMetaParseError),
1434
1435 #[error("error writing file `{path}` to disk")]
1437 WriteFile {
1438 path: Utf8PathBuf,
1440
1441 #[source]
1443 error: std::io::Error,
1444 },
1445
1446 #[error("error reporting extract status")]
1448 ReporterIo(std::io::Error),
1449}
1450
1451#[derive(Debug, Error)]
1453#[non_exhaustive]
1454pub enum WriteEventError {
1455 #[error("error writing to output")]
1457 Io(#[source] std::io::Error),
1458
1459 #[error("error operating on path {file}")]
1461 Fs {
1462 file: Utf8PathBuf,
1464
1465 #[source]
1467 error: std::io::Error,
1468 },
1469
1470 #[error("error writing JUnit output to {file}")]
1472 Junit {
1473 file: Utf8PathBuf,
1475
1476 #[source]
1478 error: quick_junit::SerializeError,
1479 },
1480}
1481
1482#[derive(Debug, Error)]
1485#[non_exhaustive]
1486pub enum CargoConfigError {
1487 #[error("failed to retrieve current directory")]
1489 GetCurrentDir(#[source] std::io::Error),
1490
1491 #[error("current directory is invalid UTF-8")]
1493 CurrentDirInvalidUtf8(#[source] FromPathBufError),
1494
1495 #[error("failed to parse --config argument `{config_str}` as TOML")]
1497 CliConfigParseError {
1498 config_str: String,
1500
1501 #[source]
1503 error: toml_edit::TomlError,
1504 },
1505
1506 #[error("failed to deserialize --config argument `{config_str}` as TOML")]
1508 CliConfigDeError {
1509 config_str: String,
1511
1512 #[source]
1514 error: toml_edit::de::Error,
1515 },
1516
1517 #[error(
1519 "invalid format for --config argument `{config_str}` (should be a dotted key expression)"
1520 )]
1521 InvalidCliConfig {
1522 config_str: String,
1524
1525 #[source]
1527 reason: InvalidCargoCliConfigReason,
1528 },
1529
1530 #[error("non-UTF-8 path encountered")]
1532 NonUtf8Path(#[source] FromPathBufError),
1533
1534 #[error("failed to retrieve the Cargo home directory")]
1536 GetCargoHome(#[source] std::io::Error),
1537
1538 #[error("failed to canonicalize path `{path}")]
1540 FailedPathCanonicalization {
1541 path: Utf8PathBuf,
1543
1544 #[source]
1546 error: std::io::Error,
1547 },
1548
1549 #[error("failed to read config at `{path}`")]
1551 ConfigReadError {
1552 path: Utf8PathBuf,
1554
1555 #[source]
1557 error: std::io::Error,
1558 },
1559
1560 #[error(transparent)]
1562 ConfigParseError(#[from] Box<CargoConfigParseError>),
1563}
1564
1565#[derive(Debug, Error)]
1569#[error("failed to parse config at `{path}`")]
1570pub struct CargoConfigParseError {
1571 pub path: Utf8PathBuf,
1573
1574 #[source]
1576 pub error: toml::de::Error,
1577}
1578
1579#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)]
1583#[non_exhaustive]
1584pub enum InvalidCargoCliConfigReason {
1585 #[error("was not a TOML dotted key expression (such as `build.jobs = 2`)")]
1587 NotDottedKv,
1588
1589 #[error("includes non-whitespace decoration")]
1591 IncludesNonWhitespaceDecoration,
1592
1593 #[error("sets a value to an inline table, which is not accepted")]
1595 SetsValueToInlineTable,
1596
1597 #[error("sets a value to an array of tables, which is not accepted")]
1599 SetsValueToArrayOfTables,
1600
1601 #[error("doesn't provide a value")]
1603 DoesntProvideValue,
1604}
1605
1606#[derive(Debug, Error)]
1608pub enum HostPlatformDetectError {
1609 #[error(
1612 "error spawning `rustc -vV`, and detecting the build \
1613 target failed as well\n\
1614 - rustc spawn error: {}\n\
1615 - build target error: {}\n",
1616 DisplayErrorChain::new_with_initial_indent(" ", error),
1617 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1618 )]
1619 RustcVvSpawnError {
1620 error: std::io::Error,
1622
1623 build_target_error: Box<target_spec::Error>,
1625 },
1626
1627 #[error(
1630 "`rustc -vV` failed with {}, and detecting the \
1631 build target failed as well\n\
1632 - `rustc -vV` stdout:\n{}\n\
1633 - `rustc -vV` stderr:\n{}\n\
1634 - build target error:\n{}\n",
1635 status,
1636 Indented { item: String::from_utf8_lossy(stdout), indent: " " },
1637 Indented { item: String::from_utf8_lossy(stderr), indent: " " },
1638 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1639 )]
1640 RustcVvFailed {
1641 status: ExitStatus,
1643
1644 stdout: Vec<u8>,
1646
1647 stderr: Vec<u8>,
1649
1650 build_target_error: Box<target_spec::Error>,
1652 },
1653
1654 #[error(
1657 "parsing `rustc -vV` output failed, and detecting the build target \
1658 failed as well\n\
1659 - host platform error:\n{}\n\
1660 - build target error:\n{}\n",
1661 DisplayErrorChain::new_with_initial_indent(" ", host_platform_error),
1662 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1663 )]
1664 HostPlatformParseError {
1665 host_platform_error: Box<target_spec::Error>,
1667
1668 build_target_error: Box<target_spec::Error>,
1670 },
1671
1672 #[error("test-only code, so `rustc -vV` was not called; failed to detect build target")]
1675 BuildTargetError {
1676 #[source]
1678 build_target_error: Box<target_spec::Error>,
1679 },
1680}
1681
1682#[derive(Debug, Error)]
1684pub enum TargetTripleError {
1685 #[error(
1687 "environment variable '{}' contained non-UTF-8 data",
1688 TargetTriple::CARGO_BUILD_TARGET_ENV
1689 )]
1690 InvalidEnvironmentVar,
1691
1692 #[error("error deserializing target triple from {source}")]
1694 TargetSpecError {
1695 source: TargetTripleSource,
1697
1698 #[source]
1700 error: target_spec::Error,
1701 },
1702
1703 #[error("target path `{path}` is not a valid file")]
1705 TargetPathReadError {
1706 source: TargetTripleSource,
1708
1709 path: Utf8PathBuf,
1711
1712 #[source]
1714 error: std::io::Error,
1715 },
1716
1717 #[error(
1719 "for custom platform obtained from {source}, \
1720 failed to create temporary directory for custom platform"
1721 )]
1722 CustomPlatformTempDirError {
1723 source: TargetTripleSource,
1725
1726 #[source]
1728 error: std::io::Error,
1729 },
1730
1731 #[error(
1733 "for custom platform obtained from {source}, \
1734 failed to write JSON to temporary path `{path}`"
1735 )]
1736 CustomPlatformWriteError {
1737 source: TargetTripleSource,
1739
1740 path: Utf8PathBuf,
1742
1743 #[source]
1745 error: std::io::Error,
1746 },
1747
1748 #[error(
1750 "for custom platform obtained from {source}, \
1751 failed to close temporary directory `{dir_path}`"
1752 )]
1753 CustomPlatformCloseError {
1754 source: TargetTripleSource,
1756
1757 dir_path: Utf8PathBuf,
1759
1760 #[source]
1762 error: std::io::Error,
1763 },
1764}
1765
1766impl TargetTripleError {
1767 pub fn source_report(&self) -> Option<miette::Report> {
1772 match self {
1773 Self::TargetSpecError { error, .. } => {
1774 Some(miette::Report::new_boxed(error.clone().into_diagnostic()))
1775 }
1776 TargetTripleError::InvalidEnvironmentVar
1778 | TargetTripleError::TargetPathReadError { .. }
1779 | TargetTripleError::CustomPlatformTempDirError { .. }
1780 | TargetTripleError::CustomPlatformWriteError { .. }
1781 | TargetTripleError::CustomPlatformCloseError { .. } => None,
1782 }
1783 }
1784}
1785
1786#[derive(Debug, Error)]
1788pub enum TargetRunnerError {
1789 #[error("environment variable '{0}' contained non-UTF-8 data")]
1791 InvalidEnvironmentVar(String),
1792
1793 #[error("runner '{key}' = '{value}' did not contain a runner binary")]
1796 BinaryNotSpecified {
1797 key: PlatformRunnerSource,
1799
1800 value: String,
1802 },
1803}
1804
1805#[derive(Debug, Error)]
1807#[error("error setting up signal handler")]
1808pub struct SignalHandlerSetupError(#[from] std::io::Error);
1809
1810#[derive(Debug, Error)]
1812pub enum ShowTestGroupsError {
1813 #[error(
1815 "unknown test groups specified: {}\n(known groups: {})",
1816 unknown_groups.iter().join(", "),
1817 known_groups.iter().join(", "),
1818 )]
1819 UnknownGroups {
1820 unknown_groups: BTreeSet<TestGroup>,
1822
1823 known_groups: BTreeSet<TestGroup>,
1825 },
1826}
1827
1828#[derive(Debug, Error, PartialEq, Eq, Hash)]
1830pub enum InheritsError {
1831 #[error("the {} profile should not inherit from other profiles", .0)]
1833 DefaultProfileInheritance(String),
1834 #[error("profile {} inherits from an unknown profile {}", .0, .1)]
1836 UnknownInheritance(String, String),
1837 #[error("a self referential inheritance is detected from profile: {}", .0)]
1839 SelfReferentialInheritance(String),
1840 #[error("inheritance cycle detected in profile configuration from: {}", .0.iter().map(|scc| {
1842 format!("[{}]", scc.iter().join(", "))
1843 }).join(", "))]
1844 InheritanceCycle(Vec<Vec<String>>),
1845}
1846
1847#[cfg(feature = "self-update")]
1848mod self_update_errors {
1849 use super::*;
1850 use mukti_metadata::ReleaseStatus;
1851 use semver::{Version, VersionReq};
1852
1853 #[cfg(feature = "self-update")]
1857 #[derive(Debug, Error)]
1858 #[non_exhaustive]
1859 pub enum UpdateError {
1860 #[error("failed to read release metadata from `{path}`")]
1862 ReadLocalMetadata {
1863 path: Utf8PathBuf,
1865
1866 #[source]
1868 error: std::io::Error,
1869 },
1870
1871 #[error("self-update failed")]
1873 SelfUpdate(#[source] self_update::errors::Error),
1874
1875 #[error("deserializing release metadata failed")]
1877 ReleaseMetadataDe(#[source] serde_json::Error),
1878
1879 #[error("version `{version}` not found (known versions: {})", known_versions(.known))]
1881 VersionNotFound {
1882 version: Version,
1884
1885 known: Vec<(Version, ReleaseStatus)>,
1887 },
1888
1889 #[error("no version found matching requirement `{req}`")]
1891 NoMatchForVersionReq {
1892 req: VersionReq,
1894 },
1895
1896 #[error("project {not_found} not found in release metadata (known projects: {})", known.join(", "))]
1898 MuktiProjectNotFound {
1899 not_found: String,
1901
1902 known: Vec<String>,
1904 },
1905
1906 #[error(
1908 "for version {version}, no release information found for target `{triple}` \
1909 (known targets: {})",
1910 known_triples.iter().join(", ")
1911 )]
1912 NoTargetData {
1913 version: Version,
1915
1916 triple: String,
1918
1919 known_triples: BTreeSet<String>,
1921 },
1922
1923 #[error("the current executable's path could not be determined")]
1925 CurrentExe(#[source] std::io::Error),
1926
1927 #[error("temporary directory could not be created at `{location}`")]
1929 TempDirCreate {
1930 location: Utf8PathBuf,
1932
1933 #[source]
1935 error: std::io::Error,
1936 },
1937
1938 #[error("temporary archive could not be created at `{archive_path}`")]
1940 TempArchiveCreate {
1941 archive_path: Utf8PathBuf,
1943
1944 #[source]
1946 error: std::io::Error,
1947 },
1948
1949 #[error("error writing to temporary archive at `{archive_path}`")]
1951 TempArchiveWrite {
1952 archive_path: Utf8PathBuf,
1954
1955 #[source]
1957 error: std::io::Error,
1958 },
1959
1960 #[error("error reading from temporary archive at `{archive_path}`")]
1962 TempArchiveRead {
1963 archive_path: Utf8PathBuf,
1965
1966 #[source]
1968 error: std::io::Error,
1969 },
1970
1971 #[error("SHA-256 checksum mismatch: expected: {expected}, actual: {actual}")]
1973 ChecksumMismatch {
1974 expected: String,
1976
1977 actual: String,
1979 },
1980
1981 #[error("error renaming `{source}` to `{dest}`")]
1983 FsRename {
1984 source: Utf8PathBuf,
1986
1987 dest: Utf8PathBuf,
1989
1990 #[source]
1992 error: std::io::Error,
1993 },
1994
1995 #[error("cargo-nextest binary updated, but error running `cargo nextest self setup`")]
1997 SelfSetup(#[source] std::io::Error),
1998 }
1999
2000 fn known_versions(versions: &[(Version, ReleaseStatus)]) -> String {
2001 use std::fmt::Write;
2002
2003 const DISPLAY_COUNT: usize = 4;
2005
2006 let display_versions: Vec<_> = versions
2007 .iter()
2008 .filter(|(v, status)| v.pre.is_empty() && *status == ReleaseStatus::Active)
2009 .map(|(v, _)| v.to_string())
2010 .take(DISPLAY_COUNT)
2011 .collect();
2012 let mut display_str = display_versions.join(", ");
2013 if versions.len() > display_versions.len() {
2014 write!(
2015 display_str,
2016 " and {} others",
2017 versions.len() - display_versions.len()
2018 )
2019 .unwrap();
2020 }
2021
2022 display_str
2023 }
2024
2025 #[cfg(feature = "self-update")]
2026 #[derive(Debug, Error)]
2028 pub enum UpdateVersionParseError {
2029 #[error("version string is empty")]
2031 EmptyString,
2032
2033 #[error(
2035 "`{input}` is not a valid semver requirement\n\
2036 (hint: see https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html for the correct format)"
2037 )]
2038 InvalidVersionReq {
2039 input: String,
2041
2042 #[source]
2044 error: semver::Error,
2045 },
2046
2047 #[error("`{input}` is not a valid semver{}", extra_semver_output(.input))]
2049 InvalidVersion {
2050 input: String,
2052
2053 #[source]
2055 error: semver::Error,
2056 },
2057 }
2058
2059 fn extra_semver_output(input: &str) -> String {
2060 if input.parse::<VersionReq>().is_ok() {
2063 format!(
2064 "\n(if you want to specify a semver range, add an explicit qualifier, like ^{input})"
2065 )
2066 } else {
2067 "".to_owned()
2068 }
2069 }
2070}
2071
2072#[cfg(feature = "self-update")]
2073pub use self_update_errors::*;
2074
2075#[cfg(test)]
2076mod tests {
2077 use super::*;
2078
2079 #[test]
2080 fn display_error_chain() {
2081 let err1 = StringError::new("err1", None);
2082
2083 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&err1)), @"err1");
2084
2085 let err2 = StringError::new("err2", Some(err1));
2086 let err3 = StringError::new("err3\nerr3 line 2", Some(err2));
2087
2088 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&err3)), @r"
2089 err3
2090 err3 line 2
2091 caused by:
2092 - err2
2093 - err1
2094 ");
2095 }
2096
2097 #[test]
2098 fn display_error_list() {
2099 let err1 = StringError::new("err1", None);
2100
2101 let error_list =
2102 ErrorList::<StringError>::new("waiting on the water to boil", vec![err1.clone()])
2103 .expect(">= 1 error");
2104 insta::assert_snapshot!(format!("{}", error_list), @"err1");
2105 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @"err1");
2106
2107 let err2 = StringError::new("err2", Some(err1));
2108 let err3 = StringError::new("err3", Some(err2));
2109
2110 let error_list =
2111 ErrorList::<StringError>::new("waiting on flowers to bloom", vec![err3.clone()])
2112 .expect(">= 1 error");
2113 insta::assert_snapshot!(format!("{}", error_list), @"err3");
2114 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @r"
2115 err3
2116 caused by:
2117 - err2
2118 - err1
2119 ");
2120
2121 let err4 = StringError::new("err4", None);
2122 let err5 = StringError::new("err5", Some(err4));
2123 let err6 = StringError::new("err6\nerr6 line 2", Some(err5));
2124
2125 let error_list = ErrorList::<StringError>::new(
2126 "waiting for the heat death of the universe",
2127 vec![err3, err6],
2128 )
2129 .expect(">= 1 error");
2130
2131 insta::assert_snapshot!(format!("{}", error_list), @r"
2132 2 errors occurred waiting for the heat death of the universe:
2133 * err3
2134 caused by:
2135 - err2
2136 - err1
2137 * err6
2138 err6 line 2
2139 caused by:
2140 - err5
2141 - err4
2142 ");
2143 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @r"
2144 2 errors occurred waiting for the heat death of the universe:
2145 * err3
2146 caused by:
2147 - err2
2148 - err1
2149 * err6
2150 err6 line 2
2151 caused by:
2152 - err5
2153 - err4
2154 ");
2155 }
2156
2157 #[derive(Clone, Debug, Error)]
2158 struct StringError {
2159 message: String,
2160 #[source]
2161 source: Option<Box<StringError>>,
2162 }
2163
2164 impl StringError {
2165 fn new(message: impl Into<String>, source: Option<StringError>) -> Self {
2166 Self {
2167 message: message.into(),
2168 source: source.map(Box::new),
2169 }
2170 }
2171 }
2172
2173 impl fmt::Display for StringError {
2174 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2175 write!(f, "{}", self.message)
2176 }
2177 }
2178}