use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProtocolDiffReport {
pub protocol: String,
pub rows: Vec<DiffRow>,
}
impl ProtocolDiffReport {
pub fn has_regressions(&self) -> bool {
self.rows.iter().any(DiffRow::is_regression)
}
pub fn regressions(&self) -> impl Iterator<Item = &DiffRow> {
self.rows.iter().filter(|r| r.is_regression())
}
pub fn improvements(&self) -> impl Iterator<Item = &DiffRow> {
self.rows.iter().filter(|r| r.is_improvement())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffRow {
pub metric: String,
pub base: f64,
pub target: f64,
pub delta: f64,
pub pct: f64,
pub higher_is_worse: bool,
}
impl DiffRow {
pub fn new(metric: &str, base: f64, target: f64, higher_is_worse: bool) -> Self {
let delta = target - base;
let pct = if base != 0.0 { delta / base * 100.0 } else { 0.0 };
Self {
metric: metric.to_string(),
base,
target,
delta,
pct,
higher_is_worse,
}
}
pub fn is_regression(&self) -> bool {
(self.higher_is_worse && self.delta > 0.0)
|| (!self.higher_is_worse && self.delta < 0.0)
}
pub fn is_improvement(&self) -> bool {
(self.higher_is_worse && self.delta < 0.0)
|| (!self.higher_is_worse && self.delta > 0.0)
}
pub fn is_neutral(&self) -> bool {
self.delta == 0.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn positive_delta_with_higher_is_worse_is_regression() {
let row = DiffRow::new("Total Gas", 100.0, 150.0, true);
assert_eq!(row.delta, 50.0);
assert_eq!(row.pct, 50.0);
assert!(row.is_regression());
assert!(!row.is_improvement());
assert!(!row.is_neutral());
}
#[test]
fn negative_delta_with_higher_is_worse_is_improvement() {
let row = DiffRow::new("Total Gas", 100.0, 80.0, true);
assert_eq!(row.delta, -20.0);
assert_eq!(row.pct, -20.0);
assert!(row.is_improvement());
assert!(!row.is_regression());
}
#[test]
fn zero_base_pct_is_zero_not_nan() {
let row = DiffRow::new("New Metric", 0.0, 42.0, true);
assert_eq!(row.delta, 42.0);
assert_eq!(row.pct, 0.0, "pct must be 0 when base is 0 to avoid NaN/inf");
}
#[test]
fn unchanged_metric_is_neutral() {
let row = DiffRow::new("Steps", 50.0, 50.0, true);
assert!(row.is_neutral());
assert!(!row.is_regression());
assert!(!row.is_improvement());
}
#[test]
fn lower_is_better_regression_when_delta_negative() {
let row = DiffRow::new("Coverage %", 80.0, 70.0, false);
assert!(row.is_regression());
assert!(!row.is_improvement());
}
#[test]
fn report_detects_regressions() {
let rows = vec![
DiffRow::new("Gas", 100.0, 120.0, true), DiffRow::new("Steps", 50.0, 50.0, true), DiffRow::new("Reads", 10.0, 8.0, true), ];
let report = ProtocolDiffReport { protocol: "Test".to_string(), rows };
assert!(report.has_regressions());
assert_eq!(report.regressions().count(), 1);
assert_eq!(report.improvements().count(), 1);
}
#[test]
fn report_with_no_regressions() {
let rows = vec![
DiffRow::new("Gas", 100.0, 90.0, true), DiffRow::new("Steps", 50.0, 50.0, true), ];
let report = ProtocolDiffReport { protocol: "Test".to_string(), rows };
assert!(!report.has_regressions());
assert_eq!(report.regressions().count(), 0);
assert_eq!(report.improvements().count(), 1);
}
}