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,
21 pub seed: u64,
22 pub verify_limits: Option<VerifyLimits>,
23 pub time_budget_secs: u64,
25}
26
27#[derive(Debug, Clone)]
32pub struct TimeBudget {
33 start: Instant,
34 limit: Duration,
35}
36
37pub fn tier_default_limits(tier: &str) -> VerifyLimits {
41 let mut defaults = VerifyLimits::default();
42 if tier.trim().to_lowercase() == "quick" {
43 defaults.max_bundle_bytes = 5 * 1024 * 1024; }
45 defaults
46}
47
48impl TimeBudget {
49 pub fn new(limit: Duration) -> Self {
50 Self {
51 start: Instant::now(),
52 limit,
53 }
54 }
55
56 pub fn default_suite() -> Self {
60 Self::new(Duration::from_secs(60))
61 }
62
63 pub fn exceeded(&self) -> bool {
64 self.start.elapsed() > self.limit
65 }
66
67 pub fn elapsed(&self) -> Duration {
68 self.start.elapsed()
69 }
70
71 pub fn remaining(&self) -> Duration {
72 self.limit.saturating_sub(self.start.elapsed())
73 }
74}
75
76pub fn run_suite(cfg: SuiteConfig) -> Result<SimReport> {
77 let mut report = SimReport::new(&format!("{:?}", cfg.tier), cfg.seed);
78 let budget = TimeBudget::new(Duration::from_secs(cfg.time_budget_secs));
79 let limits = cfg
80 .verify_limits
81 .unwrap_or_else(|| tier_default_limits(&format!("{:?}", cfg.tier).to_lowercase()));
82
83 {
90 let seed = cfg.seed;
91 let start = Instant::now();
92 let mut inner_report = SimReport::new("integrity", seed);
93 match attacks::integrity::check_integrity_attacks(&mut inner_report, seed, limits, &budget)
94 {
95 Ok(()) => {
96 for r in inner_report.results {
97 report.add_result(r);
98 }
99 }
100 Err(attacks::integrity::IntegrityError::BudgetExceeded) => {
101 for r in inner_report.results {
102 report.add_result(r);
103 }
104 report.set_time_budget_exceeded(vec!["differential".into(), "chaos".into()]);
105 report.add_result(AttackResult {
106 name: "integrity.time_budget".into(),
107 status: AttackStatus::Error,
108 error_class: None,
109 error_code: None,
110 message: Some("time budget exceeded during integrity phase".into()),
111 duration_ms: budget.elapsed().as_millis() as u64,
112 });
113 return Ok(report);
114 }
115 Err(attacks::integrity::IntegrityError::Other(e)) => {
116 for r in inner_report.results {
117 report.add_result(r);
118 }
119 report.add_result(AttackResult {
120 name: "integrity_attacks".into(),
121 status: AttackStatus::Error,
122 error_class: None,
123 error_code: None,
124 message: Some(e.to_string()),
125 duration_ms: start.elapsed().as_millis() as u64,
126 });
127 }
128 }
129 }
130
131 if budget.exceeded() {
132 report.set_time_budget_exceeded(vec!["differential".into(), "chaos".into()]);
133 report.add_result(AttackResult {
134 name: "integrity.time_budget".into(),
135 status: AttackStatus::Error,
136 error_class: None,
137 error_code: None,
138 message: Some("time budget exceeded after integrity phase".into()),
139 duration_ms: budget.elapsed().as_millis() as u64,
140 });
141 return Ok(report);
142 }
143
144 let iterations = match cfg.tier {
146 SuiteTier::Quick => 5,
147 SuiteTier::Nightly => 100,
148 SuiteTier::Stress => 1000,
149 SuiteTier::Chaos => 50,
150 };
151
152 {
153 let start = Instant::now();
154 let inner = differential::check_invariants(iterations, Some(cfg.seed));
155 let duration = start.elapsed().as_millis() as u64;
156 report.add_check("differential.invariants", inner, duration);
157 }
158
159 if budget.exceeded() {
160 report.set_time_budget_exceeded(vec!["chaos".into()]);
161 report.add_result(AttackResult {
162 name: "differential.time_budget".into(),
163 status: AttackStatus::Error,
164 error_class: None,
165 error_code: None,
166 message: Some("time budget exceeded after differential phase".into()),
167 duration_ms: budget.elapsed().as_millis() as u64,
168 });
169 return Ok(report);
170 }
171
172 if matches!(cfg.tier, SuiteTier::Chaos) {
174 run_chaos_phase(&mut report, cfg.seed, &budget);
175 }
176
177 Ok(report)
178}
179
180fn run_chaos_phase(report: &mut SimReport, seed: u64, budget: &TimeBudget) {
181 if budget.exceeded() {
183 report.set_time_budget_exceeded(vec!["chaos".into()]);
184 report.add_result(AttackResult {
185 name: "chaos.time_budget".into(),
186 status: AttackStatus::Error,
187 error_class: None,
188 error_code: None,
189 message: Some("time budget exceeded before chaos phase".into()),
190 duration_ms: budget.elapsed().as_millis() as u64,
191 });
192 report.add_result(AttackResult {
193 name: "differential.parity".into(),
194 status: AttackStatus::Error,
195 error_class: None,
196 error_code: None,
197 message: Some("skipped due to time budget".into()),
198 duration_ms: 0,
199 });
200 return;
201 }
202
203 match attacks::chaos::check_chaos_attacks(seed) {
205 Ok(results) => {
206 for r in results {
207 report.add_result(r);
208 }
209 }
210 Err(e) => {
211 report.add_result(AttackResult {
212 name: "chaos.io_faults".into(),
213 status: AttackStatus::Error,
214 error_class: None,
215 error_code: None,
216 message: Some(format!("chaos attacks failed: {}", e)),
217 duration_ms: 0,
218 });
219 }
220 }
221
222 if budget.exceeded() {
223 report.set_time_budget_exceeded(vec![]);
224 report.add_result(AttackResult {
225 name: "chaos.time_budget".into(),
226 status: AttackStatus::Error,
227 error_class: None,
228 error_code: None,
229 message: Some("time budget exceeded during chaos phase".into()),
230 duration_ms: budget.elapsed().as_millis() as u64,
231 });
232 report.add_result(AttackResult {
234 name: "differential.parity".into(),
235 status: AttackStatus::Error,
236 error_class: None,
237 error_code: None,
238 message: Some("skipped due to time budget".into()),
239 duration_ms: 0,
240 });
241 return;
242 }
243
244 match attacks::differential::check_differential_parity(seed) {
246 Ok(results) => {
247 for r in results {
248 report.add_result(r);
249 }
250 }
251 Err(e) => {
252 report.add_result(AttackResult {
253 name: "differential.parity".into(),
254 status: AttackStatus::Error,
255 error_class: None,
256 error_code: None,
257 message: Some(format!("differential parity failed: {}", e)),
258 duration_ms: 0,
259 });
260 }
261 }
262}