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 pub sancov_edges_total: usize,
65 pub sancov_edges_covered: usize,
67 pub converged: bool,
69 pub per_seed_timelines: Vec<u64>,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
75pub enum AssertionStatus {
76 Fail,
78 Miss,
80 Pass,
82}
83
84#[derive(Debug, Clone)]
86pub struct AssertionDetail {
87 pub msg: String,
89 pub kind: AssertKind,
91 pub pass_count: u64,
93 pub fail_count: u64,
95 pub watermark: i64,
97 pub frontier: u8,
99 pub status: AssertionStatus,
101}
102
103#[derive(Debug, Clone)]
105pub struct BucketSiteSummary {
106 pub msg: String,
108 pub buckets_discovered: usize,
110 pub total_hits: u64,
112}
113
114#[derive(Debug, Clone)]
116pub struct SimulationReport {
117 pub iterations: usize,
119 pub successful_runs: usize,
121 pub failed_runs: usize,
123 pub metrics: SimulationMetrics,
125 pub individual_metrics: Vec<SimulationResult<SimulationMetrics>>,
127 pub seeds_used: Vec<u64>,
129 pub seeds_failing: Vec<u64>,
131 pub assertion_results: HashMap<String, AssertionStats>,
133 pub assertion_violations: Vec<String>,
135 pub coverage_violations: Vec<String>,
137 pub exploration: Option<ExplorationReport>,
139 pub assertion_details: Vec<AssertionDetail>,
141 pub bucket_summaries: Vec<BucketSiteSummary>,
143 pub convergence_timeout: bool,
145}
146
147#[derive(Debug, thiserror::Error)]
149pub enum ReportCheckError {
150 #[error("{name}: {count} failing seeds: {seeds:?}")]
152 FailingSeeds {
153 name: String,
155 count: usize,
157 seeds: Vec<u64>,
159 },
160 #[error("{name}: assertion violations:\n{violations}")]
162 AssertionViolations {
163 name: String,
165 violations: String,
167 },
168}
169
170impl SimulationReport {
171 pub fn check(&self, name: &str) -> Result<(), ReportCheckError> {
181 if !self.seeds_failing.is_empty() {
182 return Err(ReportCheckError::FailingSeeds {
183 name: name.to_string(),
184 count: self.seeds_failing.len(),
185 seeds: self.seeds_failing.clone(),
186 });
187 }
188 if !self.assertion_violations.is_empty() {
189 return Err(ReportCheckError::AssertionViolations {
190 name: name.to_string(),
191 violations: self
192 .assertion_violations
193 .iter()
194 .map(|v| format!(" - {v}"))
195 .collect::<Vec<_>>()
196 .join("\n"),
197 });
198 }
199 Ok(())
200 }
201
202 pub fn is_success(&self) -> bool {
208 self.assertion_violations.is_empty() && !self.convergence_timeout
209 }
210
211 pub fn success_rate(&self) -> f64 {
213 if self.iterations == 0 {
214 0.0
215 } else {
216 (self.successful_runs as f64 / self.iterations as f64) * 100.0
217 }
218 }
219
220 pub fn average_wall_time(&self) -> Duration {
222 if self.successful_runs == 0 {
223 Duration::ZERO
224 } else {
225 self.metrics.wall_time / self.successful_runs as u32
226 }
227 }
228
229 pub fn average_simulated_time(&self) -> Duration {
231 if self.successful_runs == 0 {
232 Duration::ZERO
233 } else {
234 self.metrics.simulated_time / self.successful_runs as u32
235 }
236 }
237
238 pub fn average_events_processed(&self) -> f64 {
240 if self.successful_runs == 0 {
241 0.0
242 } else {
243 self.metrics.events_processed as f64 / self.successful_runs as f64
244 }
245 }
246
247 pub fn eprint(&self) {
252 super::display::eprint_report(self);
253 }
254}
255
256fn fmt_num(n: u64) -> String {
262 let s = n.to_string();
263 let mut result = String::with_capacity(s.len() + s.len() / 3);
264 for (i, c) in s.chars().rev().enumerate() {
265 if i > 0 && i % 3 == 0 {
266 result.push(',');
267 }
268 result.push(c);
269 }
270 result.chars().rev().collect()
271}
272
273fn fmt_i64(n: i64) -> String {
275 if n < 0 {
276 format!("-{}", fmt_num(n.unsigned_abs()))
277 } else {
278 fmt_num(n as u64)
279 }
280}
281
282fn fmt_duration(d: Duration) -> String {
284 let total_ms = d.as_millis();
285 if total_ms < 1000 {
286 format!("{}ms", total_ms)
287 } else if total_ms < 60_000 {
288 format!("{:.2}s", d.as_secs_f64())
289 } else {
290 let mins = d.as_secs() / 60;
291 let secs = d.as_secs() % 60;
292 format!("{}m {:02}s", mins, secs)
293 }
294}
295
296fn kind_label(kind: AssertKind) -> &'static str {
298 match kind {
299 AssertKind::Always => "always",
300 AssertKind::AlwaysOrUnreachable => "always?",
301 AssertKind::Sometimes => "sometimes",
302 AssertKind::Reachable => "reachable",
303 AssertKind::Unreachable => "unreachable",
304 AssertKind::NumericAlways => "num-always",
305 AssertKind::NumericSometimes => "numeric",
306 AssertKind::BooleanSometimesAll => "frontier",
307 }
308}
309
310fn kind_sort_order(kind: AssertKind) -> u8 {
312 match kind {
313 AssertKind::Always => 0,
314 AssertKind::AlwaysOrUnreachable => 1,
315 AssertKind::Unreachable => 2,
316 AssertKind::NumericAlways => 3,
317 AssertKind::Sometimes => 4,
318 AssertKind::Reachable => 5,
319 AssertKind::NumericSometimes => 6,
320 AssertKind::BooleanSometimesAll => 7,
321 }
322}
323
324impl fmt::Display for SimulationReport {
329 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330 writeln!(f, "=== Simulation Report ===")?;
332 writeln!(
333 f,
334 " Iterations: {} | Passed: {} | Failed: {} | Rate: {:.1}%",
335 self.iterations,
336 self.successful_runs,
337 self.failed_runs,
338 self.success_rate()
339 )?;
340 writeln!(f)?;
341
342 writeln!(
344 f,
345 " Avg Wall Time: {:<14}Total: {}",
346 fmt_duration(self.average_wall_time()),
347 fmt_duration(self.metrics.wall_time)
348 )?;
349 writeln!(
350 f,
351 " Avg Sim Time: {}",
352 fmt_duration(self.average_simulated_time())
353 )?;
354 writeln!(
355 f,
356 " Avg Events: {}",
357 fmt_num(self.average_events_processed() as u64)
358 )?;
359
360 if !self.seeds_failing.is_empty() {
362 writeln!(f)?;
363 writeln!(f, " Faulty seeds: {:?}", self.seeds_failing)?;
364 }
365
366 if let Some(ref exp) = self.exploration {
368 writeln!(f)?;
369 writeln!(f, "--- Exploration ---")?;
370 writeln!(
371 f,
372 " Timelines: {:<18}Bugs found: {}",
373 fmt_num(exp.total_timelines),
374 fmt_num(exp.bugs_found)
375 )?;
376 writeln!(
377 f,
378 " Fork points: {:<18}Coverage: {} / {} bits ({:.1}%)",
379 fmt_num(exp.fork_points),
380 fmt_num(exp.coverage_bits as u64),
381 fmt_num(exp.coverage_total as u64),
382 if exp.coverage_total > 0 {
383 (exp.coverage_bits as f64 / exp.coverage_total as f64) * 100.0
384 } else {
385 0.0
386 }
387 )?;
388 if exp.sancov_edges_total > 0 {
389 writeln!(
390 f,
391 " Sancov: {} / {} edges ({:.1}%)",
392 fmt_num(exp.sancov_edges_covered as u64),
393 fmt_num(exp.sancov_edges_total as u64),
394 (exp.sancov_edges_covered as f64 / exp.sancov_edges_total as f64) * 100.0
395 )?;
396 }
397 writeln!(
398 f,
399 " Energy left: {:<18}Realloc pool: {}",
400 fmt_i64(exp.energy_remaining),
401 fmt_i64(exp.realloc_pool_remaining)
402 )?;
403 for br in &exp.bug_recipes {
404 writeln!(
405 f,
406 " Bug recipe (seed={}): {}",
407 br.seed,
408 moonpool_explorer::format_timeline(&br.recipe)
409 )?;
410 }
411 }
412
413 if !self.assertion_details.is_empty() {
415 writeln!(f)?;
416 writeln!(f, "--- Assertions ({}) ---", self.assertion_details.len())?;
417
418 let mut sorted: Vec<&AssertionDetail> = self.assertion_details.iter().collect();
419 sorted.sort_by(|a, b| {
420 kind_sort_order(a.kind)
421 .cmp(&kind_sort_order(b.kind))
422 .then(a.status.cmp(&b.status))
423 .then(a.msg.cmp(&b.msg))
424 });
425
426 for detail in &sorted {
427 let status_tag = match detail.status {
428 AssertionStatus::Pass => "PASS",
429 AssertionStatus::Fail => "FAIL",
430 AssertionStatus::Miss => "MISS",
431 };
432 let kind_tag = kind_label(detail.kind);
433 let quoted_msg = format!("\"{}\"", detail.msg);
434
435 match detail.kind {
436 AssertKind::Sometimes | AssertKind::Reachable => {
437 let total = detail.pass_count + detail.fail_count;
438 let rate = if total > 0 {
439 (detail.pass_count as f64 / total as f64) * 100.0
440 } else {
441 0.0
442 };
443 writeln!(
444 f,
445 " {} [{:<10}] {:<34} {} / {} ({:.1}%)",
446 status_tag,
447 kind_tag,
448 quoted_msg,
449 fmt_num(detail.pass_count),
450 fmt_num(total),
451 rate
452 )?;
453 }
454 AssertKind::NumericSometimes | AssertKind::NumericAlways => {
455 writeln!(
456 f,
457 " {} [{:<10}] {:<34} {} pass {} fail watermark: {}",
458 status_tag,
459 kind_tag,
460 quoted_msg,
461 fmt_num(detail.pass_count),
462 fmt_num(detail.fail_count),
463 detail.watermark
464 )?;
465 }
466 AssertKind::BooleanSometimesAll => {
467 writeln!(
468 f,
469 " {} [{:<10}] {:<34} {} calls frontier: {}",
470 status_tag,
471 kind_tag,
472 quoted_msg,
473 fmt_num(detail.pass_count),
474 detail.frontier
475 )?;
476 }
477 _ => {
478 writeln!(
480 f,
481 " {} [{:<10}] {:<34} {} pass {} fail",
482 status_tag,
483 kind_tag,
484 quoted_msg,
485 fmt_num(detail.pass_count),
486 fmt_num(detail.fail_count)
487 )?;
488 }
489 }
490 }
491 }
492
493 if !self.assertion_violations.is_empty() {
495 writeln!(f)?;
496 writeln!(f, "--- Assertion Violations ---")?;
497 for v in &self.assertion_violations {
498 writeln!(f, " - {}", v)?;
499 }
500 }
501
502 if !self.coverage_violations.is_empty() {
504 writeln!(f)?;
505 writeln!(f, "--- Coverage Gaps ---")?;
506 for v in &self.coverage_violations {
507 writeln!(f, " - {}", v)?;
508 }
509 }
510
511 if !self.bucket_summaries.is_empty() {
513 let total_buckets: usize = self
514 .bucket_summaries
515 .iter()
516 .map(|s| s.buckets_discovered)
517 .sum();
518 writeln!(f)?;
519 writeln!(
520 f,
521 "--- Buckets ({} across {} sites) ---",
522 total_buckets,
523 self.bucket_summaries.len()
524 )?;
525 for bs in &self.bucket_summaries {
526 writeln!(
527 f,
528 " {:<34} {:>3} buckets {:>8} hits",
529 format!("\"{}\"", bs.msg),
530 bs.buckets_discovered,
531 fmt_num(bs.total_hits)
532 )?;
533 }
534 }
535
536 if self.convergence_timeout {
538 writeln!(f)?;
539 writeln!(f, "--- Convergence FAILED ---")?;
540 writeln!(f, " UntilConverged hit iteration cap without converging.")?;
541 }
542
543 if self.seeds_used.len() > 1 {
545 writeln!(f)?;
546 writeln!(f, "--- Seeds ---")?;
547 let per_seed_tl = self.exploration.as_ref().map(|e| &e.per_seed_timelines);
548 for (i, seed) in self.seeds_used.iter().enumerate() {
549 if let Some(Ok(m)) = self.individual_metrics.get(i) {
550 let tl_suffix = per_seed_tl
551 .and_then(|v| v.get(i))
552 .map(|t| format!(" timelines={}", fmt_num(*t)))
553 .unwrap_or_default();
554 writeln!(
555 f,
556 " #{:<3} seed={:<14} wall={:<10} sim={:<10} events={}{}",
557 i + 1,
558 seed,
559 fmt_duration(m.wall_time),
560 fmt_duration(m.simulated_time),
561 fmt_num(m.events_processed),
562 tl_suffix,
563 )?;
564 } else if let Some(Err(_)) = self.individual_metrics.get(i) {
565 writeln!(f, " #{:<3} seed={:<14} FAILED", i + 1, seed)?;
566 }
567 }
568 }
569
570 writeln!(f)?;
571 Ok(())
572 }
573}