1use crate::{
7 cargo_config::{TargetTriple, TargetTripleSource},
8 config::{
9 ConfigExperimental, CustomTestGroup, ProfileScriptType, ScriptId, ScriptType, TestGroup,
10 },
11 helpers::{display_exited_with, dylib_path_envvar},
12 redact::Redactor,
13 reuse_build::{ArchiveFormat, ArchiveStep},
14 target_runner::PlatformRunnerSource,
15};
16use camino::{FromPathBufError, Utf8Path, Utf8PathBuf};
17use config::ConfigError;
18use indent_write::{fmt::IndentWriter, indentable::Indented};
19use itertools::{Either, Itertools};
20use nextest_filtering::errors::FiltersetParseErrors;
21use nextest_metadata::RustBinaryId;
22use smol_str::SmolStr;
23use std::{
24 borrow::Cow,
25 collections::BTreeSet,
26 env::JoinPathsError,
27 fmt::{self, Write as _},
28 process::ExitStatus,
29 sync::Arc,
30};
31use target_spec_miette::IntoMietteDiagnostic;
32use thiserror::Error;
33
34#[derive(Debug, Error)]
36#[error(
37 "failed to parse nextest config at `{config_file}`{}",
38 provided_by_tool(tool.as_deref())
39)]
40#[non_exhaustive]
41pub struct ConfigParseError {
42 config_file: Utf8PathBuf,
43 tool: Option<String>,
44 #[source]
45 kind: ConfigParseErrorKind,
46}
47
48impl ConfigParseError {
49 pub(crate) fn new(
50 config_file: impl Into<Utf8PathBuf>,
51 tool: Option<&str>,
52 kind: ConfigParseErrorKind,
53 ) -> Self {
54 Self {
55 config_file: config_file.into(),
56 tool: tool.map(|s| s.to_owned()),
57 kind,
58 }
59 }
60
61 pub fn config_file(&self) -> &Utf8Path {
63 &self.config_file
64 }
65
66 pub fn tool(&self) -> Option<&str> {
68 self.tool.as_deref()
69 }
70
71 pub fn kind(&self) -> &ConfigParseErrorKind {
73 &self.kind
74 }
75}
76
77pub fn provided_by_tool(tool: Option<&str>) -> String {
79 match tool {
80 Some(tool) => format!(" provided by tool `{tool}`"),
81 None => String::new(),
82 }
83}
84
85#[derive(Debug, Error)]
89#[non_exhaustive]
90pub enum ConfigParseErrorKind {
91 #[error(transparent)]
93 BuildError(Box<ConfigError>),
94 #[error(transparent)]
95 DeserializeError(Box<serde_path_to_error::Error<ConfigError>>),
97 #[error(transparent)]
99 VersionOnlyReadError(std::io::Error),
100 #[error(transparent)]
102 VersionOnlyDeserializeError(Box<serde_path_to_error::Error<toml::de::Error>>),
103 #[error("error parsing compiled data (destructure this variant for more details)")]
105 CompileErrors(Vec<ConfigCompileError>),
106 #[error("invalid test groups defined: {}\n(test groups cannot start with '@tool:' unless specified by a tool)", .0.iter().join(", "))]
108 InvalidTestGroupsDefined(BTreeSet<CustomTestGroup>),
109 #[error(
111 "invalid test groups defined by tool: {}\n(test groups must start with '@tool:<tool-name>:')", .0.iter().join(", "))]
112 InvalidTestGroupsDefinedByTool(BTreeSet<CustomTestGroup>),
113 #[error("unknown test groups specified by config (destructure this variant for more details)")]
115 UnknownTestGroups {
116 errors: Vec<UnknownTestGroupError>,
118
119 known_groups: BTreeSet<TestGroup>,
121 },
122 #[error(
124 "both `[script.*]` and `[scripts.*]` defined\n\
125 (hint: [script.*] will be removed in the future: switch to [scripts.setup.*])"
126 )]
127 BothScriptAndScriptsDefined,
128 #[error("invalid config scripts defined: {}\n(config scripts cannot start with '@tool:' unless specified by a tool)", .0.iter().join(", "))]
130 InvalidConfigScriptsDefined(BTreeSet<ScriptId>),
131 #[error(
133 "invalid config scripts defined by tool: {}\n(config scripts must start with '@tool:<tool-name>:')", .0.iter().join(", "))]
134 InvalidConfigScriptsDefinedByTool(BTreeSet<ScriptId>),
135 #[error(
137 "config script names used more than once: {}\n\
138 (config script names must be unique across all script types)", .0.iter().join(", ")
139 )]
140 DuplicateConfigScriptNames(BTreeSet<ScriptId>),
141 #[error(
143 "errors in profile-specific config scripts (destructure this variant for more details)"
144 )]
145 ProfileScriptErrors {
146 errors: Box<ProfileScriptErrors>,
148
149 known_scripts: BTreeSet<ScriptId>,
151 },
152 #[error("unknown experimental features defined (destructure this variant for more details)")]
154 UnknownExperimentalFeatures {
155 unknown: BTreeSet<String>,
157
158 known: BTreeSet<ConfigExperimental>,
160 },
161 #[error(
165 "tool config file specifies experimental features `{}` \
166 -- only repository config files can do so",
167 .features.iter().join(", "),
168 )]
169 ExperimentalFeaturesInToolConfig {
170 features: BTreeSet<String>,
172 },
173 #[error("experimental features used but not enabled: {}", .missing_features.iter().join(", "))]
175 ExperimentalFeaturesNotEnabled {
176 missing_features: BTreeSet<ConfigExperimental>,
178 },
179}
180
181#[derive(Debug)]
184#[non_exhaustive]
185pub struct ConfigCompileError {
186 pub profile_name: String,
188
189 pub section: ConfigCompileSection,
191
192 pub kind: ConfigCompileErrorKind,
194}
195
196#[derive(Debug)]
199pub enum ConfigCompileSection {
200 DefaultFilter,
202
203 Override(usize),
205
206 Script(usize),
208}
209
210#[derive(Debug)]
212#[non_exhaustive]
213pub enum ConfigCompileErrorKind {
214 ConstraintsNotSpecified {
216 default_filter_specified: bool,
221 },
222
223 FilterAndDefaultFilterSpecified,
227
228 Parse {
230 host_parse_error: Option<target_spec::Error>,
232
233 target_parse_error: Option<target_spec::Error>,
235
236 filter_parse_errors: Vec<FiltersetParseErrors>,
238 },
239}
240
241impl ConfigCompileErrorKind {
242 pub fn reports(&self) -> impl Iterator<Item = miette::Report> + '_ {
244 match self {
245 Self::ConstraintsNotSpecified {
246 default_filter_specified,
247 } => {
248 let message = if *default_filter_specified {
249 "for override with `default-filter`, `platform` must also be specified"
250 } else {
251 "at least one of `platform` and `filter` must be specified"
252 };
253 Either::Left(std::iter::once(miette::Report::msg(message)))
254 }
255 Self::FilterAndDefaultFilterSpecified => {
256 Either::Left(std::iter::once(miette::Report::msg(
257 "at most one of `filter` and `default-filter` must be specified",
258 )))
259 }
260 Self::Parse {
261 host_parse_error,
262 target_parse_error,
263 filter_parse_errors,
264 } => {
265 let host_parse_report = host_parse_error
266 .as_ref()
267 .map(|error| miette::Report::new_boxed(error.clone().into_diagnostic()));
268 let target_parse_report = target_parse_error
269 .as_ref()
270 .map(|error| miette::Report::new_boxed(error.clone().into_diagnostic()));
271 let filter_parse_reports =
272 filter_parse_errors.iter().flat_map(|filter_parse_errors| {
273 filter_parse_errors.errors.iter().map(|single_error| {
274 miette::Report::new(single_error.clone())
275 .with_source_code(filter_parse_errors.input.to_owned())
276 })
277 });
278
279 Either::Right(
280 host_parse_report
281 .into_iter()
282 .chain(target_parse_report)
283 .chain(filter_parse_reports),
284 )
285 }
286 }
287 }
288}
289
290#[derive(Clone, Debug, Error)]
292#[error("test priority ({priority}) out of range: must be between -100 and 100, both inclusive")]
293pub struct TestPriorityOutOfRange {
294 pub priority: i8,
296}
297
298#[derive(Clone, Debug, Error)]
300pub enum ChildStartError {
301 #[error("error creating temporary path for setup script")]
303 TempPath(#[source] Arc<std::io::Error>),
304
305 #[error("error spawning child process")]
307 Spawn(#[source] Arc<std::io::Error>),
308}
309
310#[derive(Clone, Debug, Error)]
312pub enum SetupScriptOutputError {
313 #[error("error opening environment file `{path}`")]
315 EnvFileOpen {
316 path: Utf8PathBuf,
318
319 #[source]
321 error: Arc<std::io::Error>,
322 },
323
324 #[error("error reading environment file `{path}`")]
326 EnvFileRead {
327 path: Utf8PathBuf,
329
330 #[source]
332 error: Arc<std::io::Error>,
333 },
334
335 #[error("line `{line}` in environment file `{path}` not in KEY=VALUE format")]
337 EnvFileParse {
338 path: Utf8PathBuf,
340 line: String,
342 },
343
344 #[error("key `{key}` begins with `NEXTEST`, which is reserved for internal use")]
346 EnvFileReservedKey {
347 key: String,
349 },
350}
351
352#[derive(Clone, Debug)]
357pub struct ErrorList<T> {
358 description: &'static str,
360 inner: Vec<T>,
362}
363
364impl<T: std::error::Error> ErrorList<T> {
365 pub(crate) fn new<U>(description: &'static str, errors: Vec<U>) -> Option<Self>
366 where
367 T: From<U>,
368 {
369 if errors.is_empty() {
370 None
371 } else {
372 Some(Self {
373 description,
374 inner: errors.into_iter().map(T::from).collect(),
375 })
376 }
377 }
378
379 pub(crate) fn short_message(&self) -> String {
381 let string = self.to_string();
382 match string.lines().next() {
383 Some(first_line) => first_line.trim_end_matches(':').to_string(),
385 None => String::new(),
386 }
387 }
388
389 pub(crate) fn iter(&self) -> impl Iterator<Item = &T> {
390 self.inner.iter()
391 }
392}
393
394impl<T: std::error::Error> fmt::Display for ErrorList<T> {
395 fn fmt(&self, mut f: &mut fmt::Formatter) -> fmt::Result {
396 if self.inner.len() == 1 {
398 return write!(f, "{}", self.inner[0]);
399 }
400
401 writeln!(
403 f,
404 "{} errors occurred {}:",
405 self.inner.len(),
406 self.description,
407 )?;
408 for error in &self.inner {
409 let mut indent = IndentWriter::new_skip_initial(" ", f);
410 writeln!(indent, "* {}", DisplayErrorChain::new(error))?;
411 f = indent.into_inner();
412 }
413 Ok(())
414 }
415}
416
417impl<T: std::error::Error> std::error::Error for ErrorList<T> {
418 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
419 if self.inner.len() == 1 {
420 self.inner[0].source()
421 } else {
422 None
425 }
426 }
427}
428
429pub(crate) struct DisplayErrorChain<E> {
434 error: E,
435 initial_indent: &'static str,
436}
437
438impl<E: std::error::Error> DisplayErrorChain<E> {
439 pub(crate) fn new(error: E) -> Self {
440 Self {
441 error,
442 initial_indent: "",
443 }
444 }
445
446 pub(crate) fn new_with_initial_indent(initial_indent: &'static str, error: E) -> Self {
447 Self {
448 error,
449 initial_indent,
450 }
451 }
452}
453
454impl<E> fmt::Display for DisplayErrorChain<E>
455where
456 E: std::error::Error,
457{
458 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
459 let mut writer = IndentWriter::new(self.initial_indent, f);
460 write!(writer, "{}", self.error)?;
461
462 let Some(mut cause) = self.error.source() else {
463 return Ok(());
464 };
465
466 write!(writer, "\n caused by:")?;
467
468 loop {
469 writeln!(writer)?;
470 let mut indent = IndentWriter::new_skip_initial(" ", writer);
471 write!(indent, " - {cause}")?;
472
473 let Some(next_cause) = cause.source() else {
474 break Ok(());
475 };
476
477 cause = next_cause;
478 writer = indent.into_inner();
479 }
480 }
481}
482
483#[derive(Clone, Debug, Error)]
485pub enum ChildError {
486 #[error(transparent)]
488 Fd(#[from] ChildFdError),
489
490 #[error(transparent)]
492 SetupScriptOutput(#[from] SetupScriptOutputError),
493}
494
495#[derive(Clone, Debug, Error)]
497pub enum ChildFdError {
498 #[error("error reading standard output")]
500 ReadStdout(#[source] Arc<std::io::Error>),
501
502 #[error("error reading standard error")]
504 ReadStderr(#[source] Arc<std::io::Error>),
505
506 #[error("error reading combined stream")]
508 ReadCombined(#[source] Arc<std::io::Error>),
509
510 #[error("error waiting for child process to exit")]
512 Wait(#[source] Arc<std::io::Error>),
513}
514
515#[derive(Clone, Debug, Eq, PartialEq)]
517#[non_exhaustive]
518pub struct UnknownTestGroupError {
519 pub profile_name: String,
521
522 pub name: TestGroup,
524}
525
526#[derive(Clone, Debug, Eq, PartialEq)]
529pub struct ProfileUnknownScriptError {
530 pub profile_name: String,
532
533 pub name: ScriptId,
535}
536
537#[derive(Clone, Debug, Eq, PartialEq)]
540pub struct ProfileWrongConfigScriptTypeError {
541 pub profile_name: String,
543
544 pub name: ScriptId,
546
547 pub attempted: ProfileScriptType,
549
550 pub actual: ScriptType,
552}
553
554#[derive(Clone, Debug, Eq, PartialEq)]
557pub struct ProfileListScriptUsesRunFiltersError {
558 pub profile_name: String,
560
561 pub name: ScriptId,
563
564 pub script_type: ProfileScriptType,
566
567 pub filters: BTreeSet<String>,
569}
570
571#[derive(Clone, Debug, Default)]
573pub struct ProfileScriptErrors {
574 pub unknown_scripts: Vec<ProfileUnknownScriptError>,
576
577 pub wrong_script_types: Vec<ProfileWrongConfigScriptTypeError>,
579
580 pub list_scripts_using_run_filters: Vec<ProfileListScriptUsesRunFiltersError>,
582}
583
584impl ProfileScriptErrors {
585 pub fn is_empty(&self) -> bool {
587 self.unknown_scripts.is_empty()
588 && self.wrong_script_types.is_empty()
589 && self.list_scripts_using_run_filters.is_empty()
590 }
591}
592
593#[derive(Clone, Debug, Error)]
595#[error("profile `{profile} not found (known profiles: {})`", .all_profiles.join(", "))]
596pub struct ProfileNotFound {
597 profile: String,
598 all_profiles: Vec<String>,
599}
600
601impl ProfileNotFound {
602 pub(crate) fn new(
603 profile: impl Into<String>,
604 all_profiles: impl IntoIterator<Item = impl Into<String>>,
605 ) -> Self {
606 let mut all_profiles: Vec<_> = all_profiles.into_iter().map(|s| s.into()).collect();
607 all_profiles.sort_unstable();
608 Self {
609 profile: profile.into(),
610 all_profiles,
611 }
612 }
613}
614
615#[derive(Clone, Debug, Error, Eq, PartialEq)]
617pub enum InvalidIdentifier {
618 #[error("identifier is empty")]
620 Empty,
621
622 #[error("invalid identifier `{0}`")]
624 InvalidXid(SmolStr),
625
626 #[error("tool identifier not of the form \"@tool:tool-name:identifier\": `{0}`")]
628 ToolIdentifierInvalidFormat(SmolStr),
629
630 #[error("tool identifier has empty component: `{0}`")]
632 ToolComponentEmpty(SmolStr),
633
634 #[error("invalid tool identifier `{0}`")]
636 ToolIdentifierInvalidXid(SmolStr),
637}
638
639#[derive(Clone, Debug, Error)]
641#[error("invalid custom test group name: {0}")]
642pub struct InvalidCustomTestGroupName(pub InvalidIdentifier);
643
644#[derive(Clone, Debug, Error)]
646#[error("invalid configuration script name: {0}")]
647pub struct InvalidConfigScriptName(pub InvalidIdentifier);
648
649#[derive(Clone, Debug, Error)]
651pub enum ToolConfigFileParseError {
652 #[error(
653 "tool-config-file has invalid format: {input}\n(hint: tool configs must be in the format <tool-name>:<path>)"
654 )]
655 InvalidFormat {
657 input: String,
659 },
660
661 #[error("tool-config-file has empty tool name: {input}")]
663 EmptyToolName {
664 input: String,
666 },
667
668 #[error("tool-config-file has empty config file path: {input}")]
670 EmptyConfigFile {
671 input: String,
673 },
674
675 #[error("tool-config-file is not an absolute path: {config_file}")]
677 ConfigFileNotAbsolute {
678 config_file: Utf8PathBuf,
680 },
681}
682
683#[derive(Clone, Debug, Error)]
685#[error(
686 "unrecognized value for max-fail: {input}\n(hint: expected either a positive integer or \"all\")"
687)]
688pub struct MaxFailParseError {
689 pub input: String,
691}
692
693impl MaxFailParseError {
694 pub(crate) fn new(input: impl Into<String>) -> Self {
695 Self {
696 input: input.into(),
697 }
698 }
699}
700
701#[derive(Clone, Debug, Error)]
703#[error(
704 "unrecognized value for test-threads: {input}\n(hint: expected either an integer or \"num-cpus\")"
705)]
706pub struct TestThreadsParseError {
707 pub input: String,
709}
710
711impl TestThreadsParseError {
712 pub(crate) fn new(input: impl Into<String>) -> Self {
713 Self {
714 input: input.into(),
715 }
716 }
717}
718
719#[derive(Clone, Debug, Error)]
722pub struct PartitionerBuilderParseError {
723 expected_format: Option<&'static str>,
724 message: Cow<'static, str>,
725}
726
727impl PartitionerBuilderParseError {
728 pub(crate) fn new(
729 expected_format: Option<&'static str>,
730 message: impl Into<Cow<'static, str>>,
731 ) -> Self {
732 Self {
733 expected_format,
734 message: message.into(),
735 }
736 }
737}
738
739impl fmt::Display for PartitionerBuilderParseError {
740 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
741 match self.expected_format {
742 Some(format) => {
743 write!(
744 f,
745 "partition must be in the format \"{}\":\n{}",
746 format, self.message
747 )
748 }
749 None => write!(f, "{}", self.message),
750 }
751 }
752}
753
754#[derive(Clone, Debug, Error)]
757pub enum TestFilterBuilderError {
758 #[error("error constructing test filters")]
760 Construct {
761 #[from]
763 error: aho_corasick::BuildError,
764 },
765}
766
767#[derive(Debug, Error)]
769pub enum PathMapperConstructError {
770 #[error("{kind} `{input}` failed to canonicalize")]
772 Canonicalization {
773 kind: PathMapperConstructKind,
775
776 input: Utf8PathBuf,
778
779 #[source]
781 err: std::io::Error,
782 },
783 #[error("{kind} `{input}` canonicalized to a non-UTF-8 path")]
785 NonUtf8Path {
786 kind: PathMapperConstructKind,
788
789 input: Utf8PathBuf,
791
792 #[source]
794 err: FromPathBufError,
795 },
796 #[error("{kind} `{canonicalized_path}` is not a directory")]
798 NotADirectory {
799 kind: PathMapperConstructKind,
801
802 input: Utf8PathBuf,
804
805 canonicalized_path: Utf8PathBuf,
807 },
808}
809
810impl PathMapperConstructError {
811 pub fn kind(&self) -> PathMapperConstructKind {
813 match self {
814 Self::Canonicalization { kind, .. }
815 | Self::NonUtf8Path { kind, .. }
816 | Self::NotADirectory { kind, .. } => *kind,
817 }
818 }
819
820 pub fn input(&self) -> &Utf8Path {
822 match self {
823 Self::Canonicalization { input, .. }
824 | Self::NonUtf8Path { input, .. }
825 | Self::NotADirectory { input, .. } => input,
826 }
827 }
828}
829
830#[derive(Copy, Clone, Debug, PartialEq, Eq)]
835pub enum PathMapperConstructKind {
836 WorkspaceRoot,
838
839 TargetDir,
841}
842
843impl fmt::Display for PathMapperConstructKind {
844 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
845 match self {
846 Self::WorkspaceRoot => write!(f, "remapped workspace root"),
847 Self::TargetDir => write!(f, "remapped target directory"),
848 }
849 }
850}
851
852#[derive(Debug, Error)]
854pub enum RustBuildMetaParseError {
855 #[error("error deserializing platform from build metadata")]
857 PlatformDeserializeError(#[from] target_spec::Error),
858
859 #[error("the host platform could not be determined")]
861 DetectBuildTargetError(#[source] target_spec::Error),
862
863 #[error("unsupported features in the build metadata: {message}")]
865 Unsupported {
866 message: String,
868 },
869}
870
871#[derive(Clone, Debug, thiserror::Error)]
874#[error("invalid format version: {input}")]
875pub struct FormatVersionError {
876 pub input: String,
878 #[source]
880 pub error: FormatVersionErrorInner,
881}
882
883#[derive(Clone, Debug, thiserror::Error)]
885pub enum FormatVersionErrorInner {
886 #[error("expected format version in form of `{expected}`")]
888 InvalidFormat {
889 expected: &'static str,
891 },
892 #[error("version component `{which}` could not be parsed as an integer")]
894 InvalidInteger {
895 which: &'static str,
897 #[source]
899 err: std::num::ParseIntError,
900 },
901 #[error("version component `{which}` value {value} is out of range {range:?}")]
903 InvalidValue {
904 which: &'static str,
906 value: u8,
908 range: std::ops::Range<u8>,
910 },
911}
912
913#[derive(Debug, Error)]
916#[non_exhaustive]
917pub enum FromMessagesError {
918 #[error("error reading Cargo JSON messages")]
920 ReadMessages(#[source] std::io::Error),
921
922 #[error("error querying package graph")]
924 PackageGraph(#[source] guppy::Error),
925
926 #[error("missing kind for target {binary_name} in package {package_name}")]
928 MissingTargetKind {
929 package_name: String,
931 binary_name: String,
933 },
934}
935
936#[derive(Debug, Error)]
938#[non_exhaustive]
939pub enum CreateTestListError {
940 #[error(
942 "for `{binary_id}`, current directory `{cwd}` is not a directory\n\
943 (hint: ensure project source is available at this location)"
944 )]
945 CwdIsNotDir {
946 binary_id: RustBinaryId,
948
949 cwd: Utf8PathBuf,
951 },
952
953 #[error(
955 "for `{binary_id}`, running command `{}` failed to execute",
956 shell_words::join(command)
957 )]
958 CommandExecFail {
959 binary_id: RustBinaryId,
961
962 command: Vec<String>,
964
965 #[source]
967 error: std::io::Error,
968 },
969
970 #[error(
972 "for `{binary_id}`, command `{}` {}\n--- stdout:\n{}\n--- stderr:\n{}\n---",
973 shell_words::join(command),
974 display_exited_with(*exit_status),
975 String::from_utf8_lossy(stdout),
976 String::from_utf8_lossy(stderr),
977 )]
978 CommandFail {
979 binary_id: RustBinaryId,
981
982 command: Vec<String>,
984
985 exit_status: ExitStatus,
987
988 stdout: Vec<u8>,
990
991 stderr: Vec<u8>,
993 },
994
995 #[error(
997 "for `{binary_id}`, command `{}` produced non-UTF-8 output:\n--- stdout:\n{}\n--- stderr:\n{}\n---",
998 shell_words::join(command),
999 String::from_utf8_lossy(stdout),
1000 String::from_utf8_lossy(stderr)
1001 )]
1002 CommandNonUtf8 {
1003 binary_id: RustBinaryId,
1005
1006 command: Vec<String>,
1008
1009 stdout: Vec<u8>,
1011
1012 stderr: Vec<u8>,
1014 },
1015
1016 #[error("for `{binary_id}`, {message}\nfull output:\n{full_output}")]
1018 ParseLine {
1019 binary_id: RustBinaryId,
1021
1022 message: Cow<'static, str>,
1024
1025 full_output: String,
1027 },
1028
1029 #[error(
1031 "error joining dynamic library paths for {}: [{}]",
1032 dylib_path_envvar(),
1033 itertools::join(.new_paths, ", ")
1034 )]
1035 DylibJoinPaths {
1036 new_paths: Vec<Utf8PathBuf>,
1038
1039 #[source]
1041 error: JoinPathsError,
1042 },
1043
1044 #[error("error creating Tokio runtime")]
1046 TokioRuntimeCreate(#[source] std::io::Error),
1047}
1048
1049impl CreateTestListError {
1050 pub(crate) fn parse_line(
1051 binary_id: RustBinaryId,
1052 message: impl Into<Cow<'static, str>>,
1053 full_output: impl Into<String>,
1054 ) -> Self {
1055 Self::ParseLine {
1056 binary_id,
1057 message: message.into(),
1058 full_output: full_output.into(),
1059 }
1060 }
1061
1062 pub(crate) fn dylib_join_paths(new_paths: Vec<Utf8PathBuf>, error: JoinPathsError) -> Self {
1063 Self::DylibJoinPaths { new_paths, error }
1064 }
1065}
1066
1067#[derive(Debug, Error)]
1069#[non_exhaustive]
1070pub enum WriteTestListError {
1071 #[error("error writing to output")]
1073 Io(#[source] std::io::Error),
1074
1075 #[error("error serializing to JSON")]
1077 Json(#[source] serde_json::Error),
1078}
1079
1080#[derive(Debug, Error)]
1084pub enum ConfigureHandleInheritanceError {
1085 #[cfg(windows)]
1087 #[error("error configuring handle inheritance")]
1088 WindowsError(#[from] std::io::Error),
1089}
1090
1091#[derive(Debug, Error)]
1093#[non_exhaustive]
1094pub enum TestRunnerBuildError {
1095 #[error("error creating Tokio runtime")]
1097 TokioRuntimeCreate(#[source] std::io::Error),
1098
1099 #[error("error setting up signals")]
1101 SignalHandlerSetupError(#[from] SignalHandlerSetupError),
1102}
1103
1104#[derive(Debug, Error)]
1106pub struct TestRunnerExecuteErrors<E> {
1107 pub report_error: Option<E>,
1109
1110 pub join_errors: Vec<tokio::task::JoinError>,
1113}
1114
1115impl<E: std::error::Error> fmt::Display for TestRunnerExecuteErrors<E> {
1116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1117 if let Some(report_error) = &self.report_error {
1118 write!(f, "error reporting results: {report_error}")?;
1119 }
1120
1121 if !self.join_errors.is_empty() {
1122 if self.report_error.is_some() {
1123 write!(f, "; ")?;
1124 }
1125
1126 write!(f, "errors joining tasks: ")?;
1127
1128 for (i, join_error) in self.join_errors.iter().enumerate() {
1129 if i > 0 {
1130 write!(f, ", ")?;
1131 }
1132
1133 write!(f, "{join_error}")?;
1134 }
1135 }
1136
1137 Ok(())
1138 }
1139}
1140
1141#[derive(Debug, Error)]
1145#[error(
1146 "could not detect archive format from file name `{file_name}` (supported extensions: {})",
1147 supported_extensions()
1148)]
1149pub struct UnknownArchiveFormat {
1150 pub file_name: String,
1152}
1153
1154fn supported_extensions() -> String {
1155 ArchiveFormat::SUPPORTED_FORMATS
1156 .iter()
1157 .map(|(extension, _)| *extension)
1158 .join(", ")
1159}
1160
1161#[derive(Debug, Error)]
1163#[non_exhaustive]
1164pub enum ArchiveCreateError {
1165 #[error("error creating binary list")]
1167 CreateBinaryList(#[source] WriteTestListError),
1168
1169 #[error("extra path `{}` not found", .redactor.redact_path(path))]
1171 MissingExtraPath {
1172 path: Utf8PathBuf,
1174
1175 redactor: Redactor,
1180 },
1181
1182 #[error("while archiving {step}, error writing {} `{path}` to archive", kind_str(*.is_dir))]
1184 InputFileRead {
1185 step: ArchiveStep,
1187
1188 path: Utf8PathBuf,
1190
1191 is_dir: Option<bool>,
1193
1194 #[source]
1196 error: std::io::Error,
1197 },
1198
1199 #[error("error reading directory entry from `{path}")]
1201 DirEntryRead {
1202 path: Utf8PathBuf,
1204
1205 #[source]
1207 error: std::io::Error,
1208 },
1209
1210 #[error("error writing to archive")]
1212 OutputArchiveIo(#[source] std::io::Error),
1213
1214 #[error("error reporting archive status")]
1216 ReporterIo(#[source] std::io::Error),
1217}
1218
1219fn kind_str(is_dir: Option<bool>) -> &'static str {
1220 match is_dir {
1221 Some(true) => "directory",
1222 Some(false) => "file",
1223 None => "path",
1224 }
1225}
1226
1227#[derive(Debug, Error)]
1229pub enum MetadataMaterializeError {
1230 #[error("I/O error reading metadata file `{path}`")]
1232 Read {
1233 path: Utf8PathBuf,
1235
1236 #[source]
1238 error: std::io::Error,
1239 },
1240
1241 #[error("error deserializing metadata file `{path}`")]
1243 Deserialize {
1244 path: Utf8PathBuf,
1246
1247 #[source]
1249 error: serde_json::Error,
1250 },
1251
1252 #[error("error parsing Rust build metadata from `{path}`")]
1254 RustBuildMeta {
1255 path: Utf8PathBuf,
1257
1258 #[source]
1260 error: RustBuildMetaParseError,
1261 },
1262
1263 #[error("error building package graph from `{path}`")]
1265 PackageGraphConstruct {
1266 path: Utf8PathBuf,
1268
1269 #[source]
1271 error: guppy::Error,
1272 },
1273}
1274
1275#[derive(Debug, Error)]
1279#[non_exhaustive]
1280pub enum ArchiveReadError {
1281 #[error("I/O error reading archive")]
1283 Io(#[source] std::io::Error),
1284
1285 #[error("path in archive `{}` wasn't valid UTF-8", String::from_utf8_lossy(.0))]
1287 NonUtf8Path(Vec<u8>),
1288
1289 #[error("path in archive `{0}` doesn't start with `target/`")]
1291 NoTargetPrefix(Utf8PathBuf),
1292
1293 #[error("path in archive `{path}` contains an invalid component `{component}`")]
1295 InvalidComponent {
1296 path: Utf8PathBuf,
1298
1299 component: String,
1301 },
1302
1303 #[error("corrupted archive: checksum read error for path `{path}`")]
1305 ChecksumRead {
1306 path: Utf8PathBuf,
1308
1309 #[source]
1311 error: std::io::Error,
1312 },
1313
1314 #[error("corrupted archive: invalid checksum for path `{path}`")]
1316 InvalidChecksum {
1317 path: Utf8PathBuf,
1319
1320 expected: u32,
1322
1323 actual: u32,
1325 },
1326
1327 #[error("metadata file `{0}` not found in archive")]
1329 MetadataFileNotFound(&'static Utf8Path),
1330
1331 #[error("error deserializing metadata file `{path}` in archive")]
1333 MetadataDeserializeError {
1334 path: &'static Utf8Path,
1336
1337 #[source]
1339 error: serde_json::Error,
1340 },
1341
1342 #[error("error building package graph from `{path}` in archive")]
1344 PackageGraphConstructError {
1345 path: &'static Utf8Path,
1347
1348 #[source]
1350 error: guppy::Error,
1351 },
1352}
1353
1354#[derive(Debug, Error)]
1358#[non_exhaustive]
1359pub enum ArchiveExtractError {
1360 #[error("error creating temporary directory")]
1362 TempDirCreate(#[source] std::io::Error),
1363
1364 #[error("error canonicalizing destination directory `{dir}`")]
1366 DestDirCanonicalization {
1367 dir: Utf8PathBuf,
1369
1370 #[source]
1372 error: std::io::Error,
1373 },
1374
1375 #[error("destination `{0}` already exists")]
1377 DestinationExists(Utf8PathBuf),
1378
1379 #[error("error reading archive")]
1381 Read(#[source] ArchiveReadError),
1382
1383 #[error("error deserializing Rust build metadata")]
1385 RustBuildMeta(#[from] RustBuildMetaParseError),
1386
1387 #[error("error writing file `{path}` to disk")]
1389 WriteFile {
1390 path: Utf8PathBuf,
1392
1393 #[source]
1395 error: std::io::Error,
1396 },
1397
1398 #[error("error reporting extract status")]
1400 ReporterIo(std::io::Error),
1401}
1402
1403#[derive(Debug, Error)]
1405#[non_exhaustive]
1406pub enum WriteEventError {
1407 #[error("error writing to output")]
1409 Io(#[source] std::io::Error),
1410
1411 #[error("error operating on path {file}")]
1413 Fs {
1414 file: Utf8PathBuf,
1416
1417 #[source]
1419 error: std::io::Error,
1420 },
1421
1422 #[error("error writing JUnit output to {file}")]
1424 Junit {
1425 file: Utf8PathBuf,
1427
1428 #[source]
1430 error: quick_junit::SerializeError,
1431 },
1432}
1433
1434#[derive(Debug, Error)]
1437#[non_exhaustive]
1438pub enum CargoConfigError {
1439 #[error("failed to retrieve current directory")]
1441 GetCurrentDir(#[source] std::io::Error),
1442
1443 #[error("current directory is invalid UTF-8")]
1445 CurrentDirInvalidUtf8(#[source] FromPathBufError),
1446
1447 #[error("failed to parse --config argument `{config_str}` as TOML")]
1449 CliConfigParseError {
1450 config_str: String,
1452
1453 #[source]
1455 error: toml_edit::TomlError,
1456 },
1457
1458 #[error("failed to deserialize --config argument `{config_str}` as TOML")]
1460 CliConfigDeError {
1461 config_str: String,
1463
1464 #[source]
1466 error: toml_edit::de::Error,
1467 },
1468
1469 #[error(
1471 "invalid format for --config argument `{config_str}` (should be a dotted key expression)"
1472 )]
1473 InvalidCliConfig {
1474 config_str: String,
1476
1477 #[source]
1479 reason: InvalidCargoCliConfigReason,
1480 },
1481
1482 #[error("non-UTF-8 path encountered")]
1484 NonUtf8Path(#[source] FromPathBufError),
1485
1486 #[error("failed to retrieve the Cargo home directory")]
1488 GetCargoHome(#[source] std::io::Error),
1489
1490 #[error("failed to canonicalize path `{path}")]
1492 FailedPathCanonicalization {
1493 path: Utf8PathBuf,
1495
1496 #[source]
1498 error: std::io::Error,
1499 },
1500
1501 #[error("failed to read config at `{path}`")]
1503 ConfigReadError {
1504 path: Utf8PathBuf,
1506
1507 #[source]
1509 error: std::io::Error,
1510 },
1511
1512 #[error(transparent)]
1514 ConfigParseError(#[from] Box<CargoConfigParseError>),
1515}
1516
1517#[derive(Debug, Error)]
1521#[error("failed to parse config at `{path}`")]
1522pub struct CargoConfigParseError {
1523 pub path: Utf8PathBuf,
1525
1526 #[source]
1528 pub error: toml::de::Error,
1529}
1530
1531#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)]
1535#[non_exhaustive]
1536pub enum InvalidCargoCliConfigReason {
1537 #[error("was not a TOML dotted key expression (such as `build.jobs = 2`)")]
1539 NotDottedKv,
1540
1541 #[error("includes non-whitespace decoration")]
1543 IncludesNonWhitespaceDecoration,
1544
1545 #[error("sets a value to an inline table, which is not accepted")]
1547 SetsValueToInlineTable,
1548
1549 #[error("sets a value to an array of tables, which is not accepted")]
1551 SetsValueToArrayOfTables,
1552
1553 #[error("doesn't provide a value")]
1555 DoesntProvideValue,
1556}
1557
1558#[derive(Debug, Error)]
1560pub enum HostPlatformDetectError {
1561 #[error(
1564 "error spawning `rustc -vV`, and detecting the build \
1565 target failed as well\n\
1566 - rustc spawn error: {}\n\
1567 - build target error: {}\n",
1568 DisplayErrorChain::new_with_initial_indent(" ", error),
1569 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1570 )]
1571 RustcVvSpawnError {
1572 error: std::io::Error,
1574
1575 build_target_error: Box<target_spec::Error>,
1577 },
1578
1579 #[error(
1582 "`rustc -vV` failed with {}, and detecting the \
1583 build target failed as well\n\
1584 - `rustc -vV` stdout:\n{}\n\
1585 - `rustc -vV` stderr:\n{}\n\
1586 - build target error:\n{}\n",
1587 status,
1588 Indented { item: String::from_utf8_lossy(stdout), indent: " " },
1589 Indented { item: String::from_utf8_lossy(stderr), indent: " " },
1590 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1591 )]
1592 RustcVvFailed {
1593 status: ExitStatus,
1595
1596 stdout: Vec<u8>,
1598
1599 stderr: Vec<u8>,
1601
1602 build_target_error: Box<target_spec::Error>,
1604 },
1605
1606 #[error(
1609 "parsing `rustc -vV` output failed, and detecting the build target \
1610 failed as well\n\
1611 - host platform error:\n{}\n\
1612 - build target error:\n{}\n",
1613 DisplayErrorChain::new_with_initial_indent(" ", host_platform_error),
1614 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1615 )]
1616 HostPlatformParseError {
1617 host_platform_error: Box<target_spec::Error>,
1619
1620 build_target_error: Box<target_spec::Error>,
1622 },
1623
1624 #[error("test-only code, so `rustc -vV` was not called; failed to detect build target")]
1627 BuildTargetError {
1628 #[source]
1630 build_target_error: Box<target_spec::Error>,
1631 },
1632}
1633
1634#[derive(Debug, Error)]
1636pub enum TargetTripleError {
1637 #[error(
1639 "environment variable '{}' contained non-UTF-8 data",
1640 TargetTriple::CARGO_BUILD_TARGET_ENV
1641 )]
1642 InvalidEnvironmentVar,
1643
1644 #[error("error deserializing target triple from {source}")]
1646 TargetSpecError {
1647 source: TargetTripleSource,
1649
1650 #[source]
1652 error: target_spec::Error,
1653 },
1654
1655 #[error("target path `{path}` is not a valid file")]
1657 TargetPathReadError {
1658 source: TargetTripleSource,
1660
1661 path: Utf8PathBuf,
1663
1664 #[source]
1666 error: std::io::Error,
1667 },
1668
1669 #[error(
1671 "for custom platform obtained from {source}, \
1672 failed to create temporary directory for custom platform"
1673 )]
1674 CustomPlatformTempDirError {
1675 source: TargetTripleSource,
1677
1678 #[source]
1680 error: std::io::Error,
1681 },
1682
1683 #[error(
1685 "for custom platform obtained from {source}, \
1686 failed to write JSON to temporary path `{path}`"
1687 )]
1688 CustomPlatformWriteError {
1689 source: TargetTripleSource,
1691
1692 path: Utf8PathBuf,
1694
1695 #[source]
1697 error: std::io::Error,
1698 },
1699
1700 #[error(
1702 "for custom platform obtained from {source}, \
1703 failed to close temporary directory `{dir_path}`"
1704 )]
1705 CustomPlatformCloseError {
1706 source: TargetTripleSource,
1708
1709 dir_path: Utf8PathBuf,
1711
1712 #[source]
1714 error: std::io::Error,
1715 },
1716}
1717
1718impl TargetTripleError {
1719 pub fn source_report(&self) -> Option<miette::Report> {
1724 match self {
1725 Self::TargetSpecError { error, .. } => {
1726 Some(miette::Report::new_boxed(error.clone().into_diagnostic()))
1727 }
1728 TargetTripleError::InvalidEnvironmentVar
1730 | TargetTripleError::TargetPathReadError { .. }
1731 | TargetTripleError::CustomPlatformTempDirError { .. }
1732 | TargetTripleError::CustomPlatformWriteError { .. }
1733 | TargetTripleError::CustomPlatformCloseError { .. } => None,
1734 }
1735 }
1736}
1737
1738#[derive(Debug, Error)]
1740pub enum TargetRunnerError {
1741 #[error("environment variable '{0}' contained non-UTF-8 data")]
1743 InvalidEnvironmentVar(String),
1744
1745 #[error("runner '{key}' = '{value}' did not contain a runner binary")]
1748 BinaryNotSpecified {
1749 key: PlatformRunnerSource,
1751
1752 value: String,
1754 },
1755}
1756
1757#[derive(Debug, Error)]
1759#[error("error setting up signal handler")]
1760pub struct SignalHandlerSetupError(#[from] std::io::Error);
1761
1762#[derive(Debug, Error)]
1764pub enum ShowTestGroupsError {
1765 #[error(
1767 "unknown test groups specified: {}\n(known groups: {})",
1768 unknown_groups.iter().join(", "),
1769 known_groups.iter().join(", "),
1770 )]
1771 UnknownGroups {
1772 unknown_groups: BTreeSet<TestGroup>,
1774
1775 known_groups: BTreeSet<TestGroup>,
1777 },
1778}
1779
1780#[cfg(feature = "self-update")]
1781mod self_update_errors {
1782 use super::*;
1783 use mukti_metadata::ReleaseStatus;
1784 use semver::{Version, VersionReq};
1785
1786 #[cfg(feature = "self-update")]
1790 #[derive(Debug, Error)]
1791 #[non_exhaustive]
1792 pub enum UpdateError {
1793 #[error("failed to read release metadata from `{path}`")]
1795 ReadLocalMetadata {
1796 path: Utf8PathBuf,
1798
1799 #[source]
1801 error: std::io::Error,
1802 },
1803
1804 #[error("self-update failed")]
1806 SelfUpdate(#[source] self_update::errors::Error),
1807
1808 #[error("deserializing release metadata failed")]
1810 ReleaseMetadataDe(#[source] serde_json::Error),
1811
1812 #[error("version `{version}` not found (known versions: {})", known_versions(.known))]
1814 VersionNotFound {
1815 version: Version,
1817
1818 known: Vec<(Version, ReleaseStatus)>,
1820 },
1821
1822 #[error("no version found matching requirement `{req}`")]
1824 NoMatchForVersionReq {
1825 req: VersionReq,
1827 },
1828
1829 #[error("project {not_found} not found in release metadata (known projects: {})", known.join(", "))]
1831 MuktiProjectNotFound {
1832 not_found: String,
1834
1835 known: Vec<String>,
1837 },
1838
1839 #[error(
1841 "for version {version}, no release information found for target `{triple}` \
1842 (known targets: {})",
1843 known_triples.iter().join(", ")
1844 )]
1845 NoTargetData {
1846 version: Version,
1848
1849 triple: String,
1851
1852 known_triples: BTreeSet<String>,
1854 },
1855
1856 #[error("the current executable's path could not be determined")]
1858 CurrentExe(#[source] std::io::Error),
1859
1860 #[error("temporary directory could not be created at `{location}`")]
1862 TempDirCreate {
1863 location: Utf8PathBuf,
1865
1866 #[source]
1868 error: std::io::Error,
1869 },
1870
1871 #[error("temporary archive could not be created at `{archive_path}`")]
1873 TempArchiveCreate {
1874 archive_path: Utf8PathBuf,
1876
1877 #[source]
1879 error: std::io::Error,
1880 },
1881
1882 #[error("error writing to temporary archive at `{archive_path}`")]
1884 TempArchiveWrite {
1885 archive_path: Utf8PathBuf,
1887
1888 #[source]
1890 error: std::io::Error,
1891 },
1892
1893 #[error("error reading from temporary archive at `{archive_path}`")]
1895 TempArchiveRead {
1896 archive_path: Utf8PathBuf,
1898
1899 #[source]
1901 error: std::io::Error,
1902 },
1903
1904 #[error("SHA-256 checksum mismatch: expected: {expected}, actual: {actual}")]
1906 ChecksumMismatch {
1907 expected: String,
1909
1910 actual: String,
1912 },
1913
1914 #[error("error renaming `{source}` to `{dest}`")]
1916 FsRename {
1917 source: Utf8PathBuf,
1919
1920 dest: Utf8PathBuf,
1922
1923 #[source]
1925 error: std::io::Error,
1926 },
1927
1928 #[error("cargo-nextest binary updated, but error running `cargo nextest self setup`")]
1930 SelfSetup(#[source] std::io::Error),
1931 }
1932
1933 fn known_versions(versions: &[(Version, ReleaseStatus)]) -> String {
1934 use std::fmt::Write;
1935
1936 const DISPLAY_COUNT: usize = 4;
1938
1939 let display_versions: Vec<_> = versions
1940 .iter()
1941 .filter(|(v, status)| v.pre.is_empty() && *status == ReleaseStatus::Active)
1942 .map(|(v, _)| v.to_string())
1943 .take(DISPLAY_COUNT)
1944 .collect();
1945 let mut display_str = display_versions.join(", ");
1946 if versions.len() > display_versions.len() {
1947 write!(
1948 display_str,
1949 " and {} others",
1950 versions.len() - display_versions.len()
1951 )
1952 .unwrap();
1953 }
1954
1955 display_str
1956 }
1957
1958 #[cfg(feature = "self-update")]
1959 #[derive(Debug, Error)]
1961 pub enum UpdateVersionParseError {
1962 #[error("version string is empty")]
1964 EmptyString,
1965
1966 #[error(
1968 "`{input}` is not a valid semver requirement\n\
1969 (hint: see https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html for the correct format)"
1970 )]
1971 InvalidVersionReq {
1972 input: String,
1974
1975 #[source]
1977 error: semver::Error,
1978 },
1979
1980 #[error("`{input}` is not a valid semver{}", extra_semver_output(.input))]
1982 InvalidVersion {
1983 input: String,
1985
1986 #[source]
1988 error: semver::Error,
1989 },
1990 }
1991
1992 fn extra_semver_output(input: &str) -> String {
1993 if input.parse::<VersionReq>().is_ok() {
1996 format!(
1997 "\n(if you want to specify a semver range, add an explicit qualifier, like ^{input})"
1998 )
1999 } else {
2000 "".to_owned()
2001 }
2002 }
2003}
2004
2005#[cfg(feature = "self-update")]
2006pub use self_update_errors::*;
2007
2008#[cfg(test)]
2009mod tests {
2010 use super::*;
2011
2012 #[test]
2013 fn display_error_chain() {
2014 let err1 = StringError::new("err1", None);
2015
2016 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&err1)), @"err1");
2017
2018 let err2 = StringError::new("err2", Some(err1));
2019 let err3 = StringError::new("err3\nerr3 line 2", Some(err2));
2020
2021 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&err3)), @r"
2022 err3
2023 err3 line 2
2024 caused by:
2025 - err2
2026 - err1
2027 ");
2028 }
2029
2030 #[test]
2031 fn display_error_list() {
2032 let err1 = StringError::new("err1", None);
2033
2034 let error_list =
2035 ErrorList::<StringError>::new("waiting on the water to boil", vec![err1.clone()])
2036 .expect(">= 1 error");
2037 insta::assert_snapshot!(format!("{}", error_list), @"err1");
2038 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @"err1");
2039
2040 let err2 = StringError::new("err2", Some(err1));
2041 let err3 = StringError::new("err3", Some(err2));
2042
2043 let error_list =
2044 ErrorList::<StringError>::new("waiting on flowers to bloom", vec![err3.clone()])
2045 .expect(">= 1 error");
2046 insta::assert_snapshot!(format!("{}", error_list), @"err3");
2047 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @r"
2048 err3
2049 caused by:
2050 - err2
2051 - err1
2052 ");
2053
2054 let err4 = StringError::new("err4", None);
2055 let err5 = StringError::new("err5", Some(err4));
2056 let err6 = StringError::new("err6\nerr6 line 2", Some(err5));
2057
2058 let error_list = ErrorList::<StringError>::new(
2059 "waiting for the heat death of the universe",
2060 vec![err3, err6],
2061 )
2062 .expect(">= 1 error");
2063
2064 insta::assert_snapshot!(format!("{}", error_list), @r"
2065 2 errors occurred waiting for the heat death of the universe:
2066 * err3
2067 caused by:
2068 - err2
2069 - err1
2070 * err6
2071 err6 line 2
2072 caused by:
2073 - err5
2074 - err4
2075 ");
2076 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @r"
2077 2 errors occurred waiting for the heat death of the universe:
2078 * err3
2079 caused by:
2080 - err2
2081 - err1
2082 * err6
2083 err6 line 2
2084 caused by:
2085 - err5
2086 - err4
2087 ");
2088 }
2089
2090 #[derive(Clone, Debug, Error)]
2091 struct StringError {
2092 message: String,
2093 #[source]
2094 source: Option<Box<StringError>>,
2095 }
2096
2097 impl StringError {
2098 fn new(message: impl Into<String>, source: Option<StringError>) -> Self {
2099 Self {
2100 message: message.into(),
2101 source: source.map(Box::new),
2102 }
2103 }
2104 }
2105
2106 impl fmt::Display for StringError {
2107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2108 write!(f, "{}", self.message)
2109 }
2110 }
2111}