use crate::{ExtractOutputFormat, output::StderrStyles};
use camino::Utf8PathBuf;
use itertools::Itertools;
use nextest_filtering::errors::FiltersetParseErrors;
use nextest_metadata::NextestExitCode;
use nextest_runner::{
config::core::{ConfigExperimental, ToolName},
errors::{
PortableRecordingError, PortableRecordingReadError, RecordReadError, RecordSetupError,
RunIdResolutionError, RunStoreError, StateDirError, TestListFromSummaryError,
UserConfigError, *,
},
helpers::{format_interceptor_too_many_tests, plural},
indenter::DisplayIndented,
list::OwnedTestInstanceId,
record::{ReplayabilityStatus, RunListAlignment, StoreVersionIncompatibility},
redact::Redactor,
run_mode::NextestRunMode,
runner::{DebuggerCommand, TracerCommand},
};
use owo_colors::{OwoColorize, Style};
use quick_junit::ReportUuid;
use semver::Version;
use std::{error::Error, io, process::ExitStatus, string::FromUtf8Error};
use swrite::{SWrite, swrite, swriteln};
use thiserror::Error;
use tracing::{Level, error, info, warn};
pub(crate) type Result<T, E = ExpectedError> = std::result::Result<T, E>;
#[derive(Clone, Debug, PartialEq, Eq, Error)]
pub enum CargoMessageFormatError {
#[error(
"conflicting message formats: `{first}` and `{second}` cannot be combined\n\
(only JSON modifiers like `json-diagnostic-short` can be combined)"
)]
ConflictingBaseFormats {
first: &'static str,
second: &'static str,
},
#[error(
"cannot combine {modifiers} with `{base}` format\n\
(JSON modifiers can only be used with JSON output)"
)]
JsonModifierWithNonJson {
modifiers: &'static str,
base: &'static str,
},
#[error("duplicate message format option: `{option}`")]
Duplicate { option: &'static str },
}
#[derive(Debug)]
#[doc(hidden)]
pub enum ReuseBuildKind {
Normal,
ReuseWithWorkspaceRemap { workspace_root: Utf8PathBuf },
Reuse,
}
#[derive(Debug, Error)]
#[doc(hidden)]
pub enum ExpectedError {
#[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,
exit_status: ExitStatus,
},
#[error("cargo locate-project exec failed")]
CargoLocateProjectExecFailed {
command: String,
err: std::io::Error,
},
#[error("cargo locate-project failed")]
CargoLocateProjectFailed {
command: String,
exit_status: ExitStatus,
},
#[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("user config error")]
UserConfigError {
#[from]
err: Box<UserConfigError>,
},
#[error("test filter build error")]
TestFilterBuildError {
#[from]
err: TestFilterBuildError,
},
#[error("unknown host platform")]
HostPlatformDetectError {
#[from]
err: HostPlatformDetectError,
},
#[error("target triple error")]
TargetTripleError {
#[from]
err: TargetTripleError,
},
#[error("remap absolute error")]
RemapAbsoluteError {
arg_name: &'static str,
path: Utf8PathBuf,
error: io::Error,
},
#[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 {
rerun_available: bool,
},
#[error("rerun tests outstanding")]
RerunTestsOutstanding {
count: usize,
},
#[error("no tests to run")]
NoTestsRun {
mode: NextestRunMode,
is_default: bool,
},
#[error(
"--debugger requires exactly one {}, but \
no {} were selected",
plural::tests_plural_if(*mode, false),
plural::tests_plural(*mode)
)]
DebuggerNoTests {
debugger: DebuggerCommand,
mode: NextestRunMode,
},
#[error(
"--debugger requires exactly one {}, but \
{test_count} {} were selected",
plural::tests_plural_if(*mode, false),
plural::tests_str(*mode, *test_count)
)]
DebuggerTooManyTests {
debugger: DebuggerCommand,
mode: NextestRunMode,
test_count: usize,
test_instances: Vec<OwnedTestInstanceId>,
},
#[error(
"--tracer requires exactly one {}, but \
no {} were selected",
plural::tests_plural_if(*mode, false),
plural::tests_plural(*mode)
)]
TracerNoTests {
tracer: TracerCommand,
mode: NextestRunMode,
},
#[error(
"--tracer requires exactly one {}, but \
{test_count} {} were selected",
plural::tests_plural_if(*mode, false),
plural::tests_str(*mode, *test_count)
)]
TracerTooManyTests {
tracer: TracerCommand,
mode: NextestRunMode,
test_count: usize,
test_instances: Vec<OwnedTestInstanceId>,
},
#[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")]
#[cfg(feature = "self-update")]
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<ToolName>,
},
#[error("experimental feature not enabled")]
ExperimentalFeatureNotEnabled {
name: &'static str,
var_name: &'static str,
},
#[error("experimental features not enabled in config")]
ConfigExperimentalFeaturesNotEnabled {
config_file: Utf8PathBuf,
missing: Vec<ConfigExperimental>,
},
#[error("could not determine state directory for recording")]
RecordStateDirNotFound {
#[source]
err: StateDirError,
},
#[error("error setting up recording")]
RecordSetupError {
#[source]
err: RunStoreError,
},
#[error("error setting up recording session")]
RecordSessionSetupError {
#[source]
err: RecordSetupError,
},
#[error("filterset parse error")]
FiltersetParseError {
all_errors: Vec<FiltersetParseErrors>,
},
#[error("test binary args parse error")]
TestBinaryArgsParseError {
reason: &'static str,
args: Vec<String>,
},
#[cfg(unix)]
#[error("double-spawn parse error")]
DoubleSpawnParseArgsError {
args: String,
#[source]
err: shell_words::ParseError,
},
#[cfg(unix)]
#[error("double-spawn execution error")]
DoubleSpawnExecError {
command: Box<std::process::Command>,
current_dir: Result<std::path::PathBuf, std::io::Error>,
#[source]
err: std::io::Error,
},
#[error("message format version is not valid")]
InvalidMessageFormatVersion {
#[from]
err: FormatVersionError,
},
#[error("invalid cargo message format")]
CargoMessageFormatError {
#[from]
err: CargoMessageFormatError,
},
#[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,
},
#[error("error extracting from portable recording: {message}")]
DebugExtractRecordingError { message: String },
#[error("run ID resolution error")]
RunIdResolutionError {
#[source]
err: RunIdResolutionError,
},
#[error("error reading recorded run")]
RecordReadError {
#[source]
err: RecordReadError,
},
#[error("run {run_id} has incompatible store format: {incompatibility}")]
StoreVersionIncompatible {
run_id: ReportUuid,
incompatibility: StoreVersionIncompatibility,
},
#[error("error creating portable recording")]
PortableRecordingError {
#[source]
err: PortableRecordingError,
},
#[error("error reading portable recording")]
PortableRecordingReadError {
#[source]
err: PortableRecordingReadError,
},
#[error("error reconstructing test list from archive")]
TestListFromSummaryError {
#[source]
err: TestListFromSummaryError,
},
#[error("write error")]
WriteError {
#[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>>,
exit_status: ExitStatus,
) -> Self {
Self::CargoMetadataFailed {
command: shell_words::join(command),
exit_status,
}
}
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>>,
exit_status: ExitStatus,
) -> Self {
Self::CargoLocateProjectFailed {
command: shell_words::join(command),
exit_status,
}
}
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(rerun_available: bool) -> Self {
Self::TestRunFailed { rerun_available }
}
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::GetCurrentExeFailed { .. }
| Self::ProfileNotFound { .. }
| Self::StoreDirCreateError { .. }
| Self::RootManifestNotFound { .. }
| Self::CargoConfigError { .. }
| Self::UserConfigError { .. }
| Self::TestFilterBuildError { .. }
| Self::HostPlatformDetectError { .. }
| Self::TargetTripleError { .. }
| Self::RemapAbsoluteError { .. }
| Self::MetadataMaterializeError { .. }
| Self::UnknownArchiveFormat { .. }
| Self::ArchiveExtractError { .. }
| Self::RustBuildMetaParseError { .. }
| Self::PathMapperConstructError { .. }
| Self::TestRunnerBuildError { .. }
| Self::ConfigureHandleInheritanceError { .. }
| Self::CargoMetadataParseError { .. }
| Self::TestBinaryArgsParseError { .. }
| Self::SignalHandlerSetupError { .. }
| Self::ShowTestGroupsError { .. }
| Self::InvalidMessageFormatVersion { .. }
| Self::DebugExtractReadError { .. }
| Self::DebugExtractRecordingError { .. }
| Self::CargoMessageFormatError { .. } => NextestExitCode::SETUP_ERROR,
Self::ConfigParseError { err } => {
match err.kind() {
ConfigParseErrorKind::ExperimentalFeaturesNotEnabled { .. } => {
NextestExitCode::EXPERIMENTAL_FEATURE_NOT_ENABLED
}
_ => NextestExitCode::SETUP_ERROR,
}
}
Self::RequiredVersionNotMet { .. } => NextestExitCode::REQUIRED_VERSION_NOT_MET,
#[cfg(feature = "self-update")]
Self::DialoguerError { .. } | Self::UpdateVersionParseError { .. } => NextestExitCode::SETUP_ERROR,
#[cfg(unix)]
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::RerunTestsOutstanding { .. } => NextestExitCode::RERUN_TESTS_OUTSTANDING,
Self::NoTestsRun { .. } => NextestExitCode::NO_TESTS_RUN,
Self::DebuggerNoTests { .. } | Self::DebuggerTooManyTests { .. }
| Self::TracerNoTests { .. } | Self::TracerTooManyTests { .. } => NextestExitCode::SETUP_ERROR,
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 { .. }
| Self::ConfigExperimentalFeaturesNotEnabled { .. } => {
NextestExitCode::EXPERIMENTAL_FEATURE_NOT_ENABLED
}
Self::RecordStateDirNotFound { .. }
| Self::RecordSetupError { .. }
| Self::RecordSessionSetupError { .. }
| Self::RunIdResolutionError { .. }
| Self::RecordReadError { .. }
| Self::StoreVersionIncompatible { .. }
| Self::TestListFromSummaryError { .. } => NextestExitCode::SETUP_ERROR,
Self::PortableRecordingError { .. } => NextestExitCode::ARCHIVE_CREATION_FAILED,
Self::PortableRecordingReadError { .. } => NextestExitCode::SETUP_ERROR,
Self::WriteError { .. } => NextestExitCode::WRITE_OUTPUT_ERROR,
Self::FiltersetParseError { .. } => NextestExitCode::INVALID_FILTERSET,
}
}
pub fn display_to_stderr(&self, styles: &StderrStyles) {
let mut next_error = match &self {
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 {
command,
exit_status,
} => {
error!(
"command `{}` failed with {}",
command.style(styles.bold),
exit_status
);
None
}
Self::CargoLocateProjectExecFailed { command, err } => {
error!("failed to execute `{}`", command.style(styles.bold));
Some(err as &dyn Error)
}
Self::CargoLocateProjectFailed {
command,
exit_status,
} => {
error!(
"command `{}` failed with {}",
command.style(styles.bold),
exit_status
);
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 {
swriteln!(
errors_str,
" - group `{}` in overrides for profile `{}`",
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::ProfileScriptErrors {
errors,
known_scripts,
} => {
let ProfileScriptErrors {
unknown_scripts,
wrong_script_types,
list_scripts_using_run_filters,
} = &**errors;
let mut errors_str: String = format!(
"for config file `{}`{}, errors encountered parsing [[profile.*.scripts]]\n",
err.config_file(),
provided_by_tool(err.tool()),
);
if !unknown_scripts.is_empty() {
let known_scripts_str = known_scripts
.iter()
.map(|group_name| group_name.style(styles.bold))
.join(", ");
swriteln!(
errors_str,
"- unknown scripts specified (known scripts: {known_scripts_str}):",
);
for error in unknown_scripts {
swrite!(
errors_str,
" - script `{}` specified within profile `{}`\n",
error.name.style(styles.bold),
error.profile_name.style(styles.bold)
);
}
}
if !wrong_script_types.is_empty() {
swriteln!(errors_str, "- scripts specified with incorrect type:");
for error in wrong_script_types {
swrite!(
errors_str,
" - script `{}` specified within profile `{}` as {}, \
but is actually {}\n",
error.name.style(styles.bold),
error.profile_name.style(styles.bold),
error.attempted.style(styles.bold),
error.actual.style(styles.bold)
);
}
}
if !list_scripts_using_run_filters.is_empty() {
swriteln!(
errors_str,
"- list-wrapper scripts specified using filters \
only available at runtime:",
);
for error in list_scripts_using_run_filters {
let filters_str = plural::filters_str(error.filters.len());
let filters = error
.filters
.iter()
.map(|f| f.style(styles.bold))
.join(", ");
swriteln!(
errors_str,
" - script `{}` specified within profile `{}` \
uses runtime {filters_str}: {filters}",
error.name.style(styles.bold),
error.profile_name.style(styles.bold)
);
}
}
error!("{errors_str}");
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::UserConfigError { err } => {
error!("{err}");
err.source()
}
Self::TestFilterBuildError { err } => {
error!("{err}");
err.source()
}
Self::HostPlatformDetectError { err } => {
error!("the host platform could not be detected");
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::RemapAbsoluteError {
arg_name,
path,
error,
} => {
error!(
"error making {} path absolute: {}",
arg_name.style(styles.bold),
path.style(styles.bold),
);
Some(error as &dyn Error)
}
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 { rerun_available } => {
error!("test run failed");
if *rerun_available {
info!(
target: "cargo_nextest::no_heading",
"(hint: {} to rerun failing tests)",
"cargo nextest run -R latest".style(styles.bold),
);
}
None
}
Self::RerunTestsOutstanding { count } => {
warn!(
"{} outstanding {} still {}",
count.style(styles.bold),
plural::tests_str(NextestRunMode::Test, *count),
plural::remain_str(*count),
);
info!(
target: "cargo_nextest::no_heading",
"(hint: {} to continue rerunning)",
"cargo nextest run -R latest".style(styles.bold),
);
None
}
Self::NoTestsRun { mode, is_default } => {
let hint_str = if *is_default {
"\n(hint: use `--no-tests` to customize)"
} else {
""
};
error!(
"no {} to run{hint_str}",
plural::tests_plural_if(*mode, true),
);
None
}
Self::DebuggerNoTests { debugger: _, mode } => {
error!(
"--debugger requires exactly one {}, but no {} were selected",
plural::tests_plural_if(*mode, false),
plural::tests_plural(*mode)
);
None
}
Self::DebuggerTooManyTests {
debugger: _,
mode,
test_count,
test_instances,
} => {
let msg = format_interceptor_too_many_tests(
"debugger",
*mode,
*test_count,
test_instances,
&styles.list_styles,
styles.bold,
);
error!("{}", msg);
None
}
Self::TracerNoTests { tracer: _, mode } => {
error!(
"--tracer requires exactly one {}, but no {} were selected",
plural::tests_plural_if(*mode, false),
plural::tests_plural(*mode)
);
None
}
Self::TracerTooManyTests {
tracer: _,
mode,
test_count,
test_instances,
} => {
let msg = format_interceptor_too_many_tests(
"tracer",
*mode,
*test_count,
test_instances,
&styles.list_styles,
styles.bold,
);
error!("{}", msg);
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)
}
#[cfg(feature = "self-update")]
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::ConfigExperimentalFeaturesNotEnabled {
config_file,
missing,
} => {
error!(
"{}",
format_experimental_features_not_enabled(config_file, missing, styles.bold)
);
None
}
Self::RecordStateDirNotFound { err } => {
error!("could not determine state directory for recording test runs");
Some(err as &dyn Error)
}
Self::RecordSetupError { err } => {
error!("error setting up recording");
Some(err as &dyn Error)
}
Self::RecordSessionSetupError { err } => {
error!("error setting up recording session");
Some(err as &dyn Error)
}
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
}
#[cfg(unix)]
Self::DoubleSpawnParseArgsError { args, err } => {
error!("[double-spawn] failed to parse arguments `{args}`");
Some(err as &dyn Error)
}
#[cfg(unix)]
Self::DoubleSpawnExecError {
command,
current_dir,
err,
} => {
let current_dir_str = match current_dir {
Ok(dir) => dir.to_string_lossy().into_owned(),
Err(e) => format!("(error: {e})"),
};
error!(
"[double-spawn] failed to exec `{command:?}`, current_dir: `{current_dir_str}`"
);
Some(err as &dyn Error)
}
Self::InvalidMessageFormatVersion { err } => {
error!("error parsing message format version");
Some(err as &dyn Error)
}
Self::CargoMessageFormatError { err } => {
error!("invalid --cargo-message-format: {err}");
None
}
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)
}
Self::DebugExtractRecordingError { message } => {
error!("{message}");
None
}
Self::RunIdResolutionError { err } => {
match &err {
RunIdResolutionError::NotFound { prefix } => {
error!(
"no recorded run found matching `{}`",
prefix.style(styles.bold)
);
}
RunIdResolutionError::Ambiguous {
prefix,
count,
candidates,
run_id_index,
} => {
error!(
"prefix `{}` is ambiguous, matches {} {}:",
prefix.style(styles.bold),
count.style(styles.bold),
plural::runs_str(*count)
);
let redactor = if crate::output::should_redact() {
Redactor::for_snapshot_testing()
} else {
Redactor::noop()
};
let alignment = RunListAlignment::from_runs(candidates);
for candidate in candidates {
info!(
target: "cargo_nextest::no_heading",
"{}",
candidate.display(run_id_index, &ReplayabilityStatus::Replayable, alignment, &styles.record_styles, &redactor)
);
}
if *count > candidates.len() {
let remaining = *count - candidates.len();
info!(
target: "cargo_nextest::no_heading",
" ... and {} more {}",
remaining.style(styles.bold),
plural::runs_str(remaining)
);
}
}
RunIdResolutionError::InvalidPrefix { prefix } => {
error!(
"prefix `{}` contains invalid characters (expected hexadecimal)",
prefix.style(styles.bold)
);
}
RunIdResolutionError::NoRuns => {
error!("no recorded runs exist");
}
}
None
}
Self::RecordReadError { err } => {
error!("error reading recorded run");
Some(err as &dyn Error)
}
Self::PortableRecordingError { err } => {
error!("error creating portable recording");
Some(err as &dyn Error)
}
Self::PortableRecordingReadError { err } => {
error!("error reading portable recording");
Some(err as &dyn Error)
}
Self::StoreVersionIncompatible {
run_id,
incompatibility,
} => {
error!(
"run {} has incompatible store format: {}",
run_id.style(styles.bold),
incompatibility,
);
None
}
Self::TestListFromSummaryError { err } => {
error!("error reconstructing test list from archived summary");
Some(err as &dyn Error)
}
Self::WriteError { err } => {
error!("write error");
Some(err as &dyn Error)
}
};
while let Some(err) = next_error {
error!(
target: "cargo_nextest::no_heading",
"\nCaused by:\n{}",
DisplayIndented { item: err, indent: " " },
);
next_error = err.source();
}
}
}
pub(crate) fn format_experimental_features_not_enabled(
config_file: &Utf8PathBuf,
missing: &[ConfigExperimental],
bold: Style,
) -> String {
if missing.len() == 1 {
let env_hint = if let Some(env_var) = missing[0].env_var() {
format!(", or set {}=1", env_var)
} else {
String::new()
};
format!(
"experimental feature not enabled: {}\n\
(hint: add to the {} list in {}{})",
missing[0].style(bold),
"experimental".style(bold),
config_file.style(bold),
env_hint,
)
} else {
format!(
"experimental features not enabled: {}\n\
(hint: add to the {} list in {})",
missing.iter().map(|f| f.style(bold)).join(", "),
"experimental".style(bold),
config_file.style(bold),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use insta::assert_snapshot;
#[test]
fn test_format_experimental_features_not_enabled() {
let config_file = Utf8PathBuf::from(".config/nextest.toml");
let style = Style::default();
assert_snapshot!(
"single_with_env_var",
format_experimental_features_not_enabled(
&config_file,
&[ConfigExperimental::Benchmarks],
style,
)
);
assert_snapshot!(
"single_without_env_var",
format_experimental_features_not_enabled(
&config_file,
&[ConfigExperimental::SetupScripts],
style,
)
);
assert_snapshot!(
"multiple_features",
format_experimental_features_not_enabled(
&config_file,
&[
ConfigExperimental::Benchmarks,
ConfigExperimental::SetupScripts,
],
style,
)
);
}
}