nextest_runner/
errors.rs

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