Skip to main content

assay_sim/
suite.rs

1use crate::attacks;
2use crate::differential;
3use crate::report::{AttackResult, AttackStatus, SimReport};
4use anyhow::Result;
5use assay_evidence::VerifyLimits;
6use std::path::PathBuf;
7use std::time::{Duration, Instant};
8
9#[derive(Debug, Clone)]
10pub enum SuiteTier {
11    Quick,
12    Nightly,
13    Stress,
14    Chaos,
15}
16
17#[derive(Debug, Clone)]
18pub struct SuiteConfig {
19    pub tier: SuiteTier,
20    pub target_bundle: PathBuf, // Placeholder for future file-based targets
21    pub seed: u64,
22    pub verify_limits: Option<VerifyLimits>,
23}
24
25/// Time budget for an entire suite run.
26///
27/// If the elapsed time exceeds the budget, remaining phases are skipped and
28/// the runner reports `AttackStatus::Error` with "time budget exceeded".
29#[derive(Debug, Clone)]
30pub struct TimeBudget {
31    start: Instant,
32    limit: Duration,
33}
34
35impl TimeBudget {
36    pub fn new(limit: Duration) -> Self {
37        Self {
38            start: Instant::now(),
39            limit,
40        }
41    }
42
43    /// Default suite budget: 30 seconds.
44    pub fn default_suite() -> Self {
45        Self::new(Duration::from_secs(30))
46    }
47
48    pub fn exceeded(&self) -> bool {
49        self.start.elapsed() > self.limit
50    }
51
52    pub fn remaining(&self) -> Duration {
53        self.limit.saturating_sub(self.start.elapsed())
54    }
55}
56
57pub fn run_suite(cfg: SuiteConfig) -> Result<SimReport> {
58    let mut report = SimReport::new(&format!("{:?}", cfg.tier), cfg.seed);
59    let budget = TimeBudget::default_suite();
60
61    // 1. Integrity Attacks (all tiers)
62    //
63    // Note: The workspace uses panic="abort" in dev/release profiles, so catch_unwind
64    // is not effective. Integrity attacks run in-process (they don't trigger panics —
65    // they test verification outcomes). Chaos/differential attacks use subprocess
66    // isolation instead.
67    {
68        let seed = cfg.seed;
69        let start = Instant::now();
70        let mut inner_report = SimReport::new("integrity", seed);
71        match attacks::integrity::check_integrity_attacks(&mut inner_report, seed) {
72            Ok(()) => {
73                for r in inner_report.results {
74                    report.add_result(r);
75                }
76            }
77            Err(e) => {
78                for r in inner_report.results {
79                    report.add_result(r);
80                }
81                report.add_result(AttackResult {
82                    name: "integrity_attacks".into(),
83                    status: AttackStatus::Error,
84                    error_class: None,
85                    error_code: None,
86                    message: Some(e.to_string()),
87                    duration_ms: start.elapsed().as_millis() as u64,
88                });
89            }
90        }
91    }
92
93    if budget.exceeded() {
94        report.add_result(AttackResult {
95            name: "time_budget".into(),
96            status: AttackStatus::Error,
97            error_class: None,
98            error_code: None,
99            message: Some("time budget exceeded after integrity attacks".into()),
100            duration_ms: budget.start.elapsed().as_millis() as u64,
101        });
102        return Ok(report);
103    }
104
105    // 2. Differential Testing
106    let iterations = match cfg.tier {
107        SuiteTier::Quick => 5,
108        SuiteTier::Nightly => 100,
109        SuiteTier::Stress => 1000,
110        SuiteTier::Chaos => 50,
111    };
112
113    {
114        let start = Instant::now();
115        let inner = differential::check_invariants(iterations, Some(cfg.seed));
116        let duration = start.elapsed().as_millis() as u64;
117        report.add_check("differential.invariants", inner, duration);
118    }
119
120    if budget.exceeded() {
121        report.add_result(AttackResult {
122            name: "time_budget".into(),
123            status: AttackStatus::Error,
124            error_class: None,
125            error_code: None,
126            message: Some("time budget exceeded after differential tests".into()),
127            duration_ms: budget.start.elapsed().as_millis() as u64,
128        });
129        return Ok(report);
130    }
131
132    // 3. Chaos-tier extras (use subprocess isolation for panic=abort safety)
133    if matches!(cfg.tier, SuiteTier::Chaos) {
134        run_chaos_phase(&mut report, cfg.seed, &budget);
135    }
136
137    Ok(report)
138}
139
140fn run_chaos_phase(report: &mut SimReport, seed: u64, budget: &TimeBudget) {
141    // IO chaos attacks (in-process — these inject IO errors, not panics)
142    match attacks::chaos::check_chaos_attacks(seed) {
143        Ok(results) => {
144            for r in results {
145                report.add_result(r);
146            }
147        }
148        Err(e) => {
149            report.add_result(AttackResult {
150                name: "chaos.io_faults".into(),
151                status: AttackStatus::Error,
152                error_class: None,
153                error_code: None,
154                message: Some(format!("chaos attacks failed: {}", e)),
155                duration_ms: 0,
156            });
157        }
158    }
159
160    if budget.exceeded() {
161        report.add_result(AttackResult {
162            name: "time_budget".into(),
163            status: AttackStatus::Error,
164            error_class: None,
165            error_code: None,
166            message: Some("time budget exceeded during chaos phase".into()),
167            duration_ms: 0,
168        });
169        return;
170    }
171
172    // Differential parity checks (uses subprocess isolation for production verifier)
173    match attacks::differential::check_differential_parity(seed) {
174        Ok(results) => {
175            for r in results {
176                report.add_result(r);
177            }
178        }
179        Err(e) => {
180            report.add_result(AttackResult {
181                name: "differential.parity".into(),
182                status: AttackStatus::Error,
183                error_class: None,
184                error_code: None,
185                message: Some(format!("differential parity failed: {}", e)),
186                duration_ms: 0,
187            });
188        }
189    }
190}