use std::fmt;
use std::process::ExitCode;
use std::time::Duration;
use anyhow::Error;
use super::constants::EXIT_CODE_WARNING;
use super::diagnostics::Report;
use super::run_mode::OperationIntent;
#[derive(Debug)]
pub(crate) struct ExecutionOutcome {
pub report: Report,
pub notice: Option<ExecutionNotice>,
pub check_duration: Duration,
pub compiler_warning_count: usize,
pub compiler_fixable_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ExecutionNotice {
kinds: Vec<NoticeKind>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum NoticeKind {
ImportFixes(FixNotice),
PubUseFixes(PubUseNotice),
ImportCleanupSuggested,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum FixNotice {
NoneAvailable,
PreviewApplied(usize),
Applied(usize),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum PubUseNotice {
NoneAvailable {
skipped_unsupported: usize,
},
PreviewApplied {
applied: usize,
skipped_unsupported: usize,
},
Applied {
applied: usize,
skipped_unsupported: usize,
},
}
#[derive(Debug)]
pub(crate) enum MendFailure {
Analysis(AnalysisFailure),
FixValidation(FixValidationFailure),
Unexpected(Error),
}
#[derive(Debug)]
pub(crate) enum CompilerFailureCause {
CargoCheck,
DriverSetup(Error),
DriverExecution(Error),
Unexpected(Error),
}
#[derive(Debug)]
pub(crate) struct AnalysisFailure {
pub cause: CompilerFailureCause,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RollbackStatus {
Restored,
RestoreFailed,
}
#[derive(Debug)]
pub(crate) struct FixValidationFailure {
pub rollback: RollbackStatus,
pub cause: CompilerFailureCause,
}
impl MendFailure {
pub(crate) fn exit_code() -> ExitCode { ExitCode::from(EXIT_CODE_WARNING) }
}
impl fmt::Display for MendFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Analysis(failure) => write!(f, "{failure}"),
Self::FixValidation(failure) => write!(f, "{failure}"),
Self::Unexpected(error) => write!(f, "{error:#}"),
}
}
}
impl fmt::Display for AnalysisFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.cause {
CompilerFailureCause::CargoCheck => {
write!(
f,
"compiler failed while validating this crate\n\nmend: did not run due to compiler errors"
)
},
CompilerFailureCause::DriverSetup(error)
| CompilerFailureCause::DriverExecution(error)
| CompilerFailureCause::Unexpected(error) => write!(f, "{error:#}"),
}
}
}
impl fmt::Display for FixValidationFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let source = match &self.cause {
CompilerFailureCause::CargoCheck => {
"compiler failed after applying mend fixes".to_string()
},
CompilerFailureCause::DriverSetup(error)
| CompilerFailureCause::DriverExecution(error)
| CompilerFailureCause::Unexpected(error) => format!("{error:#}"),
};
match self.rollback {
RollbackStatus::Restored => write!(
f,
"compiler failed after applying mend fixes; changes were rolled back\n\n{source:#}"
),
RollbackStatus::RestoreFailed => write!(
f,
"compiler failed after applying mend fixes, and rollback also failed\n\n{source:#}"
),
}
}
}
impl From<Error> for MendFailure {
fn from(value: Error) -> Self { Self::Unexpected(value) }
}
impl ExecutionNotice {
pub(crate) fn from_kind(kind: NoticeKind) -> Self { Self { kinds: vec![kind] } }
pub(crate) const fn from_kinds(kinds: Vec<NoticeKind>) -> Self { Self { kinds } }
pub(crate) fn render(&self) -> String {
let parts = self
.kinds
.iter()
.map(NoticeKind::render_part)
.collect::<Vec<_>>();
format!("mend: {}", parts.join("; "))
}
}
impl NoticeKind {
fn render_part(&self) -> String {
match self {
Self::ImportFixes(notice) => notice.render(),
Self::PubUseFixes(notice) => notice.render(),
Self::ImportCleanupSuggested => {
"some imports may now be unused; consider running cargo fix or cleaning them up manually"
.to_string()
},
}
}
}
impl FixNotice {
fn render(&self) -> String {
match self {
Self::NoneAvailable => "no import fixes available".to_string(),
Self::PreviewApplied(count) => format!("would apply {count} import fix(es) in dry run"),
Self::Applied(count) => format!("applied {count} import fix(es)"),
}
}
pub(crate) const fn from_intent(intent: OperationIntent, count: usize) -> Self {
match intent {
OperationIntent::ReadOnly => Self::NoneAvailable,
OperationIntent::DryRun => {
if count == 0 {
Self::NoneAvailable
} else {
Self::PreviewApplied(count)
}
},
OperationIntent::Apply => {
if count == 0 {
Self::NoneAvailable
} else {
Self::Applied(count)
}
},
}
}
}
impl PubUseNotice {
fn render(&self) -> String {
match self {
Self::NoneAvailable {
skipped_unsupported: 0,
} => "no `pub use` fixes available".to_string(),
Self::NoneAvailable {
skipped_unsupported,
} => format!(
"no `pub use` fixes available; skipped {skipped_unsupported} unsupported `pub use` candidate(s)"
),
Self::PreviewApplied {
applied,
skipped_unsupported: 0,
} => format!("would apply {applied} `pub use` fix(es) in dry run"),
Self::PreviewApplied {
applied,
skipped_unsupported,
} => format!(
"would apply {applied} `pub use` fix(es) in dry run; skipped {skipped_unsupported} unsupported `pub use` candidate(s)"
),
Self::Applied {
applied,
skipped_unsupported: 0,
} => format!("applied {applied} `pub use` fix(es)"),
Self::Applied {
applied,
skipped_unsupported,
} => format!(
"applied {applied} `pub use` fix(es); skipped {skipped_unsupported} unsupported `pub use` candidate(s)"
),
}
}
pub(crate) const fn from_intent(
intent: OperationIntent,
applied: usize,
skipped_unsupported: usize,
) -> Self {
match intent {
OperationIntent::ReadOnly => Self::NoneAvailable {
skipped_unsupported,
},
OperationIntent::DryRun => {
if applied == 0 {
Self::NoneAvailable {
skipped_unsupported,
}
} else {
Self::PreviewApplied {
applied,
skipped_unsupported,
}
}
},
OperationIntent::Apply => {
if applied == 0 {
Self::NoneAvailable {
skipped_unsupported,
}
} else {
Self::Applied {
applied,
skipped_unsupported,
}
}
},
}
}
}
#[cfg(test)]
mod tests {
use anyhow::anyhow;
use super::AnalysisFailure;
use super::CompilerFailureCause;
use super::ExecutionNotice;
use super::FixNotice;
use super::FixValidationFailure;
use super::NoticeKind;
use super::PubUseNotice;
use super::RollbackStatus;
use crate::run_mode::OperationIntent;
#[test]
fn analysis_failure_message_uses_typed_collection_wording() {
let failure = AnalysisFailure {
cause: CompilerFailureCause::CargoCheck,
};
assert_eq!(
failure.to_string(),
"compiler failed while validating this crate\n\nmend: did not run due to compiler errors"
);
}
#[test]
fn fix_validation_failure_reports_rollback_success() {
let failure = FixValidationFailure {
rollback: RollbackStatus::Restored,
cause: CompilerFailureCause::Unexpected(anyhow!("boom")),
};
assert!(
failure
.to_string()
.contains("compiler failed after applying mend fixes; changes were rolled back")
);
}
#[test]
fn fix_validation_failure_reports_rollback_failure() {
let failure = FixValidationFailure {
rollback: RollbackStatus::RestoreFailed,
cause: CompilerFailureCause::Unexpected(anyhow!("boom")),
};
assert!(
failure
.to_string()
.contains("compiler failed after applying mend fixes, and rollback also failed")
);
}
#[test]
fn import_fix_notice_respects_operation_intent() {
let preview = FixNotice::from_intent(OperationIntent::DryRun, 2);
assert_eq!(preview.render(), "would apply 2 import fix(es) in dry run");
}
#[test]
fn combined_notice_renders_all_parts() {
let notice = ExecutionNotice::from_kinds(vec![
NoticeKind::ImportFixes(FixNotice::Applied(2)),
NoticeKind::PubUseFixes(PubUseNotice::Applied {
applied: 1,
skipped_unsupported: 0,
}),
]);
assert_eq!(
notice.render(),
"mend: applied 2 import fix(es); applied 1 `pub use` fix(es)"
);
}
}