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}
12
13#[derive(Debug, Serialize, Clone, Default)]
14pub struct SimSummary {
15    pub total: usize,
16    pub passed: usize,   // New: For invariants
17    pub blocked: usize,  // For attacks
18    pub bypassed: usize, // For attacks
19    pub failed: usize,   // New: For invariants
20    pub errors: usize,
21}
22
23#[derive(Debug, Serialize, Clone)]
24pub struct AttackResult {
25    pub name: String,
26    pub status: AttackStatus,
27    pub error_class: Option<String>,
28    pub error_code: Option<String>,
29    pub message: Option<String>,
30    pub duration_ms: u64, // New: DX requirement
31}
32
33#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
34pub enum AttackStatus {
35    Passed,   // New: Invariant held
36    Failed,   // New: Invariant broken
37    Blocked,  // Attack was stopped
38    Bypassed, // Attack succeeded
39    Error,    // Infrastructure error
40}
41
42impl SimReport {
43    pub fn new(suite: &str, seed: u64) -> Self {
44        Self {
45            suite: suite.to_string(),
46            seed,
47            summary: SimSummary::default(),
48            results: Vec::new(),
49        }
50    }
51
52    pub fn add_attack(
53        &mut self,
54        name: &str,
55        result: Result<(ErrorClass, ErrorCode), anyhow::Error>,
56        duration_ms: u64,
57    ) {
58        self.summary.total += 1;
59        let res = match result {
60            Ok((class, code)) => {
61                self.summary.blocked += 1;
62                AttackResult {
63                    name: name.to_string(),
64                    status: AttackStatus::Blocked,
65                    error_class: Some(format!("{:?}", class)),
66                    error_code: Some(format!("{:?}", code)),
67                    message: None,
68                    duration_ms,
69                }
70            }
71            Err(e) => {
72                self.summary.bypassed += 1;
73                AttackResult {
74                    name: name.to_string(),
75                    status: AttackStatus::Bypassed,
76                    error_class: None,
77                    error_code: None,
78                    message: Some(e.to_string()),
79                    duration_ms,
80                }
81            }
82        };
83        self.results.push(res);
84    }
85
86    /// Add a pre-built AttackResult directly.
87    pub fn add_result(&mut self, result: AttackResult) {
88        self.summary.total += 1;
89        match result.status {
90            AttackStatus::Passed => self.summary.passed += 1,
91            AttackStatus::Failed => self.summary.failed += 1,
92            AttackStatus::Blocked => self.summary.blocked += 1,
93            AttackStatus::Bypassed => self.summary.bypassed += 1,
94            AttackStatus::Error => self.summary.errors += 1,
95        }
96        self.results.push(result);
97    }
98
99    pub fn add_check(&mut self, name: &str, result: Result<()>, duration_ms: u64) {
100        self.summary.total += 1;
101        let res = match result {
102            Ok(_) => {
103                self.summary.passed += 1;
104                AttackResult {
105                    name: name.to_string(),
106                    status: AttackStatus::Passed,
107                    error_class: None,
108                    error_code: None,
109                    message: None,
110                    duration_ms,
111                }
112            }
113            Err(e) => {
114                self.summary.failed += 1;
115                AttackResult {
116                    name: name.to_string(),
117                    status: AttackStatus::Failed,
118                    error_class: None,
119                    error_code: None,
120                    message: Some(e.to_string()),
121                    duration_ms,
122                }
123            }
124        };
125        self.results.push(res);
126    }
127}