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