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