use serde::{Deserialize, Serialize};
use crate::baseline::BaselineComparison;
use crate::budget::PolicyResult;
use crate::confidence::Confidence;
use crate::diagnostics::Diagnostic;
use crate::metadata::RunMetadata;
use crate::parser::{CallNode, ScopeResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Status {
Pass,
Warn,
Fail,
Unknown,
}
impl Status {
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Pass => "PASS",
Self::Warn => "WARN",
Self::Fail => "FAIL",
Self::Unknown => "UNKNOWN",
}
}
#[must_use]
pub fn from_policy(p: crate::budget::PolicyStatus) -> Self {
match p {
crate::budget::PolicyStatus::Pass => Self::Pass,
crate::budget::PolicyStatus::Warn => Self::Warn,
crate::budget::PolicyStatus::Fail => Self::Fail,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InstructionMeasurement {
pub index: usize,
pub program_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub consumed: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct SampleStats {
pub count: u32,
pub min: u64,
pub median: u64,
pub max: u64,
pub variance: f64,
pub std_dev: f64,
pub cv: f64,
}
impl SampleStats {
#[must_use]
pub fn from_samples(totals: &[u64]) -> Option<Self> {
if totals.len() < 2 {
return None;
}
let count = totals.len() as u32;
let min = *totals.iter().min()?;
let max = *totals.iter().max()?;
let mut sorted = totals.to_vec();
sorted.sort_unstable();
let mid = sorted.len() / 2;
let median = if sorted.len() % 2 == 1 {
sorted[mid]
} else {
(sorted[mid - 1] + sorted[mid]) / 2
};
let n = totals.len() as f64;
let mean = totals.iter().map(|&x| x as f64).sum::<f64>() / n;
let variance = totals
.iter()
.map(|&x| {
let d = x as f64 - mean;
d * d
})
.sum::<f64>()
/ n;
let std_dev = variance.sqrt();
let cv = if mean == 0.0 { 0.0 } else { std_dev / mean };
Some(Self {
count,
min,
median,
max,
variance,
std_dev,
cv,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Measurement {
pub total_cu: u64,
pub consumed: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub requested_limit: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub over_requested: Option<u64>,
pub cpi_count: u32,
pub cpi_depth: u32,
pub unattributed_pct: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub instrumentation_overhead_pct: Option<f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub per_instruction: Vec<InstructionMeasurement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sample_stats: Option<SampleStats>,
pub simulation_success: bool,
}
impl Measurement {
#[must_use]
pub fn empty() -> Self {
Self {
total_cu: 0,
consumed: 0,
requested_limit: None,
over_requested: None,
cpi_count: 0,
cpi_depth: 0,
unattributed_pct: 0.0,
instrumentation_overhead_pct: None,
per_instruction: Vec::new(),
sample_stats: None,
simulation_success: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ScenarioReport {
pub name: String,
pub status: Status,
pub measurement: Measurement,
#[serde(skip_serializing_if = "Option::is_none")]
pub call_tree: Option<CallNode>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub scopes: Vec<ScopeResult>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub policy_results: Vec<PolicyResult>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub diagnostics: Vec<Diagnostic>,
pub confidence: Confidence,
#[serde(skip_serializing_if = "Option::is_none")]
pub baseline_comparison: Option<BaselineComparison>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub parser_warnings: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_logs: Option<Vec<String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Summary {
pub total_scenarios: usize,
pub passed: usize,
pub warned: usize,
pub failed: usize,
pub total_cu: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Report {
pub summary: Summary,
pub scenarios: Vec<ScenarioReport>,
pub metadata: RunMetadata,
}
impl Report {
#[must_use]
pub fn new(scenarios: Vec<ScenarioReport>, metadata: RunMetadata) -> Self {
let mut summary = Summary {
total_scenarios: scenarios.len(),
passed: 0,
warned: 0,
failed: 0,
total_cu: 0,
};
for s in &scenarios {
summary.total_cu = summary.total_cu.saturating_add(s.measurement.total_cu);
match s.status {
Status::Pass => summary.passed += 1,
Status::Warn => summary.warned += 1,
Status::Fail | Status::Unknown => summary.failed += 1,
}
}
Self {
summary,
scenarios,
metadata,
}
}
#[must_use]
pub fn has_failures(&self) -> bool {
self.summary.failed > 0
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::confidence::Confidence;
use crate::metadata::RunMetadata;
#[test]
fn sample_stats_needs_at_least_two_samples() {
assert!(SampleStats::from_samples(&[]).is_none());
assert!(SampleStats::from_samples(&[42]).is_none());
}
#[test]
fn sample_stats_computes_spread() {
let s = SampleStats::from_samples(&[100, 120, 110]).expect("two+ samples");
assert_eq!(s.count, 3);
assert_eq!(s.min, 100);
assert_eq!(s.max, 120);
assert_eq!(s.median, 110);
assert!((s.variance - 66.6667).abs() < 0.01);
assert!((s.std_dev - 8.165).abs() < 0.01);
assert!((s.cv - 0.0742).abs() < 0.001);
}
#[test]
fn sample_stats_identical_samples_have_zero_variance() {
let s = SampleStats::from_samples(&[500, 500, 500, 500]).unwrap();
assert_eq!(s.variance, 0.0);
assert_eq!(s.cv, 0.0);
assert_eq!(s.median, 500);
}
#[test]
fn sample_stats_even_count_medians_the_middle_pair() {
let s = SampleStats::from_samples(&[10, 20, 30, 40]).unwrap();
assert_eq!(s.median, 25); }
fn scenario(name: &str, status: Status, cu: u64) -> ScenarioReport {
ScenarioReport {
name: name.into(),
status,
measurement: Measurement {
total_cu: cu,
..Measurement::empty()
},
call_tree: None,
scopes: Vec::new(),
policy_results: Vec::new(),
diagnostics: Vec::new(),
confidence: Confidence::high(),
baseline_comparison: None,
parser_warnings: Vec::new(),
raw_logs: None,
}
}
#[test]
fn summary_counts_and_totals() {
let r = Report::new(
vec![
scenario("a", Status::Pass, 100),
scenario("b", Status::Warn, 200),
scenario("c", Status::Fail, 300),
],
RunMetadata::recorded("0.1.0"),
);
assert_eq!(r.summary.passed, 1);
assert_eq!(r.summary.warned, 1);
assert_eq!(r.summary.failed, 1);
assert_eq!(r.summary.total_cu, 600);
assert!(r.has_failures());
}
}