use crate::{
cargo_config::{TargetTriple, TargetTripleSource},
config::{CustomTestGroup, TestGroup},
helpers::{dylib_path_envvar, extract_abort_status},
reuse_build::ArchiveFormat,
runner::AbortStatus,
target_runner::PlatformRunnerSource,
};
use camino::{FromPathBufError, Utf8Path, Utf8PathBuf};
use config::ConfigError;
use itertools::Itertools;
use nextest_filtering::errors::FilterExpressionParseErrors;
use nextest_metadata::RustBinaryId;
use smol_str::SmolStr;
use std::{borrow::Cow, collections::BTreeSet, env::JoinPathsError, fmt, process::ExitStatus};
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_deref())
)]
#[non_exhaustive]
pub struct ConfigParseError {
config_file: Utf8PathBuf,
tool: Option<String>,
#[source]
kind: ConfigParseErrorKind,
}
impl ConfigParseError {
pub(crate) fn new(
config_file: impl Into<Utf8PathBuf>,
tool: Option<&str>,
kind: ConfigParseErrorKind,
) -> Self {
Self {
config_file: config_file.into(),
tool: tool.map(|s| s.to_owned()),
kind,
}
}
pub fn config_file(&self) -> &Utf8Path {
&self.config_file
}
pub fn tool(&self) -> Option<&str> {
self.tool.as_deref()
}
pub fn kind(&self) -> &ConfigParseErrorKind {
&self.kind
}
}
pub fn provided_by_tool(tool: Option<&str>) -> 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)]
DeserializeError(Box<serde_path_to_error::Error<ConfigError>>),
#[error("error parsing overrides (destructure this variant for more details)")]
OverrideError(Vec<ConfigParseOverrideError>),
#[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>,
},
}
#[derive(Debug)]
#[non_exhaustive]
pub struct ConfigParseOverrideError {
pub profile_name: String,
pub not_specified: bool,
pub platform_parse_error: Option<target_spec::Error>,
pub parse_errors: Option<FilterExpressionParseErrors>,
}
impl ConfigParseOverrideError {
pub fn reports(&self) -> impl Iterator<Item = miette::Report> + '_ {
let not_specified_report = self.not_specified.then(|| {
miette::Report::msg("at least one of `platform` and `filter` should be specified")
});
let platform_parse_report = self
.platform_parse_error
.as_ref()
.map(|error| miette::Report::new_boxed(error.clone().into_diagnostic()));
let parse_reports = self
.parse_errors
.as_ref()
.into_iter()
.flat_map(|parse_errors| {
parse_errors.errors.iter().map(|single_error| {
miette::Report::new(single_error.clone())
.with_source_code(parse_errors.input.to_owned())
})
});
not_specified_report
.into_iter()
.chain(platform_parse_report)
.chain(parse_reports)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct UnknownTestGroupError {
pub profile_name: String,
pub name: TestGroup,
}
#[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)]
#[error("invalid custom test group name: {0}")]
pub struct InvalidCustomTestGroupName(pub InvalidIdentifier);
#[derive(Clone, Debug, Error)]
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 empty tool name: {input}")]
EmptyToolName {
input: String,
},
#[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(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(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,
}
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"),
}
}
}
#[derive(Debug, Error)]
pub enum RustBuildMetaParseError {
#[error("error deserializing platform from build metadata")]
PlatformDeserializeError(#[from] target_spec::Error),
}
#[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 `{}` exited with {}\n--- stdout:\n{}\n--- stderr:\n{}\n---",
shell_words::join(command),
display_exit_status(*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 }
}
}
fn display_exit_status(exit_status: ExitStatus) -> String {
match extract_abort_status(exit_status) {
#[cfg(unix)]
Some(AbortStatus::UnixSignal(sig)) => match crate::helpers::signal_str(sig) {
Some(s) => {
format!("signal {sig} (SIG{s})")
}
None => {
format!("signal {sig}")
}
},
#[cfg(windows)]
Some(AbortStatus::WindowsNtStatus(nt_status)) => {
format!("code {}", crate::helpers::display_nt_status(nt_status))
}
None => match exit_status.code() {
Some(code) => {
format!("code {code}")
}
None => "an unknown error".to_owned(),
},
}
}
#[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] windows::core::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)]
#[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("error writing {} `{path}` to archive", kind_str(*.is_dir))]
InputFileRead {
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)]
#[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: 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::easy::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("failed to parse config at `{path}`")]
ConfigParseError {
path: Utf8PathBuf,
#[source]
error: toml_edit::easy::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)]
#[error("the host platform could not be determined")]
pub struct UnknownHostPlatform {
#[source]
pub(crate) error: 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,
},
}
#[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>,
},
}
#[cfg(feature = "self-update")]
mod self_update_errors {
use super::*;
use mukti_metadata::ReleaseStatus;
use semver::{Version, VersionReq};
use std::collections::BTreeSet;
#[cfg(feature = "self-update")]
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum UpdateError {
#[error("self-update failed")]
SelfUpdate(#[source] self_update::errors::Error),
#[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("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 renaming `{source}` to `{dest}`")]
FsRename {
source: Utf8PathBuf,
dest: Utf8PathBuf,
#[source]
error: 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_map(|(v, status)| {
(v.pre.is_empty() && *status == ReleaseStatus::Active).then(|| 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
}
#[cfg(feature = "self-update")]
#[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::*;