1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ProtocolDiffReport {
10 pub protocol: String,
12 pub rows: Vec<DiffRow>,
14}
15
16impl ProtocolDiffReport {
17 pub fn has_regressions(&self) -> bool {
19 self.rows.iter().any(DiffRow::is_regression)
20 }
21
22 pub fn regressions(&self) -> impl Iterator<Item = &DiffRow> {
24 self.rows.iter().filter(|r| r.is_regression())
25 }
26
27 pub fn improvements(&self) -> impl Iterator<Item = &DiffRow> {
29 self.rows.iter().filter(|r| r.is_improvement())
30 }
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct DiffRow {
38 pub metric: String,
40 pub base: f64,
42 pub target: f64,
44 pub delta: f64,
46 pub pct: f64,
50 pub higher_is_worse: bool,
53}
54
55impl DiffRow {
56 pub fn new(metric: &str, base: f64, target: f64, higher_is_worse: bool) -> Self {
67 let delta = target - base;
68 let pct = if base != 0.0 { delta / base * 100.0 } else { 0.0 };
69 Self {
70 metric: metric.to_string(),
71 base,
72 target,
73 delta,
74 pct,
75 higher_is_worse,
76 }
77 }
78
79 pub fn is_regression(&self) -> bool {
84 (self.higher_is_worse && self.delta > 0.0)
85 || (!self.higher_is_worse && self.delta < 0.0)
86 }
87
88 pub fn is_improvement(&self) -> bool {
90 (self.higher_is_worse && self.delta < 0.0)
91 || (!self.higher_is_worse && self.delta > 0.0)
92 }
93
94 pub fn is_neutral(&self) -> bool {
96 self.delta == 0.0
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
107 fn positive_delta_with_higher_is_worse_is_regression() {
108 let row = DiffRow::new("Total Gas", 100.0, 150.0, true);
109 assert_eq!(row.delta, 50.0);
110 assert_eq!(row.pct, 50.0);
111 assert!(row.is_regression());
112 assert!(!row.is_improvement());
113 assert!(!row.is_neutral());
114 }
115
116 #[test]
117 fn negative_delta_with_higher_is_worse_is_improvement() {
118 let row = DiffRow::new("Total Gas", 100.0, 80.0, true);
119 assert_eq!(row.delta, -20.0);
120 assert_eq!(row.pct, -20.0);
121 assert!(row.is_improvement());
122 assert!(!row.is_regression());
123 }
124
125 #[test]
126 fn zero_base_pct_is_zero_not_nan() {
127 let row = DiffRow::new("New Metric", 0.0, 42.0, true);
128 assert_eq!(row.delta, 42.0);
129 assert_eq!(row.pct, 0.0, "pct must be 0 when base is 0 to avoid NaN/inf");
130 }
131
132 #[test]
133 fn unchanged_metric_is_neutral() {
134 let row = DiffRow::new("Steps", 50.0, 50.0, true);
135 assert!(row.is_neutral());
136 assert!(!row.is_regression());
137 assert!(!row.is_improvement());
138 }
139
140 #[test]
141 fn lower_is_better_regression_when_delta_negative() {
142 let row = DiffRow::new("Coverage %", 80.0, 70.0, false);
144 assert!(row.is_regression());
145 assert!(!row.is_improvement());
146 }
147
148 #[test]
151 fn report_detects_regressions() {
152 let rows = vec![
153 DiffRow::new("Gas", 100.0, 120.0, true), DiffRow::new("Steps", 50.0, 50.0, true), DiffRow::new("Reads", 10.0, 8.0, true), ];
157 let report = ProtocolDiffReport { protocol: "Test".to_string(), rows };
158 assert!(report.has_regressions());
159 assert_eq!(report.regressions().count(), 1);
160 assert_eq!(report.improvements().count(), 1);
161 }
162
163 #[test]
164 fn report_with_no_regressions() {
165 let rows = vec![
166 DiffRow::new("Gas", 100.0, 90.0, true), DiffRow::new("Steps", 50.0, 50.0, true), ];
169 let report = ProtocolDiffReport { protocol: "Test".to_string(), rows };
170 assert!(!report.has_regressions());
171 assert_eq!(report.regressions().count(), 0);
172 assert_eq!(report.improvements().count(), 1);
173 }
174}