use crate::{output::StderrStyles, ExtractOutputFormat};
use camino::Utf8PathBuf;
use itertools::Itertools;
use nextest_filtering::errors::FiltersetParseErrors;
use nextest_metadata::NextestExitCode;
use nextest_runner::{errors::*, redact::Redactor};
use owo_colors::OwoColorize;
use semver::Version;
use std::{error::Error, string::FromUtf8Error};
use thiserror::Error;
use tracing::{error, info, Level};
pub(crate) type Result<T, E = ExpectedError> = std::result::Result<T, E>;
#[derive(Debug)]
#[doc(hidden)]
pub enum ReuseBuildKind {
Normal,
ReuseWithWorkspaceRemap { workspace_root: Utf8PathBuf },
Reuse,
}
#[derive(Debug, Error)]
#[doc(hidden)]
pub enum ExpectedError {
#[error("could not change to requested directory")]
SetCurrentDirFailed { error: std::io::Error },
#[error("failed to get current executable")]
GetCurrentExeFailed {
#[source]
err: std::io::Error,
},
#[error("cargo metadata exec failed")]
CargoMetadataExecFailed {
command: String,
err: std::io::Error,
},
#[error("cargo metadata failed")]
CargoMetadataFailed { command: String },
#[error("cargo locate-project exec failed")]
CargoLocateProjectExecFailed {
command: String,
err: std::io::Error,
},
#[error("cargo locate-project failed")]
CargoLocateProjectFailed { command: String },
#[error("workspace root is not valid UTF-8")]
WorkspaceRootInvalidUtf8 {
#[source]
err: FromUtf8Error,
},
#[error("workspace root is invalid")]
WorkspaceRootInvalid { workspace_root: Utf8PathBuf },
#[error("root manifest not found at {path}")]
RootManifestNotFound {
path: Utf8PathBuf,
reuse_build_kind: ReuseBuildKind,
},
#[error("profile not found")]
ProfileNotFound {
#[from]
err: ProfileNotFound,
},
#[error("failed to create store directory")]
StoreDirCreateError {
store_dir: Utf8PathBuf,
#[source]
err: std::io::Error,
},
#[error("cargo config error")]
CargoConfigError {
#[from]
err: Box<CargoConfigError>,
},
#[error("config parse error")]
ConfigParseError {
#[from]
err: ConfigParseError,
},
#[error("test filter build error")]
TestFilterBuilderError {
#[from]
err: TestFilterBuilderError,
},
#[error("unknown host platform")]
UnknownHostPlatform {
#[from]
err: UnknownHostPlatform,
},
#[error("target triple error")]
TargetTripleError {
#[from]
err: TargetTripleError,
},
#[error("metadata materialize error")]
MetadataMaterializeError {
arg_name: &'static str,
#[source]
err: Box<MetadataMaterializeError>,
},
#[error("unknown archive format")]
UnknownArchiveFormat {
archive_file: Utf8PathBuf,
#[source]
err: UnknownArchiveFormat,
},
#[error("archive create error")]
ArchiveCreateError {
archive_file: Utf8PathBuf,
#[source]
err: ArchiveCreateError,
redactor: Redactor,
},
#[error("archive extract error")]
ArchiveExtractError {
archive_file: Utf8PathBuf,
#[source]
err: Box<ArchiveExtractError>,
},
#[error("path mapper construct error")]
PathMapperConstructError {
arg_name: &'static str,
#[source]
err: PathMapperConstructError,
},
#[error("cargo metadata parse error")]
CargoMetadataParseError {
file_name: Option<Utf8PathBuf>,
#[source]
err: guppy::Error,
},
#[error("rust build meta parse error")]
RustBuildMetaParseError {
#[from]
err: RustBuildMetaParseError,
},
#[error("error parsing Cargo messages")]
FromMessagesError {
#[from]
err: FromMessagesError,
},
#[error("create test list error")]
CreateTestListError {
#[source]
err: CreateTestListError,
},
#[error("failed to execute build command")]
BuildExecFailed {
command: String,
#[source]
err: std::io::Error,
},
#[error("build failed")]
BuildFailed {
command: String,
exit_code: Option<i32>,
},
#[error("building test runner failed")]
TestRunnerBuildError {
#[from]
err: TestRunnerBuildError,
},
#[error("writing test list to output failed")]
WriteTestListError {
#[from]
err: WriteTestListError,
},
#[error("writing event failed")]
WriteEventError {
#[from]
err: WriteEventError,
},
#[error(transparent)]
TestRunnerExecuteErrors {
#[from]
err: TestRunnerExecuteErrors<WriteEventError>,
},
#[error(transparent)]
ConfigureHandleInheritanceError {
#[from]
err: ConfigureHandleInheritanceError,
},
#[error("show test groups error")]
ShowTestGroupsError {
#[from]
err: ShowTestGroupsError,
},
#[error("setup script failed")]
SetupScriptFailed,
#[error("test run failed")]
TestRunFailed,
#[error("no tests to run")]
NoTestsRun {
is_default: bool,
},
#[cfg(feature = "self-update")]
#[error("failed to parse --version")]
UpdateVersionParseError {
#[from]
err: UpdateVersionParseError,
},
#[cfg(feature = "self-update")]
#[error("failed to update")]
UpdateError {
#[from]
err: UpdateError,
},
#[error("error reading prompt")]
DialoguerError {
#[source]
err: dialoguer::Error,
},
#[error("failed to set up Ctrl-C handler")]
SignalHandlerSetupError {
#[from]
err: SignalHandlerSetupError,
},
#[error("required version not met")]
RequiredVersionNotMet {
required: Version,
current: Version,
tool: Option<String>,
},
#[error("experimental feature not enabled")]
ExperimentalFeatureNotEnabled {
name: &'static str,
var_name: &'static str,
},
#[error("filterset parse error")]
FiltersetParseError {
all_errors: Vec<FiltersetParseErrors>,
},
#[error("test binary args parse error")]
TestBinaryArgsParseError {
reason: &'static str,
args: Vec<String>,
},
#[error("double-spawn parse error")]
DoubleSpawnParseArgsError {
args: String,
#[source]
err: shell_words::ParseError,
},
#[error("double-spawn execution error")]
DoubleSpawnExecError {
command: std::process::Command,
#[source]
err: std::io::Error,
},
#[error("message format version is not valid")]
InvalidMessageFormatVersion {
#[from]
err: FormatVersionError,
},
#[error("extract read error")]
DebugExtractReadError {
kind: &'static str,
path: Utf8PathBuf,
#[source]
err: std::io::Error,
},
#[error("extract write error")]
DebugExtractWriteError {
format: ExtractOutputFormat,
#[source]
err: std::io::Error,
},
}
impl ExpectedError {
pub(crate) fn cargo_metadata_exec_failed(
command: impl IntoIterator<Item = impl AsRef<str>>,
err: std::io::Error,
) -> Self {
Self::CargoMetadataExecFailed {
command: shell_words::join(command),
err,
}
}
pub(crate) fn cargo_metadata_failed(
command: impl IntoIterator<Item = impl AsRef<str>>,
) -> Self {
Self::CargoMetadataFailed {
command: shell_words::join(command),
}
}
pub(crate) fn cargo_locate_project_exec_failed(
command: impl IntoIterator<Item = impl AsRef<str>>,
err: std::io::Error,
) -> Self {
Self::CargoLocateProjectExecFailed {
command: shell_words::join(command),
err,
}
}
pub(crate) fn cargo_locate_project_failed(
command: impl IntoIterator<Item = impl AsRef<str>>,
) -> Self {
Self::CargoLocateProjectFailed {
command: shell_words::join(command),
}
}
pub(crate) fn profile_not_found(err: ProfileNotFound) -> Self {
Self::ProfileNotFound { err }
}
pub(crate) fn config_parse_error(err: ConfigParseError) -> Self {
Self::ConfigParseError { err }
}
pub(crate) fn metadata_materialize_error(
arg_name: &'static str,
err: MetadataMaterializeError,
) -> Self {
Self::MetadataMaterializeError {
arg_name,
err: Box::new(err),
}
}
pub(crate) fn cargo_metadata_parse_error(
file_name: impl Into<Option<Utf8PathBuf>>,
err: guppy::Error,
) -> Self {
Self::CargoMetadataParseError {
file_name: file_name.into(),
err,
}
}
#[expect(dead_code)]
pub(crate) fn experimental_feature_error(name: &'static str, var_name: &'static str) -> Self {
Self::ExperimentalFeatureNotEnabled { name, var_name }
}
pub(crate) fn build_exec_failed(
command: impl IntoIterator<Item = impl AsRef<str>>,
err: std::io::Error,
) -> Self {
Self::BuildExecFailed {
command: shell_words::join(command),
err,
}
}
pub(crate) fn build_failed(
command: impl IntoIterator<Item = impl AsRef<str>>,
exit_code: Option<i32>,
) -> Self {
Self::BuildFailed {
command: shell_words::join(command),
exit_code,
}
}
pub(crate) fn filter_expression_parse_error(all_errors: Vec<FiltersetParseErrors>) -> Self {
Self::FiltersetParseError { all_errors }
}
pub(crate) fn setup_script_failed() -> Self {
Self::SetupScriptFailed
}
pub(crate) fn test_run_failed() -> Self {
Self::TestRunFailed
}
pub(crate) fn test_binary_args_parse_error(reason: &'static str, args: Vec<String>) -> Self {
Self::TestBinaryArgsParseError { reason, args }
}
pub fn process_exit_code(&self) -> i32 {
match self {
Self::CargoMetadataExecFailed { .. }
| Self::CargoMetadataFailed { .. }
| Self::CargoLocateProjectExecFailed { .. }
| Self::CargoLocateProjectFailed { .. } => NextestExitCode::CARGO_METADATA_FAILED,
Self::WorkspaceRootInvalidUtf8 { .. }
| Self::WorkspaceRootInvalid { .. }
| Self::SetCurrentDirFailed { .. }
| Self::GetCurrentExeFailed { .. }
| Self::ProfileNotFound { .. }
| Self::StoreDirCreateError { .. }
| Self::RootManifestNotFound { .. }
| Self::CargoConfigError { .. }
| Self::TestFilterBuilderError { .. }
| Self::UnknownHostPlatform { .. }
| Self::TargetTripleError { .. }
| Self::MetadataMaterializeError { .. }
| Self::UnknownArchiveFormat { .. }
| Self::ArchiveExtractError { .. }
| Self::RustBuildMetaParseError { .. }
| Self::PathMapperConstructError { .. }
| Self::TestRunnerBuildError { .. }
| Self::ConfigureHandleInheritanceError { .. }
| Self::CargoMetadataParseError { .. }
| Self::TestBinaryArgsParseError { .. }
| Self::DialoguerError { .. }
| Self::SignalHandlerSetupError { .. }
| Self::ShowTestGroupsError { .. }
| Self::InvalidMessageFormatVersion { .. }
| Self::DebugExtractReadError { .. } => NextestExitCode::SETUP_ERROR,
Self::ConfigParseError { err } => {
match err.kind() {
ConfigParseErrorKind::ExperimentalFeatureNotEnabled { .. } => {
NextestExitCode::EXPERIMENTAL_FEATURE_NOT_ENABLED
}
_ => NextestExitCode::SETUP_ERROR,
}
}
Self::RequiredVersionNotMet { .. } => NextestExitCode::REQUIRED_VERSION_NOT_MET,
#[cfg(feature = "self-update")]
Self::UpdateVersionParseError { .. } => NextestExitCode::SETUP_ERROR,
Self::DoubleSpawnParseArgsError { .. } | Self::DoubleSpawnExecError { .. } => {
NextestExitCode::DOUBLE_SPAWN_ERROR
}
Self::FromMessagesError { .. } | Self::CreateTestListError { .. } => {
NextestExitCode::TEST_LIST_CREATION_FAILED
}
Self::BuildExecFailed { .. } | Self::BuildFailed { .. } => {
NextestExitCode::BUILD_FAILED
}
Self::SetupScriptFailed => NextestExitCode::SETUP_SCRIPT_FAILED,
Self::TestRunFailed => NextestExitCode::TEST_RUN_FAILED,
Self::NoTestsRun { .. } => NextestExitCode::NO_TESTS_RUN,
Self::ArchiveCreateError { .. } => NextestExitCode::ARCHIVE_CREATION_FAILED,
Self::WriteTestListError { .. }
| Self::WriteEventError { .. }
| Self::TestRunnerExecuteErrors { .. }
| Self::DebugExtractWriteError { .. } => NextestExitCode::WRITE_OUTPUT_ERROR,
#[cfg(feature = "self-update")]
Self::UpdateError { .. } => NextestExitCode::UPDATE_ERROR,
Self::ExperimentalFeatureNotEnabled { .. } => {
NextestExitCode::EXPERIMENTAL_FEATURE_NOT_ENABLED
}
Self::FiltersetParseError { .. } => NextestExitCode::INVALID_FILTERSET,
}
}
pub fn display_to_stderr(&self, styles: &StderrStyles) {
let mut next_error = match &self {
Self::SetCurrentDirFailed { error } => {
error!("could not change to requested directory");
Some(error as &dyn Error)
}
Self::GetCurrentExeFailed { err } => {
error!("failed to get current executable");
Some(err as &dyn Error)
}
Self::CargoMetadataExecFailed { command, err } => {
error!("failed to execute `{}`", command.style(styles.bold));
Some(err as &dyn Error)
}
Self::CargoMetadataFailed { .. } => {
None
}
Self::CargoLocateProjectExecFailed { command, err } => {
error!("failed to execute `{}`", command.style(styles.bold));
Some(err as &dyn Error)
}
Self::CargoLocateProjectFailed { .. } => {
None
}
Self::WorkspaceRootInvalidUtf8 { err } => {
error!("workspace root is not valid UTF-8");
Some(err as &dyn Error)
}
Self::WorkspaceRootInvalid { workspace_root } => {
error!(
"workspace root `{}` is invalid",
workspace_root.style(styles.bold)
);
None
}
Self::ProfileNotFound { err } => {
error!("{}", err);
err.source()
}
Self::RootManifestNotFound {
path,
reuse_build_kind,
} => {
let hint_str = match reuse_build_kind {
ReuseBuildKind::ReuseWithWorkspaceRemap { workspace_root } => {
format!(
"\n(hint: ensure that project source is available at {})",
workspace_root.style(styles.bold)
)
}
ReuseBuildKind::Reuse => {
"\n(hint: ensure that project source is available for reused build, \
using --workspace-remap if necessary)"
.to_owned()
}
ReuseBuildKind::Normal => String::new(),
};
error!(
"workspace root manifest at {} does not exist{hint_str}",
path.style(styles.bold)
);
None
}
Self::StoreDirCreateError { store_dir, err } => {
error!(
"failed to create store dir at `{}`",
store_dir.style(styles.bold)
);
Some(err as &dyn Error)
}
Self::CargoConfigError { err } => {
error!("{}", err);
err.source()
}
Self::ConfigParseError { err } => {
match err.kind() {
ConfigParseErrorKind::CompileErrors(errors) => {
for compile_error in errors {
let section_str = match &compile_error.section {
ConfigCompileSection::DefaultFilter => {
format!("profile.{}.default-filter", compile_error.profile_name)
.style(styles.bold)
.to_string()
}
ConfigCompileSection::Override(index) => {
let overrides =
format!("profile.{}.overrides", compile_error.profile_name);
format!(
"{} at index {}",
overrides.style(styles.bold),
index.style(styles.bold)
)
}
ConfigCompileSection::Script(index) => {
let scripts =
format!("profile.{}.scripts", compile_error.profile_name);
format!(
"{} at index {}",
scripts.style(styles.bold),
index.style(styles.bold)
)
}
};
error!(
"for config file `{}`{}, failed to parse {}",
err.config_file(),
provided_by_tool(err.tool()),
section_str.style(styles.bold)
);
for report in compile_error.kind.reports() {
error!(target: "cargo_nextest::no_heading", "{report:?}");
}
}
None
}
ConfigParseErrorKind::UnknownTestGroups {
errors,
known_groups,
} => {
let known_groups_str = known_groups
.iter()
.map(|group_name| group_name.style(styles.bold))
.join(", ");
let mut errors_str = String::new();
for error in errors {
errors_str.push_str(&format!(
" - group `{}` in overrides for profile `{}`\n",
error.name.style(styles.bold),
error.profile_name.style(styles.bold)
));
}
error!(
"for config file `{}`{}, unknown test groups defined \
(known groups: {known_groups_str}):\n{errors_str}",
err.config_file(),
provided_by_tool(err.tool()),
);
None
}
ConfigParseErrorKind::UnknownConfigScripts {
errors,
known_scripts,
} => {
let known_scripts_str = known_scripts
.iter()
.map(|group_name| group_name.style(styles.bold))
.join(", ");
let mut errors_str = String::new();
for error in errors {
errors_str.push_str(&format!(
" - script `{}` specified within profile `{}`\n",
error.name.style(styles.bold),
error.profile_name.style(styles.bold)
));
}
error!(
"for config file `{}`{}, unknown scripts defined \
(known scripts: {known_scripts_str}):\n{errors_str}",
err.config_file(),
provided_by_tool(err.tool()),
);
None
}
ConfigParseErrorKind::UnknownExperimentalFeatures { unknown, known } => {
let unknown_str = unknown
.iter()
.map(|feature_name| feature_name.style(styles.bold))
.join(", ");
let known_str = known
.iter()
.map(|feature_name| feature_name.style(styles.bold))
.join(", ");
error!(
"for config file `{}`{}, unknown experimental features defined:
{unknown_str} (known features: {known_str}):",
err.config_file(),
provided_by_tool(err.tool()),
);
None
}
_ => {
error!("{}", err);
err.source()
}
}
}
Self::TestFilterBuilderError { err } => {
error!("{err}");
err.source()
}
Self::UnknownHostPlatform { err } => {
error!("the host platform was unknown to nextest");
Some(err as &dyn Error)
}
Self::TargetTripleError { err } => {
if let Some(report) = err.source_report() {
error!(target: "cargo_nextest::no_heading", "{report:?}");
None
} else {
error!("{err}");
err.source()
}
}
Self::MetadataMaterializeError { arg_name, err } => {
error!(
"error reading metadata from argument {}",
format!("--{arg_name}").style(styles.bold)
);
Some(err as &dyn Error)
}
Self::UnknownArchiveFormat { archive_file, err } => {
error!(
"failed to autodetect archive format for {}",
archive_file.style(styles.bold)
);
Some(err as &dyn Error)
}
Self::ArchiveCreateError {
archive_file,
err,
redactor,
} => {
error!(
"error creating archive `{}`",
redactor.redact_path(archive_file).style(styles.bold)
);
Some(err as &dyn Error)
}
Self::ArchiveExtractError { archive_file, err } => {
error!(
"error extracting archive `{}`",
archive_file.style(styles.bold)
);
Some(err as &dyn Error)
}
Self::RustBuildMetaParseError { err } => {
error!("error parsing Rust build metadata");
Some(err as &dyn Error)
}
Self::PathMapperConstructError { arg_name, err } => {
error!(
"argument {} specified `{}` that couldn't be read",
format!("--{arg_name}").style(styles.bold),
err.input().style(styles.bold)
);
Some(err as &dyn Error)
}
Self::CargoMetadataParseError { file_name, err } => {
let metadata_source = match file_name {
Some(path) => format!(" from file `{}`", path.style(styles.bold)),
None => "".to_owned(),
};
error!("error parsing Cargo metadata{}", metadata_source);
Some(err as &dyn Error)
}
Self::FromMessagesError { err } => {
error!("failed to parse messages generated by Cargo");
Some(err as &dyn Error)
}
Self::CreateTestListError { err } => {
error!("creating test list failed");
Some(err as &dyn Error)
}
Self::BuildExecFailed { command, err } => {
error!("failed to execute `{}`", command.style(styles.bold));
Some(err as &dyn Error)
}
Self::BuildFailed { command, exit_code } => {
let with_code_str = match exit_code {
Some(code) => {
format!(" with code {}", code.style(styles.bold))
}
None => "".to_owned(),
};
error!(
"command `{}` exited{}",
command.style(styles.bold),
with_code_str,
);
None
}
Self::TestRunnerBuildError { err } => {
error!("failed to build test runner");
Some(err as &dyn Error)
}
Self::ConfigureHandleInheritanceError { err } => {
error!("{err}");
err.source()
}
Self::WriteTestListError { err } => {
error!("failed to write test list to output");
Some(err as &dyn Error)
}
Self::WriteEventError { err } => {
error!("failed to write event to output");
Some(err as &dyn Error)
}
Self::TestRunnerExecuteErrors { err } => {
error!("{err}");
None
}
Self::SetupScriptFailed => {
error!("setup script failed");
None
}
Self::TestRunFailed => {
error!("test run failed");
None
}
Self::NoTestsRun { is_default } => {
let hint_str = if *is_default {
"\n(hint: use `--no-tests` to customize)"
} else {
""
};
error!("no tests to run{hint_str}");
None
}
Self::ShowTestGroupsError { err } => {
error!("{err}");
err.source()
}
Self::RequiredVersionNotMet {
required,
current,
tool,
} => {
error!(
"this repository requires nextest version {}, but the current version is {}",
required.style(styles.bold),
current.style(styles.bold),
);
if let Some(tool) = tool {
info!(
target: "cargo_nextest::no_heading",
"(required version specified by tool `{}`)",
tool,
);
}
crate::helpers::log_needs_update(
Level::INFO,
crate::helpers::BYPASS_VERSION_TEXT,
styles,
);
None
}
#[cfg(feature = "self-update")]
Self::UpdateVersionParseError { err } => {
error!("failed to parse --version");
Some(err as &dyn Error)
}
#[cfg(feature = "self-update")]
Self::UpdateError { err } => {
error!(
"failed to update nextest (please update manually by visiting <{}>)",
"https://get.nexte.st".style(styles.bold)
);
Some(err as &dyn Error)
}
Self::DialoguerError { err } => {
error!("error reading input prompt");
Some(err as &dyn Error)
}
Self::SignalHandlerSetupError { err } => {
error!("error setting up signal handler");
Some(err as &dyn Error)
}
Self::ExperimentalFeatureNotEnabled { name, var_name } => {
error!(
"{} is an experimental feature and must be enabled with {}=1",
name, var_name
);
None
}
Self::FiltersetParseError { all_errors } => {
for errors in all_errors {
for single_error in &errors.errors {
let report = miette::Report::new(single_error.clone())
.with_source_code(errors.input.to_owned());
error!(target: "cargo_nextest::no_heading", "{:?}", report);
}
}
error!("failed to parse filterset");
None
}
Self::TestBinaryArgsParseError { reason, args } => {
error!(
"failed to parse test binary arguments `{}`: arguments are {reason}",
args.join(", "),
);
None
}
Self::DoubleSpawnParseArgsError { args, err } => {
error!("[double-spawn] failed to parse arguments `{args}`");
Some(err as &dyn Error)
}
Self::DoubleSpawnExecError { command, err } => {
error!("[double-spawn] failed to exec `{command:?}`");
Some(err as &dyn Error)
}
Self::InvalidMessageFormatVersion { err } => {
error!("error parsing message format version");
Some(err as &dyn Error)
}
Self::DebugExtractReadError { kind, path, err } => {
error!("error reading {kind} file `{}`", path.style(styles.bold),);
Some(err as &dyn Error)
}
Self::DebugExtractWriteError { format, err } => {
error!("error writing {format} output");
Some(err as &dyn Error)
}
};
while let Some(err) = next_error {
error!(target: "cargo_nextest::no_heading", "\nCaused by:\n {}", err);
next_error = err.source();
}
}
}