1use crate::{
7 cargo_config::{TargetTriple, TargetTripleSource},
8 config::{
9 core::{ConfigExperimental, ToolName},
10 elements::{CustomTestGroup, TestGroup},
11 scripts::{ProfileScriptType, ScriptId, ScriptType},
12 },
13 helpers::{display_exited_with, dylib_path_envvar, plural},
14 indenter::{DisplayIndented, indented},
15 record::{
16 PortableRecordingFormatVersion, PortableRecordingVersionIncompatibility, RecordedRunInfo,
17 RunIdIndex, RunsJsonFormatVersion, StoreFormatVersion, StoreVersionIncompatibility,
18 },
19 redact::{Redactor, SizeDisplay},
20 reuse_build::{ArchiveFormat, ArchiveStep},
21 target_runner::PlatformRunnerSource,
22};
23use bytesize::ByteSize;
24use camino::{FromPathBufError, Utf8Path, Utf8PathBuf};
25use config::ConfigError;
26use eazip::CompressionMethod;
27use etcetera::HomeDirError;
28use itertools::{Either, Itertools};
29use nextest_filtering::errors::FiltersetParseErrors;
30use nextest_metadata::{RustBinaryId, TestCaseName};
31use quick_junit::ReportUuid;
32use serde::{Deserialize, Serialize};
33use smol_str::SmolStr;
34use std::{
35 borrow::Cow,
36 collections::BTreeSet,
37 env::JoinPathsError,
38 fmt::{self, Write as _},
39 path::PathBuf,
40 process::ExitStatus,
41 sync::Arc,
42};
43use target_spec_miette::IntoMietteDiagnostic;
44use thiserror::Error;
45
46#[derive(Debug, Error)]
48#[error(
49 "failed to parse nextest config at `{config_file}`{}",
50 provided_by_tool(tool.as_ref())
51)]
52#[non_exhaustive]
53pub struct ConfigParseError {
54 config_file: Utf8PathBuf,
55 tool: Option<ToolName>,
56 #[source]
57 kind: ConfigParseErrorKind,
58}
59
60impl ConfigParseError {
61 pub(crate) fn new(
62 config_file: impl Into<Utf8PathBuf>,
63 tool: Option<&ToolName>,
64 kind: ConfigParseErrorKind,
65 ) -> Self {
66 Self {
67 config_file: config_file.into(),
68 tool: tool.cloned(),
69 kind,
70 }
71 }
72
73 pub fn config_file(&self) -> &Utf8Path {
75 &self.config_file
76 }
77
78 pub fn tool(&self) -> Option<&ToolName> {
80 self.tool.as_ref()
81 }
82
83 pub fn kind(&self) -> &ConfigParseErrorKind {
85 &self.kind
86 }
87}
88
89pub fn provided_by_tool(tool: Option<&ToolName>) -> String {
91 match tool {
92 Some(tool) => format!(" provided by tool `{tool}`"),
93 None => String::new(),
94 }
95}
96
97#[derive(Debug, Error)]
101#[non_exhaustive]
102pub enum ConfigParseErrorKind {
103 #[error(transparent)]
105 BuildError(Box<ConfigError>),
106 #[error(transparent)]
108 TomlParseError(Box<toml::de::Error>),
109 #[error(transparent)]
110 DeserializeError(Box<serde_path_to_error::Error<ConfigError>>),
112 #[error(transparent)]
114 VersionOnlyReadError(std::io::Error),
115 #[error(transparent)]
117 VersionOnlyDeserializeError(Box<serde_path_to_error::Error<toml::de::Error>>),
118 #[error("error parsing compiled data (destructure this variant for more details)")]
120 CompileErrors(Vec<ConfigCompileError>),
121 #[error("invalid test groups defined: {}\n(test groups cannot start with '@tool:' unless specified by a tool)", .0.iter().join(", "))]
123 InvalidTestGroupsDefined(BTreeSet<CustomTestGroup>),
124 #[error(
126 "invalid test groups defined by tool: {}\n(test groups must start with '@tool:<tool-name>:')", .0.iter().join(", "))]
127 InvalidTestGroupsDefinedByTool(BTreeSet<CustomTestGroup>),
128 #[error("unknown test groups specified by config (destructure this variant for more details)")]
130 UnknownTestGroups {
131 errors: Vec<UnknownTestGroupError>,
133
134 known_groups: BTreeSet<TestGroup>,
136 },
137 #[error(
139 "both `[script.*]` and `[scripts.*]` defined\n\
140 (hint: [script.*] will be removed in the future: switch to [scripts.setup.*])"
141 )]
142 BothScriptAndScriptsDefined,
143 #[error("invalid config scripts defined: {}\n(config scripts cannot start with '@tool:' unless specified by a tool)", .0.iter().join(", "))]
145 InvalidConfigScriptsDefined(BTreeSet<ScriptId>),
146 #[error(
148 "invalid config scripts defined by tool: {}\n(config scripts must start with '@tool:<tool-name>:')", .0.iter().join(", "))]
149 InvalidConfigScriptsDefinedByTool(BTreeSet<ScriptId>),
150 #[error(
152 "config script names used more than once: {}\n\
153 (config script names must be unique across all script types)", .0.iter().join(", ")
154 )]
155 DuplicateConfigScriptNames(BTreeSet<ScriptId>),
156 #[error(
158 "errors in profile-specific config scripts (destructure this variant for more details)"
159 )]
160 ProfileScriptErrors {
161 errors: Box<ProfileScriptErrors>,
163
164 known_scripts: BTreeSet<ScriptId>,
166 },
167 #[error("unknown experimental features defined (destructure this variant for more details)")]
169 UnknownExperimentalFeatures {
170 unknown: BTreeSet<String>,
172
173 known: BTreeSet<ConfigExperimental>,
175 },
176 #[error(
180 "tool config file specifies experimental features `{}` \
181 -- only repository config files can do so",
182 .features.iter().join(", "),
183 )]
184 ExperimentalFeaturesInToolConfig {
185 features: BTreeSet<String>,
187 },
188 #[error("experimental features used but not enabled: {}", .missing_features.iter().join(", "))]
190 ExperimentalFeaturesNotEnabled {
191 missing_features: BTreeSet<ConfigExperimental>,
193 },
194 #[error("inheritance error(s) detected: {}", .0.iter().join(", "))]
196 InheritanceErrors(Vec<InheritsError>),
197}
198
199#[derive(Debug)]
202#[non_exhaustive]
203pub struct ConfigCompileError {
204 pub profile_name: String,
206
207 pub section: ConfigCompileSection,
209
210 pub kind: ConfigCompileErrorKind,
212}
213
214#[derive(Debug)]
217pub enum ConfigCompileSection {
218 DefaultFilter,
220
221 Override(usize),
223
224 Script(usize),
226}
227
228#[derive(Debug)]
230#[non_exhaustive]
231pub enum ConfigCompileErrorKind {
232 ConstraintsNotSpecified {
234 default_filter_specified: bool,
239 },
240
241 FilterAndDefaultFilterSpecified,
245
246 Parse {
248 host_parse_error: Option<target_spec::Error>,
250
251 target_parse_error: Option<target_spec::Error>,
253
254 filter_parse_errors: Vec<FiltersetParseErrors>,
256 },
257}
258
259impl ConfigCompileErrorKind {
260 pub fn reports(&self) -> impl Iterator<Item = miette::Report> + '_ {
262 match self {
263 Self::ConstraintsNotSpecified {
264 default_filter_specified,
265 } => {
266 let message = if *default_filter_specified {
267 "for override with `default-filter`, `platform` must also be specified"
268 } else {
269 "at least one of `platform` and `filter` must be specified"
270 };
271 Either::Left(std::iter::once(miette::Report::msg(message)))
272 }
273 Self::FilterAndDefaultFilterSpecified => {
274 Either::Left(std::iter::once(miette::Report::msg(
275 "at most one of `filter` and `default-filter` must be specified",
276 )))
277 }
278 Self::Parse {
279 host_parse_error,
280 target_parse_error,
281 filter_parse_errors,
282 } => {
283 let host_parse_report = host_parse_error
284 .as_ref()
285 .map(|error| miette::Report::new_boxed(error.clone().into_diagnostic()));
286 let target_parse_report = target_parse_error
287 .as_ref()
288 .map(|error| miette::Report::new_boxed(error.clone().into_diagnostic()));
289 let filter_parse_reports =
290 filter_parse_errors.iter().flat_map(|filter_parse_errors| {
291 filter_parse_errors.errors.iter().map(|single_error| {
292 miette::Report::new(single_error.clone())
293 .with_source_code(filter_parse_errors.input.to_owned())
294 })
295 });
296
297 Either::Right(
298 host_parse_report
299 .into_iter()
300 .chain(target_parse_report)
301 .chain(filter_parse_reports),
302 )
303 }
304 }
305 }
306}
307
308#[derive(Clone, Debug, Error)]
310#[error("test priority ({priority}) out of range: must be between -100 and 100, both inclusive")]
311pub struct TestPriorityOutOfRange {
312 pub priority: i8,
314}
315
316#[derive(Clone, Debug, Error)]
318pub enum ChildStartError {
319 #[error("error creating temporary path for setup script")]
321 TempPath(#[source] Arc<std::io::Error>),
322
323 #[error("error spawning child process")]
325 Spawn(#[source] Arc<std::io::Error>),
326}
327
328#[derive(Clone, Debug, Error)]
330pub enum SetupScriptOutputError {
331 #[error("error opening environment file `{path}`")]
333 EnvFileOpen {
334 path: Utf8PathBuf,
336
337 #[source]
339 error: Arc<std::io::Error>,
340 },
341
342 #[error("error reading environment file `{path}`")]
344 EnvFileRead {
345 path: Utf8PathBuf,
347
348 #[source]
350 error: Arc<std::io::Error>,
351 },
352
353 #[error("line `{line}` in environment file `{path}` not in KEY=VALUE format")]
355 EnvFileParse {
356 path: Utf8PathBuf,
358 line: String,
360 },
361
362 #[error("error in environment file `{path}`")]
364 EnvFileInvalidKey {
365 path: Utf8PathBuf,
367
368 #[source]
370 error: EnvVarError,
371 },
372}
373
374#[derive(Clone, Debug, Error)]
376pub enum EnvVarError {
377 #[error("key `{key}` begins with `NEXTEST`, which is reserved for internal use")]
379 ReservedKey {
380 key: String,
382 },
383
384 #[error("key `{key}` does not consist solely of letters, digits, and underscores")]
387 InvalidKey {
388 key: String,
390 },
391
392 #[error("key `{key}` does not start with a letter or underscore")]
394 InvalidKeyStartChar {
395 key: String,
397 },
398}
399
400impl serde::de::Expected for EnvVarError {
404 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
405 f.write_str(match self {
406 Self::ReservedKey { .. } => {
407 "a key that does not begin with `NEXTEST`, which is reserved for internal use"
408 }
409 Self::InvalidKey { .. } => {
410 "a key that consists solely of letters, digits, and underscores"
411 }
412 Self::InvalidKeyStartChar { .. } => "a key that starts with a letter or underscore",
413 })
414 }
415}
416
417#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
422pub struct ErrorList<T> {
423 description: Cow<'static, str>,
425 inner: Vec<T>,
427}
428
429impl<T: std::error::Error> ErrorList<T> {
430 pub(crate) fn new<U>(description: &'static str, errors: Vec<U>) -> Option<Self>
431 where
432 T: From<U>,
433 {
434 if errors.is_empty() {
435 None
436 } else {
437 Some(Self {
438 description: Cow::Borrowed(description),
439 inner: errors.into_iter().map(T::from).collect(),
440 })
441 }
442 }
443
444 pub(crate) fn short_message(&self) -> String {
446 let string = self.to_string();
447 match string.lines().next() {
448 Some(first_line) => first_line.trim_end_matches(':').to_string(),
450 None => String::new(),
451 }
452 }
453
454 pub fn description(&self) -> &str {
456 &self.description
457 }
458
459 pub fn iter(&self) -> impl Iterator<Item = &T> {
461 self.inner.iter()
462 }
463
464 pub fn map<U, F>(self, f: F) -> ErrorList<U>
466 where
467 U: std::error::Error,
468 F: FnMut(T) -> U,
469 {
470 ErrorList {
471 description: self.description,
472 inner: self.inner.into_iter().map(f).collect(),
473 }
474 }
475}
476
477impl<T: std::error::Error> IntoIterator for ErrorList<T> {
478 type Item = T;
479 type IntoIter = std::vec::IntoIter<T>;
480
481 fn into_iter(self) -> Self::IntoIter {
482 self.inner.into_iter()
483 }
484}
485
486impl<T: std::error::Error> fmt::Display for ErrorList<T> {
487 fn fmt(&self, mut f: &mut fmt::Formatter) -> fmt::Result {
488 if self.inner.len() == 1 {
490 return write!(f, "{}", self.inner[0]);
491 }
492
493 writeln!(
495 f,
496 "{} errors occurred {}:",
497 self.inner.len(),
498 self.description,
499 )?;
500 for error in &self.inner {
501 let mut indent = indented(f).with_str(" ").skip_initial();
502 writeln!(indent, "* {}", DisplayErrorChain::new(error))?;
503 f = indent.into_inner();
504 }
505 Ok(())
506 }
507}
508
509#[cfg(test)]
510impl<T: proptest::arbitrary::Arbitrary + std::fmt::Debug + 'static> proptest::arbitrary::Arbitrary
511 for ErrorList<T>
512{
513 type Parameters = ();
514 type Strategy = proptest::strategy::BoxedStrategy<Self>;
515
516 fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
517 use proptest::prelude::*;
518
519 proptest::collection::vec(any::<T>(), 1..=5)
521 .prop_map(|inner| ErrorList {
522 description: Cow::Borrowed("test errors"),
523 inner,
524 })
525 .boxed()
526 }
527}
528
529impl<T: std::error::Error> std::error::Error for ErrorList<T> {
530 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
531 if self.inner.len() == 1 {
532 self.inner[0].source()
533 } else {
534 None
537 }
538 }
539}
540
541pub struct DisplayErrorChain<E> {
546 error: E,
547 initial_indent: &'static str,
548}
549
550impl<E: std::error::Error> DisplayErrorChain<E> {
551 pub fn new(error: E) -> Self {
553 Self {
554 error,
555 initial_indent: "",
556 }
557 }
558
559 pub fn new_with_initial_indent(initial_indent: &'static str, error: E) -> Self {
561 Self {
562 error,
563 initial_indent,
564 }
565 }
566}
567
568impl<E> fmt::Display for DisplayErrorChain<E>
569where
570 E: std::error::Error,
571{
572 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
573 let mut writer = indented(f).with_str(self.initial_indent);
574 write!(writer, "{}", self.error)?;
575
576 let Some(mut cause) = self.error.source() else {
577 return Ok(());
578 };
579
580 write!(writer, "\n caused by:")?;
581
582 loop {
583 writeln!(writer)?;
584 let mut indent = indented(&mut writer).with_str(" ").skip_initial();
586 write!(indent, " - {cause}")?;
587
588 let Some(next_cause) = cause.source() else {
589 break Ok(());
590 };
591
592 cause = next_cause;
593 }
594 }
595}
596
597#[derive(Clone, Debug, Error)]
599pub enum ChildError {
600 #[error(transparent)]
602 Fd(#[from] ChildFdError),
603
604 #[error(transparent)]
606 SetupScriptOutput(#[from] SetupScriptOutputError),
607}
608
609#[derive(Clone, Debug, Error)]
611pub enum ChildFdError {
612 #[error("error reading standard output")]
614 ReadStdout(#[source] Arc<std::io::Error>),
615
616 #[error("error reading standard error")]
618 ReadStderr(#[source] Arc<std::io::Error>),
619
620 #[error("error reading combined stream")]
622 ReadCombined(#[source] Arc<std::io::Error>),
623
624 #[error("error waiting for child process to exit")]
626 Wait(#[source] Arc<std::io::Error>),
627}
628
629#[derive(Clone, Debug, Eq, PartialEq)]
631#[non_exhaustive]
632pub struct UnknownTestGroupError {
633 pub profile_name: String,
635
636 pub name: TestGroup,
638}
639
640#[derive(Clone, Debug, Eq, PartialEq)]
643pub struct ProfileUnknownScriptError {
644 pub profile_name: String,
646
647 pub name: ScriptId,
649}
650
651#[derive(Clone, Debug, Eq, PartialEq)]
654pub struct ProfileWrongConfigScriptTypeError {
655 pub profile_name: String,
657
658 pub name: ScriptId,
660
661 pub attempted: ProfileScriptType,
663
664 pub actual: ScriptType,
666}
667
668#[derive(Clone, Debug, Eq, PartialEq)]
671pub struct ProfileListScriptUsesRunFiltersError {
672 pub profile_name: String,
674
675 pub name: ScriptId,
677
678 pub script_type: ProfileScriptType,
680
681 pub filters: BTreeSet<String>,
683}
684
685#[derive(Clone, Debug, Default)]
687pub struct ProfileScriptErrors {
688 pub unknown_scripts: Vec<ProfileUnknownScriptError>,
690
691 pub wrong_script_types: Vec<ProfileWrongConfigScriptTypeError>,
693
694 pub list_scripts_using_run_filters: Vec<ProfileListScriptUsesRunFiltersError>,
696}
697
698impl ProfileScriptErrors {
699 pub fn is_empty(&self) -> bool {
701 self.unknown_scripts.is_empty()
702 && self.wrong_script_types.is_empty()
703 && self.list_scripts_using_run_filters.is_empty()
704 }
705}
706
707#[derive(Clone, Debug, Error)]
709#[error("profile `{profile}` not found (known profiles: {})", .all_profiles.join(", "))]
710pub struct ProfileNotFound {
711 profile: String,
712 all_profiles: Vec<String>,
713}
714
715impl ProfileNotFound {
716 pub(crate) fn new(
717 profile: impl Into<String>,
718 all_profiles: impl IntoIterator<Item = impl Into<String>>,
719 ) -> Self {
720 let mut all_profiles: Vec<_> = all_profiles.into_iter().map(|s| s.into()).collect();
721 all_profiles.sort_unstable();
722 Self {
723 profile: profile.into(),
724 all_profiles,
725 }
726 }
727}
728
729#[derive(Clone, Debug, Error, Eq, PartialEq)]
731pub enum InvalidIdentifier {
732 #[error("identifier is empty")]
734 Empty,
735
736 #[error("invalid identifier `{0}`")]
738 InvalidXid(SmolStr),
739
740 #[error("tool identifier not of the form \"@tool:tool-name:identifier\": `{0}`")]
742 ToolIdentifierInvalidFormat(SmolStr),
743
744 #[error("tool identifier has empty component: `{0}`")]
746 ToolComponentEmpty(SmolStr),
747
748 #[error("invalid tool identifier `{0}`")]
750 ToolIdentifierInvalidXid(SmolStr),
751}
752
753#[derive(Clone, Debug, Error, Eq, PartialEq)]
755pub enum InvalidToolName {
756 #[error("tool name is empty")]
758 Empty,
759
760 #[error("invalid tool name `{0}`")]
762 InvalidXid(SmolStr),
763
764 #[error("tool name cannot start with \"@tool\": `{0}`")]
766 StartsWithToolPrefix(SmolStr),
767}
768
769#[derive(Clone, Debug, Error)]
771#[error("invalid custom test group name: {0}")]
772pub struct InvalidCustomTestGroupName(pub InvalidIdentifier);
773
774#[derive(Clone, Debug, Error)]
776#[error("invalid configuration script name: {0}")]
777pub struct InvalidConfigScriptName(pub InvalidIdentifier);
778
779#[derive(Clone, Debug, Error, PartialEq, Eq)]
781pub enum ToolConfigFileParseError {
782 #[error(
783 "tool-config-file has invalid format: {input}\n(hint: tool configs must be in the format <tool-name>:<path>)"
784 )]
785 InvalidFormat {
787 input: String,
789 },
790
791 #[error("tool-config-file has invalid tool name: {input}")]
793 InvalidToolName {
794 input: String,
796
797 #[source]
799 error: InvalidToolName,
800 },
801
802 #[error("tool-config-file has empty config file path: {input}")]
804 EmptyConfigFile {
805 input: String,
807 },
808
809 #[error("tool-config-file is not an absolute path: {config_file}")]
811 ConfigFileNotAbsolute {
812 config_file: Utf8PathBuf,
814 },
815}
816
817#[derive(Debug, Error)]
819#[non_exhaustive]
820pub enum UserConfigError {
821 #[error("user config file not found at {path}")]
824 FileNotFound {
825 path: Utf8PathBuf,
827 },
828
829 #[error("failed to read user config at {path}")]
831 Read {
832 path: Utf8PathBuf,
834 #[source]
836 error: std::io::Error,
837 },
838
839 #[error("failed to parse user config at {path}")]
841 Parse {
842 path: Utf8PathBuf,
844 #[source]
846 error: toml::de::Error,
847 },
848
849 #[error("user config path contains non-UTF-8 characters")]
851 NonUtf8Path {
852 #[source]
854 error: FromPathBufError,
855 },
856
857 #[error(
859 "for user config at {path}, failed to compile platform spec in [[overrides]] at index {index}"
860 )]
861 OverridePlatformSpec {
862 path: Utf8PathBuf,
864 index: usize,
866 #[source]
868 error: target_spec::Error,
869 },
870}
871
872#[derive(Clone, Debug, Error)]
874#[error("unrecognized value for max-fail: {reason}")]
875pub struct MaxFailParseError {
876 pub reason: String,
878}
879
880impl MaxFailParseError {
881 pub(crate) fn new(reason: impl Into<String>) -> Self {
882 Self {
883 reason: reason.into(),
884 }
885 }
886}
887
888#[derive(Clone, Debug, Error)]
890#[error(
891 "unrecognized value for stress-count: {input}\n\
892 (hint: expected either a positive integer or \"infinite\")"
893)]
894pub struct StressCountParseError {
895 pub input: String,
897}
898
899impl StressCountParseError {
900 pub(crate) fn new(input: impl Into<String>) -> Self {
901 Self {
902 input: input.into(),
903 }
904 }
905}
906
907#[derive(Clone, Debug, Error)]
909#[non_exhaustive]
910pub enum DebuggerCommandParseError {
911 #[error(transparent)]
913 ShellWordsParse(shell_words::ParseError),
914
915 #[error("debugger command cannot be empty")]
917 EmptyCommand,
918}
919
920#[derive(Clone, Debug, Error)]
922#[non_exhaustive]
923pub enum TracerCommandParseError {
924 #[error(transparent)]
926 ShellWordsParse(shell_words::ParseError),
927
928 #[error("tracer command cannot be empty")]
930 EmptyCommand,
931}
932
933#[derive(Clone, Debug, Error)]
935#[error(
936 "unrecognized value for test-threads: {input}\n(hint: expected either an integer or \"num-cpus\")"
937)]
938pub struct TestThreadsParseError {
939 pub input: String,
941}
942
943impl TestThreadsParseError {
944 pub(crate) fn new(input: impl Into<String>) -> Self {
945 Self {
946 input: input.into(),
947 }
948 }
949}
950
951#[derive(Clone, Debug, Error)]
954pub struct PartitionerBuilderParseError {
955 expected_format: Option<&'static str>,
956 message: Cow<'static, str>,
957}
958
959impl PartitionerBuilderParseError {
960 pub(crate) fn new(
961 expected_format: Option<&'static str>,
962 message: impl Into<Cow<'static, str>>,
963 ) -> Self {
964 Self {
965 expected_format,
966 message: message.into(),
967 }
968 }
969}
970
971impl fmt::Display for PartitionerBuilderParseError {
972 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
973 match self.expected_format {
974 Some(format) => {
975 write!(
976 f,
977 "partition must be in the format \"{}\":\n{}",
978 format, self.message
979 )
980 }
981 None => write!(f, "{}", self.message),
982 }
983 }
984}
985
986#[derive(Clone, Debug, Error)]
989pub enum TestFilterBuildError {
990 #[error("error constructing test filters")]
992 Construct {
993 #[from]
995 error: aho_corasick::BuildError,
996 },
997}
998
999#[derive(Debug, Error)]
1001pub enum PathMapperConstructError {
1002 #[error("{kind} `{input}` failed to canonicalize")]
1004 Canonicalization {
1005 kind: PathMapperConstructKind,
1007
1008 input: Utf8PathBuf,
1010
1011 #[source]
1013 err: std::io::Error,
1014 },
1015 #[error("{kind} `{input}` canonicalized to a non-UTF-8 path")]
1017 NonUtf8Path {
1018 kind: PathMapperConstructKind,
1020
1021 input: Utf8PathBuf,
1023
1024 #[source]
1026 err: FromPathBufError,
1027 },
1028 #[error("{kind} `{canonicalized_path}` is not a directory")]
1030 NotADirectory {
1031 kind: PathMapperConstructKind,
1033
1034 input: Utf8PathBuf,
1036
1037 canonicalized_path: Utf8PathBuf,
1039 },
1040}
1041
1042impl PathMapperConstructError {
1043 pub fn kind(&self) -> PathMapperConstructKind {
1045 match self {
1046 Self::Canonicalization { kind, .. }
1047 | Self::NonUtf8Path { kind, .. }
1048 | Self::NotADirectory { kind, .. } => *kind,
1049 }
1050 }
1051
1052 pub fn input(&self) -> &Utf8Path {
1054 match self {
1055 Self::Canonicalization { input, .. }
1056 | Self::NonUtf8Path { input, .. }
1057 | Self::NotADirectory { input, .. } => input,
1058 }
1059 }
1060}
1061
1062#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1067pub enum PathMapperConstructKind {
1068 WorkspaceRoot,
1070
1071 TargetDir,
1073
1074 BuildDir,
1076}
1077
1078impl fmt::Display for PathMapperConstructKind {
1079 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1080 match self {
1081 Self::WorkspaceRoot => write!(f, "remapped workspace root"),
1082 Self::TargetDir => write!(f, "remapped target directory"),
1083 Self::BuildDir => write!(f, "remapped build directory"),
1084 }
1085 }
1086}
1087
1088#[derive(Debug, Error)]
1090pub enum RustBuildMetaParseError {
1091 #[error("error deserializing platform from build metadata")]
1093 PlatformDeserializeError(#[from] target_spec::Error),
1094
1095 #[error("the host platform could not be determined")]
1097 DetectBuildTargetError(#[source] target_spec::Error),
1098
1099 #[error("unsupported features in the build metadata: {message}")]
1101 Unsupported {
1102 message: String,
1104 },
1105}
1106
1107#[derive(Clone, Debug, thiserror::Error)]
1110#[error("invalid format version: {input}")]
1111pub struct FormatVersionError {
1112 pub input: String,
1114 #[source]
1116 pub error: FormatVersionErrorInner,
1117}
1118
1119#[derive(Clone, Debug, thiserror::Error)]
1121pub enum FormatVersionErrorInner {
1122 #[error("expected format version in form of `{expected}`")]
1124 InvalidFormat {
1125 expected: &'static str,
1127 },
1128 #[error("version component `{which}` could not be parsed as an integer")]
1130 InvalidInteger {
1131 which: &'static str,
1133 #[source]
1135 err: std::num::ParseIntError,
1136 },
1137 #[error("version component `{which}` value {value} is out of range {range:?}")]
1139 InvalidValue {
1140 which: &'static str,
1142 value: u8,
1144 range: std::ops::Range<u8>,
1146 },
1147}
1148
1149#[derive(Debug, Error)]
1153#[non_exhaustive]
1154pub enum FromMessagesError {
1155 #[error("error reading Cargo JSON messages")]
1157 ReadMessages(#[source] std::io::Error),
1158
1159 #[error("error querying package graph")]
1161 PackageGraph(#[source] guppy::Error),
1162
1163 #[error("missing kind for target {binary_name} in package {package_name}")]
1165 MissingTargetKind {
1166 package_name: String,
1168 binary_name: String,
1170 },
1171}
1172
1173#[derive(Debug, Error)]
1175#[non_exhaustive]
1176pub enum CreateTestListError {
1177 #[error(
1179 "for `{binary_id}`, current directory `{cwd}` is not a directory\n\
1180 (hint: ensure project source is available at this location)"
1181 )]
1182 CwdIsNotDir {
1183 binary_id: RustBinaryId,
1185
1186 cwd: Utf8PathBuf,
1188 },
1189
1190 #[error(
1192 "for `{binary_id}`, running command `{}` failed to execute",
1193 shell_words::join(command)
1194 )]
1195 CommandExecFail {
1196 binary_id: RustBinaryId,
1198
1199 command: Vec<String>,
1201
1202 #[source]
1204 error: std::io::Error,
1205 },
1206
1207 #[error(
1209 "for `{binary_id}`, command `{}` {}\n--- stdout:\n{}\n--- stderr:\n{}\n---",
1210 shell_words::join(command),
1211 display_exited_with(*exit_status),
1212 String::from_utf8_lossy(stdout),
1213 String::from_utf8_lossy(stderr),
1214 )]
1215 CommandFail {
1216 binary_id: RustBinaryId,
1218
1219 command: Vec<String>,
1221
1222 exit_status: ExitStatus,
1224
1225 stdout: Vec<u8>,
1227
1228 stderr: Vec<u8>,
1230 },
1231
1232 #[error(
1234 "for `{binary_id}`, command `{}` produced non-UTF-8 output:\n--- stdout:\n{}\n--- stderr:\n{}\n---",
1235 shell_words::join(command),
1236 String::from_utf8_lossy(stdout),
1237 String::from_utf8_lossy(stderr)
1238 )]
1239 CommandNonUtf8 {
1240 binary_id: RustBinaryId,
1242
1243 command: Vec<String>,
1245
1246 stdout: Vec<u8>,
1248
1249 stderr: Vec<u8>,
1251 },
1252
1253 #[error("for `{binary_id}`, {message}\nfull output:\n{full_output}")]
1255 ParseLine {
1256 binary_id: RustBinaryId,
1258
1259 message: Cow<'static, str>,
1261
1262 full_output: String,
1264 },
1265
1266 #[error(
1268 "error joining dynamic library paths for {}: [{}]",
1269 dylib_path_envvar(),
1270 itertools::join(.new_paths, ", ")
1271 )]
1272 DylibJoinPaths {
1273 new_paths: Vec<Utf8PathBuf>,
1275
1276 #[source]
1278 error: JoinPathsError,
1279 },
1280
1281 #[error("error creating Tokio runtime")]
1283 TokioRuntimeCreate(#[source] std::io::Error),
1284}
1285
1286impl CreateTestListError {
1287 pub(crate) fn parse_line(
1288 binary_id: RustBinaryId,
1289 message: impl Into<Cow<'static, str>>,
1290 full_output: impl Into<String>,
1291 ) -> Self {
1292 Self::ParseLine {
1293 binary_id,
1294 message: message.into(),
1295 full_output: full_output.into(),
1296 }
1297 }
1298
1299 pub(crate) fn dylib_join_paths(new_paths: Vec<Utf8PathBuf>, error: JoinPathsError) -> Self {
1300 Self::DylibJoinPaths { new_paths, error }
1301 }
1302}
1303
1304#[derive(Debug, Error)]
1306#[non_exhaustive]
1307pub enum WriteTestListError {
1308 #[error("error writing to output")]
1310 Io(#[source] std::io::Error),
1311
1312 #[error("error serializing to JSON")]
1314 Json(#[source] serde_json::Error),
1315}
1316
1317#[derive(Debug, Error)]
1321pub enum ConfigureHandleInheritanceError {
1322 #[cfg(windows)]
1324 #[error("error configuring handle inheritance")]
1325 WindowsError(#[from] std::io::Error),
1326}
1327
1328#[derive(Debug, Error)]
1330#[non_exhaustive]
1331pub enum TestRunnerBuildError {
1332 #[error("error creating Tokio runtime")]
1334 TokioRuntimeCreate(#[source] std::io::Error),
1335
1336 #[error("error setting up signals")]
1338 SignalHandlerSetupError(#[from] SignalHandlerSetupError),
1339}
1340
1341#[derive(Debug, Error)]
1343pub struct TestRunnerExecuteErrors<E> {
1344 pub report_error: Option<E>,
1346
1347 pub join_errors: Vec<tokio::task::JoinError>,
1350}
1351
1352impl<E: std::error::Error> fmt::Display for TestRunnerExecuteErrors<E> {
1353 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1354 if let Some(report_error) = &self.report_error {
1355 write!(f, "error reporting results: {report_error}")?;
1356 }
1357
1358 if !self.join_errors.is_empty() {
1359 if self.report_error.is_some() {
1360 write!(f, "; ")?;
1361 }
1362
1363 write!(f, "errors joining tasks: ")?;
1364
1365 for (i, join_error) in self.join_errors.iter().enumerate() {
1366 if i > 0 {
1367 write!(f, ", ")?;
1368 }
1369
1370 write!(f, "{join_error}")?;
1371 }
1372 }
1373
1374 Ok(())
1375 }
1376}
1377
1378#[derive(Debug, Error)]
1382#[error(
1383 "could not detect archive format from file name `{file_name}` (supported extensions: {})",
1384 supported_extensions()
1385)]
1386pub struct UnknownArchiveFormat {
1387 pub file_name: String,
1389}
1390
1391fn supported_extensions() -> String {
1392 ArchiveFormat::SUPPORTED_FORMATS
1393 .iter()
1394 .map(|(extension, _)| *extension)
1395 .join(", ")
1396}
1397
1398#[derive(Debug, Error)]
1400#[non_exhaustive]
1401pub enum ArchiveCreateError {
1402 #[error("error creating binary list")]
1404 CreateBinaryList(#[source] WriteTestListError),
1405
1406 #[error("extra path `{}` not found", .redactor.redact_path(path))]
1408 MissingExtraPath {
1409 path: Utf8PathBuf,
1411
1412 redactor: Redactor,
1417 },
1418
1419 #[error("while archiving {step}, error writing {} `{path}` to archive", kind_str(*.is_dir))]
1421 InputFileRead {
1422 step: ArchiveStep,
1424
1425 path: Utf8PathBuf,
1427
1428 is_dir: Option<bool>,
1430
1431 #[source]
1433 error: std::io::Error,
1434 },
1435
1436 #[error("error reading directory entry from `{path}")]
1438 DirEntryRead {
1439 path: Utf8PathBuf,
1441
1442 #[source]
1444 error: std::io::Error,
1445 },
1446
1447 #[error("error writing to archive")]
1449 OutputArchiveIo(#[source] std::io::Error),
1450
1451 #[error("error reporting archive status")]
1453 ReporterIo(#[source] std::io::Error),
1454}
1455
1456fn kind_str(is_dir: Option<bool>) -> &'static str {
1457 match is_dir {
1458 Some(true) => "directory",
1459 Some(false) => "file",
1460 None => "path",
1461 }
1462}
1463
1464#[derive(Debug, Error)]
1466pub enum MetadataMaterializeError {
1467 #[error("I/O error reading metadata file `{path}`")]
1469 Read {
1470 path: Utf8PathBuf,
1472
1473 #[source]
1475 error: std::io::Error,
1476 },
1477
1478 #[error("error deserializing metadata file `{path}`")]
1480 Deserialize {
1481 path: Utf8PathBuf,
1483
1484 #[source]
1486 error: serde_json::Error,
1487 },
1488
1489 #[error("error parsing Rust build metadata from `{path}`")]
1491 RustBuildMeta {
1492 path: Utf8PathBuf,
1494
1495 #[source]
1497 error: RustBuildMetaParseError,
1498 },
1499
1500 #[error("error building package graph from `{path}`")]
1502 PackageGraphConstruct {
1503 path: Utf8PathBuf,
1505
1506 #[source]
1508 error: guppy::Error,
1509 },
1510}
1511
1512#[derive(Debug, Error)]
1516#[non_exhaustive]
1517pub enum ArchiveReadError {
1518 #[error("I/O error reading archive")]
1520 Io(#[source] std::io::Error),
1521
1522 #[error("path in archive `{}` wasn't valid UTF-8", String::from_utf8_lossy(.0))]
1524 NonUtf8Path(Vec<u8>),
1525
1526 #[error("path in archive `{0}` doesn't start with `target/`")]
1528 NoTargetPrefix(Utf8PathBuf),
1529
1530 #[error("path in archive `{path}` contains an invalid component `{component}`")]
1532 InvalidComponent {
1533 path: Utf8PathBuf,
1535
1536 component: String,
1538 },
1539
1540 #[error("corrupted archive: checksum read error for path `{path}`")]
1542 ChecksumRead {
1543 path: Utf8PathBuf,
1545
1546 #[source]
1548 error: std::io::Error,
1549 },
1550
1551 #[error("corrupted archive: invalid checksum for path `{path}`")]
1553 InvalidChecksum {
1554 path: Utf8PathBuf,
1556
1557 expected: u32,
1559
1560 actual: u32,
1562 },
1563
1564 #[error("metadata file `{0}` not found in archive")]
1566 MetadataFileNotFound(&'static Utf8Path),
1567
1568 #[error("error deserializing metadata file `{path}` in archive")]
1570 MetadataDeserializeError {
1571 path: &'static Utf8Path,
1573
1574 #[source]
1576 error: serde_json::Error,
1577 },
1578
1579 #[error("error building package graph from `{path}` in archive")]
1581 PackageGraphConstructError {
1582 path: &'static Utf8Path,
1584
1585 #[source]
1587 error: guppy::Error,
1588 },
1589}
1590
1591#[derive(Debug, Error)]
1595#[non_exhaustive]
1596pub enum ArchiveExtractError {
1597 #[error("error creating temporary directory")]
1599 TempDirCreate(#[source] std::io::Error),
1600
1601 #[error("error canonicalizing destination directory `{dir}`")]
1603 DestDirCanonicalization {
1604 dir: Utf8PathBuf,
1606
1607 #[source]
1609 error: std::io::Error,
1610 },
1611
1612 #[error("destination `{0}` already exists")]
1614 DestinationExists(Utf8PathBuf),
1615
1616 #[error("error reading archive")]
1618 Read(#[source] ArchiveReadError),
1619
1620 #[error("error deserializing Rust build metadata")]
1622 RustBuildMeta(#[from] RustBuildMetaParseError),
1623
1624 #[error("error writing file `{path}` to disk")]
1626 WriteFile {
1627 path: Utf8PathBuf,
1629
1630 #[source]
1632 error: std::io::Error,
1633 },
1634
1635 #[error("error reporting extract status")]
1637 ReporterIo(std::io::Error),
1638}
1639
1640#[derive(Debug, Error)]
1642#[non_exhaustive]
1643pub enum WriteEventError {
1644 #[error("error writing to output")]
1646 Io(#[source] std::io::Error),
1647
1648 #[error("error operating on path {file}")]
1650 Fs {
1651 file: Utf8PathBuf,
1653
1654 #[source]
1656 error: std::io::Error,
1657 },
1658
1659 #[error("error writing JUnit output to {file}")]
1661 Junit {
1662 file: Utf8PathBuf,
1664
1665 #[source]
1667 error: quick_junit::SerializeError,
1668 },
1669}
1670
1671#[derive(Debug, Error)]
1674#[non_exhaustive]
1675pub enum CargoConfigError {
1676 #[error("failed to retrieve current directory")]
1678 GetCurrentDir(#[source] std::io::Error),
1679
1680 #[error("current directory is invalid UTF-8")]
1682 CurrentDirInvalidUtf8(#[source] FromPathBufError),
1683
1684 #[error("failed to parse --config argument `{config_str}` as TOML")]
1686 CliConfigParseError {
1687 config_str: String,
1689
1690 #[source]
1692 error: toml_edit::TomlError,
1693 },
1694
1695 #[error("failed to deserialize --config argument `{config_str}` as TOML")]
1697 CliConfigDeError {
1698 config_str: String,
1700
1701 #[source]
1703 error: toml_edit::de::Error,
1704 },
1705
1706 #[error(
1708 "invalid format for --config argument `{config_str}` (should be a dotted key expression)"
1709 )]
1710 InvalidCliConfig {
1711 config_str: String,
1713
1714 #[source]
1716 reason: InvalidCargoCliConfigReason,
1717 },
1718
1719 #[error("non-UTF-8 path encountered")]
1721 NonUtf8Path(#[source] FromPathBufError),
1722
1723 #[error("failed to retrieve the Cargo home directory")]
1725 GetCargoHome(#[source] std::io::Error),
1726
1727 #[error("failed to canonicalize path `{path}")]
1729 FailedPathCanonicalization {
1730 path: Utf8PathBuf,
1732
1733 #[source]
1735 error: std::io::Error,
1736 },
1737
1738 #[error("failed to read config at `{path}`")]
1740 ConfigReadError {
1741 path: Utf8PathBuf,
1743
1744 #[source]
1746 error: std::io::Error,
1747 },
1748
1749 #[error(transparent)]
1751 ConfigParseError(#[from] Box<CargoConfigParseError>),
1752}
1753
1754#[derive(Debug, Error)]
1758#[error("failed to parse config at `{path}`")]
1759pub struct CargoConfigParseError {
1760 pub path: Utf8PathBuf,
1762
1763 #[source]
1765 pub error: toml::de::Error,
1766}
1767
1768#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)]
1772#[non_exhaustive]
1773pub enum InvalidCargoCliConfigReason {
1774 #[error("was not a TOML dotted key expression (such as `build.jobs = 2`)")]
1776 NotDottedKv,
1777
1778 #[error("includes non-whitespace decoration")]
1780 IncludesNonWhitespaceDecoration,
1781
1782 #[error("sets a value to an inline table, which is not accepted")]
1784 SetsValueToInlineTable,
1785
1786 #[error("sets a value to an array of tables, which is not accepted")]
1788 SetsValueToArrayOfTables,
1789
1790 #[error("doesn't provide a value")]
1792 DoesntProvideValue,
1793}
1794
1795#[derive(Debug, Error)]
1797pub enum HostPlatformDetectError {
1798 #[error(
1801 "error spawning `rustc -vV`, and detecting the build \
1802 target failed as well\n\
1803 - rustc spawn error: {}\n\
1804 - build target error: {}\n",
1805 DisplayErrorChain::new_with_initial_indent(" ", error),
1806 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1807 )]
1808 RustcVvSpawnError {
1809 error: std::io::Error,
1811
1812 build_target_error: Box<target_spec::Error>,
1814 },
1815
1816 #[error(
1819 "`rustc -vV` failed with {}, and detecting the \
1820 build target failed as well\n\
1821 - `rustc -vV` stdout:\n{}\n\
1822 - `rustc -vV` stderr:\n{}\n\
1823 - build target error:\n{}\n",
1824 status,
1825 DisplayIndented { item: String::from_utf8_lossy(stdout), indent: " " },
1826 DisplayIndented { item: String::from_utf8_lossy(stderr), indent: " " },
1827 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1828 )]
1829 RustcVvFailed {
1830 status: ExitStatus,
1832
1833 stdout: Vec<u8>,
1835
1836 stderr: Vec<u8>,
1838
1839 build_target_error: Box<target_spec::Error>,
1841 },
1842
1843 #[error(
1846 "parsing `rustc -vV` output failed, and detecting the build target \
1847 failed as well\n\
1848 - host platform error:\n{}\n\
1849 - build target error:\n{}\n",
1850 DisplayErrorChain::new_with_initial_indent(" ", host_platform_error),
1851 DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
1852 )]
1853 HostPlatformParseError {
1854 host_platform_error: Box<target_spec::Error>,
1856
1857 build_target_error: Box<target_spec::Error>,
1859 },
1860
1861 #[error("test-only code, so `rustc -vV` was not called; failed to detect build target")]
1864 BuildTargetError {
1865 #[source]
1867 build_target_error: Box<target_spec::Error>,
1868 },
1869}
1870
1871#[derive(Debug, Error)]
1873pub enum TargetTripleError {
1874 #[error(
1876 "environment variable '{}' contained non-UTF-8 data",
1877 TargetTriple::CARGO_BUILD_TARGET_ENV
1878 )]
1879 InvalidEnvironmentVar,
1880
1881 #[error("error deserializing target triple from {source}")]
1883 TargetSpecError {
1884 source: TargetTripleSource,
1886
1887 #[source]
1889 error: target_spec::Error,
1890 },
1891
1892 #[error("target path `{path}` is not a valid file")]
1894 TargetPathReadError {
1895 source: TargetTripleSource,
1897
1898 path: Utf8PathBuf,
1900
1901 #[source]
1903 error: std::io::Error,
1904 },
1905
1906 #[error(
1908 "for custom platform obtained from {source}, \
1909 failed to create temporary directory for custom platform"
1910 )]
1911 CustomPlatformTempDirError {
1912 source: TargetTripleSource,
1914
1915 #[source]
1917 error: std::io::Error,
1918 },
1919
1920 #[error(
1922 "for custom platform obtained from {source}, \
1923 failed to write JSON to temporary path `{path}`"
1924 )]
1925 CustomPlatformWriteError {
1926 source: TargetTripleSource,
1928
1929 path: Utf8PathBuf,
1931
1932 #[source]
1934 error: std::io::Error,
1935 },
1936
1937 #[error(
1939 "for custom platform obtained from {source}, \
1940 failed to close temporary directory `{dir_path}`"
1941 )]
1942 CustomPlatformCloseError {
1943 source: TargetTripleSource,
1945
1946 dir_path: Utf8PathBuf,
1948
1949 #[source]
1951 error: std::io::Error,
1952 },
1953}
1954
1955impl TargetTripleError {
1956 pub fn source_report(&self) -> Option<miette::Report> {
1961 match self {
1962 Self::TargetSpecError { error, .. } => {
1963 Some(miette::Report::new_boxed(error.clone().into_diagnostic()))
1964 }
1965 TargetTripleError::InvalidEnvironmentVar
1967 | TargetTripleError::TargetPathReadError { .. }
1968 | TargetTripleError::CustomPlatformTempDirError { .. }
1969 | TargetTripleError::CustomPlatformWriteError { .. }
1970 | TargetTripleError::CustomPlatformCloseError { .. } => None,
1971 }
1972 }
1973}
1974
1975#[derive(Debug, Error)]
1977pub enum TargetRunnerError {
1978 #[error("environment variable '{0}' contained non-UTF-8 data")]
1980 InvalidEnvironmentVar(String),
1981
1982 #[error("runner '{key}' = '{value}' did not contain a runner binary")]
1985 BinaryNotSpecified {
1986 key: PlatformRunnerSource,
1988
1989 value: String,
1991 },
1992}
1993
1994#[derive(Debug, Error)]
1996#[error("error setting up signal handler")]
1997pub struct SignalHandlerSetupError(#[from] std::io::Error);
1998
1999#[derive(Debug, Error)]
2001pub enum ShowTestGroupsError {
2002 #[error(
2004 "unknown test groups specified: {}\n(known groups: {})",
2005 unknown_groups.iter().join(", "),
2006 known_groups.iter().join(", "),
2007 )]
2008 UnknownGroups {
2009 unknown_groups: BTreeSet<TestGroup>,
2011
2012 known_groups: BTreeSet<TestGroup>,
2014 },
2015}
2016
2017#[derive(Debug, Error, PartialEq, Eq, Hash)]
2019pub enum InheritsError {
2020 #[error("the {} profile should not inherit from other profiles", .0)]
2022 DefaultProfileInheritance(String),
2023 #[error("profile {} inherits from an unknown profile {}", .0, .1)]
2025 UnknownInheritance(String, String),
2026 #[error("a self referential inheritance is detected from profile: {}", .0)]
2028 SelfReferentialInheritance(String),
2029 #[error("inheritance cycle detected in profile configuration from: {}", .0.iter().map(|scc| {
2031 format!("[{}]", scc.iter().join(", "))
2032 }).join(", "))]
2033 InheritanceCycle(Vec<Vec<String>>),
2034}
2035
2036#[derive(Debug, Error)]
2042pub enum RunStoreError {
2043 #[error("error creating run directory `{run_dir}`")]
2045 RunDirCreate {
2046 run_dir: Utf8PathBuf,
2048
2049 #[source]
2051 error: std::io::Error,
2052 },
2053
2054 #[error("error acquiring lock on `{path}`")]
2056 FileLock {
2057 path: Utf8PathBuf,
2059
2060 #[source]
2062 error: std::io::Error,
2063 },
2064
2065 #[error(
2067 "timed out acquiring lock on `{path}` after {timeout_secs}s (is the state directory \
2068 on a networked filesystem?)"
2069 )]
2070 FileLockTimeout {
2071 path: Utf8PathBuf,
2073
2074 timeout_secs: u64,
2076 },
2077
2078 #[error("error reading run list from `{path}`")]
2080 RunListRead {
2081 path: Utf8PathBuf,
2083
2084 #[source]
2086 error: std::io::Error,
2087 },
2088
2089 #[error("error deserializing run list from `{path}`")]
2091 RunListDeserialize {
2092 path: Utf8PathBuf,
2094
2095 #[source]
2097 error: serde_json::Error,
2098 },
2099
2100 #[error("error serializing run list to `{path}`")]
2102 RunListSerialize {
2103 path: Utf8PathBuf,
2105
2106 #[source]
2108 error: serde_json::Error,
2109 },
2110
2111 #[error("error serializing rerun info")]
2113 RerunInfoSerialize {
2114 #[source]
2116 error: serde_json::Error,
2117 },
2118
2119 #[error("error serializing test list")]
2121 TestListSerialize {
2122 #[source]
2124 error: serde_json::Error,
2125 },
2126
2127 #[error("error serializing record options")]
2129 RecordOptionsSerialize {
2130 #[source]
2132 error: serde_json::Error,
2133 },
2134
2135 #[error("error serializing test event")]
2137 TestEventSerialize {
2138 #[source]
2140 error: serde_json::Error,
2141 },
2142
2143 #[error("error writing run list to `{path}`")]
2145 RunListWrite {
2146 path: Utf8PathBuf,
2148
2149 #[source]
2151 error: atomicwrites::Error<std::io::Error>,
2152 },
2153
2154 #[error("error writing to store at `{store_path}`")]
2156 StoreWrite {
2157 store_path: Utf8PathBuf,
2159
2160 #[source]
2162 error: StoreWriterError,
2163 },
2164
2165 #[error("error creating run log at `{path}`")]
2167 RunLogCreate {
2168 path: Utf8PathBuf,
2170
2171 #[source]
2173 error: std::io::Error,
2174 },
2175
2176 #[error("error writing to run log at `{path}`")]
2178 RunLogWrite {
2179 path: Utf8PathBuf,
2181
2182 #[source]
2184 error: std::io::Error,
2185 },
2186
2187 #[error("error flushing run log at `{path}`")]
2189 RunLogFlush {
2190 path: Utf8PathBuf,
2192
2193 #[source]
2195 error: std::io::Error,
2196 },
2197
2198 #[error(
2200 "cannot write to record store: runs.json.zst format version {file_version} is newer than \
2201 supported version {max_supported_version}"
2202 )]
2203 FormatVersionTooNew {
2204 file_version: RunsJsonFormatVersion,
2206 max_supported_version: RunsJsonFormatVersion,
2208 },
2209}
2210
2211#[derive(Debug, Error)]
2213#[non_exhaustive]
2214pub enum StoreWriterError {
2215 #[error("error creating store")]
2217 Create {
2218 #[source]
2220 error: std::io::Error,
2221 },
2222
2223 #[error("error writing to path `{path}` in store")]
2225 Write {
2226 path: Utf8PathBuf,
2228
2229 #[source]
2231 error: std::io::Error,
2232 },
2233
2234 #[error("error compressing data")]
2236 Compress {
2237 #[source]
2239 error: std::io::Error,
2240 },
2241
2242 #[error("error finalizing store")]
2244 Finish {
2245 #[source]
2247 error: std::io::Error,
2248 },
2249
2250 #[error("error flushing store")]
2252 Flush {
2253 #[source]
2255 error: std::io::Error,
2256 },
2257}
2258
2259#[derive(Debug, Error)]
2261pub enum RecordReporterError {
2262 #[error(transparent)]
2264 RunStore(RunStoreError),
2265
2266 #[error("record writer thread panicked: {message}")]
2268 WriterPanic {
2269 message: String,
2271 },
2272}
2273
2274#[derive(Debug, Error)]
2276pub enum StateDirError {
2277 #[error("could not determine platform base directory strategy")]
2281 BaseDirStrategy(#[source] HomeDirError),
2282
2283 #[error("platform state directory is not valid UTF-8: {path:?}")]
2285 StateDirNotUtf8 {
2286 path: PathBuf,
2288 },
2289
2290 #[error("could not canonicalize workspace path `{workspace_root}`")]
2292 Canonicalize {
2293 workspace_root: Utf8PathBuf,
2295 #[source]
2297 error: std::io::Error,
2298 },
2299}
2300
2301#[derive(Debug, Error)]
2303pub enum RecordSetupError {
2304 #[error("could not determine platform state directory for recording")]
2306 StateDirNotFound(#[source] StateDirError),
2307
2308 #[error("failed to create run store")]
2310 StoreCreate(#[source] RunStoreError),
2311
2312 #[error("failed to lock run store")]
2314 StoreLock(#[source] RunStoreError),
2315
2316 #[error("failed to create run recorder")]
2318 RecorderCreate(#[source] RunStoreError),
2319}
2320
2321impl RecordSetupError {
2322 pub fn disabled_error(&self) -> Option<&(dyn std::error::Error + 'static)> {
2329 match self {
2330 RecordSetupError::RecorderCreate(err @ RunStoreError::FormatVersionTooNew { .. }) => {
2331 Some(err)
2332 }
2333 _ => None,
2334 }
2335 }
2336}
2337
2338#[derive(Debug, Error)]
2340pub enum RecordPruneError {
2341 #[error("error deleting run `{run_id}` at `{path}`")]
2343 DeleteRun {
2344 run_id: ReportUuid,
2346
2347 path: Utf8PathBuf,
2349
2350 #[source]
2352 error: std::io::Error,
2353 },
2354
2355 #[error("error calculating size of `{path}`")]
2357 CalculateSize {
2358 path: Utf8PathBuf,
2360
2361 #[source]
2363 error: std::io::Error,
2364 },
2365
2366 #[error("error deleting orphaned directory `{path}`")]
2368 DeleteOrphan {
2369 path: Utf8PathBuf,
2371
2372 #[source]
2374 error: std::io::Error,
2375 },
2376
2377 #[error("error reading runs directory `{path}`")]
2379 ReadRunsDir {
2380 path: Utf8PathBuf,
2382
2383 #[source]
2385 error: std::io::Error,
2386 },
2387
2388 #[error("error reading directory entry in `{dir}`")]
2390 ReadDirEntry {
2391 dir: Utf8PathBuf,
2393
2394 #[source]
2396 error: std::io::Error,
2397 },
2398
2399 #[error("error reading file type for `{path}`")]
2401 ReadFileType {
2402 path: Utf8PathBuf,
2404
2405 #[source]
2407 error: std::io::Error,
2408 },
2409}
2410
2411#[derive(Clone, Debug, PartialEq, Eq, Error)]
2416#[error("invalid run ID selector `{input}`: expected `latest` or hex digits")]
2417pub struct InvalidRunIdSelector {
2418 pub input: String,
2420}
2421
2422#[derive(Clone, Debug, PartialEq, Eq, Error)]
2428#[error(
2429 "invalid run ID selector `{input}`: expected `latest`, hex digits, \
2430 or a file path (ending in `.zip` or containing path separators)"
2431)]
2432pub struct InvalidRunIdOrRecordingSelector {
2433 pub input: String,
2435}
2436
2437#[derive(Debug, Error)]
2439pub enum RunIdResolutionError {
2440 #[error("no recorded run found matching `{prefix}`")]
2442 NotFound {
2443 prefix: String,
2445 },
2446
2447 #[error("prefix `{prefix}` is ambiguous, matches {count} runs")]
2449 Ambiguous {
2450 prefix: String,
2452
2453 count: usize,
2455
2456 candidates: Vec<RecordedRunInfo>,
2458
2459 run_id_index: RunIdIndex,
2461 },
2462
2463 #[error("prefix `{prefix}` contains invalid characters (expected hexadecimal)")]
2465 InvalidPrefix {
2466 prefix: String,
2468 },
2469
2470 #[error("no recorded runs exist")]
2472 NoRuns,
2473}
2474
2475#[derive(Debug, Error)]
2477pub enum RecordReadError {
2478 #[error("run not found at `{path}`")]
2480 RunNotFound {
2481 path: Utf8PathBuf,
2483 },
2484
2485 #[error("error opening archive at `{path}`")]
2487 OpenArchive {
2488 path: Utf8PathBuf,
2490
2491 #[source]
2493 error: std::io::Error,
2494 },
2495
2496 #[error("error parsing archive at `{path}`")]
2498 ParseArchive {
2499 path: Utf8PathBuf,
2501
2502 #[source]
2504 error: std::io::Error,
2505 },
2506
2507 #[error("error reading `{file_name}` from archive")]
2509 ReadArchiveFile {
2510 file_name: String,
2512
2513 #[source]
2515 error: std::io::Error,
2516 },
2517
2518 #[error("error opening run log at `{path}`")]
2520 OpenRunLog {
2521 path: Utf8PathBuf,
2523
2524 #[source]
2526 error: std::io::Error,
2527 },
2528
2529 #[error("error reading line {line_number} from run log")]
2531 ReadRunLog {
2532 line_number: usize,
2534
2535 #[source]
2537 error: std::io::Error,
2538 },
2539
2540 #[error("error parsing event at line {line_number}")]
2542 ParseEvent {
2543 line_number: usize,
2545
2546 #[source]
2548 error: serde_json::Error,
2549 },
2550
2551 #[error("required file `{file_name}` not found in archive")]
2553 FileNotFound {
2554 file_name: String,
2556 },
2557
2558 #[error("error decompressing data from `{file_name}`")]
2560 Decompress {
2561 file_name: String,
2563
2564 #[source]
2566 error: std::io::Error,
2567 },
2568
2569 #[error(
2574 "unknown output file type `{file_name}` in archive \
2575 (archive may have been created by a newer version of nextest)"
2576 )]
2577 UnknownOutputType {
2578 file_name: String,
2580 },
2581
2582 #[error(
2584 "file `{file_name}` in archive exceeds maximum size ({size} bytes, limit is {limit} bytes)"
2585 )]
2586 FileTooLarge {
2587 file_name: String,
2589
2590 size: u64,
2592
2593 limit: u64,
2595 },
2596
2597 #[error(
2602 "file `{file_name}` size mismatch: header claims {claimed_size} bytes, \
2603 but read {actual_size} bytes (archive may be corrupt or tampered)"
2604 )]
2605 SizeMismatch {
2606 file_name: String,
2608
2609 claimed_size: u64,
2611
2612 actual_size: u64,
2614 },
2615
2616 #[error("error deserializing `{file_name}`")]
2618 DeserializeMetadata {
2619 file_name: String,
2621
2622 #[source]
2624 error: serde_json::Error,
2625 },
2626
2627 #[error("failed to extract `{store_path}` to `{output_path}`")]
2629 ExtractFile {
2630 store_path: String,
2632
2633 output_path: Utf8PathBuf,
2635
2636 #[source]
2638 error: std::io::Error,
2639 },
2640
2641 #[error("error reading portable recording")]
2643 PortableRecording(#[source] PortableRecordingReadError),
2644}
2645
2646#[derive(Debug, Error)]
2648#[non_exhaustive]
2649pub enum PortableRecordingError {
2650 #[error("run directory does not exist: {path}")]
2652 RunDirNotFound {
2653 path: Utf8PathBuf,
2655 },
2656
2657 #[error("required file missing from run directory `{run_dir}`: `{file_name}`")]
2659 RequiredFileMissing {
2660 run_dir: Utf8PathBuf,
2662 file_name: &'static str,
2664 },
2665
2666 #[error("failed to serialize manifest")]
2668 SerializeManifest(#[source] serde_json::Error),
2669
2670 #[error("failed to start file {file_name} in archive")]
2672 ZipStartFile {
2673 file_name: &'static str,
2675 #[source]
2677 source: std::io::Error,
2678 },
2679
2680 #[error("failed to write {file_name} to archive")]
2682 ZipWrite {
2683 file_name: &'static str,
2685 #[source]
2687 source: std::io::Error,
2688 },
2689
2690 #[error("failed to read {file_name}")]
2692 ReadFile {
2693 file_name: &'static str,
2695 #[source]
2697 source: std::io::Error,
2698 },
2699
2700 #[error("failed to finalize archive")]
2702 ZipFinalize(#[source] std::io::Error),
2703
2704 #[error("failed to write archive atomically to {path}")]
2706 AtomicWrite {
2707 path: Utf8PathBuf,
2709 #[source]
2711 source: std::io::Error,
2712 },
2713}
2714
2715#[derive(Debug, Error)]
2717#[non_exhaustive]
2718pub enum PortableRecordingReadError {
2719 #[error("failed to open archive at `{path}`")]
2721 OpenArchive {
2722 path: Utf8PathBuf,
2724 #[source]
2726 error: std::io::Error,
2727 },
2728
2729 #[error("failed to read archive at `{path}`")]
2731 ReadArchive {
2732 path: Utf8PathBuf,
2734 #[source]
2736 error: std::io::Error,
2737 },
2738
2739 #[error("required file `{file_name}` missing from archive at `{path}`")]
2741 MissingFile {
2742 path: Utf8PathBuf,
2744 file_name: Cow<'static, str>,
2746 },
2747
2748 #[error("failed to parse manifest from archive at `{path}`")]
2750 ParseManifest {
2751 path: Utf8PathBuf,
2753 #[source]
2755 error: serde_json::Error,
2756 },
2757
2758 #[error(
2760 "portable recording format version {found} in `{path}` is incompatible: {incompatibility} \
2761 (this nextest supports version {supported})"
2762 )]
2763 UnsupportedFormatVersion {
2764 path: Utf8PathBuf,
2766 found: PortableRecordingFormatVersion,
2768 supported: PortableRecordingFormatVersion,
2770 incompatibility: PortableRecordingVersionIncompatibility,
2772 },
2773
2774 #[error(
2776 "store format version {found} in `{path}` is incompatible: {incompatibility} \
2777 (this nextest supports version {supported})"
2778 )]
2779 UnsupportedStoreFormatVersion {
2780 path: Utf8PathBuf,
2782 found: StoreFormatVersion,
2784 supported: StoreFormatVersion,
2786 incompatibility: StoreVersionIncompatibility,
2788 },
2789
2790 #[error(
2792 "file `{file_name}` in archive `{path}` is too large \
2793 ({size} bytes, limit is {limit} bytes)"
2794 )]
2795 FileTooLarge {
2796 path: Utf8PathBuf,
2798 file_name: Cow<'static, str>,
2800 size: u64,
2802 limit: u64,
2804 },
2805
2806 #[error("failed to extract `{file_name}` from archive `{archive_path}` to `{output_path}`")]
2808 ExtractFile {
2809 archive_path: Utf8PathBuf,
2811 file_name: &'static str,
2813 output_path: Utf8PathBuf,
2815 #[source]
2817 error: std::io::Error,
2818 },
2819
2820 #[error(
2824 "for portable recording `{archive_path}`, the inner archive is stored \
2825 with {:?} compression -- it must be stored uncompressed",
2826 compression
2827 )]
2828 CompressedInnerArchive {
2829 archive_path: Utf8PathBuf,
2831 compression: CompressionMethod,
2833 },
2834
2835 #[error(
2839 "archive at `{path}` has no manifest and is not a wrapper archive \
2840 (contains {file_count} {}, {zip_count} of which {} in .zip)",
2841 plural::files_str(*file_count),
2842 plural::end_str(*zip_count)
2843 )]
2844 NotAWrapperArchive {
2845 path: Utf8PathBuf,
2847 file_count: usize,
2849 zip_count: usize,
2851 },
2852
2853 #[error("unexpected I/O error while probing seekability of `{path}`")]
2860 SeekProbe {
2861 path: Utf8PathBuf,
2863 #[source]
2865 error: std::io::Error,
2866 },
2867
2868 #[error("failed to spool non-seekable input `{path}` to a temporary file")]
2874 SpoolTempFile {
2875 path: Utf8PathBuf,
2877 #[source]
2879 error: std::io::Error,
2880 },
2881
2882 #[error(
2884 "recording at `{path}` exceeds the spool size limit \
2885 ({}); use a file path instead of process substitution",
2886 SizeDisplay(.limit.0)
2887 )]
2888 SpoolTooLarge {
2889 path: Utf8PathBuf,
2891 limit: ByteSize,
2893 },
2894}
2895
2896#[derive(Debug, Error)]
2898pub enum ChromeTraceError {
2899 #[error("error reading recorded events")]
2901 ReadError(#[source] RecordReadError),
2902
2903 #[error(
2905 "event for test `{test_name}` in binary `{binary_id}` \
2906 has no prior TestStarted event (corrupt or truncated log?)"
2907 )]
2908 MissingTestStart {
2909 test_name: TestCaseName,
2911
2912 binary_id: RustBinaryId,
2914 },
2915
2916 #[error(
2918 "SetupScriptSlow for script `{script_id}` \
2919 has no prior SetupScriptStarted event (corrupt or truncated log?)"
2920 )]
2921 MissingScriptStart {
2922 script_id: ScriptId,
2924 },
2925
2926 #[error(
2929 "StressSubRunFinished has no prior StressSubRunStarted event \
2930 (corrupt or truncated log?)"
2931 )]
2932 MissingStressSubRunStart,
2933
2934 #[error("error serializing Chrome trace JSON")]
2936 SerializeError(#[source] serde_json::Error),
2937}
2938
2939#[derive(Debug, Error)]
2943pub enum TestListFromSummaryError {
2944 #[error("package `{name}` (id: `{package_id}`) not found in cargo metadata")]
2946 PackageNotFound {
2947 name: String,
2949
2950 package_id: String,
2952 },
2953
2954 #[error("error parsing rust build metadata")]
2956 RustBuildMeta(#[source] RustBuildMetaParseError),
2957}
2958
2959#[cfg(feature = "self-update")]
2960mod self_update_errors {
2961 use super::*;
2962 use crate::update::PrereleaseKind;
2963 use mukti_metadata::ReleaseStatus;
2964 use semver::{Version, VersionReq};
2965
2966 #[derive(Debug, Error)]
2970 #[non_exhaustive]
2971 pub enum UpdateError {
2972 #[error("failed to read release metadata from `{path}`")]
2974 ReadLocalMetadata {
2975 path: Utf8PathBuf,
2977
2978 #[source]
2980 error: std::io::Error,
2981 },
2982
2983 #[error("self-update failed")]
2985 SelfUpdate(#[source] self_update::errors::Error),
2986
2987 #[error("error performing HTTP request")]
2989 Http(#[source] ureq::Error),
2990
2991 #[error("error reading HTTP response body")]
2993 HttpBody(#[source] std::io::Error),
2994
2995 #[error("Content-Length header present but could not be parsed as an integer: {value:?}")]
2998 ContentLengthInvalid {
2999 value: String,
3001 },
3002
3003 #[error("content length mismatch: expected {expected} bytes, received {actual} bytes")]
3006 ContentLengthMismatch {
3007 expected: u64,
3009 actual: u64,
3011 },
3012
3013 #[error("deserializing release metadata failed")]
3015 ReleaseMetadataDe(#[source] serde_json::Error),
3016
3017 #[error("version `{version}` not found (known versions: {})", known_versions(.known))]
3019 VersionNotFound {
3020 version: Version,
3022
3023 known: Vec<(Version, ReleaseStatus)>,
3025 },
3026
3027 #[error("no version found matching requirement `{req}`")]
3029 NoMatchForVersionReq {
3030 req: VersionReq,
3032 },
3033
3034 #[error("no stable version found")]
3036 NoStableVersion,
3037
3038 #[error("no version found matching {} channel", kind.description())]
3040 NoVersionForPrereleaseKind {
3041 kind: PrereleaseKind,
3043 },
3044
3045 #[error("project {not_found} not found in release metadata (known projects: {})", known.join(", "))]
3047 MuktiProjectNotFound {
3048 not_found: String,
3050
3051 known: Vec<String>,
3053 },
3054
3055 #[error(
3057 "for version {version}, no release information found for target `{triple}` \
3058 (known targets: {})",
3059 known_triples.iter().join(", ")
3060 )]
3061 NoTargetData {
3062 version: Version,
3064
3065 triple: String,
3067
3068 known_triples: BTreeSet<String>,
3070 },
3071
3072 #[error("the current executable's path could not be determined")]
3074 CurrentExe(#[source] std::io::Error),
3075
3076 #[error("temporary directory could not be created at `{location}`")]
3078 TempDirCreate {
3079 location: Utf8PathBuf,
3081
3082 #[source]
3084 error: std::io::Error,
3085 },
3086
3087 #[error("temporary archive could not be created at `{archive_path}`")]
3089 TempArchiveCreate {
3090 archive_path: Utf8PathBuf,
3092
3093 #[source]
3095 error: std::io::Error,
3096 },
3097
3098 #[error("error writing to temporary archive at `{archive_path}`")]
3100 TempArchiveWrite {
3101 archive_path: Utf8PathBuf,
3103
3104 #[source]
3106 error: std::io::Error,
3107 },
3108
3109 #[error("error reading from temporary archive at `{archive_path}`")]
3111 TempArchiveRead {
3112 archive_path: Utf8PathBuf,
3114
3115 #[source]
3117 error: std::io::Error,
3118 },
3119
3120 #[error("SHA-256 checksum mismatch: expected: {expected}, actual: {actual}")]
3122 ChecksumMismatch {
3123 expected: String,
3125
3126 actual: String,
3128 },
3129
3130 #[error("error renaming `{source}` to `{dest}`")]
3132 FsRename {
3133 source: Utf8PathBuf,
3135
3136 dest: Utf8PathBuf,
3138
3139 #[source]
3141 error: std::io::Error,
3142 },
3143
3144 #[error("cargo-nextest binary updated, but error running `cargo nextest self setup`")]
3146 SelfSetup(#[source] std::io::Error),
3147 }
3148
3149 fn known_versions(versions: &[(Version, ReleaseStatus)]) -> String {
3150 use std::fmt::Write;
3151
3152 const DISPLAY_COUNT: usize = 4;
3154
3155 let display_versions: Vec<_> = versions
3156 .iter()
3157 .filter(|(v, status)| v.pre.is_empty() && *status == ReleaseStatus::Active)
3158 .map(|(v, _)| v.to_string())
3159 .take(DISPLAY_COUNT)
3160 .collect();
3161 let mut display_str = display_versions.join(", ");
3162 if versions.len() > display_versions.len() {
3163 write!(
3164 display_str,
3165 " and {} others",
3166 versions.len() - display_versions.len()
3167 )
3168 .unwrap();
3169 }
3170
3171 display_str
3172 }
3173
3174 #[derive(Debug, Error)]
3176 pub enum UpdateVersionParseError {
3177 #[error("version string is empty")]
3179 EmptyString,
3180
3181 #[error(
3183 "`{input}` is not a valid semver requirement\n\
3184 (hint: see https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html for the correct format)"
3185 )]
3186 InvalidVersionReq {
3187 input: String,
3189
3190 #[source]
3192 error: semver::Error,
3193 },
3194
3195 #[error("`{input}` is not a valid semver{}", extra_semver_output(.input))]
3197 InvalidVersion {
3198 input: String,
3200
3201 #[source]
3203 error: semver::Error,
3204 },
3205 }
3206
3207 fn extra_semver_output(input: &str) -> String {
3208 if input.parse::<VersionReq>().is_ok() {
3211 format!(
3212 "\n(if you want to specify a semver range, add an explicit qualifier, like ^{input})"
3213 )
3214 } else {
3215 "".to_owned()
3216 }
3217 }
3218}
3219
3220#[cfg(feature = "self-update")]
3221pub use self_update_errors::*;
3222
3223#[cfg(test)]
3224mod tests {
3225 use super::*;
3226
3227 #[test]
3228 fn display_error_chain() {
3229 let err1 = StringError::new("err1", None);
3230
3231 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&err1)), @"err1");
3232
3233 let err2 = StringError::new("err2", Some(err1));
3234 let err3 = StringError::new("err3\nerr3 line 2", Some(err2));
3235
3236 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&err3)), @"
3237 err3
3238 err3 line 2
3239 caused by:
3240 - err2
3241 - err1
3242 ");
3243 }
3244
3245 #[test]
3246 fn display_error_list() {
3247 let err1 = StringError::new("err1", None);
3248
3249 let error_list =
3250 ErrorList::<StringError>::new("waiting on the water to boil", vec![err1.clone()])
3251 .expect(">= 1 error");
3252 insta::assert_snapshot!(format!("{}", error_list), @"err1");
3253 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @"err1");
3254
3255 let err2 = StringError::new("err2", Some(err1));
3256 let err3 = StringError::new("err3", Some(err2));
3257
3258 let error_list =
3259 ErrorList::<StringError>::new("waiting on flowers to bloom", vec![err3.clone()])
3260 .expect(">= 1 error");
3261 insta::assert_snapshot!(format!("{}", error_list), @"err3");
3262 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @"
3263 err3
3264 caused by:
3265 - err2
3266 - err1
3267 ");
3268
3269 let err4 = StringError::new("err4", None);
3270 let err5 = StringError::new("err5", Some(err4));
3271 let err6 = StringError::new("err6\nerr6 line 2", Some(err5));
3272
3273 let error_list = ErrorList::<StringError>::new(
3274 "waiting for the heat death of the universe",
3275 vec![err3, err6],
3276 )
3277 .expect(">= 1 error");
3278
3279 insta::assert_snapshot!(format!("{}", error_list), @"
3280 2 errors occurred waiting for the heat death of the universe:
3281 * err3
3282 caused by:
3283 - err2
3284 - err1
3285 * err6
3286 err6 line 2
3287 caused by:
3288 - err5
3289 - err4
3290 ");
3291 insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @"
3292 2 errors occurred waiting for the heat death of the universe:
3293 * err3
3294 caused by:
3295 - err2
3296 - err1
3297 * err6
3298 err6 line 2
3299 caused by:
3300 - err5
3301 - err4
3302 ");
3303 }
3304
3305 #[derive(Clone, Debug, Error)]
3306 struct StringError {
3307 message: String,
3308 #[source]
3309 source: Option<Box<StringError>>,
3310 }
3311
3312 impl StringError {
3313 fn new(message: impl Into<String>, source: Option<StringError>) -> Self {
3314 Self {
3315 message: message.into(),
3316 source: source.map(Box::new),
3317 }
3318 }
3319 }
3320
3321 impl fmt::Display for StringError {
3322 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3323 write!(f, "{}", self.message)
3324 }
3325 }
3326}