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