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}
182
183#[derive(Debug)]
186#[non_exhaustive]
187pub struct ConfigCompileError {
188 pub profile_name: String,
190
191 pub section: ConfigCompileSection,
193
194 pub kind: ConfigCompileErrorKind,
196}
197
198#[derive(Debug)]
201pub enum ConfigCompileSection {
202 DefaultFilter,
204
205 Override(usize),
207
208 Script(usize),
210}
211
212#[derive(Debug)]
214#[non_exhaustive]
215pub enum ConfigCompileErrorKind {
216 ConstraintsNotSpecified {
218 default_filter_specified: bool,
223 },
224
225 FilterAndDefaultFilterSpecified,
229
230 Parse {
232 host_parse_error: Option<target_spec::Error>,
234
235 target_parse_error: Option<target_spec::Error>,
237
238 filter_parse_errors: Vec<FiltersetParseErrors>,
240 },
241}
242
243impl ConfigCompileErrorKind {
244 pub fn reports(&self) -> impl Iterator<Item = miette::Report> + '_ {
246 match self {
247 Self::ConstraintsNotSpecified {
248 default_filter_specified,
249 } => {
250 let message = if *default_filter_specified {
251 "for override with `default-filter`, `platform` must also be specified"
252 } else {
253 "at least one of `platform` and `filter` must be specified"
254 };
255 Either::Left(std::iter::once(miette::Report::msg(message)))
256 }
257 Self::FilterAndDefaultFilterSpecified => {
258 Either::Left(std::iter::once(miette::Report::msg(
259 "at most one of `filter` and `default-filter` must be specified",
260 )))
261 }
262 Self::Parse {
263 host_parse_error,
264 target_parse_error,
265 filter_parse_errors,
266 } => {
267 let host_parse_report = host_parse_error
268 .as_ref()
269 .map(|error| miette::Report::new_boxed(error.clone().into_diagnostic()));
270 let target_parse_report = target_parse_error
271 .as_ref()
272 .map(|error| miette::Report::new_boxed(error.clone().into_diagnostic()));
273 let filter_parse_reports =
274 filter_parse_errors.iter().flat_map(|filter_parse_errors| {
275 filter_parse_errors.errors.iter().map(|single_error| {
276 miette::Report::new(single_error.clone())
277 .with_source_code(filter_parse_errors.input.to_owned())
278 })
279 });
280
281 Either::Right(
282 host_parse_report
283 .into_iter()
284 .chain(target_parse_report)
285 .chain(filter_parse_reports),
286 )
287 }
288 }
289 }
290}
291
292#[derive(Clone, Debug, Error)]
294#[error("test priority ({priority}) out of range: must be between -100 and 100, both inclusive")]
295pub struct TestPriorityOutOfRange {
296 pub priority: i8,
298}
299
300#[derive(Clone, Debug, Error)]
302pub enum ChildStartError {
303 #[error("error creating temporary path for setup script")]
305 TempPath(#[source] Arc<std::io::Error>),
306
307 #[error("error spawning child process")]
309 Spawn(#[source] Arc<std::io::Error>),
310}
311
312#[derive(Clone, Debug, Error)]
314pub enum SetupScriptOutputError {
315 #[error("error opening environment file `{path}`")]
317 EnvFileOpen {
318 path: Utf8PathBuf,
320
321 #[source]
323 error: Arc<std::io::Error>,
324 },
325
326 #[error("error reading environment file `{path}`")]
328 EnvFileRead {
329 path: Utf8PathBuf,
331
332 #[source]
334 error: Arc<std::io::Error>,
335 },
336
337 #[error("line `{line}` in environment file `{path}` not in KEY=VALUE format")]
339 EnvFileParse {
340 path: Utf8PathBuf,
342 line: String,
344 },
345
346 #[error("key `{key}` begins with `NEXTEST`, which is reserved for internal use")]
348 EnvFileReservedKey {
349 key: String,
351 },
352}
353
354#[derive(Clone, Debug)]
359pub struct ErrorList<T> {
360 description: &'static str,
362 inner: Vec<T>,
364}
365
366impl<T: std::error::Error> ErrorList<T> {
367 pub(crate) fn new<U>(description: &'static str, errors: Vec<U>) -> Option<Self>
368 where
369 T: From<U>,
370 {
371 if errors.is_empty() {
372 None
373 } else {
374 Some(Self {
375 description,
376 inner: errors.into_iter().map(T::from).collect(),
377 })
378 }
379 }
380
381 pub(crate) fn short_message(&self) -> String {
383 let string = self.to_string();
384 match string.lines().next() {
385 Some(first_line) => first_line.trim_end_matches(':').to_string(),
387 None => String::new(),
388 }
389 }
390
391 pub(crate) fn iter(&self) -> impl Iterator<Item = &T> {
392 self.inner.iter()
393 }
394}
395
396impl<T: std::error::Error> fmt::Display for ErrorList<T> {
397 fn fmt(&self, mut f: &mut fmt::Formatter) -> fmt::Result {
398 if self.inner.len() == 1 {
400 return write!(f, "{}", self.inner[0]);
401 }
402
403 writeln!(
405 f,
406 "{} errors occurred {}:",
407 self.inner.len(),
408 self.description,
409 )?;
410 for error in &self.inner {
411 let mut indent = IndentWriter::new_skip_initial(" ", f);
412 writeln!(indent, "* {}", DisplayErrorChain::new(error))?;
413 f = indent.into_inner();
414 }
415 Ok(())
416 }
417}
418
419impl<T: std::error::Error> std::error::Error for ErrorList<T> {
420 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
421 if self.inner.len() == 1 {
422 self.inner[0].source()
423 } else {
424 None
427 }
428 }
429}
430
431pub(crate) struct DisplayErrorChain<E> {
436 error: E,
437 initial_indent: &'static str,
438}
439
440impl<E: std::error::Error> DisplayErrorChain<E> {
441 pub(crate) fn new(error: E) -> Self {
442 Self {
443 error,
444 initial_indent: "",
445 }
446 }
447
448 pub(crate) fn new_with_initial_indent(initial_indent: &'static str, error: E) -> Self {
449 Self {
450 error,
451 initial_indent,
452 }
453 }
454}
455
456impl<E> fmt::Display for DisplayErrorChain<E>
457where
458 E: std::error::Error,
459{
460 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
461 let mut writer = IndentWriter::new(self.initial_indent, f);
462 write!(writer, "{}", self.error)?;
463
464 let Some(mut cause) = self.error.source() else {
465 return Ok(());
466 };
467
468 write!(writer, "\n caused by:")?;
469
470 loop {
471 writeln!(writer)?;
472 let mut indent = IndentWriter::new_skip_initial(" ", writer);
473 write!(indent, " - {cause}")?;
474
475 let Some(next_cause) = cause.source() else {
476 break Ok(());
477 };
478
479 cause = next_cause;
480 writer = indent.into_inner();
481 }
482 }
483}
484
485#[derive(Clone, Debug, Error)]
487pub enum ChildError {
488 #[error(transparent)]
490 Fd(#[from] ChildFdError),
491
492 #[error(transparent)]
494 SetupScriptOutput(#[from] SetupScriptOutputError),
495}
496
497#[derive(Clone, Debug, Error)]
499pub enum ChildFdError {
500 #[error("error reading standard output")]
502 ReadStdout(#[source] Arc<std::io::Error>),
503
504 #[error("error reading standard error")]
506 ReadStderr(#[source] Arc<std::io::Error>),
507
508 #[error("error reading combined stream")]
510 ReadCombined(#[source] Arc<std::io::Error>),
511
512 #[error("error waiting for child process to exit")]
514 Wait(#[source] Arc<std::io::Error>),
515}
516
517#[derive(Clone, Debug, Eq, PartialEq)]
519#[non_exhaustive]
520pub struct UnknownTestGroupError {
521 pub profile_name: String,
523
524 pub name: TestGroup,
526}
527
528#[derive(Clone, Debug, Eq, PartialEq)]
531pub struct ProfileUnknownScriptError {
532 pub profile_name: String,
534
535 pub name: ScriptId,
537}
538
539#[derive(Clone, Debug, Eq, PartialEq)]
542pub struct ProfileWrongConfigScriptTypeError {
543 pub profile_name: String,
545
546 pub name: ScriptId,
548
549 pub attempted: ProfileScriptType,
551
552 pub actual: ScriptType,
554}
555
556#[derive(Clone, Debug, Eq, PartialEq)]
559pub struct ProfileListScriptUsesRunFiltersError {
560 pub profile_name: String,
562
563 pub name: ScriptId,
565
566 pub script_type: ProfileScriptType,
568
569 pub filters: BTreeSet<String>,
571}
572
573#[derive(Clone, Debug, Default)]
575pub struct ProfileScriptErrors {
576 pub unknown_scripts: Vec<ProfileUnknownScriptError>,
578
579 pub wrong_script_types: Vec<ProfileWrongConfigScriptTypeError>,
581
582 pub list_scripts_using_run_filters: Vec<ProfileListScriptUsesRunFiltersError>,
584}
585
586impl ProfileScriptErrors {
587 pub fn is_empty(&self) -> bool {
589 self.unknown_scripts.is_empty()
590 && self.wrong_script_types.is_empty()
591 && self.list_scripts_using_run_filters.is_empty()
592 }
593}
594
595#[derive(Clone, Debug, Error)]
597#[error("profile `{profile} not found (known profiles: {})`", .all_profiles.join(", "))]
598pub struct ProfileNotFound {
599 profile: String,
600 all_profiles: Vec<String>,
601}
602
603impl ProfileNotFound {
604 pub(crate) fn new(
605 profile: impl Into<String>,
606 all_profiles: impl IntoIterator<Item = impl Into<String>>,
607 ) -> Self {
608 let mut all_profiles: Vec<_> = all_profiles.into_iter().map(|s| s.into()).collect();
609 all_profiles.sort_unstable();
610 Self {
611 profile: profile.into(),
612 all_profiles,
613 }
614 }
615}
616
617#[derive(Clone, Debug, Error, Eq, PartialEq)]
619pub enum InvalidIdentifier {
620 #[error("identifier is empty")]
622 Empty,
623
624 #[error("invalid identifier `{0}`")]
626 InvalidXid(SmolStr),
627
628 #[error("tool identifier not of the form \"@tool:tool-name:identifier\": `{0}`")]
630 ToolIdentifierInvalidFormat(SmolStr),
631
632 #[error("tool identifier has empty component: `{0}`")]
634 ToolComponentEmpty(SmolStr),
635
636 #[error("invalid tool identifier `{0}`")]
638 ToolIdentifierInvalidXid(SmolStr),
639}
640
641#[derive(Clone, Debug, Error)]
643#[error("invalid custom test group name: {0}")]
644pub struct InvalidCustomTestGroupName(pub InvalidIdentifier);
645
646#[derive(Clone, Debug, Error)]
648#[error("invalid configuration script name: {0}")]
649pub struct InvalidConfigScriptName(pub InvalidIdentifier);
650
651#[derive(Clone, Debug, Error)]
653pub enum ToolConfigFileParseError {
654 #[error(
655 "tool-config-file has invalid format: {input}\n(hint: tool configs must be in the format <tool-name>:<path>)"
656 )]
657 InvalidFormat {
659 input: String,
661 },
662
663 #[error("tool-config-file has empty tool name: {input}")]
665 EmptyToolName {
666 input: String,
668 },
669
670 #[error("tool-config-file has empty config file path: {input}")]
672 EmptyConfigFile {
673 input: String,
675 },
676
677 #[error("tool-config-file is not an absolute path: {config_file}")]
679 ConfigFileNotAbsolute {
680 config_file: Utf8PathBuf,
682 },
683}
684
685#[derive(Clone, Debug, Error)]
687#[error("unrecognized value for max-fail: {reason}")]
688pub struct MaxFailParseError {
689 pub reason: String,
691}
692
693impl MaxFailParseError {
694 pub(crate) fn new(reason: impl Into<String>) -> Self {
695 Self {
696 reason: reason.into(),
697 }
698 }
699}
700
701#[derive(Clone, Debug, Error)]
703#[error(
704 "unrecognized value for stress-count: {input}\n\
705 (hint: expected either a positive integer or \"infinite\")"
706)]
707pub struct StressCountParseError {
708 pub input: String,
710}
711
712impl StressCountParseError {
713 pub(crate) fn new(input: impl Into<String>) -> Self {
714 Self {
715 input: input.into(),
716 }
717 }
718}
719
720#[derive(Clone, Debug, Error)]
722#[non_exhaustive]
723pub enum DebuggerCommandParseError {
724 #[error(transparent)]
726 ShellWordsParse(shell_words::ParseError),
727
728 #[error("debugger command cannot be empty")]
730 EmptyCommand,
731}
732
733#[derive(Clone, Debug, Error)]
735#[non_exhaustive]
736pub enum TracerCommandParseError {
737 #[error(transparent)]
739 ShellWordsParse(shell_words::ParseError),
740
741 #[error("tracer command cannot be empty")]
743 EmptyCommand,
744}
745
746#[derive(Clone, Debug, Error)]
748#[error(
749 "unrecognized value for test-threads: {input}\n(hint: expected either an integer or \"num-cpus\")"
750)]
751pub struct TestThreadsParseError {
752 pub input: String,
754}
755
756impl TestThreadsParseError {
757 pub(crate) fn new(input: impl Into<String>) -> Self {
758 Self {
759 input: input.into(),
760 }
761 }
762}
763
764#[derive(Clone, Debug, Error)]
767pub struct PartitionerBuilderParseError {
768 expected_format: Option<&'static str>,
769 message: Cow<'static, str>,
770}
771
772impl PartitionerBuilderParseError {
773 pub(crate) fn new(
774 expected_format: Option<&'static str>,
775 message: impl Into<Cow<'static, str>>,
776 ) -> Self {
777 Self {
778 expected_format,
779 message: message.into(),
780 }
781 }
782}
783
784impl fmt::Display for PartitionerBuilderParseError {
785 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
786 match self.expected_format {
787 Some(format) => {
788 write!(
789 f,
790 "partition must be in the format \"{}\":\n{}",
791 format, self.message
792 )
793 }
794 None => write!(f, "{}", self.message),
795 }
796 }
797}
798
799#[derive(Clone, Debug, Error)]
802pub enum TestFilterBuilderError {
803 #[error("error constructing test filters")]
805 Construct {
806 #[from]
808 error: aho_corasick::BuildError,
809 },
810}
811
812#[derive(Debug, Error)]
814pub enum PathMapperConstructError {
815 #[error("{kind} `{input}` failed to canonicalize")]
817 Canonicalization {
818 kind: PathMapperConstructKind,
820
821 input: Utf8PathBuf,
823
824 #[source]
826 err: std::io::Error,
827 },
828 #[error("{kind} `{input}` canonicalized to a non-UTF-8 path")]
830 NonUtf8Path {
831 kind: PathMapperConstructKind,
833
834 input: Utf8PathBuf,
836
837 #[source]
839 err: FromPathBufError,
840 },
841 #[error("{kind} `{canonicalized_path}` is not a directory")]
843 NotADirectory {
844 kind: PathMapperConstructKind,
846
847 input: Utf8PathBuf,
849
850 canonicalized_path: Utf8PathBuf,
852 },
853}
854
855impl PathMapperConstructError {
856 pub fn kind(&self) -> PathMapperConstructKind {
858 match self {
859 Self::Canonicalization { kind, .. }
860 | Self::NonUtf8Path { kind, .. }
861 | Self::NotADirectory { kind, .. } => *kind,
862 }
863 }
864
865 pub fn input(&self) -> &Utf8Path {
867 match self {
868 Self::Canonicalization { input, .. }
869 | Self::NonUtf8Path { input, .. }
870 | Self::NotADirectory { input, .. } => input,
871 }
872 }
873}
874
875#[derive(Copy, Clone, Debug, PartialEq, Eq)]
880pub enum PathMapperConstructKind {
881 WorkspaceRoot,
883
884 TargetDir,
886}
887
888impl fmt::Display for PathMapperConstructKind {
889 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
890 match self {
891 Self::WorkspaceRoot => write!(f, "remapped workspace root"),
892 Self::TargetDir => write!(f, "remapped target directory"),
893 }
894 }
895}
896
897#[derive(Debug, Error)]
899pub enum RustBuildMetaParseError {
900 #[error("error deserializing platform from build metadata")]
902 PlatformDeserializeError(#[from] target_spec::Error),
903
904 #[error("the host platform could not be determined")]
906 DetectBuildTargetError(#[source] target_spec::Error),
907
908 #[error("unsupported features in the build metadata: {message}")]
910 Unsupported {
911 message: String,
913 },
914}
915
916#[derive(Clone, Debug, thiserror::Error)]
919#[error("invalid format version: {input}")]
920pub struct FormatVersionError {
921 pub input: String,
923 #[source]
925 pub error: FormatVersionErrorInner,
926}
927
928#[derive(Clone, Debug, thiserror::Error)]
930pub enum FormatVersionErrorInner {
931 #[error("expected format version in form of `{expected}`")]
933 InvalidFormat {
934 expected: &'static str,
936 },
937 #[error("version component `{which}` could not be parsed as an integer")]
939 InvalidInteger {
940 which: &'static str,
942 #[source]
944 err: std::num::ParseIntError,
945 },
946 #[error("version component `{which}` value {value} is out of range {range:?}")]
948 InvalidValue {
949 which: &'static str,
951 value: u8,
953 range: std::ops::Range<u8>,
955 },
956}
957
958#[derive(Debug, Error)]
961#[non_exhaustive]
962pub enum FromMessagesError {
963 #[error("error reading Cargo JSON messages")]
965 ReadMessages(#[source] std::io::Error),
966
967 #[error("error querying package graph")]
969 PackageGraph(#[source] guppy::Error),
970
971 #[error("missing kind for target {binary_name} in package {package_name}")]
973 MissingTargetKind {
974 package_name: String,
976 binary_name: String,
978 },
979}
980
981#[derive(Debug, Error)]
983#[non_exhaustive]
984pub enum CreateTestListError {
985 #[error(
987 "for `{binary_id}`, current directory `{cwd}` is not a directory\n\
988 (hint: ensure project source is available at this location)"
989 )]
990 CwdIsNotDir {
991 binary_id: RustBinaryId,
993
994 cwd: Utf8PathBuf,
996 },
997
998 #[error(
1000 "for `{binary_id}`, running command `{}` failed to execute",
1001 shell_words::join(command)
1002 )]
1003 CommandExecFail {
1004 binary_id: RustBinaryId,
1006
1007 command: Vec<String>,
1009
1010 #[source]
1012 error: std::io::Error,
1013 },
1014
1015 #[error(
1017 "for `{binary_id}`, command `{}` {}\n--- stdout:\n{}\n--- stderr:\n{}\n---",
1018 shell_words::join(command),
1019 display_exited_with(*exit_status),
1020 String::from_utf8_lossy(stdout),
1021 String::from_utf8_lossy(stderr),
1022 )]
1023 CommandFail {
1024 binary_id: RustBinaryId,
1026
1027 command: Vec<String>,
1029
1030 exit_status: ExitStatus,
1032
1033 stdout: Vec<u8>,
1035
1036 stderr: Vec<u8>,
1038 },
1039
1040 #[error(
1042 "for `{binary_id}`, command `{}` produced non-UTF-8 output:\n--- stdout:\n{}\n--- stderr:\n{}\n---",
1043 shell_words::join(command),
1044 String::from_utf8_lossy(stdout),
1045 String::from_utf8_lossy(stderr)
1046 )]
1047 CommandNonUtf8 {
1048 binary_id: RustBinaryId,
1050
1051 command: Vec<String>,
1053
1054 stdout: Vec<u8>,
1056
1057 stderr: Vec<u8>,
1059 },
1060
1061 #[error("for `{binary_id}`, {message}\nfull output:\n{full_output}")]
1063 ParseLine {
1064 binary_id: RustBinaryId,
1066
1067 message: Cow<'static, str>,
1069
1070 full_output: String,
1072 },
1073
1074 #[error(
1076 "error joining dynamic library paths for {}: [{}]",
1077 dylib_path_envvar(),
1078 itertools::join(.new_paths, ", ")
1079 )]
1080 DylibJoinPaths {
1081 new_paths: Vec<Utf8PathBuf>,
1083
1084 #[source]
1086 error: JoinPathsError,
1087 },
1088
1089 #[error("error creating Tokio runtime")]
1091 TokioRuntimeCreate(#[source] std::io::Error),
1092}
1093
1094impl CreateTestListError {
1095 pub(crate) fn parse_line(
1096 binary_id: RustBinaryId,
1097 message: impl Into<Cow<'static, str>>,
1098 full_output: impl Into<String>,
1099 ) -> Self {
1100 Self::ParseLine {
1101 binary_id,
1102 message: message.into(),
1103 full_output: full_output.into(),
1104 }
1105 }
1106
1107 pub(crate) fn dylib_join_paths(new_paths: Vec<Utf8PathBuf>, error: JoinPathsError) -> Self {
1108 Self::DylibJoinPaths { new_paths, error }
1109 }
1110}
1111
1112#[derive(Debug, Error)]
1114#[non_exhaustive]
1115pub enum WriteTestListError {
1116 #[error("error writing to output")]
1118 Io(#[source] std::io::Error),
1119
1120 #[error("error serializing to JSON")]
1122 Json(#[source] serde_json::Error),
1123}
1124
1125#[derive(Debug, Error)]
1129pub enum ConfigureHandleInheritanceError {
1130 #[cfg(windows)]
1132 #[error("error configuring handle inheritance")]
1133 WindowsError(#[from] std::io::Error),
1134}
1135
1136#[derive(Debug, Error)]
1138#[non_exhaustive]
1139pub enum TestRunnerBuildError {
1140 #[error("error creating Tokio runtime")]
1142 TokioRuntimeCreate(#[source] std::io::Error),
1143
1144 #[error("error setting up signals")]
1146 SignalHandlerSetupError(#[from] SignalHandlerSetupError),
1147}
1148
1149#[derive(Debug, Error)]
1151pub struct TestRunnerExecuteErrors<E> {
1152 pub report_error: Option<E>,
1154
1155 pub join_errors: Vec<tokio::task::JoinError>,
1158}
1159
1160impl<E: std::error::Error> fmt::Display for TestRunnerExecuteErrors<E> {
1161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1162 if let Some(report_error) = &self.report_error {
1163 write!(f, "error reporting results: {report_error}")?;
1164 }
1165
1166 if !self.join_errors.is_empty() {
1167 if self.report_error.is_some() {
1168 write!(f, "; ")?;
1169 }
1170
1171 write!(f, "errors joining tasks: ")?;
1172
1173 for (i, join_error) in self.join_errors.iter().enumerate() {
1174 if i > 0 {
1175 write!(f, ", ")?;
1176 }
1177
1178 write!(f, "{join_error}")?;
1179 }
1180 }
1181
1182 Ok(())
1183 }
1184}
1185
1186#[derive(Debug, Error)]
1190#[error(
1191 "could not detect archive format from file name `{file_name}` (supported extensions: {})",
1192 supported_extensions()
1193)]
1194pub struct UnknownArchiveFormat {
1195 pub file_name: String,
1197}
1198
1199fn supported_extensions() -> String {
1200 ArchiveFormat::SUPPORTED_FORMATS
1201 .iter()
1202 .map(|(extension, _)| *extension)
1203 .join(", ")
1204}
1205
1206#[derive(Debug, Error)]
1208#[non_exhaustive]
1209pub enum ArchiveCreateError {
1210 #[error("error creating binary list")]
1212 CreateBinaryList(#[source] WriteTestListError),
1213
1214 #[error("extra path `{}` not found", .redactor.redact_path(path))]
1216 MissingExtraPath {
1217 path: Utf8PathBuf,
1219
1220 redactor: Redactor,
1225 },
1226
1227 #[error("while archiving {step}, error writing {} `{path}` to archive", kind_str(*.is_dir))]
1229 InputFileRead {
1230 step: ArchiveStep,
1232
1233 path: Utf8PathBuf,
1235
1236 is_dir: Option<bool>,
1238
1239 #[source]
1241 error: std::io::Error,
1242 },
1243
1244 #[error("error reading directory entry from `{path}")]
1246 DirEntryRead {
1247 path: Utf8PathBuf,
1249
1250 #[source]
1252 error: std::io::Error,
1253 },
1254
1255 #[error("error writing to archive")]
1257 OutputArchiveIo(#[source] std::io::Error),
1258
1259 #[error("error reporting archive status")]
1261 ReporterIo(#[source] std::io::Error),
1262}
1263
1264fn kind_str(is_dir: Option<bool>) -> &'static str {
1265 match is_dir {
1266 Some(true) => "directory",
1267 Some(false) => "file",
1268 None => "path",
1269 }
1270}
1271
1272#[derive(Debug, Error)]
1274pub enum MetadataMaterializeError {
1275 #[error("I/O error reading metadata file `{path}`")]
1277 Read {
1278 path: Utf8PathBuf,
1280
1281 #[source]
1283 error: std::io::Error,
1284 },
1285
1286 #[error("error deserializing metadata file `{path}`")]
1288 Deserialize {
1289 path: Utf8PathBuf,
1291
1292 #[source]
1294 error: serde_json::Error,
1295 },
1296
1297 #[error("error parsing Rust build metadata from `{path}`")]
1299 RustBuildMeta {
1300 path: Utf8PathBuf,
1302
1303 #[source]
1305 error: RustBuildMetaParseError,
1306 },
1307
1308 #[error("error building package graph from `{path}`")]
1310 PackageGraphConstruct {
1311 path: Utf8PathBuf,
1313
1314 #[source]
1316 error: guppy::Error,
1317 },
1318}
1319
1320#[derive(Debug, Error)]
1324#[non_exhaustive]
1325pub enum ArchiveReadError {
1326 #[error("I/O error reading archive")]
1328 Io(#[source] std::io::Error),
1329
1330 #[error("path in archive `{}` wasn't valid UTF-8", String::from_utf8_lossy(.0))]
1332 NonUtf8Path(Vec<u8>),
1333
1334 #[error("path in archive `{0}` doesn't start with `target/`")]
1336 NoTargetPrefix(Utf8PathBuf),
1337
1338 #[error("path in archive `{path}` contains an invalid component `{component}`")]
1340 InvalidComponent {
1341 path: Utf8PathBuf,
1343
1344 component: String,
1346 },
1347
1348 #[error("corrupted archive: checksum read error for path `{path}`")]
1350 ChecksumRead {
1351 path: Utf8PathBuf,
1353
1354 #[source]
1356 error: std::io::Error,
1357 },
1358
1359 #[error("corrupted archive: invalid checksum for path `{path}`")]
1361 InvalidChecksum {
1362 path: Utf8PathBuf,
1364
1365 expected: u32,
1367
1368 actual: u32,
1370 },
1371
1372 #[error("metadata file `{0}` not found in archive")]
1374 MetadataFileNotFound(&'static Utf8Path),
1375
1376 #[error("error deserializing metadata file `{path}` in archive")]
1378 MetadataDeserializeError {
1379 path: &'static Utf8Path,
1381
1382 #[source]
1384 error: serde_json::Error,
1385 },
1386
1387 #[error("error building package graph from `{path}` in archive")]
1389 PackageGraphConstructError {
1390 path: &'static Utf8Path,
1392
1393 #[source]
1395 error: guppy::Error,
1396 },
1397}
1398
1399#[derive(Debug, Error)]
1403#[non_exhaustive]
1404pub enum ArchiveExtractError {
1405 #[error("error creating temporary directory")]
1407 TempDirCreate(#[source] std::io::Error),
1408
1409 #[error("error canonicalizing destination directory `{dir}`")]
1411 DestDirCanonicalization {
1412 dir: Utf8PathBuf,
1414
1415 #[source]
1417 error: std::io::Error,
1418 },
1419
1420 #[error("destination `{0}` already exists")]
1422 DestinationExists(Utf8PathBuf),
1423
1424 #[error("error reading archive")]
1426 Read(#[source] ArchiveReadError),
1427
1428 #[error("error deserializing Rust build metadata")]
1430 RustBuildMeta(#[from] RustBuildMetaParseError),
1431
1432 #[error("error writing file `{path}` to disk")]
1434 WriteFile {
1435 path: Utf8PathBuf,
1437
1438 #[source]
1440 error: std::io::Error,
1441 },
1442
1443 #[error("error reporting extract status")]
1445 ReporterIo(std::io::Error),
1446}
1447
1448#[derive(Debug, Error)]
1450#[non_exhaustive]
1451pub enum WriteEventError {
1452 #[error("error writing to output")]
1454 Io(#[source] std::io::Error),
1455
1456 #[error("error operating on path {file}")]
1458 Fs {
1459 file: Utf8PathBuf,
1461
1462 #[source]
1464 error: std::io::Error,
1465 },
1466
1467 #[error("error writing JUnit output to {file}")]
1469 Junit {
1470 file: Utf8PathBuf,
1472
1473 #[source]
1475 error: quick_junit::SerializeError,
1476 },
1477}
1478
1479#[derive(Debug, Error)]
1482#[non_exhaustive]
1483pub enum CargoConfigError {
1484 #[error("failed to retrieve current directory")]
1486 GetCurrentDir(#[source] std::io::Error),
1487
1488 #[error("current directory is invalid UTF-8")]
1490 CurrentDirInvalidUtf8(#[source] FromPathBufError),
1491
1492 #[error("failed to parse --config argument `{config_str}` as TOML")]
1494 CliConfigParseError {
1495 config_str: String,
1497
1498 #[source]
1500 error: toml_edit::TomlError,
1501 },
1502
1503 #[error("failed to deserialize --config argument `{config_str}` as TOML")]
1505 CliConfigDeError {
1506 config_str: String,
1508
1509 #[source]
1511 error: toml_edit::de::Error,
1512 },
1513
1514 #[error(
1516 "invalid format for --config argument `{config_str}` (should be a dotted key expression)"
1517 )]
1518 InvalidCliConfig {
1519 config_str: String,
1521
1522 #[source]
1524 reason: InvalidCargoCliConfigReason,
1525 },
1526
1527 #[error("non-UTF-8 path encountered")]
1529 NonUtf8Path(#[source] FromPathBufError),
1530
1531 #[error("failed to retrieve the Cargo home directory")]
1533 GetCargoHome(#[source] std::io::Error),
1534
1535 #[error("failed to canonicalize path `{path}")]
1537 FailedPathCanonicalization {
1538 path: Utf8PathBuf,
1540
1541 #[source]
1543 error: std::io::Error,
1544 },
1545
1546 #[error("failed to read config at `{path}`")]
1548 ConfigReadError {
1549 path: Utf8PathBuf,
1551
1552 #[source]
1554 error: std::io::Error,
1555 },
1556
1557 #[error(transparent)]
1559 ConfigParseError(#[from] Box<CargoConfigParseError>),
1560}
1561
1562#[derive(Debug, Error)]
1566#[error("failed to parse config at `{path}`")]
1567pub struct CargoConfigParseError {
1568 pub path: Utf8PathBuf,
1570
1571 #[source]
1573 pub error: toml::de::Error,
1574}
1575
1576#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)]
1580#[non_exhaustive]
1581pub enum InvalidCargoCliConfigReason {
1582 #[error("was not a TOML dotted key expression (such as `build.jobs = 2`)")]
1584 NotDottedKv,
1585
1586 #[error("includes non-whitespace decoration")]
1588 IncludesNonWhitespaceDecoration,
1589
1590 #[error("sets a value to an inline table, which is not accepted")]
1592 SetsValueToInlineTable,
1593
1594 #[error("sets a value to an array of tables, which is not accepted")]
1596 SetsValueToArrayOfTables,
1597
1598 #[error("doesn't provide a value")]
1600 DoesntProvideValue,
1601}
1602
1603#[derive(Debug, Error)]
1605pub enum HostPlatformDetectError {
1606 #[error(
1609 "error spawning `rustc -vV`, and detecting the build \
1610 target failed as well\n\
1611 - rustc spawn error: {}\n\
1612 - build target error: {}\n",
1613 DisplayErrorChain::new_with_initial_indent(" ", error),
1614 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1615 )]
1616 RustcVvSpawnError {
1617 error: std::io::Error,
1619
1620 build_target_error: Box<target_spec::Error>,
1622 },
1623
1624 #[error(
1627 "`rustc -vV` failed with {}, and detecting the \
1628 build target failed as well\n\
1629 - `rustc -vV` stdout:\n{}\n\
1630 - `rustc -vV` stderr:\n{}\n\
1631 - build target error:\n{}\n",
1632 status,
1633 Indented { item: String::from_utf8_lossy(stdout), indent: " " },
1634 Indented { item: String::from_utf8_lossy(stderr), indent: " " },
1635 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1636 )]
1637 RustcVvFailed {
1638 status: ExitStatus,
1640
1641 stdout: Vec<u8>,
1643
1644 stderr: Vec<u8>,
1646
1647 build_target_error: Box<target_spec::Error>,
1649 },
1650
1651 #[error(
1654 "parsing `rustc -vV` output failed, and detecting the build target \
1655 failed as well\n\
1656 - host platform error:\n{}\n\
1657 - build target error:\n{}\n",
1658 DisplayErrorChain::new_with_initial_indent(" ", host_platform_error),
1659 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1660 )]
1661 HostPlatformParseError {
1662 host_platform_error: Box<target_spec::Error>,
1664
1665 build_target_error: Box<target_spec::Error>,
1667 },
1668
1669 #[error("test-only code, so `rustc -vV` was not called; failed to detect build target")]
1672 BuildTargetError {
1673 #[source]
1675 build_target_error: Box<target_spec::Error>,
1676 },
1677}
1678
1679#[derive(Debug, Error)]
1681pub enum TargetTripleError {
1682 #[error(
1684 "environment variable '{}' contained non-UTF-8 data",
1685 TargetTriple::CARGO_BUILD_TARGET_ENV
1686 )]
1687 InvalidEnvironmentVar,
1688
1689 #[error("error deserializing target triple from {source}")]
1691 TargetSpecError {
1692 source: TargetTripleSource,
1694
1695 #[source]
1697 error: target_spec::Error,
1698 },
1699
1700 #[error("target path `{path}` is not a valid file")]
1702 TargetPathReadError {
1703 source: TargetTripleSource,
1705
1706 path: Utf8PathBuf,
1708
1709 #[source]
1711 error: std::io::Error,
1712 },
1713
1714 #[error(
1716 "for custom platform obtained from {source}, \
1717 failed to create temporary directory for custom platform"
1718 )]
1719 CustomPlatformTempDirError {
1720 source: TargetTripleSource,
1722
1723 #[source]
1725 error: std::io::Error,
1726 },
1727
1728 #[error(
1730 "for custom platform obtained from {source}, \
1731 failed to write JSON to temporary path `{path}`"
1732 )]
1733 CustomPlatformWriteError {
1734 source: TargetTripleSource,
1736
1737 path: Utf8PathBuf,
1739
1740 #[source]
1742 error: std::io::Error,
1743 },
1744
1745 #[error(
1747 "for custom platform obtained from {source}, \
1748 failed to close temporary directory `{dir_path}`"
1749 )]
1750 CustomPlatformCloseError {
1751 source: TargetTripleSource,
1753
1754 dir_path: Utf8PathBuf,
1756
1757 #[source]
1759 error: std::io::Error,
1760 },
1761}
1762
1763impl TargetTripleError {
1764 pub fn source_report(&self) -> Option<miette::Report> {
1769 match self {
1770 Self::TargetSpecError { error, .. } => {
1771 Some(miette::Report::new_boxed(error.clone().into_diagnostic()))
1772 }
1773 TargetTripleError::InvalidEnvironmentVar
1775 | TargetTripleError::TargetPathReadError { .. }
1776 | TargetTripleError::CustomPlatformTempDirError { .. }
1777 | TargetTripleError::CustomPlatformWriteError { .. }
1778 | TargetTripleError::CustomPlatformCloseError { .. } => None,
1779 }
1780 }
1781}
1782
1783#[derive(Debug, Error)]
1785pub enum TargetRunnerError {
1786 #[error("environment variable '{0}' contained non-UTF-8 data")]
1788 InvalidEnvironmentVar(String),
1789
1790 #[error("runner '{key}' = '{value}' did not contain a runner binary")]
1793 BinaryNotSpecified {
1794 key: PlatformRunnerSource,
1796
1797 value: String,
1799 },
1800}
1801
1802#[derive(Debug, Error)]
1804#[error("error setting up signal handler")]
1805pub struct SignalHandlerSetupError(#[from] std::io::Error);
1806
1807#[derive(Debug, Error)]
1809pub enum ShowTestGroupsError {
1810 #[error(
1812 "unknown test groups specified: {}\n(known groups: {})",
1813 unknown_groups.iter().join(", "),
1814 known_groups.iter().join(", "),
1815 )]
1816 UnknownGroups {
1817 unknown_groups: BTreeSet<TestGroup>,
1819
1820 known_groups: BTreeSet<TestGroup>,
1822 },
1823}
1824
1825#[cfg(feature = "self-update")]
1826mod self_update_errors {
1827 use super::*;
1828 use mukti_metadata::ReleaseStatus;
1829 use semver::{Version, VersionReq};
1830
1831 #[cfg(feature = "self-update")]
1835 #[derive(Debug, Error)]
1836 #[non_exhaustive]
1837 pub enum UpdateError {
1838 #[error("failed to read release metadata from `{path}`")]
1840 ReadLocalMetadata {
1841 path: Utf8PathBuf,
1843
1844 #[source]
1846 error: std::io::Error,
1847 },
1848
1849 #[error("self-update failed")]
1851 SelfUpdate(#[source] self_update::errors::Error),
1852
1853 #[error("deserializing release metadata failed")]
1855 ReleaseMetadataDe(#[source] serde_json::Error),
1856
1857 #[error("version `{version}` not found (known versions: {})", known_versions(.known))]
1859 VersionNotFound {
1860 version: Version,
1862
1863 known: Vec<(Version, ReleaseStatus)>,
1865 },
1866
1867 #[error("no version found matching requirement `{req}`")]
1869 NoMatchForVersionReq {
1870 req: VersionReq,
1872 },
1873
1874 #[error("project {not_found} not found in release metadata (known projects: {})", known.join(", "))]
1876 MuktiProjectNotFound {
1877 not_found: String,
1879
1880 known: Vec<String>,
1882 },
1883
1884 #[error(
1886 "for version {version}, no release information found for target `{triple}` \
1887 (known targets: {})",
1888 known_triples.iter().join(", ")
1889 )]
1890 NoTargetData {
1891 version: Version,
1893
1894 triple: String,
1896
1897 known_triples: BTreeSet<String>,
1899 },
1900
1901 #[error("the current executable's path could not be determined")]
1903 CurrentExe(#[source] std::io::Error),
1904
1905 #[error("temporary directory could not be created at `{location}`")]
1907 TempDirCreate {
1908 location: Utf8PathBuf,
1910
1911 #[source]
1913 error: std::io::Error,
1914 },
1915
1916 #[error("temporary archive could not be created at `{archive_path}`")]
1918 TempArchiveCreate {
1919 archive_path: Utf8PathBuf,
1921
1922 #[source]
1924 error: std::io::Error,
1925 },
1926
1927 #[error("error writing to temporary archive at `{archive_path}`")]
1929 TempArchiveWrite {
1930 archive_path: Utf8PathBuf,
1932
1933 #[source]
1935 error: std::io::Error,
1936 },
1937
1938 #[error("error reading from temporary archive at `{archive_path}`")]
1940 TempArchiveRead {
1941 archive_path: Utf8PathBuf,
1943
1944 #[source]
1946 error: std::io::Error,
1947 },
1948
1949 #[error("SHA-256 checksum mismatch: expected: {expected}, actual: {actual}")]
1951 ChecksumMismatch {
1952 expected: String,
1954
1955 actual: String,
1957 },
1958
1959 #[error("error renaming `{source}` to `{dest}`")]
1961 FsRename {
1962 source: Utf8PathBuf,
1964
1965 dest: Utf8PathBuf,
1967
1968 #[source]
1970 error: std::io::Error,
1971 },
1972
1973 #[error("cargo-nextest binary updated, but error running `cargo nextest self setup`")]
1975 SelfSetup(#[source] std::io::Error),
1976 }
1977
1978 fn known_versions(versions: &[(Version, ReleaseStatus)]) -> String {
1979 use std::fmt::Write;
1980
1981 const DISPLAY_COUNT: usize = 4;
1983
1984 let display_versions: Vec<_> = versions
1985 .iter()
1986 .filter(|(v, status)| v.pre.is_empty() && *status == ReleaseStatus::Active)
1987 .map(|(v, _)| v.to_string())
1988 .take(DISPLAY_COUNT)
1989 .collect();
1990 let mut display_str = display_versions.join(", ");
1991 if versions.len() > display_versions.len() {
1992 write!(
1993 display_str,
1994 " and {} others",
1995 versions.len() - display_versions.len()
1996 )
1997 .unwrap();
1998 }
1999
2000 display_str
2001 }
2002
2003 #[cfg(feature = "self-update")]
2004 #[derive(Debug, Error)]
2006 pub enum UpdateVersionParseError {
2007 #[error("version string is empty")]
2009 EmptyString,
2010
2011 #[error(
2013 "`{input}` is not a valid semver requirement\n\
2014 (hint: see https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html for the correct format)"
2015 )]
2016 InvalidVersionReq {
2017 input: String,
2019
2020 #[source]
2022 error: semver::Error,
2023 },
2024
2025 #[error("`{input}` is not a valid semver{}", extra_semver_output(.input))]
2027 InvalidVersion {
2028 input: String,
2030
2031 #[source]
2033 error: semver::Error,
2034 },
2035 }
2036
2037 fn extra_semver_output(input: &str) -> String {
2038 if input.parse::<VersionReq>().is_ok() {
2041 format!(
2042 "\n(if you want to specify a semver range, add an explicit qualifier, like ^{input})"
2043 )
2044 } else {
2045 "".to_owned()
2046 }
2047 }
2048}
2049
2050#[cfg(feature = "self-update")]
2051pub use self_update_errors::*;
2052
2053#[cfg(test)]
2054mod tests {
2055 use super::*;
2056
2057 #[test]
2058 fn display_error_chain() {
2059 let err1 = StringError::new("err1", None);
2060
2061 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&err1)), @"err1");
2062
2063 let err2 = StringError::new("err2", Some(err1));
2064 let err3 = StringError::new("err3\nerr3 line 2", Some(err2));
2065
2066 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&err3)), @r"
2067 err3
2068 err3 line 2
2069 caused by:
2070 - err2
2071 - err1
2072 ");
2073 }
2074
2075 #[test]
2076 fn display_error_list() {
2077 let err1 = StringError::new("err1", None);
2078
2079 let error_list =
2080 ErrorList::<StringError>::new("waiting on the water to boil", vec![err1.clone()])
2081 .expect(">= 1 error");
2082 insta::assert_snapshot!(format!("{}", error_list), @"err1");
2083 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @"err1");
2084
2085 let err2 = StringError::new("err2", Some(err1));
2086 let err3 = StringError::new("err3", Some(err2));
2087
2088 let error_list =
2089 ErrorList::<StringError>::new("waiting on flowers to bloom", vec![err3.clone()])
2090 .expect(">= 1 error");
2091 insta::assert_snapshot!(format!("{}", error_list), @"err3");
2092 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @r"
2093 err3
2094 caused by:
2095 - err2
2096 - err1
2097 ");
2098
2099 let err4 = StringError::new("err4", None);
2100 let err5 = StringError::new("err5", Some(err4));
2101 let err6 = StringError::new("err6\nerr6 line 2", Some(err5));
2102
2103 let error_list = ErrorList::<StringError>::new(
2104 "waiting for the heat death of the universe",
2105 vec![err3, err6],
2106 )
2107 .expect(">= 1 error");
2108
2109 insta::assert_snapshot!(format!("{}", error_list), @r"
2110 2 errors occurred waiting for the heat death of the universe:
2111 * err3
2112 caused by:
2113 - err2
2114 - err1
2115 * err6
2116 err6 line 2
2117 caused by:
2118 - err5
2119 - err4
2120 ");
2121 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @r"
2122 2 errors occurred waiting for the heat death of the universe:
2123 * err3
2124 caused by:
2125 - err2
2126 - err1
2127 * err6
2128 err6 line 2
2129 caused by:
2130 - err5
2131 - err4
2132 ");
2133 }
2134
2135 #[derive(Clone, Debug, Error)]
2136 struct StringError {
2137 message: String,
2138 #[source]
2139 source: Option<Box<StringError>>,
2140 }
2141
2142 impl StringError {
2143 fn new(message: impl Into<String>, source: Option<StringError>) -> Self {
2144 Self {
2145 message: message.into(),
2146 source: source.map(Box::new),
2147 }
2148 }
2149 }
2150
2151 impl fmt::Display for StringError {
2152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2153 write!(f, "{}", self.message)
2154 }
2155 }
2156}