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, PartialEq, Serialize, Deserialize)]
69pub struct Measurement {
70 pub total_cu: u64,
72 pub consumed: u64,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub requested_limit: Option<u64>,
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub over_requested: Option<u64>,
80 pub cpi_count: u32,
82 pub cpi_depth: u32,
84 pub unattributed_pct: f64,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub instrumentation_overhead_pct: Option<f64>,
89 #[serde(default, skip_serializing_if = "Vec::is_empty")]
91 pub per_instruction: Vec<InstructionMeasurement>,
92 pub simulation_success: bool,
94}
95
96impl Measurement {
97 #[must_use]
100 pub fn empty() -> Self {
101 Self {
102 total_cu: 0,
103 consumed: 0,
104 requested_limit: None,
105 over_requested: None,
106 cpi_count: 0,
107 cpi_depth: 0,
108 unattributed_pct: 0.0,
109 instrumentation_overhead_pct: None,
110 per_instruction: Vec::new(),
111 simulation_success: true,
112 }
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
118pub struct ScenarioReport {
119 pub name: String,
121 pub status: Status,
123 pub measurement: Measurement,
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub call_tree: Option<CallNode>,
128 #[serde(default, skip_serializing_if = "Vec::is_empty")]
130 pub scopes: Vec<ScopeResult>,
131 #[serde(default, skip_serializing_if = "Vec::is_empty")]
133 pub policy_results: Vec<PolicyResult>,
134 #[serde(default, skip_serializing_if = "Vec::is_empty")]
136 pub diagnostics: Vec<Diagnostic>,
137 pub confidence: Confidence,
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub baseline_comparison: Option<BaselineComparison>,
142 #[serde(default, skip_serializing_if = "Vec::is_empty")]
144 pub parser_warnings: Vec<String>,
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub raw_logs: Option<Vec<String>>,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
152pub struct Summary {
153 pub total_scenarios: usize,
155 pub passed: usize,
157 pub warned: usize,
159 pub failed: usize,
161 pub total_cu: u64,
163}
164
165#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
167pub struct Report {
168 pub summary: Summary,
170 pub scenarios: Vec<ScenarioReport>,
172 pub metadata: RunMetadata,
174}
175
176impl Report {
177 #[must_use]
179 pub fn new(scenarios: Vec<ScenarioReport>, metadata: RunMetadata) -> Self {
180 let mut summary = Summary {
181 total_scenarios: scenarios.len(),
182 passed: 0,
183 warned: 0,
184 failed: 0,
185 total_cu: 0,
186 };
187 for s in &scenarios {
188 summary.total_cu = summary.total_cu.saturating_add(s.measurement.total_cu);
189 match s.status {
190 Status::Pass => summary.passed += 1,
191 Status::Warn => summary.warned += 1,
192 Status::Fail | Status::Unknown => summary.failed += 1,
193 }
194 }
195 Self {
196 summary,
197 scenarios,
198 metadata,
199 }
200 }
201
202 #[must_use]
204 pub fn has_failures(&self) -> bool {
205 self.summary.failed > 0
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use crate::confidence::Confidence;
213 use crate::metadata::RunMetadata;
214
215 fn scenario(name: &str, status: Status, cu: u64) -> ScenarioReport {
216 ScenarioReport {
217 name: name.into(),
218 status,
219 measurement: Measurement {
220 total_cu: cu,
221 ..Measurement::empty()
222 },
223 call_tree: None,
224 scopes: Vec::new(),
225 policy_results: Vec::new(),
226 diagnostics: Vec::new(),
227 confidence: Confidence::high(),
228 baseline_comparison: None,
229 parser_warnings: Vec::new(),
230 raw_logs: None,
231 }
232 }
233
234 #[test]
235 fn summary_counts_and_totals() {
236 let r = Report::new(
237 vec![
238 scenario("a", Status::Pass, 100),
239 scenario("b", Status::Warn, 200),
240 scenario("c", Status::Fail, 300),
241 ],
242 RunMetadata::recorded("0.1.0"),
243 );
244 assert_eq!(r.summary.passed, 1);
245 assert_eq!(r.summary.warned, 1);
246 assert_eq!(r.summary.failed, 1);
247 assert_eq!(r.summary.total_cu, 600);
248 assert!(r.has_failures());
249 }
250}