nextest_runner/
errors.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Errors produced by nextest.
5
6use 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},
14    indenter::{DisplayIndented, indented},
15    redact::Redactor,
16    reuse_build::{ArchiveFormat, ArchiveStep},
17    target_runner::PlatformRunnerSource,
18};
19use camino::{FromPathBufError, Utf8Path, Utf8PathBuf};
20use config::ConfigError;
21use itertools::{Either, Itertools};
22use nextest_filtering::errors::FiltersetParseErrors;
23use nextest_metadata::RustBinaryId;
24use smol_str::SmolStr;
25use std::{
26    borrow::Cow,
27    collections::BTreeSet,
28    env::JoinPathsError,
29    fmt::{self, Write as _},
30    process::ExitStatus,
31    sync::Arc,
32};
33use target_spec_miette::IntoMietteDiagnostic;
34use thiserror::Error;
35
36/// An error that occurred while parsing the config.
37#[derive(Debug, Error)]
38#[error(
39    "failed to parse nextest config at `{config_file}`{}",
40    provided_by_tool(tool.as_ref())
41)]
42#[non_exhaustive]
43pub struct ConfigParseError {
44    config_file: Utf8PathBuf,
45    tool: Option<ToolName>,
46    #[source]
47    kind: ConfigParseErrorKind,
48}
49
50impl ConfigParseError {
51    pub(crate) fn new(
52        config_file: impl Into<Utf8PathBuf>,
53        tool: Option<&ToolName>,
54        kind: ConfigParseErrorKind,
55    ) -> Self {
56        Self {
57            config_file: config_file.into(),
58            tool: tool.cloned(),
59            kind,
60        }
61    }
62
63    /// Returns the config file for this error.
64    pub fn config_file(&self) -> &Utf8Path {
65        &self.config_file
66    }
67
68    /// Returns the tool name associated with this error.
69    pub fn tool(&self) -> Option<&ToolName> {
70        self.tool.as_ref()
71    }
72
73    /// Returns the kind of error this is.
74    pub fn kind(&self) -> &ConfigParseErrorKind {
75        &self.kind
76    }
77}
78
79/// Returns the string ` provided by tool <tool>`, if `tool` is `Some`.
80pub fn provided_by_tool(tool: Option<&ToolName>) -> String {
81    match tool {
82        Some(tool) => format!(" provided by tool `{tool}`"),
83        None => String::new(),
84    }
85}
86
87/// The kind of error that occurred while parsing a config.
88///
89/// Returned by [`ConfigParseError::kind`].
90#[derive(Debug, Error)]
91#[non_exhaustive]
92pub enum ConfigParseErrorKind {
93    /// An error occurred while building the config.
94    #[error(transparent)]
95    BuildError(Box<ConfigError>),
96    /// An error occurred while parsing the config into a table.
97    #[error(transparent)]
98    TomlParseError(Box<toml::de::Error>),
99    #[error(transparent)]
100    /// An error occurred while deserializing the config.
101    DeserializeError(Box<serde_path_to_error::Error<ConfigError>>),
102    /// An error occurred while reading the config file (version only).
103    #[error(transparent)]
104    VersionOnlyReadError(std::io::Error),
105    /// An error occurred while deserializing the config (version only).
106    #[error(transparent)]
107    VersionOnlyDeserializeError(Box<serde_path_to_error::Error<toml::de::Error>>),
108    /// Errors occurred while compiling configuration strings.
109    #[error("error parsing compiled data (destructure this variant for more details)")]
110    CompileErrors(Vec<ConfigCompileError>),
111    /// An invalid set of test groups was defined by the user.
112    #[error("invalid test groups defined: {}\n(test groups cannot start with '@tool:' unless specified by a tool)", .0.iter().join(", "))]
113    InvalidTestGroupsDefined(BTreeSet<CustomTestGroup>),
114    /// An invalid set of test groups was defined by a tool config file.
115    #[error(
116        "invalid test groups defined by tool: {}\n(test groups must start with '@tool:<tool-name>:')", .0.iter().join(", "))]
117    InvalidTestGroupsDefinedByTool(BTreeSet<CustomTestGroup>),
118    /// Some test groups were unknown.
119    #[error("unknown test groups specified by config (destructure this variant for more details)")]
120    UnknownTestGroups {
121        /// The list of errors that occurred.
122        errors: Vec<UnknownTestGroupError>,
123
124        /// Known groups up to this point.
125        known_groups: BTreeSet<TestGroup>,
126    },
127    /// Both `[script.*]` and `[scripts.*]` were defined.
128    #[error(
129        "both `[script.*]` and `[scripts.*]` defined\n\
130         (hint: [script.*] will be removed in the future: switch to [scripts.setup.*])"
131    )]
132    BothScriptAndScriptsDefined,
133    /// An invalid set of config scripts was defined by the user.
134    #[error("invalid config scripts defined: {}\n(config scripts cannot start with '@tool:' unless specified by a tool)", .0.iter().join(", "))]
135    InvalidConfigScriptsDefined(BTreeSet<ScriptId>),
136    /// An invalid set of config scripts was defined by a tool config file.
137    #[error(
138        "invalid config scripts defined by tool: {}\n(config scripts must start with '@tool:<tool-name>:')", .0.iter().join(", "))]
139    InvalidConfigScriptsDefinedByTool(BTreeSet<ScriptId>),
140    /// The same config script name was used across config script types.
141    #[error(
142        "config script names used more than once: {}\n\
143         (config script names must be unique across all script types)", .0.iter().join(", ")
144    )]
145    DuplicateConfigScriptNames(BTreeSet<ScriptId>),
146    /// Errors occurred while parsing `[[profile.<profile-name>.scripts]]`.
147    #[error(
148        "errors in profile-specific config scripts (destructure this variant for more details)"
149    )]
150    ProfileScriptErrors {
151        /// The errors that occurred.
152        errors: Box<ProfileScriptErrors>,
153
154        /// Known scripts up to this point.
155        known_scripts: BTreeSet<ScriptId>,
156    },
157    /// An unknown experimental feature or features were defined.
158    #[error("unknown experimental features defined (destructure this variant for more details)")]
159    UnknownExperimentalFeatures {
160        /// The set of unknown features.
161        unknown: BTreeSet<String>,
162
163        /// The set of known features.
164        known: BTreeSet<ConfigExperimental>,
165    },
166    /// A tool specified an experimental feature.
167    ///
168    /// Tools are not allowed to specify experimental features.
169    #[error(
170        "tool config file specifies experimental features `{}` \
171         -- only repository config files can do so",
172        .features.iter().join(", "),
173    )]
174    ExperimentalFeaturesInToolConfig {
175        /// The name of the experimental feature.
176        features: BTreeSet<String>,
177    },
178    /// Experimental features were used but not enabled.
179    #[error("experimental features used but not enabled: {}", .missing_features.iter().join(", "))]
180    ExperimentalFeaturesNotEnabled {
181        /// The features that were not enabled.
182        missing_features: BTreeSet<ConfigExperimental>,
183    },
184    /// An inheritance cycle was detected in the profile configuration.
185    #[error("inheritance error(s) detected: {}", .0.iter().join(", "))]
186    InheritanceErrors(Vec<InheritsError>),
187}
188
189/// An error that occurred while compiling overrides or scripts specified in
190/// configuration.
191#[derive(Debug)]
192#[non_exhaustive]
193pub struct ConfigCompileError {
194    /// The name of the profile under which the data was found.
195    pub profile_name: String,
196
197    /// The section within the profile where the error occurred.
198    pub section: ConfigCompileSection,
199
200    /// The kind of error that occurred.
201    pub kind: ConfigCompileErrorKind,
202}
203
204/// For a [`ConfigCompileError`], the section within the profile where the error
205/// occurred.
206#[derive(Debug)]
207pub enum ConfigCompileSection {
208    /// `profile.<profile-name>.default-filter`.
209    DefaultFilter,
210
211    /// `[[profile.<profile-name>.overrides]]` at the corresponding index.
212    Override(usize),
213
214    /// `[[profile.<profile-name>.scripts]]` at the corresponding index.
215    Script(usize),
216}
217
218/// The kind of error that occurred while parsing config overrides.
219#[derive(Debug)]
220#[non_exhaustive]
221pub enum ConfigCompileErrorKind {
222    /// Neither `platform` nor `filter` were specified.
223    ConstraintsNotSpecified {
224        /// Whether `default-filter` was specified.
225        ///
226        /// If default-filter is specified, then specifying `filter` is not
227        /// allowed -- so we show a different message in that case.
228        default_filter_specified: bool,
229    },
230
231    /// Both `filter` and `default-filter` were specified.
232    ///
233    /// It only makes sense to specify one of the two.
234    FilterAndDefaultFilterSpecified,
235
236    /// One or more errors occured while parsing expressions.
237    Parse {
238        /// A potential error that occurred while parsing the host platform expression.
239        host_parse_error: Option<target_spec::Error>,
240
241        /// A potential error that occurred while parsing the target platform expression.
242        target_parse_error: Option<target_spec::Error>,
243
244        /// Filterset or default filter parse errors.
245        filter_parse_errors: Vec<FiltersetParseErrors>,
246    },
247}
248
249impl ConfigCompileErrorKind {
250    /// Returns [`miette::Report`]s for each error recorded by self.
251    pub fn reports(&self) -> impl Iterator<Item = miette::Report> + '_ {
252        match self {
253            Self::ConstraintsNotSpecified {
254                default_filter_specified,
255            } => {
256                let message = if *default_filter_specified {
257                    "for override with `default-filter`, `platform` must also be specified"
258                } else {
259                    "at least one of `platform` and `filter` must be specified"
260                };
261                Either::Left(std::iter::once(miette::Report::msg(message)))
262            }
263            Self::FilterAndDefaultFilterSpecified => {
264                Either::Left(std::iter::once(miette::Report::msg(
265                    "at most one of `filter` and `default-filter` must be specified",
266                )))
267            }
268            Self::Parse {
269                host_parse_error,
270                target_parse_error,
271                filter_parse_errors,
272            } => {
273                let host_parse_report = host_parse_error
274                    .as_ref()
275                    .map(|error| miette::Report::new_boxed(error.clone().into_diagnostic()));
276                let target_parse_report = target_parse_error
277                    .as_ref()
278                    .map(|error| miette::Report::new_boxed(error.clone().into_diagnostic()));
279                let filter_parse_reports =
280                    filter_parse_errors.iter().flat_map(|filter_parse_errors| {
281                        filter_parse_errors.errors.iter().map(|single_error| {
282                            miette::Report::new(single_error.clone())
283                                .with_source_code(filter_parse_errors.input.to_owned())
284                        })
285                    });
286
287                Either::Right(
288                    host_parse_report
289                        .into_iter()
290                        .chain(target_parse_report)
291                        .chain(filter_parse_reports),
292                )
293            }
294        }
295    }
296}
297
298/// A test priority specified was out of range.
299#[derive(Clone, Debug, Error)]
300#[error("test priority ({priority}) out of range: must be between -100 and 100, both inclusive")]
301pub struct TestPriorityOutOfRange {
302    /// The priority that was out of range.
303    pub priority: i8,
304}
305
306/// An execution error occurred while attempting to start a test.
307#[derive(Clone, Debug, Error)]
308pub enum ChildStartError {
309    /// An error occurred while creating a temporary path for a setup script.
310    #[error("error creating temporary path for setup script")]
311    TempPath(#[source] Arc<std::io::Error>),
312
313    /// An error occurred while spawning the child process.
314    #[error("error spawning child process")]
315    Spawn(#[source] Arc<std::io::Error>),
316}
317
318/// An error that occurred while reading the output of a setup script.
319#[derive(Clone, Debug, Error)]
320pub enum SetupScriptOutputError {
321    /// An error occurred while opening the setup script environment file.
322    #[error("error opening environment file `{path}`")]
323    EnvFileOpen {
324        /// The path to the environment file.
325        path: Utf8PathBuf,
326
327        /// The underlying error.
328        #[source]
329        error: Arc<std::io::Error>,
330    },
331
332    /// An error occurred while reading the setup script environment file.
333    #[error("error reading environment file `{path}`")]
334    EnvFileRead {
335        /// The path to the environment file.
336        path: Utf8PathBuf,
337
338        /// The underlying error.
339        #[source]
340        error: Arc<std::io::Error>,
341    },
342
343    /// An error occurred while parsing the setup script environment file.
344    #[error("line `{line}` in environment file `{path}` not in KEY=VALUE format")]
345    EnvFileParse {
346        /// The path to the environment file.
347        path: Utf8PathBuf,
348        /// The line at issue.
349        line: String,
350    },
351
352    /// An environment variable key was reserved.
353    #[error("key `{key}` begins with `NEXTEST`, which is reserved for internal use")]
354    EnvFileReservedKey {
355        /// The environment variable name.
356        key: String,
357    },
358}
359
360/// A list of errors that implements `Error`.
361///
362/// In the future, we'll likely want to replace this with a `miette::Diagnostic`-based error, since
363/// that supports multiple causes via "related".
364#[derive(Clone, Debug)]
365pub struct ErrorList<T> {
366    // A description of what the errors are.
367    description: &'static str,
368    // Invariant: this list is non-empty.
369    inner: Vec<T>,
370}
371
372impl<T: std::error::Error> ErrorList<T> {
373    pub(crate) fn new<U>(description: &'static str, errors: Vec<U>) -> Option<Self>
374    where
375        T: From<U>,
376    {
377        if errors.is_empty() {
378            None
379        } else {
380            Some(Self {
381                description,
382                inner: errors.into_iter().map(T::from).collect(),
383            })
384        }
385    }
386
387    /// Returns a short summary of the error list.
388    pub(crate) fn short_message(&self) -> String {
389        let string = self.to_string();
390        match string.lines().next() {
391            // Remove a trailing colon if it exists for a better UX.
392            Some(first_line) => first_line.trim_end_matches(':').to_string(),
393            None => String::new(),
394        }
395    }
396
397    pub(crate) fn iter(&self) -> impl Iterator<Item = &T> {
398        self.inner.iter()
399    }
400}
401
402impl<T: std::error::Error> fmt::Display for ErrorList<T> {
403    fn fmt(&self, mut f: &mut fmt::Formatter) -> fmt::Result {
404        // If a single error occurred, pretend that this is just that.
405        if self.inner.len() == 1 {
406            return write!(f, "{}", self.inner[0]);
407        }
408
409        // Otherwise, list all errors.
410        writeln!(
411            f,
412            "{} errors occurred {}:",
413            self.inner.len(),
414            self.description,
415        )?;
416        for error in &self.inner {
417            let mut indent = indented(f).with_str("  ").skip_initial();
418            writeln!(indent, "* {}", DisplayErrorChain::new(error))?;
419            f = indent.into_inner();
420        }
421        Ok(())
422    }
423}
424
425impl<T: std::error::Error> std::error::Error for ErrorList<T> {
426    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
427        if self.inner.len() == 1 {
428            self.inner[0].source()
429        } else {
430            // More than one error occurred, so we can't return a single error here. Instead, we
431            // return `None` and display the chain of causes in `fmt::Display`.
432            None
433        }
434    }
435}
436
437/// A wrapper type to display a chain of errors with internal indentation.
438///
439/// This is similar to the display-error-chain crate, but uses IndentWriter
440/// internally to ensure that subsequent lines are also nested.
441pub(crate) struct DisplayErrorChain<E> {
442    error: E,
443    initial_indent: &'static str,
444}
445
446impl<E: std::error::Error> DisplayErrorChain<E> {
447    pub(crate) fn new(error: E) -> Self {
448        Self {
449            error,
450            initial_indent: "",
451        }
452    }
453
454    pub(crate) fn new_with_initial_indent(initial_indent: &'static str, error: E) -> Self {
455        Self {
456            error,
457            initial_indent,
458        }
459    }
460}
461
462impl<E> fmt::Display for DisplayErrorChain<E>
463where
464    E: std::error::Error,
465{
466    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
467        let mut writer = indented(f).with_str(self.initial_indent);
468        write!(writer, "{}", self.error)?;
469
470        let Some(mut cause) = self.error.source() else {
471            return Ok(());
472        };
473
474        write!(writer, "\n  caused by:")?;
475
476        loop {
477            writeln!(writer)?;
478            // Wrap the existing writer to accumulate indentation.
479            let mut indent = indented(&mut writer).with_str("    ").skip_initial();
480            write!(indent, "  - {cause}")?;
481
482            let Some(next_cause) = cause.source() else {
483                break Ok(());
484            };
485
486            cause = next_cause;
487        }
488    }
489}
490
491/// An error was returned while managing a child process or reading its output.
492#[derive(Clone, Debug, Error)]
493pub enum ChildError {
494    /// An error occurred while reading from a child file descriptor.
495    #[error(transparent)]
496    Fd(#[from] ChildFdError),
497
498    /// An error occurred while reading the output of a setup script.
499    #[error(transparent)]
500    SetupScriptOutput(#[from] SetupScriptOutputError),
501}
502
503/// An error was returned while reading from child a file descriptor.
504#[derive(Clone, Debug, Error)]
505pub enum ChildFdError {
506    /// An error occurred while reading standard output.
507    #[error("error reading standard output")]
508    ReadStdout(#[source] Arc<std::io::Error>),
509
510    /// An error occurred while reading standard error.
511    #[error("error reading standard error")]
512    ReadStderr(#[source] Arc<std::io::Error>),
513
514    /// An error occurred while reading a combined stream.
515    #[error("error reading combined stream")]
516    ReadCombined(#[source] Arc<std::io::Error>),
517
518    /// An error occurred while waiting for the child process to exit.
519    #[error("error waiting for child process to exit")]
520    Wait(#[source] Arc<std::io::Error>),
521}
522
523/// An unknown test group was specified in the config.
524#[derive(Clone, Debug, Eq, PartialEq)]
525#[non_exhaustive]
526pub struct UnknownTestGroupError {
527    /// The name of the profile under which the unknown test group was found.
528    pub profile_name: String,
529
530    /// The name of the unknown test group.
531    pub name: TestGroup,
532}
533
534/// While parsing profile-specific config scripts, an unknown script was
535/// encountered.
536#[derive(Clone, Debug, Eq, PartialEq)]
537pub struct ProfileUnknownScriptError {
538    /// The name of the profile under which the errors occurred.
539    pub profile_name: String,
540
541    /// The name of the unknown script.
542    pub name: ScriptId,
543}
544
545/// While parsing profile-specific config scripts, a script of the wrong type
546/// was encountered.
547#[derive(Clone, Debug, Eq, PartialEq)]
548pub struct ProfileWrongConfigScriptTypeError {
549    /// The name of the profile under which the errors occurred.
550    pub profile_name: String,
551
552    /// The name of the config script.
553    pub name: ScriptId,
554
555    /// The script type that the user attempted to use the script as.
556    pub attempted: ProfileScriptType,
557
558    /// The script type that the script actually is.
559    pub actual: ScriptType,
560}
561
562/// While parsing profile-specific config scripts, a list-time-enabled script
563/// used a filter that can only be used at test run time.
564#[derive(Clone, Debug, Eq, PartialEq)]
565pub struct ProfileListScriptUsesRunFiltersError {
566    /// The name of the profile under which the errors occurred.
567    pub profile_name: String,
568
569    /// The name of the config script.
570    pub name: ScriptId,
571
572    /// The script type.
573    pub script_type: ProfileScriptType,
574
575    /// The filters that were used.
576    pub filters: BTreeSet<String>,
577}
578
579/// Errors that occurred while parsing `[[profile.*.scripts]]`.
580#[derive(Clone, Debug, Default)]
581pub struct ProfileScriptErrors {
582    /// The list of unknown script errors.
583    pub unknown_scripts: Vec<ProfileUnknownScriptError>,
584
585    /// The list of wrong script type errors.
586    pub wrong_script_types: Vec<ProfileWrongConfigScriptTypeError>,
587
588    /// The list of list-time-enabled scripts that used a run-time filter.
589    pub list_scripts_using_run_filters: Vec<ProfileListScriptUsesRunFiltersError>,
590}
591
592impl ProfileScriptErrors {
593    /// Returns true if there are no errors recorded.
594    pub fn is_empty(&self) -> bool {
595        self.unknown_scripts.is_empty()
596            && self.wrong_script_types.is_empty()
597            && self.list_scripts_using_run_filters.is_empty()
598    }
599}
600
601/// An error which indicates that a profile was requested but not known to nextest.
602#[derive(Clone, Debug, Error)]
603#[error("profile `{profile}` not found (known profiles: {})", .all_profiles.join(", "))]
604pub struct ProfileNotFound {
605    profile: String,
606    all_profiles: Vec<String>,
607}
608
609impl ProfileNotFound {
610    pub(crate) fn new(
611        profile: impl Into<String>,
612        all_profiles: impl IntoIterator<Item = impl Into<String>>,
613    ) -> Self {
614        let mut all_profiles: Vec<_> = all_profiles.into_iter().map(|s| s.into()).collect();
615        all_profiles.sort_unstable();
616        Self {
617            profile: profile.into(),
618            all_profiles,
619        }
620    }
621}
622
623/// An identifier is invalid.
624#[derive(Clone, Debug, Error, Eq, PartialEq)]
625pub enum InvalidIdentifier {
626    /// The identifier is empty.
627    #[error("identifier is empty")]
628    Empty,
629
630    /// The identifier is not in the correct Unicode format.
631    #[error("invalid identifier `{0}`")]
632    InvalidXid(SmolStr),
633
634    /// This tool identifier doesn't match the expected pattern.
635    #[error("tool identifier not of the form \"@tool:tool-name:identifier\": `{0}`")]
636    ToolIdentifierInvalidFormat(SmolStr),
637
638    /// One of the components of this tool identifier is empty.
639    #[error("tool identifier has empty component: `{0}`")]
640    ToolComponentEmpty(SmolStr),
641
642    /// The tool identifier is not in the correct Unicode format.
643    #[error("invalid tool identifier `{0}`")]
644    ToolIdentifierInvalidXid(SmolStr),
645}
646
647/// A tool name is invalid.
648#[derive(Clone, Debug, Error, Eq, PartialEq)]
649pub enum InvalidToolName {
650    /// The tool name is empty.
651    #[error("tool name is empty")]
652    Empty,
653
654    /// The tool name is not in the correct Unicode format.
655    #[error("invalid tool name `{0}`")]
656    InvalidXid(SmolStr),
657
658    /// The tool name starts with "@tool", which is reserved for tool identifiers.
659    #[error("tool name cannot start with \"@tool\": `{0}`")]
660    StartsWithToolPrefix(SmolStr),
661}
662
663/// The name of a test group is invalid (not a valid identifier).
664#[derive(Clone, Debug, Error)]
665#[error("invalid custom test group name: {0}")]
666pub struct InvalidCustomTestGroupName(pub InvalidIdentifier);
667
668/// The name of a configuration script is invalid (not a valid identifier).
669#[derive(Clone, Debug, Error)]
670#[error("invalid configuration script name: {0}")]
671pub struct InvalidConfigScriptName(pub InvalidIdentifier);
672
673/// Error returned while parsing a [`ToolConfigFile`](crate::config::core::ToolConfigFile) value.
674#[derive(Clone, Debug, Error, PartialEq, Eq)]
675pub enum ToolConfigFileParseError {
676    #[error(
677        "tool-config-file has invalid format: {input}\n(hint: tool configs must be in the format <tool-name>:<path>)"
678    )]
679    /// The input was not in the format "tool:path".
680    InvalidFormat {
681        /// The input that failed to parse.
682        input: String,
683    },
684
685    /// The tool name was invalid.
686    #[error("tool-config-file has invalid tool name: {input}")]
687    InvalidToolName {
688        /// The input that failed to parse.
689        input: String,
690
691        /// The error that occurred.
692        #[source]
693        error: InvalidToolName,
694    },
695
696    /// The config file path was empty.
697    #[error("tool-config-file has empty config file path: {input}")]
698    EmptyConfigFile {
699        /// The input that failed to parse.
700        input: String,
701    },
702
703    /// The config file was not an absolute path.
704    #[error("tool-config-file is not an absolute path: {config_file}")]
705    ConfigFileNotAbsolute {
706        /// The file name that wasn't absolute.
707        config_file: Utf8PathBuf,
708    },
709}
710
711/// Errors that can occur while loading user config.
712#[derive(Debug, Error)]
713#[non_exhaustive]
714pub enum UserConfigError {
715    /// Failed to read the user config file.
716    #[error("failed to read user config at {path}")]
717    Read {
718        /// The path to the config file.
719        path: Utf8PathBuf,
720        /// The underlying I/O error.
721        #[source]
722        error: std::io::Error,
723    },
724
725    /// Failed to parse the user config file.
726    #[error("failed to parse user config at {path}")]
727    Parse {
728        /// The path to the config file.
729        path: Utf8PathBuf,
730        /// The underlying TOML parse error.
731        #[source]
732        error: toml::de::Error,
733    },
734
735    /// The user config path contains non-UTF-8 characters.
736    #[error("user config path contains non-UTF-8 characters")]
737    NonUtf8Path {
738        /// The underlying error from path conversion.
739        #[source]
740        error: FromPathBufError,
741    },
742
743    /// Failed to compile a platform spec in an override.
744    #[error(
745        "for user config at {path}, failed to compile platform spec in [[overrides]] at index {index}"
746    )]
747    OverridePlatformSpec {
748        /// The path to the config file.
749        path: Utf8PathBuf,
750        /// The index of the override in the array.
751        index: usize,
752        /// The underlying target-spec error.
753        #[source]
754        error: target_spec::Error,
755    },
756}
757
758/// Error returned while parsing a [`MaxFail`](crate::config::elements::MaxFail) input.
759#[derive(Clone, Debug, Error)]
760#[error("unrecognized value for max-fail: {reason}")]
761pub struct MaxFailParseError {
762    /// The reason parsing failed.
763    pub reason: String,
764}
765
766impl MaxFailParseError {
767    pub(crate) fn new(reason: impl Into<String>) -> Self {
768        Self {
769            reason: reason.into(),
770        }
771    }
772}
773
774/// Error returned while parsing a [`StressCount`](crate::runner::StressCount) input.
775#[derive(Clone, Debug, Error)]
776#[error(
777    "unrecognized value for stress-count: {input}\n\
778     (hint: expected either a positive integer or \"infinite\")"
779)]
780pub struct StressCountParseError {
781    /// The input that failed to parse.
782    pub input: String,
783}
784
785impl StressCountParseError {
786    pub(crate) fn new(input: impl Into<String>) -> Self {
787        Self {
788            input: input.into(),
789        }
790    }
791}
792
793/// An error that occurred while parsing a debugger command.
794#[derive(Clone, Debug, Error)]
795#[non_exhaustive]
796pub enum DebuggerCommandParseError {
797    /// The command string could not be parsed as shell words.
798    #[error(transparent)]
799    ShellWordsParse(shell_words::ParseError),
800
801    /// The command was empty.
802    #[error("debugger command cannot be empty")]
803    EmptyCommand,
804}
805
806/// An error that occurred while parsing a tracer command.
807#[derive(Clone, Debug, Error)]
808#[non_exhaustive]
809pub enum TracerCommandParseError {
810    /// The command string could not be parsed as shell words.
811    #[error(transparent)]
812    ShellWordsParse(shell_words::ParseError),
813
814    /// The command was empty.
815    #[error("tracer command cannot be empty")]
816    EmptyCommand,
817}
818
819/// Error returned while parsing a [`TestThreads`](crate::config::elements::TestThreads) value.
820#[derive(Clone, Debug, Error)]
821#[error(
822    "unrecognized value for test-threads: {input}\n(hint: expected either an integer or \"num-cpus\")"
823)]
824pub struct TestThreadsParseError {
825    /// The input that failed to parse.
826    pub input: String,
827}
828
829impl TestThreadsParseError {
830    pub(crate) fn new(input: impl Into<String>) -> Self {
831        Self {
832            input: input.into(),
833        }
834    }
835}
836
837/// An error that occurs while parsing a
838/// [`PartitionerBuilder`](crate::partition::PartitionerBuilder) input.
839#[derive(Clone, Debug, Error)]
840pub struct PartitionerBuilderParseError {
841    expected_format: Option<&'static str>,
842    message: Cow<'static, str>,
843}
844
845impl PartitionerBuilderParseError {
846    pub(crate) fn new(
847        expected_format: Option<&'static str>,
848        message: impl Into<Cow<'static, str>>,
849    ) -> Self {
850        Self {
851            expected_format,
852            message: message.into(),
853        }
854    }
855}
856
857impl fmt::Display for PartitionerBuilderParseError {
858    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
859        match self.expected_format {
860            Some(format) => {
861                write!(
862                    f,
863                    "partition must be in the format \"{}\":\n{}",
864                    format, self.message
865                )
866            }
867            None => write!(f, "{}", self.message),
868        }
869    }
870}
871
872/// An error that occurs while operating on a
873/// [`TestFilterBuilder`](crate::test_filter::TestFilterBuilder).
874#[derive(Clone, Debug, Error)]
875pub enum TestFilterBuilderError {
876    /// An error that occurred while constructing test filters.
877    #[error("error constructing test filters")]
878    Construct {
879        /// The underlying error.
880        #[from]
881        error: aho_corasick::BuildError,
882    },
883}
884
885/// An error occurred in [`PathMapper::new`](crate::reuse_build::PathMapper::new).
886#[derive(Debug, Error)]
887pub enum PathMapperConstructError {
888    /// An error occurred while canonicalizing a directory.
889    #[error("{kind} `{input}` failed to canonicalize")]
890    Canonicalization {
891        /// The directory that failed to be canonicalized.
892        kind: PathMapperConstructKind,
893
894        /// The input provided.
895        input: Utf8PathBuf,
896
897        /// The error that occurred.
898        #[source]
899        err: std::io::Error,
900    },
901    /// The canonicalized path isn't valid UTF-8.
902    #[error("{kind} `{input}` canonicalized to a non-UTF-8 path")]
903    NonUtf8Path {
904        /// The directory that failed to be canonicalized.
905        kind: PathMapperConstructKind,
906
907        /// The input provided.
908        input: Utf8PathBuf,
909
910        /// The underlying error.
911        #[source]
912        err: FromPathBufError,
913    },
914    /// A provided input is not a directory.
915    #[error("{kind} `{canonicalized_path}` is not a directory")]
916    NotADirectory {
917        /// The directory that failed to be canonicalized.
918        kind: PathMapperConstructKind,
919
920        /// The input provided.
921        input: Utf8PathBuf,
922
923        /// The canonicalized path that wasn't a directory.
924        canonicalized_path: Utf8PathBuf,
925    },
926}
927
928impl PathMapperConstructError {
929    /// The kind of directory.
930    pub fn kind(&self) -> PathMapperConstructKind {
931        match self {
932            Self::Canonicalization { kind, .. }
933            | Self::NonUtf8Path { kind, .. }
934            | Self::NotADirectory { kind, .. } => *kind,
935        }
936    }
937
938    /// The input path that failed.
939    pub fn input(&self) -> &Utf8Path {
940        match self {
941            Self::Canonicalization { input, .. }
942            | Self::NonUtf8Path { input, .. }
943            | Self::NotADirectory { input, .. } => input,
944        }
945    }
946}
947
948/// The kind of directory that failed to be read in
949/// [`PathMapper::new`](crate::reuse_build::PathMapper::new).
950///
951/// Returned as part of [`PathMapperConstructError`].
952#[derive(Copy, Clone, Debug, PartialEq, Eq)]
953pub enum PathMapperConstructKind {
954    /// The workspace root.
955    WorkspaceRoot,
956
957    /// The target directory.
958    TargetDir,
959}
960
961impl fmt::Display for PathMapperConstructKind {
962    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
963        match self {
964            Self::WorkspaceRoot => write!(f, "remapped workspace root"),
965            Self::TargetDir => write!(f, "remapped target directory"),
966        }
967    }
968}
969
970/// An error that occurs while parsing Rust build metadata from a summary.
971#[derive(Debug, Error)]
972pub enum RustBuildMetaParseError {
973    /// An error occurred while deserializing the platform.
974    #[error("error deserializing platform from build metadata")]
975    PlatformDeserializeError(#[from] target_spec::Error),
976
977    /// The host platform could not be determined.
978    #[error("the host platform could not be determined")]
979    DetectBuildTargetError(#[source] target_spec::Error),
980
981    /// The build metadata includes features unsupported.
982    #[error("unsupported features in the build metadata: {message}")]
983    Unsupported {
984        /// The detailed error message.
985        message: String,
986    },
987}
988
989/// Error returned when a user-supplied format version fails to be parsed to a
990/// valid and supported version.
991#[derive(Clone, Debug, thiserror::Error)]
992#[error("invalid format version: {input}")]
993pub struct FormatVersionError {
994    /// The input that failed to parse.
995    pub input: String,
996    /// The underlying error.
997    #[source]
998    pub error: FormatVersionErrorInner,
999}
1000
1001/// The different errors that can occur when parsing and validating a format version.
1002#[derive(Clone, Debug, thiserror::Error)]
1003pub enum FormatVersionErrorInner {
1004    /// The input did not have a valid syntax.
1005    #[error("expected format version in form of `{expected}`")]
1006    InvalidFormat {
1007        /// The expected pseudo format.
1008        expected: &'static str,
1009    },
1010    /// A decimal integer was expected but could not be parsed.
1011    #[error("version component `{which}` could not be parsed as an integer")]
1012    InvalidInteger {
1013        /// Which component was invalid.
1014        which: &'static str,
1015        /// The parse failure.
1016        #[source]
1017        err: std::num::ParseIntError,
1018    },
1019    /// The version component was not within the expected range.
1020    #[error("version component `{which}` value {value} is out of range {range:?}")]
1021    InvalidValue {
1022        /// The component which was out of range.
1023        which: &'static str,
1024        /// The value that was parsed.
1025        value: u8,
1026        /// The range of valid values for the component.
1027        range: std::ops::Range<u8>,
1028    },
1029}
1030
1031/// An error that occurs in [`BinaryList::from_messages`](crate::list::BinaryList::from_messages) or
1032/// [`RustTestArtifact::from_binary_list`](crate::list::RustTestArtifact::from_binary_list).
1033#[derive(Debug, Error)]
1034#[non_exhaustive]
1035pub enum FromMessagesError {
1036    /// An error occurred while reading Cargo's JSON messages.
1037    #[error("error reading Cargo JSON messages")]
1038    ReadMessages(#[source] std::io::Error),
1039
1040    /// An error occurred while querying the package graph.
1041    #[error("error querying package graph")]
1042    PackageGraph(#[source] guppy::Error),
1043
1044    /// A target in the package graph was missing `kind` information.
1045    #[error("missing kind for target {binary_name} in package {package_name}")]
1046    MissingTargetKind {
1047        /// The name of the malformed package.
1048        package_name: String,
1049        /// The name of the malformed target within the package.
1050        binary_name: String,
1051    },
1052}
1053
1054/// An error that occurs while parsing test list output.
1055#[derive(Debug, Error)]
1056#[non_exhaustive]
1057pub enum CreateTestListError {
1058    /// The proposed cwd for a process is not a directory.
1059    #[error(
1060        "for `{binary_id}`, current directory `{cwd}` is not a directory\n\
1061         (hint: ensure project source is available at this location)"
1062    )]
1063    CwdIsNotDir {
1064        /// The binary ID for which the current directory wasn't found.
1065        binary_id: RustBinaryId,
1066
1067        /// The current directory that wasn't found.
1068        cwd: Utf8PathBuf,
1069    },
1070
1071    /// Running a command to gather the list of tests failed to execute.
1072    #[error(
1073        "for `{binary_id}`, running command `{}` failed to execute",
1074        shell_words::join(command)
1075    )]
1076    CommandExecFail {
1077        /// The binary ID for which gathering the list of tests failed.
1078        binary_id: RustBinaryId,
1079
1080        /// The command that was run.
1081        command: Vec<String>,
1082
1083        /// The underlying error.
1084        #[source]
1085        error: std::io::Error,
1086    },
1087
1088    /// Running a command to gather the list of tests failed failed with a non-zero exit code.
1089    #[error(
1090        "for `{binary_id}`, command `{}` {}\n--- stdout:\n{}\n--- stderr:\n{}\n---",
1091        shell_words::join(command),
1092        display_exited_with(*exit_status),
1093        String::from_utf8_lossy(stdout),
1094        String::from_utf8_lossy(stderr),
1095    )]
1096    CommandFail {
1097        /// The binary ID for which gathering the list of tests failed.
1098        binary_id: RustBinaryId,
1099
1100        /// The command that was run.
1101        command: Vec<String>,
1102
1103        /// The exit status with which the command failed.
1104        exit_status: ExitStatus,
1105
1106        /// Standard output for the command.
1107        stdout: Vec<u8>,
1108
1109        /// Standard error for the command.
1110        stderr: Vec<u8>,
1111    },
1112
1113    /// Running a command to gather the list of tests produced a non-UTF-8 standard output.
1114    #[error(
1115        "for `{binary_id}`, command `{}` produced non-UTF-8 output:\n--- stdout:\n{}\n--- stderr:\n{}\n---",
1116        shell_words::join(command),
1117        String::from_utf8_lossy(stdout),
1118        String::from_utf8_lossy(stderr)
1119    )]
1120    CommandNonUtf8 {
1121        /// The binary ID for which gathering the list of tests failed.
1122        binary_id: RustBinaryId,
1123
1124        /// The command that was run.
1125        command: Vec<String>,
1126
1127        /// Standard output for the command.
1128        stdout: Vec<u8>,
1129
1130        /// Standard error for the command.
1131        stderr: Vec<u8>,
1132    },
1133
1134    /// An error occurred while parsing a line in the test output.
1135    #[error("for `{binary_id}`, {message}\nfull output:\n{full_output}")]
1136    ParseLine {
1137        /// The binary ID for which parsing the list of tests failed.
1138        binary_id: RustBinaryId,
1139
1140        /// A descriptive message.
1141        message: Cow<'static, str>,
1142
1143        /// The full output.
1144        full_output: String,
1145    },
1146
1147    /// An error occurred while joining paths for dynamic libraries.
1148    #[error(
1149        "error joining dynamic library paths for {}: [{}]",
1150        dylib_path_envvar(),
1151        itertools::join(.new_paths, ", ")
1152    )]
1153    DylibJoinPaths {
1154        /// New paths attempted to be added to the dynamic library environment variable.
1155        new_paths: Vec<Utf8PathBuf>,
1156
1157        /// The underlying error.
1158        #[source]
1159        error: JoinPathsError,
1160    },
1161
1162    /// Creating a Tokio runtime failed.
1163    #[error("error creating Tokio runtime")]
1164    TokioRuntimeCreate(#[source] std::io::Error),
1165}
1166
1167impl CreateTestListError {
1168    pub(crate) fn parse_line(
1169        binary_id: RustBinaryId,
1170        message: impl Into<Cow<'static, str>>,
1171        full_output: impl Into<String>,
1172    ) -> Self {
1173        Self::ParseLine {
1174            binary_id,
1175            message: message.into(),
1176            full_output: full_output.into(),
1177        }
1178    }
1179
1180    pub(crate) fn dylib_join_paths(new_paths: Vec<Utf8PathBuf>, error: JoinPathsError) -> Self {
1181        Self::DylibJoinPaths { new_paths, error }
1182    }
1183}
1184
1185/// An error that occurs while writing list output.
1186#[derive(Debug, Error)]
1187#[non_exhaustive]
1188pub enum WriteTestListError {
1189    /// An error occurred while writing the list to the provided output.
1190    #[error("error writing to output")]
1191    Io(#[source] std::io::Error),
1192
1193    /// An error occurred while serializing JSON, or while writing it to the provided output.
1194    #[error("error serializing to JSON")]
1195    Json(#[source] serde_json::Error),
1196}
1197
1198/// An error occurred while configuring handles.
1199///
1200/// Only relevant on Windows.
1201#[derive(Debug, Error)]
1202pub enum ConfigureHandleInheritanceError {
1203    /// An error occurred. This can only happen on Windows.
1204    #[cfg(windows)]
1205    #[error("error configuring handle inheritance")]
1206    WindowsError(#[from] std::io::Error),
1207}
1208
1209/// An error that occurs while building the test runner.
1210#[derive(Debug, Error)]
1211#[non_exhaustive]
1212pub enum TestRunnerBuildError {
1213    /// An error occurred while creating the Tokio runtime.
1214    #[error("error creating Tokio runtime")]
1215    TokioRuntimeCreate(#[source] std::io::Error),
1216
1217    /// An error occurred while setting up signals.
1218    #[error("error setting up signals")]
1219    SignalHandlerSetupError(#[from] SignalHandlerSetupError),
1220}
1221
1222/// Errors that occurred while managing test runner Tokio tasks.
1223#[derive(Debug, Error)]
1224pub struct TestRunnerExecuteErrors<E> {
1225    /// An error that occurred while reporting results to the reporter callback.
1226    pub report_error: Option<E>,
1227
1228    /// Join errors (typically panics) that occurred while running the test
1229    /// runner.
1230    pub join_errors: Vec<tokio::task::JoinError>,
1231}
1232
1233impl<E: std::error::Error> fmt::Display for TestRunnerExecuteErrors<E> {
1234    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1235        if let Some(report_error) = &self.report_error {
1236            write!(f, "error reporting results: {report_error}")?;
1237        }
1238
1239        if !self.join_errors.is_empty() {
1240            if self.report_error.is_some() {
1241                write!(f, "; ")?;
1242            }
1243
1244            write!(f, "errors joining tasks: ")?;
1245
1246            for (i, join_error) in self.join_errors.iter().enumerate() {
1247                if i > 0 {
1248                    write!(f, ", ")?;
1249                }
1250
1251                write!(f, "{join_error}")?;
1252            }
1253        }
1254
1255        Ok(())
1256    }
1257}
1258
1259/// Represents an unknown archive format.
1260///
1261/// Returned by [`ArchiveFormat::autodetect`].
1262#[derive(Debug, Error)]
1263#[error(
1264    "could not detect archive format from file name `{file_name}` (supported extensions: {})",
1265    supported_extensions()
1266)]
1267pub struct UnknownArchiveFormat {
1268    /// The name of the archive file without any leading components.
1269    pub file_name: String,
1270}
1271
1272fn supported_extensions() -> String {
1273    ArchiveFormat::SUPPORTED_FORMATS
1274        .iter()
1275        .map(|(extension, _)| *extension)
1276        .join(", ")
1277}
1278
1279/// An error that occurs while archiving data.
1280#[derive(Debug, Error)]
1281#[non_exhaustive]
1282pub enum ArchiveCreateError {
1283    /// An error occurred while creating the binary list to be written.
1284    #[error("error creating binary list")]
1285    CreateBinaryList(#[source] WriteTestListError),
1286
1287    /// An extra path was missing.
1288    #[error("extra path `{}` not found", .redactor.redact_path(path))]
1289    MissingExtraPath {
1290        /// The path that was missing.
1291        path: Utf8PathBuf,
1292
1293        /// A redactor for the path.
1294        ///
1295        /// (This should eventually move to being a field for a wrapper struct, but it's okay for
1296        /// now.)
1297        redactor: Redactor,
1298    },
1299
1300    /// An error occurred while reading data from a file on disk.
1301    #[error("while archiving {step}, error writing {} `{path}` to archive", kind_str(*.is_dir))]
1302    InputFileRead {
1303        /// The step that the archive errored at.
1304        step: ArchiveStep,
1305
1306        /// The name of the file that could not be read.
1307        path: Utf8PathBuf,
1308
1309        /// Whether this is a directory. `None` means the status was unknown.
1310        is_dir: Option<bool>,
1311
1312        /// The error that occurred.
1313        #[source]
1314        error: std::io::Error,
1315    },
1316
1317    /// An error occurred while reading entries from a directory on disk.
1318    #[error("error reading directory entry from `{path}")]
1319    DirEntryRead {
1320        /// The name of the directory from which entries couldn't be read.
1321        path: Utf8PathBuf,
1322
1323        /// The error that occurred.
1324        #[source]
1325        error: std::io::Error,
1326    },
1327
1328    /// An error occurred while writing data to the output file.
1329    #[error("error writing to archive")]
1330    OutputArchiveIo(#[source] std::io::Error),
1331
1332    /// An error occurred in the reporter.
1333    #[error("error reporting archive status")]
1334    ReporterIo(#[source] std::io::Error),
1335}
1336
1337fn kind_str(is_dir: Option<bool>) -> &'static str {
1338    match is_dir {
1339        Some(true) => "directory",
1340        Some(false) => "file",
1341        None => "path",
1342    }
1343}
1344
1345/// An error occurred while materializing a metadata path.
1346#[derive(Debug, Error)]
1347pub enum MetadataMaterializeError {
1348    /// An I/O error occurred while reading the metadata file.
1349    #[error("I/O error reading metadata file `{path}`")]
1350    Read {
1351        /// The path that was being read.
1352        path: Utf8PathBuf,
1353
1354        /// The error that occurred.
1355        #[source]
1356        error: std::io::Error,
1357    },
1358
1359    /// A JSON deserialization error occurred while reading the metadata file.
1360    #[error("error deserializing metadata file `{path}`")]
1361    Deserialize {
1362        /// The path that was being read.
1363        path: Utf8PathBuf,
1364
1365        /// The error that occurred.
1366        #[source]
1367        error: serde_json::Error,
1368    },
1369
1370    /// An error occurred while parsing Rust build metadata.
1371    #[error("error parsing Rust build metadata from `{path}`")]
1372    RustBuildMeta {
1373        /// The path that was deserialized.
1374        path: Utf8PathBuf,
1375
1376        /// The error that occurred.
1377        #[source]
1378        error: RustBuildMetaParseError,
1379    },
1380
1381    /// An error occurred converting data into a `PackageGraph`.
1382    #[error("error building package graph from `{path}`")]
1383    PackageGraphConstruct {
1384        /// The path that was deserialized.
1385        path: Utf8PathBuf,
1386
1387        /// The error that occurred.
1388        #[source]
1389        error: guppy::Error,
1390    },
1391}
1392
1393/// An error occurred while reading a file.
1394///
1395/// Returned as part of both [`ArchiveCreateError`] and [`ArchiveExtractError`].
1396#[derive(Debug, Error)]
1397#[non_exhaustive]
1398pub enum ArchiveReadError {
1399    /// An I/O error occurred while reading the archive.
1400    #[error("I/O error reading archive")]
1401    Io(#[source] std::io::Error),
1402
1403    /// A path wasn't valid UTF-8.
1404    #[error("path in archive `{}` wasn't valid UTF-8", String::from_utf8_lossy(.0))]
1405    NonUtf8Path(Vec<u8>),
1406
1407    /// A file path within the archive didn't begin with "target/".
1408    #[error("path in archive `{0}` doesn't start with `target/`")]
1409    NoTargetPrefix(Utf8PathBuf),
1410
1411    /// A file path within the archive had an invalid component within it.
1412    #[error("path in archive `{path}` contains an invalid component `{component}`")]
1413    InvalidComponent {
1414        /// The path that had an invalid component.
1415        path: Utf8PathBuf,
1416
1417        /// The invalid component.
1418        component: String,
1419    },
1420
1421    /// An error occurred while reading a checksum.
1422    #[error("corrupted archive: checksum read error for path `{path}`")]
1423    ChecksumRead {
1424        /// The path for which there was a checksum read error.
1425        path: Utf8PathBuf,
1426
1427        /// The error that occurred.
1428        #[source]
1429        error: std::io::Error,
1430    },
1431
1432    /// An entry had an invalid checksum.
1433    #[error("corrupted archive: invalid checksum for path `{path}`")]
1434    InvalidChecksum {
1435        /// The path that had an invalid checksum.
1436        path: Utf8PathBuf,
1437
1438        /// The expected checksum.
1439        expected: u32,
1440
1441        /// The actual checksum.
1442        actual: u32,
1443    },
1444
1445    /// A metadata file wasn't found.
1446    #[error("metadata file `{0}` not found in archive")]
1447    MetadataFileNotFound(&'static Utf8Path),
1448
1449    /// An error occurred while deserializing a metadata file.
1450    #[error("error deserializing metadata file `{path}` in archive")]
1451    MetadataDeserializeError {
1452        /// The name of the metadata file.
1453        path: &'static Utf8Path,
1454
1455        /// The deserialize error.
1456        #[source]
1457        error: serde_json::Error,
1458    },
1459
1460    /// An error occurred while building a `PackageGraph`.
1461    #[error("error building package graph from `{path}` in archive")]
1462    PackageGraphConstructError {
1463        /// The name of the metadata file.
1464        path: &'static Utf8Path,
1465
1466        /// The error.
1467        #[source]
1468        error: guppy::Error,
1469    },
1470}
1471
1472/// An error occurred while extracting a file.
1473///
1474/// Returned by [`extract_archive`](crate::reuse_build::ReuseBuildInfo::extract_archive).
1475#[derive(Debug, Error)]
1476#[non_exhaustive]
1477pub enum ArchiveExtractError {
1478    /// An error occurred while creating a temporary directory.
1479    #[error("error creating temporary directory")]
1480    TempDirCreate(#[source] std::io::Error),
1481
1482    /// An error occurred while canonicalizing the destination directory.
1483    #[error("error canonicalizing destination directory `{dir}`")]
1484    DestDirCanonicalization {
1485        /// The directory that failed to canonicalize.
1486        dir: Utf8PathBuf,
1487
1488        /// The error that occurred.
1489        #[source]
1490        error: std::io::Error,
1491    },
1492
1493    /// The destination already exists and `--overwrite` was not passed in.
1494    #[error("destination `{0}` already exists")]
1495    DestinationExists(Utf8PathBuf),
1496
1497    /// An error occurred while reading the archive.
1498    #[error("error reading archive")]
1499    Read(#[source] ArchiveReadError),
1500
1501    /// An error occurred while deserializing Rust build metadata.
1502    #[error("error deserializing Rust build metadata")]
1503    RustBuildMeta(#[from] RustBuildMetaParseError),
1504
1505    /// An error occurred while writing out a file to the destination directory.
1506    #[error("error writing file `{path}` to disk")]
1507    WriteFile {
1508        /// The path that we couldn't write out.
1509        path: Utf8PathBuf,
1510
1511        /// The error that occurred.
1512        #[source]
1513        error: std::io::Error,
1514    },
1515
1516    /// An error occurred while reporting the extraction status.
1517    #[error("error reporting extract status")]
1518    ReporterIo(std::io::Error),
1519}
1520
1521/// An error that occurs while writing an event.
1522#[derive(Debug, Error)]
1523#[non_exhaustive]
1524pub enum WriteEventError {
1525    /// An error occurred while writing the event to the provided output.
1526    #[error("error writing to output")]
1527    Io(#[source] std::io::Error),
1528
1529    /// An error occurred while operating on the file system.
1530    #[error("error operating on path {file}")]
1531    Fs {
1532        /// The file being operated on.
1533        file: Utf8PathBuf,
1534
1535        /// The underlying IO error.
1536        #[source]
1537        error: std::io::Error,
1538    },
1539
1540    /// An error occurred while producing JUnit XML.
1541    #[error("error writing JUnit output to {file}")]
1542    Junit {
1543        /// The output file.
1544        file: Utf8PathBuf,
1545
1546        /// The underlying error.
1547        #[source]
1548        error: quick_junit::SerializeError,
1549    },
1550}
1551
1552/// An error occurred while constructing a [`CargoConfigs`](crate::cargo_config::CargoConfigs)
1553/// instance.
1554#[derive(Debug, Error)]
1555#[non_exhaustive]
1556pub enum CargoConfigError {
1557    /// Failed to retrieve the current directory.
1558    #[error("failed to retrieve current directory")]
1559    GetCurrentDir(#[source] std::io::Error),
1560
1561    /// The current directory was invalid UTF-8.
1562    #[error("current directory is invalid UTF-8")]
1563    CurrentDirInvalidUtf8(#[source] FromPathBufError),
1564
1565    /// Parsing a CLI config option failed.
1566    #[error("failed to parse --config argument `{config_str}` as TOML")]
1567    CliConfigParseError {
1568        /// The CLI config option.
1569        config_str: String,
1570
1571        /// The error that occurred trying to parse the config.
1572        #[source]
1573        error: toml_edit::TomlError,
1574    },
1575
1576    /// Deserializing a CLI config option into domain types failed.
1577    #[error("failed to deserialize --config argument `{config_str}` as TOML")]
1578    CliConfigDeError {
1579        /// The CLI config option.
1580        config_str: String,
1581
1582        /// The error that occurred trying to deserialize the config.
1583        #[source]
1584        error: toml_edit::de::Error,
1585    },
1586
1587    /// A CLI config option is not in the dotted key format.
1588    #[error(
1589        "invalid format for --config argument `{config_str}` (should be a dotted key expression)"
1590    )]
1591    InvalidCliConfig {
1592        /// The CLI config option.
1593        config_str: String,
1594
1595        /// The reason why this Cargo CLI config is invalid.
1596        #[source]
1597        reason: InvalidCargoCliConfigReason,
1598    },
1599
1600    /// A non-UTF-8 path was encountered.
1601    #[error("non-UTF-8 path encountered")]
1602    NonUtf8Path(#[source] FromPathBufError),
1603
1604    /// Failed to retrieve the Cargo home directory.
1605    #[error("failed to retrieve the Cargo home directory")]
1606    GetCargoHome(#[source] std::io::Error),
1607
1608    /// Failed to canonicalize a path
1609    #[error("failed to canonicalize path `{path}")]
1610    FailedPathCanonicalization {
1611        /// The path that failed to canonicalize
1612        path: Utf8PathBuf,
1613
1614        /// The error the occurred during canonicalization
1615        #[source]
1616        error: std::io::Error,
1617    },
1618
1619    /// Failed to read config file
1620    #[error("failed to read config at `{path}`")]
1621    ConfigReadError {
1622        /// The path of the config file
1623        path: Utf8PathBuf,
1624
1625        /// The error that occurred trying to read the config file
1626        #[source]
1627        error: std::io::Error,
1628    },
1629
1630    /// Failed to deserialize config file
1631    #[error(transparent)]
1632    ConfigParseError(#[from] Box<CargoConfigParseError>),
1633}
1634
1635/// Failed to deserialize config file
1636///
1637/// We introduce this extra indirection, because of the `clippy::result_large_err` rule on Windows.
1638#[derive(Debug, Error)]
1639#[error("failed to parse config at `{path}`")]
1640pub struct CargoConfigParseError {
1641    /// The path of the config file
1642    pub path: Utf8PathBuf,
1643
1644    /// The error that occurred trying to deserialize the config file
1645    #[source]
1646    pub error: toml::de::Error,
1647}
1648
1649/// The reason an invalid CLI config failed.
1650///
1651/// Part of [`CargoConfigError::InvalidCliConfig`].
1652#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)]
1653#[non_exhaustive]
1654pub enum InvalidCargoCliConfigReason {
1655    /// The argument is not a TOML dotted key expression.
1656    #[error("was not a TOML dotted key expression (such as `build.jobs = 2`)")]
1657    NotDottedKv,
1658
1659    /// The argument includes non-whitespace decoration.
1660    #[error("includes non-whitespace decoration")]
1661    IncludesNonWhitespaceDecoration,
1662
1663    /// The argument sets a value to an inline table.
1664    #[error("sets a value to an inline table, which is not accepted")]
1665    SetsValueToInlineTable,
1666
1667    /// The argument sets a value to an array of tables.
1668    #[error("sets a value to an array of tables, which is not accepted")]
1669    SetsValueToArrayOfTables,
1670
1671    /// The argument doesn't provide a value.
1672    #[error("doesn't provide a value")]
1673    DoesntProvideValue,
1674}
1675
1676/// The host platform couldn't be detected.
1677#[derive(Debug, Error)]
1678pub enum HostPlatformDetectError {
1679    /// Spawning `rustc -vV` failed, and detecting the build target failed as
1680    /// well.
1681    #[error(
1682        "error spawning `rustc -vV`, and detecting the build \
1683         target failed as well\n\
1684         - rustc spawn error: {}\n\
1685         - build target error: {}\n",
1686        DisplayErrorChain::new_with_initial_indent("  ", error),
1687        DisplayErrorChain::new_with_initial_indent("  ", build_target_error)
1688    )]
1689    RustcVvSpawnError {
1690        /// The error.
1691        error: std::io::Error,
1692
1693        /// The error that occurred while detecting the build target.
1694        build_target_error: Box<target_spec::Error>,
1695    },
1696
1697    /// `rustc -vV` exited with a non-zero code, and detecting the build target
1698    /// failed as well.
1699    #[error(
1700        "`rustc -vV` failed with {}, and detecting the \
1701         build target failed as well\n\
1702         - `rustc -vV` stdout:\n{}\n\
1703         - `rustc -vV` stderr:\n{}\n\
1704         - build target error:\n{}\n",
1705        status,
1706        DisplayIndented { item: String::from_utf8_lossy(stdout), indent: "  " },
1707        DisplayIndented { item: String::from_utf8_lossy(stderr), indent: "  " },
1708        DisplayErrorChain::new_with_initial_indent("  ", build_target_error)
1709    )]
1710    RustcVvFailed {
1711        /// The status.
1712        status: ExitStatus,
1713
1714        /// The standard output from `rustc -vV`.
1715        stdout: Vec<u8>,
1716
1717        /// The standard error from `rustc -vV`.
1718        stderr: Vec<u8>,
1719
1720        /// The error that occurred while detecting the build target.
1721        build_target_error: Box<target_spec::Error>,
1722    },
1723
1724    /// Parsing the host platform failed, and detecting the build target failed
1725    /// as well.
1726    #[error(
1727        "parsing `rustc -vV` output failed, and detecting the build target \
1728         failed as well\n\
1729         - host platform error:\n{}\n\
1730         - build target error:\n{}\n",
1731        DisplayErrorChain::new_with_initial_indent("  ", host_platform_error),
1732        DisplayErrorChain::new_with_initial_indent("  ", build_target_error)
1733    )]
1734    HostPlatformParseError {
1735        /// The error that occurred while parsing the host platform.
1736        host_platform_error: Box<target_spec::Error>,
1737
1738        /// The error that occurred while detecting the build target.
1739        build_target_error: Box<target_spec::Error>,
1740    },
1741
1742    /// Test-only code: `rustc -vV` was not queried, and detecting the build
1743    /// target failed as well.
1744    #[error("test-only code, so `rustc -vV` was not called; failed to detect build target")]
1745    BuildTargetError {
1746        /// The error that occurred while detecting the build target.
1747        #[source]
1748        build_target_error: Box<target_spec::Error>,
1749    },
1750}
1751
1752/// An error occurred while determining the cross-compiling target triple.
1753#[derive(Debug, Error)]
1754pub enum TargetTripleError {
1755    /// The environment variable contained non-utf8 content
1756    #[error(
1757        "environment variable '{}' contained non-UTF-8 data",
1758        TargetTriple::CARGO_BUILD_TARGET_ENV
1759    )]
1760    InvalidEnvironmentVar,
1761
1762    /// An error occurred while deserializing the platform.
1763    #[error("error deserializing target triple from {source}")]
1764    TargetSpecError {
1765        /// The source from which the triple couldn't be parsed.
1766        source: TargetTripleSource,
1767
1768        /// The error that occurred parsing the triple.
1769        #[source]
1770        error: target_spec::Error,
1771    },
1772
1773    /// For a custom platform, reading the target path failed.
1774    #[error("target path `{path}` is not a valid file")]
1775    TargetPathReadError {
1776        /// The source from which the triple couldn't be parsed.
1777        source: TargetTripleSource,
1778
1779        /// The path that we tried to read.
1780        path: Utf8PathBuf,
1781
1782        /// The error that occurred parsing the triple.
1783        #[source]
1784        error: std::io::Error,
1785    },
1786
1787    /// Failed to create a temporary directory for a custom platform.
1788    #[error(
1789        "for custom platform obtained from {source}, \
1790         failed to create temporary directory for custom platform"
1791    )]
1792    CustomPlatformTempDirError {
1793        /// The source of the target triple.
1794        source: TargetTripleSource,
1795
1796        /// The error that occurred during the create.
1797        #[source]
1798        error: std::io::Error,
1799    },
1800
1801    /// Failed to write a custom platform to disk.
1802    #[error(
1803        "for custom platform obtained from {source}, \
1804         failed to write JSON to temporary path `{path}`"
1805    )]
1806    CustomPlatformWriteError {
1807        /// The source of the target triple.
1808        source: TargetTripleSource,
1809
1810        /// The path that we tried to write to.
1811        path: Utf8PathBuf,
1812
1813        /// The error that occurred during the write.
1814        #[source]
1815        error: std::io::Error,
1816    },
1817
1818    /// Failed to close a temporary directory for an extracted custom platform.
1819    #[error(
1820        "for custom platform obtained from {source}, \
1821         failed to close temporary directory `{dir_path}`"
1822    )]
1823    CustomPlatformCloseError {
1824        /// The source of the target triple.
1825        source: TargetTripleSource,
1826
1827        /// The directory that we tried to delete.
1828        dir_path: Utf8PathBuf,
1829
1830        /// The error that occurred during the close.
1831        #[source]
1832        error: std::io::Error,
1833    },
1834}
1835
1836impl TargetTripleError {
1837    /// Returns a [`miette::Report`] for the source, if available.
1838    ///
1839    /// This should be preferred over [`std::error::Error::source`] if
1840    /// available.
1841    pub fn source_report(&self) -> Option<miette::Report> {
1842        match self {
1843            Self::TargetSpecError { error, .. } => {
1844                Some(miette::Report::new_boxed(error.clone().into_diagnostic()))
1845            }
1846            // The remaining types are covered via the error source path.
1847            TargetTripleError::InvalidEnvironmentVar
1848            | TargetTripleError::TargetPathReadError { .. }
1849            | TargetTripleError::CustomPlatformTempDirError { .. }
1850            | TargetTripleError::CustomPlatformWriteError { .. }
1851            | TargetTripleError::CustomPlatformCloseError { .. } => None,
1852        }
1853    }
1854}
1855
1856/// An error occurred determining the target runner
1857#[derive(Debug, Error)]
1858pub enum TargetRunnerError {
1859    /// An environment variable contained non-utf8 content
1860    #[error("environment variable '{0}' contained non-UTF-8 data")]
1861    InvalidEnvironmentVar(String),
1862
1863    /// An environment variable or config key was found that matches the target
1864    /// triple, but it didn't actually contain a binary
1865    #[error("runner '{key}' = '{value}' did not contain a runner binary")]
1866    BinaryNotSpecified {
1867        /// The source under consideration.
1868        key: PlatformRunnerSource,
1869
1870        /// The value that was read from the key
1871        value: String,
1872    },
1873}
1874
1875/// An error that occurred while setting up the signal handler.
1876#[derive(Debug, Error)]
1877#[error("error setting up signal handler")]
1878pub struct SignalHandlerSetupError(#[from] std::io::Error);
1879
1880/// An error occurred while showing test groups.
1881#[derive(Debug, Error)]
1882pub enum ShowTestGroupsError {
1883    /// Unknown test groups were specified.
1884    #[error(
1885        "unknown test groups specified: {}\n(known groups: {})",
1886        unknown_groups.iter().join(", "),
1887        known_groups.iter().join(", "),
1888    )]
1889    UnknownGroups {
1890        /// The unknown test groups.
1891        unknown_groups: BTreeSet<TestGroup>,
1892
1893        /// All known test groups.
1894        known_groups: BTreeSet<TestGroup>,
1895    },
1896}
1897
1898/// An error occurred while processing profile's inherits setting
1899#[derive(Debug, Error, PartialEq, Eq, Hash)]
1900pub enum InheritsError {
1901    /// The default profile should not be able to inherit from other profiles
1902    #[error("the {} profile should not inherit from other profiles", .0)]
1903    DefaultProfileInheritance(String),
1904    /// An unknown/unfound profile was detected to inherit from in profile configuration
1905    #[error("profile {} inherits from an unknown profile {}", .0, .1)]
1906    UnknownInheritance(String, String),
1907    /// A self referential inheritance is detected from this profile
1908    #[error("a self referential inheritance is detected from profile: {}", .0)]
1909    SelfReferentialInheritance(String),
1910    /// An inheritance cycle was detected in the profile configuration.
1911    #[error("inheritance cycle detected in profile configuration from: {}", .0.iter().map(|scc| {
1912        format!("[{}]", scc.iter().join(", "))
1913    }).join(", "))]
1914    InheritanceCycle(Vec<Vec<String>>),
1915}
1916
1917#[cfg(feature = "self-update")]
1918mod self_update_errors {
1919    use super::*;
1920    use crate::update::PrereleaseKind;
1921    use mukti_metadata::ReleaseStatus;
1922    use semver::{Version, VersionReq};
1923
1924    /// An error that occurs while performing a self-update.
1925    ///
1926    /// Returned by methods in the [`update`](crate::update) module.
1927    #[derive(Debug, Error)]
1928    #[non_exhaustive]
1929    pub enum UpdateError {
1930        /// Failed to read release metadata from a local path on disk.
1931        #[error("failed to read release metadata from `{path}`")]
1932        ReadLocalMetadata {
1933            /// The path that was read.
1934            path: Utf8PathBuf,
1935
1936            /// The error that occurred.
1937            #[source]
1938            error: std::io::Error,
1939        },
1940
1941        /// An error was generated by `self_update`.
1942        #[error("self-update failed")]
1943        SelfUpdate(#[source] self_update::errors::Error),
1944
1945        /// Deserializing release metadata failed.
1946        #[error("deserializing release metadata failed")]
1947        ReleaseMetadataDe(#[source] serde_json::Error),
1948
1949        /// This version was not found.
1950        #[error("version `{version}` not found (known versions: {})", known_versions(.known))]
1951        VersionNotFound {
1952            /// The version that wasn't found.
1953            version: Version,
1954
1955            /// A list of all known versions.
1956            known: Vec<(Version, ReleaseStatus)>,
1957        },
1958
1959        /// No version was found matching a requirement.
1960        #[error("no version found matching requirement `{req}`")]
1961        NoMatchForVersionReq {
1962            /// The version requirement that had no matches.
1963            req: VersionReq,
1964        },
1965
1966        /// No stable (non-prerelease) version was found.
1967        #[error("no stable version found")]
1968        NoStableVersion,
1969
1970        /// No version matching the requested prerelease kind was found.
1971        #[error("no version found matching {} channel", kind.description())]
1972        NoVersionForPrereleaseKind {
1973            /// The kind of prerelease that was requested.
1974            kind: PrereleaseKind,
1975        },
1976
1977        /// The specified mukti project was not found.
1978        #[error("project {not_found} not found in release metadata (known projects: {})", known.join(", "))]
1979        MuktiProjectNotFound {
1980            /// The project that was not found.
1981            not_found: String,
1982
1983            /// Known projects.
1984            known: Vec<String>,
1985        },
1986
1987        /// No release information was found for the given target triple.
1988        #[error(
1989            "for version {version}, no release information found for target `{triple}` \
1990            (known targets: {})",
1991            known_triples.iter().join(", ")
1992        )]
1993        NoTargetData {
1994            /// The version that was fetched.
1995            version: Version,
1996
1997            /// The target triple.
1998            triple: String,
1999
2000            /// The triples that were found.
2001            known_triples: BTreeSet<String>,
2002        },
2003
2004        /// The current executable could not be determined.
2005        #[error("the current executable's path could not be determined")]
2006        CurrentExe(#[source] std::io::Error),
2007
2008        /// A temporary directory could not be created.
2009        #[error("temporary directory could not be created at `{location}`")]
2010        TempDirCreate {
2011            /// The location where the temporary directory could not be created.
2012            location: Utf8PathBuf,
2013
2014            /// The error that occurred.
2015            #[source]
2016            error: std::io::Error,
2017        },
2018
2019        /// The temporary archive could not be created.
2020        #[error("temporary archive could not be created at `{archive_path}`")]
2021        TempArchiveCreate {
2022            /// The archive file that couldn't be created.
2023            archive_path: Utf8PathBuf,
2024
2025            /// The error that occurred.
2026            #[source]
2027            error: std::io::Error,
2028        },
2029
2030        /// An error occurred while writing to a temporary archive.
2031        #[error("error writing to temporary archive at `{archive_path}`")]
2032        TempArchiveWrite {
2033            /// The archive path for which there was an error.
2034            archive_path: Utf8PathBuf,
2035
2036            /// The error that occurred.
2037            #[source]
2038            error: std::io::Error,
2039        },
2040
2041        /// An error occurred while reading from a temporary archive.
2042        #[error("error reading from temporary archive at `{archive_path}`")]
2043        TempArchiveRead {
2044            /// The archive path for which there was an error.
2045            archive_path: Utf8PathBuf,
2046
2047            /// The error that occurred.
2048            #[source]
2049            error: std::io::Error,
2050        },
2051
2052        /// A checksum mismatch occurred. (Currently, the SHA-256 checksum is checked.)
2053        #[error("SHA-256 checksum mismatch: expected: {expected}, actual: {actual}")]
2054        ChecksumMismatch {
2055            /// The expected checksum.
2056            expected: String,
2057
2058            /// The actual checksum.
2059            actual: String,
2060        },
2061
2062        /// An error occurred while renaming a file.
2063        #[error("error renaming `{source}` to `{dest}`")]
2064        FsRename {
2065            /// The rename source.
2066            source: Utf8PathBuf,
2067
2068            /// The rename destination.
2069            dest: Utf8PathBuf,
2070
2071            /// The error that occurred.
2072            #[source]
2073            error: std::io::Error,
2074        },
2075
2076        /// An error occurred while running `cargo nextest self setup`.
2077        #[error("cargo-nextest binary updated, but error running `cargo nextest self setup`")]
2078        SelfSetup(#[source] std::io::Error),
2079    }
2080
2081    fn known_versions(versions: &[(Version, ReleaseStatus)]) -> String {
2082        use std::fmt::Write;
2083
2084        // Take the first few versions here.
2085        const DISPLAY_COUNT: usize = 4;
2086
2087        let display_versions: Vec<_> = versions
2088            .iter()
2089            .filter(|(v, status)| v.pre.is_empty() && *status == ReleaseStatus::Active)
2090            .map(|(v, _)| v.to_string())
2091            .take(DISPLAY_COUNT)
2092            .collect();
2093        let mut display_str = display_versions.join(", ");
2094        if versions.len() > display_versions.len() {
2095            write!(
2096                display_str,
2097                " and {} others",
2098                versions.len() - display_versions.len()
2099            )
2100            .unwrap();
2101        }
2102
2103        display_str
2104    }
2105
2106    /// An error occurred while parsing an [`UpdateVersion`](crate::update::UpdateVersion).
2107    #[derive(Debug, Error)]
2108    pub enum UpdateVersionParseError {
2109        /// The version string is empty.
2110        #[error("version string is empty")]
2111        EmptyString,
2112
2113        /// The input is not a valid version requirement.
2114        #[error(
2115            "`{input}` is not a valid semver requirement\n\
2116                (hint: see https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html for the correct format)"
2117        )]
2118        InvalidVersionReq {
2119            /// The input that was provided.
2120            input: String,
2121
2122            /// The error.
2123            #[source]
2124            error: semver::Error,
2125        },
2126
2127        /// The version is not a valid semver.
2128        #[error("`{input}` is not a valid semver{}", extra_semver_output(.input))]
2129        InvalidVersion {
2130            /// The input that was provided.
2131            input: String,
2132
2133            /// The error.
2134            #[source]
2135            error: semver::Error,
2136        },
2137    }
2138
2139    fn extra_semver_output(input: &str) -> String {
2140        // If it is not a valid version but it is a valid version
2141        // requirement, add a note to the warning
2142        if input.parse::<VersionReq>().is_ok() {
2143            format!(
2144                "\n(if you want to specify a semver range, add an explicit qualifier, like ^{input})"
2145            )
2146        } else {
2147            "".to_owned()
2148        }
2149    }
2150}
2151
2152#[cfg(feature = "self-update")]
2153pub use self_update_errors::*;
2154
2155#[cfg(test)]
2156mod tests {
2157    use super::*;
2158
2159    #[test]
2160    fn display_error_chain() {
2161        let err1 = StringError::new("err1", None);
2162
2163        insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&err1)), @"err1");
2164
2165        let err2 = StringError::new("err2", Some(err1));
2166        let err3 = StringError::new("err3\nerr3 line 2", Some(err2));
2167
2168        insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&err3)), @r"
2169        err3
2170        err3 line 2
2171          caused by:
2172          - err2
2173          - err1
2174        ");
2175    }
2176
2177    #[test]
2178    fn display_error_list() {
2179        let err1 = StringError::new("err1", None);
2180
2181        let error_list =
2182            ErrorList::<StringError>::new("waiting on the water to boil", vec![err1.clone()])
2183                .expect(">= 1 error");
2184        insta::assert_snapshot!(format!("{}", error_list), @"err1");
2185        insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @"err1");
2186
2187        let err2 = StringError::new("err2", Some(err1));
2188        let err3 = StringError::new("err3", Some(err2));
2189
2190        let error_list =
2191            ErrorList::<StringError>::new("waiting on flowers to bloom", vec![err3.clone()])
2192                .expect(">= 1 error");
2193        insta::assert_snapshot!(format!("{}", error_list), @"err3");
2194        insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @r"
2195        err3
2196          caused by:
2197          - err2
2198          - err1
2199        ");
2200
2201        let err4 = StringError::new("err4", None);
2202        let err5 = StringError::new("err5", Some(err4));
2203        let err6 = StringError::new("err6\nerr6 line 2", Some(err5));
2204
2205        let error_list = ErrorList::<StringError>::new(
2206            "waiting for the heat death of the universe",
2207            vec![err3, err6],
2208        )
2209        .expect(">= 1 error");
2210
2211        insta::assert_snapshot!(format!("{}", error_list), @r"
2212        2 errors occurred waiting for the heat death of the universe:
2213        * err3
2214            caused by:
2215            - err2
2216            - err1
2217        * err6
2218          err6 line 2
2219            caused by:
2220            - err5
2221            - err4
2222        ");
2223        insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @r"
2224        2 errors occurred waiting for the heat death of the universe:
2225        * err3
2226            caused by:
2227            - err2
2228            - err1
2229        * err6
2230          err6 line 2
2231            caused by:
2232            - err5
2233            - err4
2234        ");
2235    }
2236
2237    #[derive(Clone, Debug, Error)]
2238    struct StringError {
2239        message: String,
2240        #[source]
2241        source: Option<Box<StringError>>,
2242    }
2243
2244    impl StringError {
2245        fn new(message: impl Into<String>, source: Option<StringError>) -> Self {
2246            Self {
2247                message: message.into(),
2248                source: source.map(Box::new),
2249            }
2250        }
2251    }
2252
2253    impl fmt::Display for StringError {
2254        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2255            write!(f, "{}", self.message)
2256        }
2257    }
2258}