Skip to main content

assay_sim/
report.rs

1use anyhow::Result;
2use assay_evidence::bundle::writer::{ErrorClass, ErrorCode};
3use serde::Serialize;
4
5#[derive(Debug, Serialize, Clone)]
6pub struct SimReport {
7    pub suite: String,
8    pub seed: u64,
9    pub summary: SimSummary,
10    pub results: Vec<AttackResult>,
11    /// True when suite exited early due to time budget
12    #[serde(skip_serializing_if = "std::ops::Not::not")]
13    pub time_budget_exceeded: bool,
14    /// Phases skipped when time budget exceeded
15    #[serde(skip_serializing_if = "Vec::is_empty")]
16    pub skipped_phases: Vec<String>,
17}
18
19#[derive(Debug, Serialize, Clone, Default)]
20pub struct SimSummary {
21    pub total: usize,
22    pub passed: usize,   // For invariant checks
23    pub blocked: usize,  // For attacks
24    pub bypassed: usize, // For attacks
25    pub failed: usize,   // For invariant checks
26    pub errors: usize,
27}
28
29#[derive(Debug, Serialize, Clone)]
30pub struct AttackResult {
31    pub name: String,
32    pub status: AttackStatus,
33    pub error_class: Option<String>,
34    pub error_code: Option<String>,
35    pub message: Option<String>,
36    pub duration_ms: u64,
37}
38
39#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
40pub enum AttackStatus {
41    Passed,   // Invariant held
42    Failed,   // Invariant broken
43    Blocked,  // Attack was stopped
44    Bypassed, // Attack succeeded
45    Error,    // Infrastructure error
46}
47
48impl SimReport {
49    pub fn new(suite: &str, seed: u64) -> Self {
50        Self {
51            suite: suite.to_string(),
52            seed,
53            summary: SimSummary::default(),
54            results: Vec::new(),
55            time_budget_exceeded: false,
56            skipped_phases: Vec::new(),
57        }
58    }
59
60    pub fn set_time_budget_exceeded(&mut self, skipped: Vec<String>) {
61        self.time_budget_exceeded = true;
62        self.skipped_phases = skipped;
63    }
64
65    pub fn add_attack(
66        &mut self,
67        name: &str,
68        result: Result<(ErrorClass, ErrorCode), anyhow::Error>,
69        duration_ms: u64,
70    ) {
71        self.summary.total += 1;
72        let res = match result {
73            Ok((class, code)) => {
74                self.summary.blocked += 1;
75                AttackResult {
76                    name: name.to_string(),
77                    status: AttackStatus::Blocked,
78                    error_class: Some(format!("{:?}", class)),
79                    error_code: Some(format!("{:?}", code)),
80                    message: None,
81                    duration_ms,
82                }
83            }
84            Err(e) => {
85                self.summary.bypassed += 1;
86                AttackResult {
87                    name: name.to_string(),
88                    status: AttackStatus::Bypassed,
89                    error_class: None,
90                    error_code: None,
91                    message: Some(e.to_string()),
92                    duration_ms,
93                }
94            }
95        };
96        self.results.push(res);
97    }
98
99    /// Add a pre-built AttackResult directly.
100    pub fn add_result(&mut self, result: AttackResult) {
101        self.summary.total += 1;
102        match result.status {
103            AttackStatus::Passed => self.summary.passed += 1,
104            AttackStatus::Failed => self.summary.failed += 1,
105            AttackStatus::Blocked => self.summary.blocked += 1,
106            AttackStatus::Bypassed => self.summary.bypassed += 1,
107            AttackStatus::Error => self.summary.errors += 1,
108        }
109        self.results.push(result);
110    }
111
112    pub fn add_check(&mut self, name: &str, result: Result<()>, duration_ms: u64) {
113        self.summary.total += 1;
114        let res = match result {
115            Ok(_) => {
116                self.summary.passed += 1;
117                AttackResult {
118                    name: name.to_string(),
119                    status: AttackStatus::Passed,
120                    error_class: None,
121                    error_code: None,
122                    message: None,
123                    duration_ms,
124                }
125            }
126            Err(e) => {
127                self.summary.failed += 1;
128                AttackResult {
129                    name: name.to_string(),
130                    status: AttackStatus::Failed,
131                    error_class: None,
132                    error_code: None,
133                    message: Some(e.to_string()),
134                    duration_ms,
135                }
136            }
137        };
138        self.results.push(res);
139    }
140}