1use std::collections::HashMap;
6use std::fmt;
7use std::time::Duration;
8
9use moonpool_explorer::AssertKind;
10
11use crate::SimulationResult;
12use crate::chaos::AssertionStats;
13
14#[derive(Debug, Clone, PartialEq)]
16pub struct SimulationMetrics {
17 pub wall_time: Duration,
19 pub simulated_time: Duration,
21 pub events_processed: u64,
23}
24
25impl Default for SimulationMetrics {
26 fn default() -> Self {
27 Self {
28 wall_time: Duration::ZERO,
29 simulated_time: Duration::ZERO,
30 events_processed: 0,
31 }
32 }
33}
34
35#[derive(Debug, Clone, PartialEq)]
37pub struct BugRecipe {
38 pub seed: u64,
40 pub recipe: Vec<(u64, u64)>,
42}
43
44#[derive(Debug, Clone)]
46pub struct ExplorationReport {
47 pub total_timelines: u64,
49 pub fork_points: u64,
51 pub bugs_found: u64,
53 pub bug_recipes: Vec<BugRecipe>,
55 pub energy_remaining: i64,
57 pub realloc_pool_remaining: i64,
59 pub coverage_bits: u32,
61 pub coverage_total: u32,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
67pub enum AssertionStatus {
68 Fail,
70 Miss,
72 Pass,
74}
75
76#[derive(Debug, Clone)]
78pub struct AssertionDetail {
79 pub msg: String,
81 pub kind: AssertKind,
83 pub pass_count: u64,
85 pub fail_count: u64,
87 pub watermark: i64,
89 pub frontier: u8,
91 pub status: AssertionStatus,
93}
94
95#[derive(Debug, Clone)]
97pub struct BucketSiteSummary {
98 pub msg: String,
100 pub buckets_discovered: usize,
102 pub total_hits: u64,
104}
105
106#[derive(Debug, Clone)]
108pub struct SimulationReport {
109 pub iterations: usize,
111 pub successful_runs: usize,
113 pub failed_runs: usize,
115 pub metrics: SimulationMetrics,
117 pub individual_metrics: Vec<SimulationResult<SimulationMetrics>>,
119 pub seeds_used: Vec<u64>,
121 pub seeds_failing: Vec<u64>,
123 pub assertion_results: HashMap<String, AssertionStats>,
125 pub assertion_violations: Vec<String>,
127 pub coverage_violations: Vec<String>,
129 pub exploration: Option<ExplorationReport>,
131 pub assertion_details: Vec<AssertionDetail>,
133 pub bucket_summaries: Vec<BucketSiteSummary>,
135}
136
137impl SimulationReport {
138 pub fn success_rate(&self) -> f64 {
140 if self.iterations == 0 {
141 0.0
142 } else {
143 (self.successful_runs as f64 / self.iterations as f64) * 100.0
144 }
145 }
146
147 pub fn average_wall_time(&self) -> Duration {
149 if self.successful_runs == 0 {
150 Duration::ZERO
151 } else {
152 self.metrics.wall_time / self.successful_runs as u32
153 }
154 }
155
156 pub fn average_simulated_time(&self) -> Duration {
158 if self.successful_runs == 0 {
159 Duration::ZERO
160 } else {
161 self.metrics.simulated_time / self.successful_runs as u32
162 }
163 }
164
165 pub fn average_events_processed(&self) -> f64 {
167 if self.successful_runs == 0 {
168 0.0
169 } else {
170 self.metrics.events_processed as f64 / self.successful_runs as f64
171 }
172 }
173}
174
175fn fmt_num(n: u64) -> String {
181 let s = n.to_string();
182 let mut result = String::with_capacity(s.len() + s.len() / 3);
183 for (i, c) in s.chars().rev().enumerate() {
184 if i > 0 && i % 3 == 0 {
185 result.push(',');
186 }
187 result.push(c);
188 }
189 result.chars().rev().collect()
190}
191
192fn fmt_duration(d: Duration) -> String {
194 let total_ms = d.as_millis();
195 if total_ms < 1000 {
196 format!("{}ms", total_ms)
197 } else if total_ms < 60_000 {
198 format!("{:.2}s", d.as_secs_f64())
199 } else {
200 let mins = d.as_secs() / 60;
201 let secs = d.as_secs() % 60;
202 format!("{}m {:02}s", mins, secs)
203 }
204}
205
206fn kind_label(kind: AssertKind) -> &'static str {
208 match kind {
209 AssertKind::Always => "always",
210 AssertKind::AlwaysOrUnreachable => "always?",
211 AssertKind::Sometimes => "sometimes",
212 AssertKind::Reachable => "reachable",
213 AssertKind::Unreachable => "unreachable",
214 AssertKind::NumericAlways => "num-always",
215 AssertKind::NumericSometimes => "numeric",
216 AssertKind::BooleanSometimesAll => "frontier",
217 }
218}
219
220fn kind_sort_order(kind: AssertKind) -> u8 {
222 match kind {
223 AssertKind::Always => 0,
224 AssertKind::AlwaysOrUnreachable => 1,
225 AssertKind::Unreachable => 2,
226 AssertKind::NumericAlways => 3,
227 AssertKind::Sometimes => 4,
228 AssertKind::Reachable => 5,
229 AssertKind::NumericSometimes => 6,
230 AssertKind::BooleanSometimesAll => 7,
231 }
232}
233
234impl fmt::Display for SimulationReport {
239 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240 writeln!(f, "=== Simulation Report ===")?;
242 writeln!(
243 f,
244 " Iterations: {} | Passed: {} | Failed: {} | Rate: {:.1}%",
245 self.iterations,
246 self.successful_runs,
247 self.failed_runs,
248 self.success_rate()
249 )?;
250 writeln!(f)?;
251
252 writeln!(
254 f,
255 " Avg Wall Time: {:<14}Total: {}",
256 fmt_duration(self.average_wall_time()),
257 fmt_duration(self.metrics.wall_time)
258 )?;
259 writeln!(
260 f,
261 " Avg Sim Time: {}",
262 fmt_duration(self.average_simulated_time())
263 )?;
264 writeln!(
265 f,
266 " Avg Events: {}",
267 fmt_num(self.average_events_processed() as u64)
268 )?;
269
270 if !self.seeds_failing.is_empty() {
272 writeln!(f)?;
273 writeln!(f, " Faulty seeds: {:?}", self.seeds_failing)?;
274 }
275
276 if let Some(ref exp) = self.exploration {
278 writeln!(f)?;
279 writeln!(f, "--- Exploration ---")?;
280 writeln!(
281 f,
282 " Timelines: {:<18}Bugs found: {}",
283 fmt_num(exp.total_timelines),
284 fmt_num(exp.bugs_found)
285 )?;
286 writeln!(
287 f,
288 " Fork points: {:<18}Coverage: {} / {} bits ({:.1}%)",
289 fmt_num(exp.fork_points),
290 fmt_num(exp.coverage_bits as u64),
291 fmt_num(exp.coverage_total as u64),
292 if exp.coverage_total > 0 {
293 (exp.coverage_bits as f64 / exp.coverage_total as f64) * 100.0
294 } else {
295 0.0
296 }
297 )?;
298 writeln!(
299 f,
300 " Energy left: {:<18}Realloc pool: {}",
301 exp.energy_remaining, exp.realloc_pool_remaining
302 )?;
303 for br in &exp.bug_recipes {
304 writeln!(
305 f,
306 " Bug recipe (seed={}): {}",
307 br.seed,
308 moonpool_explorer::format_timeline(&br.recipe)
309 )?;
310 }
311 }
312
313 if !self.assertion_details.is_empty() {
315 writeln!(f)?;
316 writeln!(f, "--- Assertions ({}) ---", self.assertion_details.len())?;
317
318 let mut sorted: Vec<&AssertionDetail> = self.assertion_details.iter().collect();
319 sorted.sort_by(|a, b| {
320 kind_sort_order(a.kind)
321 .cmp(&kind_sort_order(b.kind))
322 .then(a.status.cmp(&b.status))
323 .then(a.msg.cmp(&b.msg))
324 });
325
326 for detail in &sorted {
327 let status_tag = match detail.status {
328 AssertionStatus::Pass => "PASS",
329 AssertionStatus::Fail => "FAIL",
330 AssertionStatus::Miss => "MISS",
331 };
332 let kind_tag = kind_label(detail.kind);
333 let quoted_msg = format!("\"{}\"", detail.msg);
334
335 match detail.kind {
336 AssertKind::Sometimes | AssertKind::Reachable => {
337 let total = detail.pass_count + detail.fail_count;
338 let rate = if total > 0 {
339 (detail.pass_count as f64 / total as f64) * 100.0
340 } else {
341 0.0
342 };
343 writeln!(
344 f,
345 " {} [{:<10}] {:<34} {} / {} ({:.1}%)",
346 status_tag,
347 kind_tag,
348 quoted_msg,
349 fmt_num(detail.pass_count),
350 fmt_num(total),
351 rate
352 )?;
353 }
354 AssertKind::NumericSometimes | AssertKind::NumericAlways => {
355 writeln!(
356 f,
357 " {} [{:<10}] {:<34} {} pass {} fail watermark: {}",
358 status_tag,
359 kind_tag,
360 quoted_msg,
361 fmt_num(detail.pass_count),
362 fmt_num(detail.fail_count),
363 detail.watermark
364 )?;
365 }
366 AssertKind::BooleanSometimesAll => {
367 writeln!(
368 f,
369 " {} [{:<10}] {:<34} {} calls frontier: {}",
370 status_tag,
371 kind_tag,
372 quoted_msg,
373 fmt_num(detail.pass_count),
374 detail.frontier
375 )?;
376 }
377 _ => {
378 writeln!(
380 f,
381 " {} [{:<10}] {:<34} {} pass {} fail",
382 status_tag,
383 kind_tag,
384 quoted_msg,
385 fmt_num(detail.pass_count),
386 fmt_num(detail.fail_count)
387 )?;
388 }
389 }
390 }
391 }
392
393 if !self.assertion_violations.is_empty() {
395 writeln!(f)?;
396 writeln!(f, "--- Assertion Violations ---")?;
397 for v in &self.assertion_violations {
398 writeln!(f, " - {}", v)?;
399 }
400 }
401
402 if !self.coverage_violations.is_empty() {
404 writeln!(f)?;
405 writeln!(f, "--- Coverage Gaps ---")?;
406 for v in &self.coverage_violations {
407 writeln!(f, " - {}", v)?;
408 }
409 }
410
411 if !self.bucket_summaries.is_empty() {
413 let total_buckets: usize = self
414 .bucket_summaries
415 .iter()
416 .map(|s| s.buckets_discovered)
417 .sum();
418 writeln!(f)?;
419 writeln!(
420 f,
421 "--- Buckets ({} across {} sites) ---",
422 total_buckets,
423 self.bucket_summaries.len()
424 )?;
425 for bs in &self.bucket_summaries {
426 writeln!(
427 f,
428 " {:<34} {:>3} buckets {:>8} hits",
429 format!("\"{}\"", bs.msg),
430 bs.buckets_discovered,
431 fmt_num(bs.total_hits)
432 )?;
433 }
434 }
435
436 if self.seeds_used.len() > 1 {
438 writeln!(f)?;
439 writeln!(f, "--- Seeds ---")?;
440 for (i, seed) in self.seeds_used.iter().enumerate() {
441 if let Some(Ok(m)) = self.individual_metrics.get(i) {
442 writeln!(
443 f,
444 " #{:<3} seed={:<14} wall={:<10} sim={:<10} events={}",
445 i + 1,
446 seed,
447 fmt_duration(m.wall_time),
448 fmt_duration(m.simulated_time),
449 fmt_num(m.events_processed)
450 )?;
451 } else if let Some(Err(_)) = self.individual_metrics.get(i) {
452 writeln!(f, " #{:<3} seed={:<14} FAILED", i + 1, seed)?;
453 }
454 }
455 }
456
457 writeln!(f)?;
458 Ok(())
459 }
460}