use std::fmt;
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Outcome {
AlreadyConverged(String),
Acted(String),
Skipped(String),
}
impl Outcome {
#[must_use]
pub const fn tag(&self) -> &'static str {
match self {
Self::AlreadyConverged(_) => "already",
Self::Acted(_) => "acted",
Self::Skipped(_) => "skipped",
}
}
#[must_use]
pub fn detail(&self) -> &str {
match self {
Self::AlreadyConverged(detail) | Self::Acted(detail) | Self::Skipped(detail) => detail,
}
}
#[must_use]
pub const fn changed_anything(&self) -> bool {
matches!(self, Self::Acted(_))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Failure {
pub step: &'static str,
pub purpose: &'static str,
pub expected: String,
pub found: String,
}
impl fmt::Display for Failure {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"step {} failed\n expected: {}\n found: {}\n why: {}",
self.step, self.expected, self.found, self.purpose
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StepReport {
pub step: &'static str,
pub outcome: Result<Outcome, Failure>,
pub took: Duration,
}
impl StepReport {
#[must_use]
pub fn line(&self) -> String {
let millis = self.took.as_millis();
match &self.outcome {
Ok(outcome) => format!(
"{:<22} {:<8} {} ({millis} ms)",
self.step,
outcome.tag(),
outcome.detail()
),
Err(failure) => format!("{:<22} {:<8} {}", self.step, "failed", failure.expected),
}
}
#[must_use]
pub const fn failed(&self) -> bool {
self.outcome.is_err()
}
}
#[derive(Debug, Default)]
pub struct Report {
pub steps: Vec<StepReport>,
}
impl Report {
pub fn run(
&mut self,
step: &'static str,
action: impl FnOnce() -> Result<Outcome, Failure>,
) -> bool {
let started = std::time::Instant::now();
let outcome = action();
let failed = outcome.is_err();
self.steps.push(StepReport {
step,
outcome,
took: started.elapsed(),
});
!failed
}
#[must_use]
pub fn converged(&self) -> bool {
self.steps.iter().all(|step| !step.failed())
}
#[must_use]
pub fn changed_anything(&self) -> bool {
self.steps
.iter()
.filter_map(|step| step.outcome.as_ref().ok())
.any(Outcome::changed_anything)
}
#[must_use]
pub fn failure(&self) -> Option<&Failure> {
self.steps
.iter()
.find_map(|step| step.outcome.as_ref().err())
}
#[must_use]
pub fn skips(&self) -> Vec<(&'static str, &str)> {
self.steps
.iter()
.filter_map(|step| match step.outcome.as_ref().ok()? {
Outcome::Skipped(reason) => Some((step.step, reason.as_str())),
_ => None,
})
.collect()
}
#[must_use]
pub fn total(&self) -> Duration {
self.steps.iter().map(|step| step.took).sum()
}
pub fn print(&self) {
for step in &self.steps {
println!("{}", step.line());
}
if let Some(failure) = self.failure() {
eprintln!("\n{failure}");
}
let skips = self.skips();
if !skips.is_empty() {
println!("\nskipped:");
for (step, reason) in skips {
println!(" {step}: {reason}");
}
}
println!("\ntotal {} ms", self.total().as_millis());
}
}