1use serde::{Deserialize, Serialize};
7
8use crate::baseline::BaselineComparison;
9use crate::budget::PolicyResult;
10use crate::confidence::Confidence;
11use crate::diagnostics::Diagnostic;
12use crate::metadata::RunMetadata;
13use crate::parser::{CallNode, ScopeResult};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "lowercase")]
18pub enum Status {
19 Pass,
21 Warn,
23 Fail,
25 Unknown,
27}
28
29impl Status {
30 #[must_use]
32 pub fn label(self) -> &'static str {
33 match self {
34 Self::Pass => "PASS",
35 Self::Warn => "WARN",
36 Self::Fail => "FAIL",
37 Self::Unknown => "UNKNOWN",
38 }
39 }
40
41 #[must_use]
43 pub fn from_policy(p: crate::budget::PolicyStatus) -> Self {
44 match p {
45 crate::budget::PolicyStatus::Pass => Self::Pass,
46 crate::budget::PolicyStatus::Warn => Self::Warn,
47 crate::budget::PolicyStatus::Fail => Self::Fail,
48 }
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54pub struct InstructionMeasurement {
55 pub index: usize,
57 pub program_id: String,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub label: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub consumed: Option<u64>,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
73pub struct SampleStats {
74 pub count: u32,
76 pub min: u64,
78 pub median: u64,
80 pub max: u64,
82 pub variance: f64,
84 pub std_dev: f64,
86 pub cv: f64,
88}
89
90impl SampleStats {
91 #[must_use]
94 pub fn from_samples(totals: &[u64]) -> Option<Self> {
95 if totals.len() < 2 {
96 return None;
97 }
98 let count = totals.len() as u32;
99 let min = *totals.iter().min()?;
100 let max = *totals.iter().max()?;
101
102 let mut sorted = totals.to_vec();
103 sorted.sort_unstable();
104 let mid = sorted.len() / 2;
105 let median = if sorted.len() % 2 == 1 {
106 sorted[mid]
107 } else {
108 (sorted[mid - 1] + sorted[mid]) / 2
109 };
110
111 let n = totals.len() as f64;
112 let mean = totals.iter().map(|&x| x as f64).sum::<f64>() / n;
113 let variance = totals
114 .iter()
115 .map(|&x| {
116 let d = x as f64 - mean;
117 d * d
118 })
119 .sum::<f64>()
120 / n;
121 let std_dev = variance.sqrt();
122 let cv = if mean == 0.0 { 0.0 } else { std_dev / mean };
123
124 Some(Self {
125 count,
126 min,
127 median,
128 max,
129 variance,
130 std_dev,
131 cv,
132 })
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
138pub struct Measurement {
139 pub total_cu: u64,
141 pub consumed: u64,
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub requested_limit: Option<u64>,
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub over_requested: Option<u64>,
149 pub cpi_count: u32,
151 pub cpi_depth: u32,
153 pub unattributed_pct: f64,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub instrumentation_overhead_pct: Option<f64>,
158 #[serde(default, skip_serializing_if = "Vec::is_empty")]
160 pub per_instruction: Vec<InstructionMeasurement>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub sample_stats: Option<SampleStats>,
165 pub simulation_success: bool,
167}
168
169impl Measurement {
170 #[must_use]
173 pub fn empty() -> Self {
174 Self {
175 total_cu: 0,
176 consumed: 0,
177 requested_limit: None,
178 over_requested: None,
179 cpi_count: 0,
180 cpi_depth: 0,
181 unattributed_pct: 0.0,
182 instrumentation_overhead_pct: None,
183 per_instruction: Vec::new(),
184 sample_stats: None,
185 simulation_success: true,
186 }
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192pub struct ScenarioReport {
193 pub name: String,
195 pub status: Status,
197 pub measurement: Measurement,
199 #[serde(skip_serializing_if = "Option::is_none")]
201 pub call_tree: Option<CallNode>,
202 #[serde(default, skip_serializing_if = "Vec::is_empty")]
204 pub scopes: Vec<ScopeResult>,
205 #[serde(default, skip_serializing_if = "Vec::is_empty")]
207 pub policy_results: Vec<PolicyResult>,
208 #[serde(default, skip_serializing_if = "Vec::is_empty")]
210 pub diagnostics: Vec<Diagnostic>,
211 pub confidence: Confidence,
213 #[serde(skip_serializing_if = "Option::is_none")]
215 pub baseline_comparison: Option<BaselineComparison>,
216 #[serde(default, skip_serializing_if = "Vec::is_empty")]
218 pub parser_warnings: Vec<String>,
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub raw_logs: Option<Vec<String>>,
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
226pub struct Summary {
227 pub total_scenarios: usize,
229 pub passed: usize,
231 pub warned: usize,
233 pub failed: usize,
235 pub total_cu: u64,
237}
238
239#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
241pub struct Report {
242 pub summary: Summary,
244 pub scenarios: Vec<ScenarioReport>,
246 pub metadata: RunMetadata,
248}
249
250impl Report {
251 #[must_use]
253 pub fn new(scenarios: Vec<ScenarioReport>, metadata: RunMetadata) -> Self {
254 let mut summary = Summary {
255 total_scenarios: scenarios.len(),
256 passed: 0,
257 warned: 0,
258 failed: 0,
259 total_cu: 0,
260 };
261 for s in &scenarios {
262 summary.total_cu = summary.total_cu.saturating_add(s.measurement.total_cu);
263 match s.status {
264 Status::Pass => summary.passed += 1,
265 Status::Warn => summary.warned += 1,
266 Status::Fail | Status::Unknown => summary.failed += 1,
267 }
268 }
269 Self {
270 summary,
271 scenarios,
272 metadata,
273 }
274 }
275
276 #[must_use]
278 pub fn has_failures(&self) -> bool {
279 self.summary.failed > 0
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use crate::confidence::Confidence;
287 use crate::metadata::RunMetadata;
288
289 #[test]
290 fn sample_stats_needs_at_least_two_samples() {
291 assert!(SampleStats::from_samples(&[]).is_none());
292 assert!(SampleStats::from_samples(&[42]).is_none());
293 }
294
295 #[test]
296 fn sample_stats_computes_spread() {
297 let s = SampleStats::from_samples(&[100, 120, 110]).expect("two+ samples");
298 assert_eq!(s.count, 3);
299 assert_eq!(s.min, 100);
300 assert_eq!(s.max, 120);
301 assert_eq!(s.median, 110);
302 assert!((s.variance - 66.6667).abs() < 0.01);
304 assert!((s.std_dev - 8.165).abs() < 0.01);
305 assert!((s.cv - 0.0742).abs() < 0.001);
306 }
307
308 #[test]
309 fn sample_stats_identical_samples_have_zero_variance() {
310 let s = SampleStats::from_samples(&[500, 500, 500, 500]).unwrap();
311 assert_eq!(s.variance, 0.0);
312 assert_eq!(s.cv, 0.0);
313 assert_eq!(s.median, 500);
314 }
315
316 #[test]
317 fn sample_stats_even_count_medians_the_middle_pair() {
318 let s = SampleStats::from_samples(&[10, 20, 30, 40]).unwrap();
319 assert_eq!(s.median, 25); }
321
322 fn scenario(name: &str, status: Status, cu: u64) -> ScenarioReport {
323 ScenarioReport {
324 name: name.into(),
325 status,
326 measurement: Measurement {
327 total_cu: cu,
328 ..Measurement::empty()
329 },
330 call_tree: None,
331 scopes: Vec::new(),
332 policy_results: Vec::new(),
333 diagnostics: Vec::new(),
334 confidence: Confidence::high(),
335 baseline_comparison: None,
336 parser_warnings: Vec::new(),
337 raw_logs: None,
338 }
339 }
340
341 #[test]
342 fn summary_counts_and_totals() {
343 let r = Report::new(
344 vec![
345 scenario("a", Status::Pass, 100),
346 scenario("b", Status::Warn, 200),
347 scenario("c", Status::Fail, 300),
348 ],
349 RunMetadata::recorded("0.1.0"),
350 );
351 assert_eq!(r.summary.passed, 1);
352 assert_eq!(r.summary.warned, 1);
353 assert_eq!(r.summary.failed, 1);
354 assert_eq!(r.summary.total_cu, 600);
355 assert!(r.has_failures());
356 }
357}