use crate::{
cargo_config::{TargetTriple, TargetTripleSource},
config::{
core::{ConfigExperimental, ToolName},
elements::{CustomTestGroup, TestGroup},
scripts::{ProfileScriptType, ScriptId, ScriptType},
},
helpers::{display_exited_with, dylib_path_envvar, plural},
indenter::{DisplayIndented, indented},
record::{
PortableRecordingFormatVersion, PortableRecordingVersionIncompatibility, RecordedRunInfo,
RunIdIndex, RunsJsonFormatVersion, StoreFormatVersion, StoreVersionIncompatibility,
},
redact::{Redactor, SizeDisplay},
reuse_build::{ArchiveFormat, ArchiveStep},
target_runner::PlatformRunnerSource,
};
use bytesize::ByteSize;
use camino::{FromPathBufError, Utf8Path, Utf8PathBuf};
use config::ConfigError;
use eazip::CompressionMethod;
use etcetera::HomeDirError;
use itertools::{Either, Itertools};
use nextest_filtering::errors::FiltersetParseErrors;
use nextest_metadata::{RustBinaryId, TestCaseName};
use quick_junit::ReportUuid;
use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
use std::{
borrow::Cow,
collections::BTreeSet,
env::JoinPathsError,
fmt::{self, Write as _},
path::PathBuf,
process::ExitStatus,
sync::Arc,
};
use target_spec_miette::IntoMietteDiagnostic;
use thiserror::Error;
#[derive(Debug, Error)]
#[error(
"failed to parse nextest config at `{config_file}`{}",
provided_by_tool(tool.as_ref())
)]
#[non_exhaustive]
pub struct ConfigParseError {
config_file: Utf8PathBuf,
tool: Option<ToolName>,
#[source]
kind: ConfigParseErrorKind,
}
impl ConfigParseError {
pub(crate) fn new(
config_file: impl Into<Utf8PathBuf>,
tool: Option<&ToolName>,
kind: ConfigParseErrorKind,
) -> Self {
Self {
config_file: config_file.into(),
tool: tool.cloned(),
kind,
}
}
pub fn config_file(&self) -> &Utf8Path {
&self.config_file
}
pub fn tool(&self) -> Option<&ToolName> {
self.tool.as_ref()
}
pub fn kind(&self) -> &ConfigParseErrorKind {
&self.kind
}
}
pub fn provided_by_tool(tool: Option<&ToolName>) -> String {
match tool {
Some(tool) => format!(" provided by tool `{tool}`"),
None => String::new(),
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ConfigParseErrorKind {
#[error(transparent)]
BuildError(Box<ConfigError>),
#[error(transparent)]
TomlParseError(Box<toml::de::Error>),
#[error(transparent)]
DeserializeError(Box<serde_path_to_error::Error<ConfigError>>),
#[error(transparent)]
VersionOnlyReadError(std::io::Error),
#[error(transparent)]
VersionOnlyDeserializeError(Box<serde_path_to_error::Error<toml::de::Error>>),
#[error("error parsing compiled data (destructure this variant for more details)")]
CompileErrors(Vec<ConfigCompileError>),
#[error("invalid test groups defined: {}\n(test groups cannot start with '@tool:' unless specified by a tool)", .0.iter().join(", "))]
InvalidTestGroupsDefined(BTreeSet<CustomTestGroup>),
#[error(
"invalid test groups defined by tool: {}\n(test groups must start with '@tool:<tool-name>:')", .0.iter().join(", "))]
InvalidTestGroupsDefinedByTool(BTreeSet<CustomTestGroup>),
#[error("unknown test groups specified by config (destructure this variant for more details)")]
UnknownTestGroups {
errors: Vec<UnknownTestGroupError>,
known_groups: BTreeSet<TestGroup>,
},
#[error(
"both `[script.*]` and `[scripts.*]` defined\n\
(hint: [script.*] will be removed in the future: switch to [scripts.setup.*])"
)]
BothScriptAndScriptsDefined,
#[error("invalid config scripts defined: {}\n(config scripts cannot start with '@tool:' unless specified by a tool)", .0.iter().join(", "))]
InvalidConfigScriptsDefined(BTreeSet<ScriptId>),
#[error(
"invalid config scripts defined by tool: {}\n(config scripts must start with '@tool:<tool-name>:')", .0.iter().join(", "))]
InvalidConfigScriptsDefinedByTool(BTreeSet<ScriptId>),
#[error(
"config script names used more than once: {}\n\
(config script names must be unique across all script types)", .0.iter().join(", ")
)]
DuplicateConfigScriptNames(BTreeSet<ScriptId>),
#[error(
"errors in profile-specific config scripts (destructure this variant for more details)"
)]
ProfileScriptErrors {
errors: Box<ProfileScriptErrors>,
known_scripts: BTreeSet<ScriptId>,
},
#[error("unknown experimental features defined (destructure this variant for more details)")]
UnknownExperimentalFeatures {
unknown: BTreeSet<String>,
known: BTreeSet<ConfigExperimental>,
},
#[error(
"tool config file specifies experimental features `{}` \
-- only repository config files can do so",
.features.iter().join(", "),
)]
ExperimentalFeaturesInToolConfig {
features: BTreeSet<String>,
},
#[error("experimental features used but not enabled: {}", .missing_features.iter().join(", "))]
ExperimentalFeaturesNotEnabled {
missing_features: BTreeSet<ConfigExperimental>,
},
#[error("inheritance error(s) detected: {}", .0.iter().join(", "))]
InheritanceErrors(Vec<InheritsError>),
}
#[derive(Debug)]
#[non_exhaustive]
pub struct ConfigCompileError {
pub profile_name: String,
pub section: ConfigCompileSection,
pub kind: ConfigCompileErrorKind,
}
#[derive(Debug)]
pub enum ConfigCompileSection {
DefaultFilter,
Override(usize),
Script(usize),
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ConfigCompileErrorKind {
ConstraintsNotSpecified {
default_filter_specified: bool,
},
FilterAndDefaultFilterSpecified,
Parse {
host_parse_error: Option<target_spec::Error>,
target_parse_error: Option<target_spec::Error>,
filter_parse_errors: Vec<FiltersetParseErrors>,
},
}
impl ConfigCompileErrorKind {
pub fn reports(&self) -> impl Iterator<Item = miette::Report> + '_ {
match self {
Self::ConstraintsNotSpecified {
default_filter_specified,
} => {
let message = if *default_filter_specified {
"for override with `default-filter`, `platform` must also be specified"
} else {
"at least one of `platform` and `filter` must be specified"
};
Either::Left(std::iter::once(miette::Report::msg(message)))
}
Self::FilterAndDefaultFilterSpecified => {
Either::Left(std::iter::once(miette::Report::msg(
"at most one of `filter` and `default-filter` must be specified",
)))
}
Self::Parse {
host_parse_error,
target_parse_error,
filter_parse_errors,
} => {
let host_parse_report = host_parse_error
.as_ref()
.map(|error| miette::Report::new_boxed(error.clone().into_diagnostic()));
let target_parse_report = target_parse_error
.as_ref()
.map(|error| miette::Report::new_boxed(error.clone().into_diagnostic()));
let filter_parse_reports =
filter_parse_errors.iter().flat_map(|filter_parse_errors| {
filter_parse_errors.errors.iter().map(|single_error| {
miette::Report::new(single_error.clone())
.with_source_code(filter_parse_errors.input.to_owned())
})
});
Either::Right(
host_parse_report
.into_iter()
.chain(target_parse_report)
.chain(filter_parse_reports),
)
}
}
}
}
#[derive(Clone, Debug, Error)]
#[error("test priority ({priority}) out of range: must be between -100 and 100, both inclusive")]
pub struct TestPriorityOutOfRange {
pub priority: i8,
}
#[derive(Clone, Debug, Error)]
pub enum ChildStartError {
#[error("error creating temporary path for setup script")]
TempPath(#[source] Arc<std::io::Error>),
#[error("error spawning child process")]
Spawn(#[source] Arc<std::io::Error>),
}
#[derive(Clone, Debug, Error)]
pub enum SetupScriptOutputError {
#[error("error opening environment file `{path}`")]
EnvFileOpen {
path: Utf8PathBuf,
#[source]
error: Arc<std::io::Error>,
},
#[error("error reading environment file `{path}`")]
EnvFileRead {
path: Utf8PathBuf,
#[source]
error: Arc<std::io::Error>,
},
#[error("line `{line}` in environment file `{path}` not in KEY=VALUE format")]
EnvFileParse {
path: Utf8PathBuf,
line: String,
},
#[error("error in environment file `{path}`")]
EnvFileInvalidKey {
path: Utf8PathBuf,
#[source]
error: EnvVarError,
},
}
#[derive(Clone, Debug, Error)]
pub enum EnvVarError {
#[error("key `{key}` begins with `NEXTEST`, which is reserved for internal use")]
ReservedKey {
key: String,
},
#[error("key `{key}` does not consist solely of letters, digits, and underscores")]
InvalidKey {
key: String,
},
#[error("key `{key}` does not start with a letter or underscore")]
InvalidKeyStartChar {
key: String,
},
}
impl serde::de::Expected for EnvVarError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::ReservedKey { .. } => {
"a key that does not begin with `NEXTEST`, which is reserved for internal use"
}
Self::InvalidKey { .. } => {
"a key that consists solely of letters, digits, and underscores"
}
Self::InvalidKeyStartChar { .. } => "a key that starts with a letter or underscore",
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ErrorList<T> {
description: Cow<'static, str>,
inner: Vec<T>,
}
impl<T: std::error::Error> ErrorList<T> {
pub(crate) fn new<U>(description: &'static str, errors: Vec<U>) -> Option<Self>
where
T: From<U>,
{
if errors.is_empty() {
None
} else {
Some(Self {
description: Cow::Borrowed(description),
inner: errors.into_iter().map(T::from).collect(),
})
}
}
pub(crate) fn short_message(&self) -> String {
let string = self.to_string();
match string.lines().next() {
Some(first_line) => first_line.trim_end_matches(':').to_string(),
None => String::new(),
}
}
pub fn description(&self) -> &str {
&self.description
}
pub fn iter(&self) -> impl Iterator<Item = &T> {
self.inner.iter()
}
pub fn map<U, F>(self, f: F) -> ErrorList<U>
where
U: std::error::Error,
F: FnMut(T) -> U,
{
ErrorList {
description: self.description,
inner: self.inner.into_iter().map(f).collect(),
}
}
}
impl<T: std::error::Error> IntoIterator for ErrorList<T> {
type Item = T;
type IntoIter = std::vec::IntoIter<T>;
fn into_iter(self) -> Self::IntoIter {
self.inner.into_iter()
}
}
impl<T: std::error::Error> fmt::Display for ErrorList<T> {
fn fmt(&self, mut f: &mut fmt::Formatter) -> fmt::Result {
if self.inner.len() == 1 {
return write!(f, "{}", self.inner[0]);
}
writeln!(
f,
"{} errors occurred {}:",
self.inner.len(),
self.description,
)?;
for error in &self.inner {
let mut indent = indented(f).with_str(" ").skip_initial();
writeln!(indent, "* {}", DisplayErrorChain::new(error))?;
f = indent.into_inner();
}
Ok(())
}
}
#[cfg(test)]
impl<T: proptest::arbitrary::Arbitrary + std::fmt::Debug + 'static> proptest::arbitrary::Arbitrary
for ErrorList<T>
{
type Parameters = ();
type Strategy = proptest::strategy::BoxedStrategy<Self>;
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
use proptest::prelude::*;
proptest::collection::vec(any::<T>(), 1..=5)
.prop_map(|inner| ErrorList {
description: Cow::Borrowed("test errors"),
inner,
})
.boxed()
}
}
impl<T: std::error::Error> std::error::Error for ErrorList<T> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
if self.inner.len() == 1 {
self.inner[0].source()
} else {
None
}
}
}
pub struct DisplayErrorChain<E> {
error: E,
initial_indent: &'static str,
}
impl<E: std::error::Error> DisplayErrorChain<E> {
pub fn new(error: E) -> Self {
Self {
error,
initial_indent: "",
}
}
pub fn new_with_initial_indent(initial_indent: &'static str, error: E) -> Self {
Self {
error,
initial_indent,
}
}
}
impl<E> fmt::Display for DisplayErrorChain<E>
where
E: std::error::Error,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut writer = indented(f).with_str(self.initial_indent);
write!(writer, "{}", self.error)?;
let Some(mut cause) = self.error.source() else {
return Ok(());
};
write!(writer, "\n caused by:")?;
loop {
writeln!(writer)?;
let mut indent = indented(&mut writer).with_str(" ").skip_initial();
write!(indent, " - {cause}")?;
let Some(next_cause) = cause.source() else {
break Ok(());
};
cause = next_cause;
}
}
}
#[derive(Clone, Debug, Error)]
pub enum ChildError {
#[error(transparent)]
Fd(#[from] ChildFdError),
#[error(transparent)]
SetupScriptOutput(#[from] SetupScriptOutputError),
}
#[derive(Clone, Debug, Error)]
pub enum ChildFdError {
#[error("error reading standard output")]
ReadStdout(#[source] Arc<std::io::Error>),
#[error("error reading standard error")]
ReadStderr(#[source] Arc<std::io::Error>),
#[error("error reading combined stream")]
ReadCombined(#[source] Arc<std::io::Error>),
#[error("error waiting for child process to exit")]
Wait(#[source] Arc<std::io::Error>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct UnknownTestGroupError {
pub profile_name: String,
pub name: TestGroup,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProfileUnknownScriptError {
pub profile_name: String,
pub name: ScriptId,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProfileWrongConfigScriptTypeError {
pub profile_name: String,
pub name: ScriptId,
pub attempted: ProfileScriptType,
pub actual: ScriptType,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProfileListScriptUsesRunFiltersError {
pub profile_name: String,
pub name: ScriptId,
pub script_type: ProfileScriptType,
pub filters: BTreeSet<String>,
}
#[derive(Clone, Debug, Default)]
pub struct ProfileScriptErrors {
pub unknown_scripts: Vec<ProfileUnknownScriptError>,
pub wrong_script_types: Vec<ProfileWrongConfigScriptTypeError>,
pub list_scripts_using_run_filters: Vec<ProfileListScriptUsesRunFiltersError>,
}
impl ProfileScriptErrors {
pub fn is_empty(&self) -> bool {
self.unknown_scripts.is_empty()
&& self.wrong_script_types.is_empty()
&& self.list_scripts_using_run_filters.is_empty()
}
}
#[derive(Clone, Debug, Error)]
#[error("profile `{profile}` not found (known profiles: {})", .all_profiles.join(", "))]
pub struct ProfileNotFound {
profile: String,
all_profiles: Vec<String>,
}
impl ProfileNotFound {
pub(crate) fn new(
profile: impl Into<String>,
all_profiles: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
let mut all_profiles: Vec<_> = all_profiles.into_iter().map(|s| s.into()).collect();
all_profiles.sort_unstable();
Self {
profile: profile.into(),
all_profiles,
}
}
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum InvalidIdentifier {
#[error("identifier is empty")]
Empty,
#[error("invalid identifier `{0}`")]
InvalidXid(SmolStr),
#[error("tool identifier not of the form \"@tool:tool-name:identifier\": `{0}`")]
ToolIdentifierInvalidFormat(SmolStr),
#[error("tool identifier has empty component: `{0}`")]
ToolComponentEmpty(SmolStr),
#[error("invalid tool identifier `{0}`")]
ToolIdentifierInvalidXid(SmolStr),
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum InvalidToolName {
#[error("tool name is empty")]
Empty,
#[error("invalid tool name `{0}`")]
InvalidXid(SmolStr),
#[error("tool name cannot start with \"@tool\": `{0}`")]
StartsWithToolPrefix(SmolStr),
}
#[derive(Clone, Debug, Error)]
#[error("invalid custom test group name: {0}")]
pub struct InvalidCustomTestGroupName(pub InvalidIdentifier);
#[derive(Clone, Debug, Error)]
#[error("invalid configuration script name: {0}")]
pub struct InvalidConfigScriptName(pub InvalidIdentifier);
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum ToolConfigFileParseError {
#[error(
"tool-config-file has invalid format: {input}\n(hint: tool configs must be in the format <tool-name>:<path>)"
)]
InvalidFormat {
input: String,
},
#[error("tool-config-file has invalid tool name: {input}")]
InvalidToolName {
input: String,
#[source]
error: InvalidToolName,
},
#[error("tool-config-file has empty config file path: {input}")]
EmptyConfigFile {
input: String,
},
#[error("tool-config-file is not an absolute path: {config_file}")]
ConfigFileNotAbsolute {
config_file: Utf8PathBuf,
},
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum UserConfigError {
#[error("user config file not found at {path}")]
FileNotFound {
path: Utf8PathBuf,
},
#[error("failed to read user config at {path}")]
Read {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("failed to parse user config at {path}")]
Parse {
path: Utf8PathBuf,
#[source]
error: toml::de::Error,
},
#[error("user config path contains non-UTF-8 characters")]
NonUtf8Path {
#[source]
error: FromPathBufError,
},
#[error(
"for user config at {path}, failed to compile platform spec in [[overrides]] at index {index}"
)]
OverridePlatformSpec {
path: Utf8PathBuf,
index: usize,
#[source]
error: Box<target_spec::Error>,
},
}
#[derive(Clone, Debug, Error)]
#[error("unrecognized value for max-fail: {reason}")]
pub struct MaxFailParseError {
pub reason: String,
}
impl MaxFailParseError {
pub(crate) fn new(reason: impl Into<String>) -> Self {
Self {
reason: reason.into(),
}
}
}
#[derive(Clone, Debug, Error)]
#[error(
"unrecognized value for stress-count: {input}\n\
(hint: expected either a positive integer or \"infinite\")"
)]
pub struct StressCountParseError {
pub input: String,
}
impl StressCountParseError {
pub(crate) fn new(input: impl Into<String>) -> Self {
Self {
input: input.into(),
}
}
}
#[derive(Clone, Debug, Error)]
#[non_exhaustive]
pub enum DebuggerCommandParseError {
#[error(transparent)]
ShellWordsParse(shell_words::ParseError),
#[error("debugger command cannot be empty")]
EmptyCommand,
}
#[derive(Clone, Debug, Error)]
#[non_exhaustive]
pub enum TracerCommandParseError {
#[error(transparent)]
ShellWordsParse(shell_words::ParseError),
#[error("tracer command cannot be empty")]
EmptyCommand,
}
#[derive(Clone, Debug, Error)]
#[error(
"unrecognized value for test-threads: {input}\n(hint: expected either an integer or \"num-cpus\")"
)]
pub struct TestThreadsParseError {
pub input: String,
}
impl TestThreadsParseError {
pub(crate) fn new(input: impl Into<String>) -> Self {
Self {
input: input.into(),
}
}
}
#[derive(Clone, Debug, Error)]
pub struct PartitionerBuilderParseError {
expected_format: Option<&'static str>,
message: Cow<'static, str>,
}
impl PartitionerBuilderParseError {
pub(crate) fn new(
expected_format: Option<&'static str>,
message: impl Into<Cow<'static, str>>,
) -> Self {
Self {
expected_format,
message: message.into(),
}
}
}
impl fmt::Display for PartitionerBuilderParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.expected_format {
Some(format) => {
write!(
f,
"partition must be in the format \"{}\":\n{}",
format, self.message
)
}
None => write!(f, "{}", self.message),
}
}
}
#[derive(Clone, Debug, Error)]
pub enum TestFilterBuildError {
#[error("error constructing test filters")]
Construct {
#[from]
error: aho_corasick::BuildError,
},
}
#[derive(Debug, Error)]
pub enum PathMapperConstructError {
#[error("{kind} `{input}` failed to canonicalize")]
Canonicalization {
kind: PathMapperConstructKind,
input: Utf8PathBuf,
#[source]
err: std::io::Error,
},
#[error("{kind} `{input}` canonicalized to a non-UTF-8 path")]
NonUtf8Path {
kind: PathMapperConstructKind,
input: Utf8PathBuf,
#[source]
err: FromPathBufError,
},
#[error("{kind} `{canonicalized_path}` is not a directory")]
NotADirectory {
kind: PathMapperConstructKind,
input: Utf8PathBuf,
canonicalized_path: Utf8PathBuf,
},
}
impl PathMapperConstructError {
pub fn kind(&self) -> PathMapperConstructKind {
match self {
Self::Canonicalization { kind, .. }
| Self::NonUtf8Path { kind, .. }
| Self::NotADirectory { kind, .. } => *kind,
}
}
pub fn input(&self) -> &Utf8Path {
match self {
Self::Canonicalization { input, .. }
| Self::NonUtf8Path { input, .. }
| Self::NotADirectory { input, .. } => input,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PathMapperConstructKind {
WorkspaceRoot,
TargetDir,
BuildDir,
}
impl fmt::Display for PathMapperConstructKind {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::WorkspaceRoot => write!(f, "remapped workspace root"),
Self::TargetDir => write!(f, "remapped target directory"),
Self::BuildDir => write!(f, "remapped build directory"),
}
}
}
#[derive(Debug, Error)]
pub enum RustBuildMetaParseError {
#[error("error deserializing platform from build metadata")]
PlatformDeserializeError(#[from] target_spec::Error),
#[error("the host platform could not be determined")]
DetectBuildTargetError(#[source] target_spec::Error),
#[error("unsupported features in the build metadata: {message}")]
Unsupported {
message: String,
},
}
#[derive(Clone, Debug, thiserror::Error)]
#[error("invalid format version: {input}")]
pub struct FormatVersionError {
pub input: String,
#[source]
pub error: FormatVersionErrorInner,
}
#[derive(Clone, Debug, thiserror::Error)]
pub enum FormatVersionErrorInner {
#[error("expected format version in form of `{expected}`")]
InvalidFormat {
expected: &'static str,
},
#[error("version component `{which}` could not be parsed as an integer")]
InvalidInteger {
which: &'static str,
#[source]
err: std::num::ParseIntError,
},
#[error("version component `{which}` value {value} is out of range {range:?}")]
InvalidValue {
which: &'static str,
value: u8,
range: std::ops::Range<u8>,
},
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum FromMessagesError {
#[error("error reading Cargo JSON messages")]
ReadMessages(#[source] std::io::Error),
#[error("error querying package graph")]
PackageGraph(#[source] guppy::Error),
#[error("missing kind for target {binary_name} in package {package_name}")]
MissingTargetKind {
package_name: String,
binary_name: String,
},
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum CreateTestListError {
#[error(
"for `{binary_id}`, current directory `{cwd}` is not a directory\n\
(hint: ensure project source is available at this location)"
)]
CwdIsNotDir {
binary_id: RustBinaryId,
cwd: Utf8PathBuf,
},
#[error(
"for `{binary_id}`, running command `{}` failed to execute",
shell_words::join(command)
)]
CommandExecFail {
binary_id: RustBinaryId,
command: Vec<String>,
#[source]
error: std::io::Error,
},
#[error(
"for `{binary_id}`, command `{}` {}\n--- stdout:\n{}\n--- stderr:\n{}\n---",
shell_words::join(command),
display_exited_with(*exit_status),
String::from_utf8_lossy(stdout),
String::from_utf8_lossy(stderr),
)]
CommandFail {
binary_id: RustBinaryId,
command: Vec<String>,
exit_status: ExitStatus,
stdout: Vec<u8>,
stderr: Vec<u8>,
},
#[error(
"for `{binary_id}`, command `{}` produced non-UTF-8 output:\n--- stdout:\n{}\n--- stderr:\n{}\n---",
shell_words::join(command),
String::from_utf8_lossy(stdout),
String::from_utf8_lossy(stderr)
)]
CommandNonUtf8 {
binary_id: RustBinaryId,
command: Vec<String>,
stdout: Vec<u8>,
stderr: Vec<u8>,
},
#[error("for `{binary_id}`, {message}\nfull output:\n{full_output}")]
ParseLine {
binary_id: RustBinaryId,
message: Cow<'static, str>,
full_output: String,
},
#[error(
"error joining dynamic library paths for {}: [{}]",
dylib_path_envvar(),
itertools::join(.new_paths, ", ")
)]
DylibJoinPaths {
new_paths: Vec<Utf8PathBuf>,
#[source]
error: JoinPathsError,
},
#[error("error creating Tokio runtime")]
TokioRuntimeCreate(#[source] std::io::Error),
}
impl CreateTestListError {
pub(crate) fn parse_line(
binary_id: RustBinaryId,
message: impl Into<Cow<'static, str>>,
full_output: impl Into<String>,
) -> Self {
Self::ParseLine {
binary_id,
message: message.into(),
full_output: full_output.into(),
}
}
pub(crate) fn dylib_join_paths(new_paths: Vec<Utf8PathBuf>, error: JoinPathsError) -> Self {
Self::DylibJoinPaths { new_paths, error }
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum WriteTestListError {
#[error("error writing to output")]
Io(#[source] std::io::Error),
#[error("error serializing to JSON")]
Json(#[source] serde_json::Error),
}
#[derive(Debug, Error)]
pub enum ConfigureHandleInheritanceError {
#[cfg(windows)]
#[error("error configuring handle inheritance")]
WindowsError(#[from] std::io::Error),
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum TestRunnerBuildError {
#[error("error creating Tokio runtime")]
TokioRuntimeCreate(#[source] std::io::Error),
#[error("error setting up signals")]
SignalHandlerSetupError(#[from] SignalHandlerSetupError),
}
#[derive(Debug, Error)]
pub struct TestRunnerExecuteErrors<E> {
pub report_error: Option<E>,
pub join_errors: Vec<tokio::task::JoinError>,
}
impl<E: std::error::Error> fmt::Display for TestRunnerExecuteErrors<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(report_error) = &self.report_error {
write!(f, "error reporting results: {report_error}")?;
}
if !self.join_errors.is_empty() {
if self.report_error.is_some() {
write!(f, "; ")?;
}
write!(f, "errors joining tasks: ")?;
for (i, join_error) in self.join_errors.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{join_error}")?;
}
}
Ok(())
}
}
#[derive(Debug, Error)]
#[error(
"could not detect archive format from file name `{file_name}` (supported extensions: {})",
supported_extensions()
)]
pub struct UnknownArchiveFormat {
pub file_name: String,
}
fn supported_extensions() -> String {
ArchiveFormat::SUPPORTED_FORMATS
.iter()
.map(|(extension, _)| *extension)
.join(", ")
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ArchiveCreateError {
#[error("error creating binary list")]
CreateBinaryList(#[source] WriteTestListError),
#[error("extra path `{}` not found", .redactor.redact_path(path))]
MissingExtraPath {
path: Utf8PathBuf,
redactor: Redactor,
},
#[error("while archiving {step}, error writing {} `{path}` to archive", kind_str(*.is_dir))]
InputFileRead {
step: ArchiveStep,
path: Utf8PathBuf,
is_dir: Option<bool>,
#[source]
error: std::io::Error,
},
#[error("error reading directory entry from `{path}")]
DirEntryRead {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("error writing to archive")]
OutputArchiveIo(#[source] std::io::Error),
#[error("error reporting archive status")]
ReporterIo(#[source] std::io::Error),
}
fn kind_str(is_dir: Option<bool>) -> &'static str {
match is_dir {
Some(true) => "directory",
Some(false) => "file",
None => "path",
}
}
#[derive(Debug, Error)]
pub enum MetadataMaterializeError {
#[error("I/O error reading metadata file `{path}`")]
Read {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("error deserializing metadata file `{path}`")]
Deserialize {
path: Utf8PathBuf,
#[source]
error: serde_json::Error,
},
#[error("error parsing Rust build metadata from `{path}`")]
RustBuildMeta {
path: Utf8PathBuf,
#[source]
error: Box<RustBuildMetaParseError>,
},
#[error("error building package graph from `{path}`")]
PackageGraphConstruct {
path: Utf8PathBuf,
#[source]
error: Box<guppy::Error>,
},
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ArchiveReadError {
#[error("I/O error reading archive")]
Io(#[source] std::io::Error),
#[error("path in archive `{}` wasn't valid UTF-8", String::from_utf8_lossy(.0))]
NonUtf8Path(Vec<u8>),
#[error("path in archive `{0}` doesn't start with `target/`")]
NoTargetPrefix(Utf8PathBuf),
#[error("path in archive `{path}` contains an invalid component `{component}`")]
InvalidComponent {
path: Utf8PathBuf,
component: String,
},
#[error("corrupted archive: checksum read error for path `{path}`")]
ChecksumRead {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("corrupted archive: invalid checksum for path `{path}`")]
InvalidChecksum {
path: Utf8PathBuf,
expected: u32,
actual: u32,
},
#[error("metadata file `{0}` not found in archive")]
MetadataFileNotFound(&'static Utf8Path),
#[error("error deserializing metadata file `{path}` in archive")]
MetadataDeserializeError {
path: &'static Utf8Path,
#[source]
error: serde_json::Error,
},
#[error("error building package graph from `{path}` in archive")]
PackageGraphConstructError {
path: &'static Utf8Path,
#[source]
error: Box<guppy::Error>,
},
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ArchiveExtractError {
#[error("error creating temporary directory")]
TempDirCreate(#[source] std::io::Error),
#[error("error canonicalizing destination directory `{dir}`")]
DestDirCanonicalization {
dir: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("destination `{0}` already exists")]
DestinationExists(Utf8PathBuf),
#[error("error reading archive")]
Read(#[source] ArchiveReadError),
#[error("error deserializing Rust build metadata")]
RustBuildMeta(#[from] RustBuildMetaParseError),
#[error("error writing file `{path}` to disk")]
WriteFile {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("error reporting extract status")]
ReporterIo(std::io::Error),
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum WriteEventError {
#[error("error writing to output")]
Io(#[source] std::io::Error),
#[error("error operating on path {file}")]
Fs {
file: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("error writing JUnit output to {file}")]
Junit {
file: Utf8PathBuf,
#[source]
error: quick_junit::SerializeError,
},
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum CargoConfigError {
#[error("failed to retrieve current directory")]
GetCurrentDir(#[source] std::io::Error),
#[error("current directory is invalid UTF-8")]
CurrentDirInvalidUtf8(#[source] FromPathBufError),
#[error("failed to parse --config argument `{config_str}` as TOML")]
CliConfigParseError {
config_str: String,
#[source]
error: toml_edit::TomlError,
},
#[error("failed to deserialize --config argument `{config_str}` as TOML")]
CliConfigDeError {
config_str: String,
#[source]
error: toml_edit::de::Error,
},
#[error(
"invalid format for --config argument `{config_str}` (should be a dotted key expression)"
)]
InvalidCliConfig {
config_str: String,
#[source]
reason: InvalidCargoCliConfigReason,
},
#[error("non-UTF-8 path encountered")]
NonUtf8Path(#[source] FromPathBufError),
#[error("failed to retrieve the Cargo home directory")]
GetCargoHome(#[source] std::io::Error),
#[error("failed to canonicalize path `{path}")]
FailedPathCanonicalization {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("failed to read config at `{path}`")]
ConfigReadError {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error(transparent)]
ConfigParseError(#[from] Box<CargoConfigParseError>),
}
#[derive(Debug, Error)]
#[error("failed to parse config at `{path}`")]
pub struct CargoConfigParseError {
pub path: Utf8PathBuf,
#[source]
pub error: toml::de::Error,
}
#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)]
#[non_exhaustive]
pub enum InvalidCargoCliConfigReason {
#[error("was not a TOML dotted key expression (such as `build.jobs = 2`)")]
NotDottedKv,
#[error("includes non-whitespace decoration")]
IncludesNonWhitespaceDecoration,
#[error("sets a value to an inline table, which is not accepted")]
SetsValueToInlineTable,
#[error("sets a value to an array of tables, which is not accepted")]
SetsValueToArrayOfTables,
#[error("doesn't provide a value")]
DoesntProvideValue,
}
#[derive(Debug, Error)]
pub enum HostPlatformDetectError {
#[error(
"error spawning `rustc -vV`, and detecting the build \
target failed as well\n\
- rustc spawn error: {}\n\
- build target error: {}\n",
DisplayErrorChain::new_with_initial_indent(" ", error),
DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
)]
RustcVvSpawnError {
error: std::io::Error,
build_target_error: Box<target_spec::Error>,
},
#[error(
"`rustc -vV` failed with {}, and detecting the \
build target failed as well\n\
- `rustc -vV` stdout:\n{}\n\
- `rustc -vV` stderr:\n{}\n\
- build target error:\n{}\n",
status,
DisplayIndented { item: String::from_utf8_lossy(stdout), indent: " " },
DisplayIndented { item: String::from_utf8_lossy(stderr), indent: " " },
DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
)]
RustcVvFailed {
status: ExitStatus,
stdout: Vec<u8>,
stderr: Vec<u8>,
build_target_error: Box<target_spec::Error>,
},
#[error(
"parsing `rustc -vV` output failed, and detecting the build target \
failed as well\n\
- host platform error:\n{}\n\
- build target error:\n{}\n",
DisplayErrorChain::new_with_initial_indent(" ", host_platform_error),
DisplayErrorChain::new_with_initial_indent(" ", build_target_error)
)]
HostPlatformParseError {
host_platform_error: Box<target_spec::Error>,
build_target_error: Box<target_spec::Error>,
},
#[error("test-only code, so `rustc -vV` was not called; failed to detect build target")]
BuildTargetError {
#[source]
build_target_error: Box<target_spec::Error>,
},
}
#[derive(Debug, Error)]
pub enum TargetTripleError {
#[error(
"environment variable '{}' contained non-UTF-8 data",
TargetTriple::CARGO_BUILD_TARGET_ENV
)]
InvalidEnvironmentVar,
#[error("error deserializing target triple from {source}")]
TargetSpecError {
source: TargetTripleSource,
#[source]
error: target_spec::Error,
},
#[error("target path `{path}` is not a valid file")]
TargetPathReadError {
source: TargetTripleSource,
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error(
"for custom platform obtained from {source}, \
failed to create temporary directory for custom platform"
)]
CustomPlatformTempDirError {
source: TargetTripleSource,
#[source]
error: std::io::Error,
},
#[error(
"for custom platform obtained from {source}, \
failed to write JSON to temporary path `{path}`"
)]
CustomPlatformWriteError {
source: TargetTripleSource,
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error(
"for custom platform obtained from {source}, \
failed to close temporary directory `{dir_path}`"
)]
CustomPlatformCloseError {
source: TargetTripleSource,
dir_path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
}
impl TargetTripleError {
pub fn source_report(&self) -> Option<miette::Report> {
match self {
Self::TargetSpecError { error, .. } => {
Some(miette::Report::new_boxed(error.clone().into_diagnostic()))
}
TargetTripleError::InvalidEnvironmentVar
| TargetTripleError::TargetPathReadError { .. }
| TargetTripleError::CustomPlatformTempDirError { .. }
| TargetTripleError::CustomPlatformWriteError { .. }
| TargetTripleError::CustomPlatformCloseError { .. } => None,
}
}
}
#[derive(Debug, Error)]
pub enum TargetRunnerError {
#[error("environment variable '{0}' contained non-UTF-8 data")]
InvalidEnvironmentVar(String),
#[error("runner '{key}' = '{value}' did not contain a runner binary")]
BinaryNotSpecified {
key: PlatformRunnerSource,
value: String,
},
}
#[derive(Debug, Error)]
#[error("error setting up signal handler")]
pub struct SignalHandlerSetupError(#[from] std::io::Error);
#[derive(Debug, Error)]
pub enum ShowTestGroupsError {
#[error(
"unknown test groups specified: {}\n(known groups: {})",
unknown_groups.iter().join(", "),
known_groups.iter().join(", "),
)]
UnknownGroups {
unknown_groups: BTreeSet<TestGroup>,
known_groups: BTreeSet<TestGroup>,
},
}
#[derive(Debug, Error, PartialEq, Eq, Hash)]
pub enum InheritsError {
#[error("the {} profile should not inherit from other profiles", .0)]
DefaultProfileInheritance(String),
#[error("profile {} inherits from an unknown profile {}", .0, .1)]
UnknownInheritance(String, String),
#[error("a self referential inheritance is detected from profile: {}", .0)]
SelfReferentialInheritance(String),
#[error("inheritance cycle detected in profile configuration from: {}", .0.iter().map(|scc| {
format!("[{}]", scc.iter().join(", "))
}).join(", "))]
InheritanceCycle(Vec<Vec<String>>),
}
#[derive(Debug, Error)]
pub enum RunStoreError {
#[error("error creating run directory `{run_dir}`")]
RunDirCreate {
run_dir: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("error acquiring lock on `{path}`")]
FileLock {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error(
"timed out acquiring lock on `{path}` after {timeout_secs}s (is the state directory \
on a networked filesystem?)"
)]
FileLockTimeout {
path: Utf8PathBuf,
timeout_secs: u64,
},
#[error("error reading run list from `{path}`")]
RunListRead {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("error deserializing run list from `{path}`")]
RunListDeserialize {
path: Utf8PathBuf,
#[source]
error: serde_json::Error,
},
#[error("error serializing run list to `{path}`")]
RunListSerialize {
path: Utf8PathBuf,
#[source]
error: serde_json::Error,
},
#[error("error serializing rerun info")]
RerunInfoSerialize {
#[source]
error: serde_json::Error,
},
#[error("error serializing test list")]
TestListSerialize {
#[source]
error: serde_json::Error,
},
#[error("error serializing record options")]
RecordOptionsSerialize {
#[source]
error: serde_json::Error,
},
#[error("error serializing test event")]
TestEventSerialize {
#[source]
error: serde_json::Error,
},
#[error("error writing run list to `{path}`")]
RunListWrite {
path: Utf8PathBuf,
#[source]
error: atomicwrites::Error<std::io::Error>,
},
#[error("error writing to store at `{store_path}`")]
StoreWrite {
store_path: Utf8PathBuf,
#[source]
error: StoreWriterError,
},
#[error("error creating run log at `{path}`")]
RunLogCreate {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("error writing to run log at `{path}`")]
RunLogWrite {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("error flushing run log at `{path}`")]
RunLogFlush {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error(
"cannot write to record store: runs.json.zst format version {file_version} is newer than \
supported version {max_supported_version}"
)]
FormatVersionTooNew {
file_version: RunsJsonFormatVersion,
max_supported_version: RunsJsonFormatVersion,
},
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum StoreWriterError {
#[error("error creating store")]
Create {
#[source]
error: std::io::Error,
},
#[error("error writing to path `{path}` in store")]
Write {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("error compressing data")]
Compress {
#[source]
error: std::io::Error,
},
#[error("error finalizing store")]
Finish {
#[source]
error: std::io::Error,
},
#[error("error flushing store")]
Flush {
#[source]
error: std::io::Error,
},
}
#[derive(Debug, Error)]
pub enum RecordReporterError {
#[error(transparent)]
RunStore(RunStoreError),
#[error("record writer thread panicked: {message}")]
WriterPanic {
message: String,
},
}
#[derive(Debug, Error)]
pub enum StateDirError {
#[error("could not determine platform base directory strategy")]
BaseDirStrategy(#[source] HomeDirError),
#[error("platform state directory is not valid UTF-8: {path:?}")]
StateDirNotUtf8 {
path: PathBuf,
},
#[error("could not canonicalize workspace path `{workspace_root}`")]
Canonicalize {
workspace_root: Utf8PathBuf,
#[source]
error: std::io::Error,
},
}
#[derive(Debug, Error)]
pub enum RecordSetupError {
#[error("could not determine platform state directory for recording")]
StateDirNotFound(#[source] StateDirError),
#[error("failed to create run store")]
StoreCreate(#[source] RunStoreError),
#[error("failed to lock run store")]
StoreLock(#[source] RunStoreError),
#[error("failed to create run recorder")]
RecorderCreate(#[source] RunStoreError),
}
impl RecordSetupError {
pub fn disabled_error(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
RecordSetupError::RecorderCreate(err @ RunStoreError::FormatVersionTooNew { .. }) => {
Some(err)
}
_ => None,
}
}
}
#[derive(Debug, Error)]
pub enum RecordPruneError {
#[error("error deleting run `{run_id}` at `{path}`")]
DeleteRun {
run_id: ReportUuid,
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("error calculating size of `{path}`")]
CalculateSize {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("error deleting orphaned directory `{path}`")]
DeleteOrphan {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("error reading runs directory `{path}`")]
ReadRunsDir {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("error reading directory entry in `{dir}`")]
ReadDirEntry {
dir: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("error reading file type for `{path}`")]
ReadFileType {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Error)]
#[error("invalid run ID selector `{input}`: expected `latest` or hex digits")]
pub struct InvalidRunIdSelector {
pub input: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Error)]
#[error(
"invalid run ID selector `{input}`: expected `latest`, hex digits, \
or a file path (ending in `.zip` or containing path separators)"
)]
pub struct InvalidRunIdOrRecordingSelector {
pub input: String,
}
#[derive(Debug, Error)]
pub enum RunIdResolutionError {
#[error("no recorded run found matching `{prefix}`")]
NotFound {
prefix: String,
},
#[error("prefix `{prefix}` is ambiguous, matches {count} runs")]
Ambiguous {
prefix: String,
count: usize,
candidates: Vec<RecordedRunInfo>,
run_id_index: RunIdIndex,
},
#[error("prefix `{prefix}` contains invalid characters (expected hexadecimal)")]
InvalidPrefix {
prefix: String,
},
#[error("no recorded runs exist")]
NoRuns,
}
#[derive(Debug, Error)]
pub enum RecordReadError {
#[error("run not found at `{path}`")]
RunNotFound {
path: Utf8PathBuf,
},
#[error("error opening archive at `{path}`")]
OpenArchive {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("error parsing archive at `{path}`")]
ParseArchive {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("error reading `{file_name}` from archive")]
ReadArchiveFile {
file_name: String,
#[source]
error: std::io::Error,
},
#[error("error opening run log at `{path}`")]
OpenRunLog {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("error reading line {line_number} from run log")]
ReadRunLog {
line_number: usize,
#[source]
error: std::io::Error,
},
#[error("error parsing event at line {line_number}")]
ParseEvent {
line_number: usize,
#[source]
error: serde_json::Error,
},
#[error("required file `{file_name}` not found in archive")]
FileNotFound {
file_name: String,
},
#[error("error decompressing data from `{file_name}`")]
Decompress {
file_name: String,
#[source]
error: std::io::Error,
},
#[error(
"unknown output file type `{file_name}` in archive \
(archive may have been created by a newer version of nextest)"
)]
UnknownOutputType {
file_name: String,
},
#[error(
"file `{file_name}` in archive exceeds maximum size ({size} bytes, limit is {limit} bytes)"
)]
FileTooLarge {
file_name: String,
size: u64,
limit: u64,
},
#[error(
"file `{file_name}` size mismatch: header claims {claimed_size} bytes, \
but read {actual_size} bytes (archive may be corrupt or tampered)"
)]
SizeMismatch {
file_name: String,
claimed_size: u64,
actual_size: u64,
},
#[error("error deserializing `{file_name}`")]
DeserializeMetadata {
file_name: String,
#[source]
error: serde_json::Error,
},
#[error("failed to extract `{store_path}` to `{output_path}`")]
ExtractFile {
store_path: String,
output_path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("error reading portable recording")]
PortableRecording(#[source] PortableRecordingReadError),
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum PortableRecordingError {
#[error("run directory does not exist: {path}")]
RunDirNotFound {
path: Utf8PathBuf,
},
#[error("required file missing from run directory `{run_dir}`: `{file_name}`")]
RequiredFileMissing {
run_dir: Utf8PathBuf,
file_name: &'static str,
},
#[error("failed to serialize manifest")]
SerializeManifest(#[source] serde_json::Error),
#[error("failed to start file {file_name} in archive")]
ZipStartFile {
file_name: &'static str,
#[source]
source: std::io::Error,
},
#[error("failed to write {file_name} to archive")]
ZipWrite {
file_name: &'static str,
#[source]
source: std::io::Error,
},
#[error("failed to read {file_name}")]
ReadFile {
file_name: &'static str,
#[source]
source: std::io::Error,
},
#[error("failed to finalize archive")]
ZipFinalize(#[source] std::io::Error),
#[error("failed to write archive atomically to {path}")]
AtomicWrite {
path: Utf8PathBuf,
#[source]
source: std::io::Error,
},
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum PortableRecordingReadError {
#[error("failed to open archive at `{path}`")]
OpenArchive {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("failed to read archive at `{path}`")]
ReadArchive {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("required file `{file_name}` missing from archive at `{path}`")]
MissingFile {
path: Utf8PathBuf,
file_name: Cow<'static, str>,
},
#[error("failed to parse manifest from archive at `{path}`")]
ParseManifest {
path: Utf8PathBuf,
#[source]
error: serde_json::Error,
},
#[error(
"portable recording format version {found} in `{path}` is incompatible: {incompatibility} \
(this nextest supports version {supported})"
)]
UnsupportedFormatVersion {
path: Utf8PathBuf,
found: PortableRecordingFormatVersion,
supported: PortableRecordingFormatVersion,
incompatibility: PortableRecordingVersionIncompatibility,
},
#[error(
"store format version {found} in `{path}` is incompatible: {incompatibility} \
(this nextest supports version {supported})"
)]
UnsupportedStoreFormatVersion {
path: Utf8PathBuf,
found: StoreFormatVersion,
supported: StoreFormatVersion,
incompatibility: StoreVersionIncompatibility,
},
#[error(
"file `{file_name}` in archive `{path}` is too large \
({size} bytes, limit is {limit} bytes)"
)]
FileTooLarge {
path: Utf8PathBuf,
file_name: Cow<'static, str>,
size: u64,
limit: u64,
},
#[error("failed to extract `{file_name}` from archive `{archive_path}` to `{output_path}`")]
ExtractFile {
archive_path: Utf8PathBuf,
file_name: &'static str,
output_path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error(
"for portable recording `{archive_path}`, the inner archive is stored \
with {:?} compression -- it must be stored uncompressed",
compression
)]
CompressedInnerArchive {
archive_path: Utf8PathBuf,
compression: CompressionMethod,
},
#[error(
"archive at `{path}` has no manifest and is not a wrapper archive \
(contains {file_count} {}, {zip_count} of which {} in .zip)",
plural::files_str(*file_count),
plural::end_str(*zip_count)
)]
NotAWrapperArchive {
path: Utf8PathBuf,
file_count: usize,
zip_count: usize,
},
#[error("unexpected I/O error while probing seekability of `{path}`")]
SeekProbe {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("failed to spool non-seekable input `{path}` to a temporary file")]
SpoolTempFile {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error(
"recording at `{path}` exceeds the spool size limit \
({}); use a file path instead of process substitution",
SizeDisplay(.limit.0)
)]
SpoolTooLarge {
path: Utf8PathBuf,
limit: ByteSize,
},
}
#[derive(Debug, Error)]
pub enum ChromeTraceError {
#[error("error reading recorded events")]
ReadError(#[source] RecordReadError),
#[error(
"event for test `{test_name}` in binary `{binary_id}` \
has no prior TestStarted event (corrupt or truncated log?)"
)]
MissingTestStart {
test_name: TestCaseName,
binary_id: RustBinaryId,
},
#[error(
"SetupScriptSlow for script `{script_id}` \
has no prior SetupScriptStarted event (corrupt or truncated log?)"
)]
MissingScriptStart {
script_id: ScriptId,
},
#[error(
"StressSubRunFinished has no prior StressSubRunStarted event \
(corrupt or truncated log?)"
)]
MissingStressSubRunStart,
#[error("error serializing Chrome trace JSON")]
SerializeError(#[source] serde_json::Error),
}
#[derive(Debug, Error)]
pub enum TestListFromSummaryError {
#[error("package `{name}` (id: `{package_id}`) not found in cargo metadata")]
PackageNotFound {
name: String,
package_id: String,
},
#[error("error parsing rust build metadata")]
RustBuildMeta(#[source] RustBuildMetaParseError),
}
#[cfg(feature = "self-update")]
mod self_update_errors {
use super::*;
use crate::update::PrereleaseKind;
use mukti_metadata::ReleaseStatus;
use semver::{Version, VersionReq};
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum UpdateError {
#[error("failed to read release metadata from `{path}`")]
ReadLocalMetadata {
path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("self-update failed")]
SelfUpdate(#[source] self_update::errors::Error),
#[error("error performing HTTP request")]
Http(#[source] ureq::Error),
#[error("error reading HTTP response body")]
HttpBody(#[source] std::io::Error),
#[error("Content-Length header present but could not be parsed as an integer: {value:?}")]
ContentLengthInvalid {
value: String,
},
#[error("content length mismatch: expected {expected} bytes, received {actual} bytes")]
ContentLengthMismatch {
expected: u64,
actual: u64,
},
#[error("deserializing release metadata failed")]
ReleaseMetadataDe(#[source] serde_json::Error),
#[error("version `{version}` not found (known versions: {})", known_versions(.known))]
VersionNotFound {
version: Version,
known: Vec<(Version, ReleaseStatus)>,
},
#[error("no version found matching requirement `{req}`")]
NoMatchForVersionReq {
req: VersionReq,
},
#[error("no stable version found")]
NoStableVersion,
#[error("no version found matching {} channel", kind.description())]
NoVersionForPrereleaseKind {
kind: PrereleaseKind,
},
#[error("project {not_found} not found in release metadata (known projects: {})", known.join(", "))]
MuktiProjectNotFound {
not_found: String,
known: Vec<String>,
},
#[error(
"for version {version}, no release information found for target `{triple}` \
(known targets: {})",
known_triples.iter().join(", ")
)]
NoTargetData {
version: Version,
triple: String,
known_triples: BTreeSet<String>,
},
#[error("the current executable's path could not be determined")]
CurrentExe(#[source] std::io::Error),
#[error("temporary directory could not be created at `{location}`")]
TempDirCreate {
location: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("temporary archive could not be created at `{archive_path}`")]
TempArchiveCreate {
archive_path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("error writing to temporary archive at `{archive_path}`")]
TempArchiveWrite {
archive_path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("error reading from temporary archive at `{archive_path}`")]
TempArchiveRead {
archive_path: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("SHA-256 checksum mismatch: expected: {expected}, actual: {actual}")]
ChecksumMismatch {
expected: String,
actual: String,
},
#[error("error renaming `{source}` to `{dest}`")]
FsRename {
source: Utf8PathBuf,
dest: Utf8PathBuf,
#[source]
error: std::io::Error,
},
#[error("cargo-nextest binary updated, but error running `cargo nextest self setup`")]
SelfSetup(#[source] std::io::Error),
}
fn known_versions(versions: &[(Version, ReleaseStatus)]) -> String {
use std::fmt::Write;
const DISPLAY_COUNT: usize = 4;
let display_versions: Vec<_> = versions
.iter()
.filter(|(v, status)| v.pre.is_empty() && *status == ReleaseStatus::Active)
.map(|(v, _)| v.to_string())
.take(DISPLAY_COUNT)
.collect();
let mut display_str = display_versions.join(", ");
if versions.len() > display_versions.len() {
write!(
display_str,
" and {} others",
versions.len() - display_versions.len()
)
.unwrap();
}
display_str
}
#[derive(Debug, Error)]
pub enum UpdateVersionParseError {
#[error("version string is empty")]
EmptyString,
#[error(
"`{input}` is not a valid semver requirement\n\
(hint: see https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html for the correct format)"
)]
InvalidVersionReq {
input: String,
#[source]
error: semver::Error,
},
#[error("`{input}` is not a valid semver{}", extra_semver_output(.input))]
InvalidVersion {
input: String,
#[source]
error: semver::Error,
},
}
fn extra_semver_output(input: &str) -> String {
if input.parse::<VersionReq>().is_ok() {
format!(
"\n(if you want to specify a semver range, add an explicit qualifier, like ^{input})"
)
} else {
"".to_owned()
}
}
}
#[cfg(feature = "self-update")]
pub use self_update_errors::*;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_error_chain() {
let err1 = StringError::new("err1", None);
insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&err1)), @"err1");
let err2 = StringError::new("err2", Some(err1));
let err3 = StringError::new("err3\nerr3 line 2", Some(err2));
insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&err3)), @"
err3
err3 line 2
caused by:
- err2
- err1
");
}
#[test]
fn display_error_list() {
let err1 = StringError::new("err1", None);
let error_list =
ErrorList::<StringError>::new("waiting on the water to boil", vec![err1.clone()])
.expect(">= 1 error");
insta::assert_snapshot!(format!("{}", error_list), @"err1");
insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @"err1");
let err2 = StringError::new("err2", Some(err1));
let err3 = StringError::new("err3", Some(err2));
let error_list =
ErrorList::<StringError>::new("waiting on flowers to bloom", vec![err3.clone()])
.expect(">= 1 error");
insta::assert_snapshot!(format!("{}", error_list), @"err3");
insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @"
err3
caused by:
- err2
- err1
");
let err4 = StringError::new("err4", None);
let err5 = StringError::new("err5", Some(err4));
let err6 = StringError::new("err6\nerr6 line 2", Some(err5));
let error_list = ErrorList::<StringError>::new(
"waiting for the heat death of the universe",
vec![err3, err6],
)
.expect(">= 1 error");
insta::assert_snapshot!(format!("{}", error_list), @"
2 errors occurred waiting for the heat death of the universe:
* err3
caused by:
- err2
- err1
* err6
err6 line 2
caused by:
- err5
- err4
");
insta::assert_snapshot!(format!("{}", DisplayErrorChain::new(&error_list)), @"
2 errors occurred waiting for the heat death of the universe:
* err3
caused by:
- err2
- err1
* err6
err6 line 2
caused by:
- err5
- err4
");
}
#[derive(Clone, Debug, Error)]
struct StringError {
message: String,
#[source]
source: Option<Box<StringError>>,
}
impl StringError {
fn new(message: impl Into<String>, source: Option<StringError>) -> Self {
Self {
message: message.into(),
source: source.map(Box::new),
}
}
}
impl fmt::Display for StringError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
}