1use std::collections::HashMap;
6use std::fmt;
7use std::time::Duration;
8
9use moonpool_assertions::AssertKind;
10
11use crate::SimulationResult;
12use crate::chaos::AssertionStats;
13
14pub(crate) fn format_recipe(recipe: &[(u64, u64)]) -> String {
18 recipe
19 .iter()
20 .map(|(count, seed)| format!("{count}@{seed}"))
21 .collect::<Vec<_>>()
22 .join(" -> ")
23}
24
25#[derive(Debug, Clone, PartialEq)]
27pub struct SimulationMetrics {
28 pub wall_time: Duration,
30 pub simulated_time: Duration,
32 pub events_processed: u64,
34}
35
36impl Default for SimulationMetrics {
37 fn default() -> Self {
38 Self {
39 wall_time: Duration::ZERO,
40 simulated_time: Duration::ZERO,
41 events_processed: 0,
42 }
43 }
44}
45
46#[derive(Debug, Clone, PartialEq)]
48pub struct BugRecipe {
49 pub seed: u64,
51 pub recipe: Vec<(u64, u64)>,
53}
54
55#[derive(Debug, Clone)]
57pub struct ExplorationReport {
58 pub total_timelines: u64,
60 pub fork_points: u64,
62 pub bugs_found: u64,
64 pub bug_recipes: Vec<BugRecipe>,
66 pub energy_remaining: i64,
68 pub realloc_pool_remaining: i64,
70 pub coverage_bits: u32,
72 pub coverage_total: u32,
74 pub sancov_edges_total: usize,
76 pub sancov_edges_covered: usize,
78 pub converged: bool,
80 pub per_seed_timelines: Vec<u64>,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum SaturationSignal {
89 CodeCoverage,
91 AssertionCoverage,
94}
95
96#[derive(Debug, Clone)]
102pub struct SaturationReport {
103 pub signal: SaturationSignal,
105 pub edges_covered: usize,
107 pub edges_total: usize,
109 pub sometimes_hit: usize,
111 pub sometimes_total: usize,
113 pub plateau_seeds: usize,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
119pub enum AssertionStatus {
120 Fail,
122 Miss,
124 Pass,
126}
127
128#[derive(Debug, Clone)]
130pub struct AssertionDetail {
131 pub msg: String,
133 pub kind: AssertKind,
135 pub pass_count: u64,
137 pub fail_count: u64,
139 pub watermark: i64,
141 pub frontier: u8,
143 pub status: AssertionStatus,
145}
146
147#[derive(Debug, Clone)]
149pub struct BucketSiteSummary {
150 pub msg: String,
152 pub buckets_discovered: usize,
154 pub total_hits: u64,
156}
157
158#[derive(Debug, Clone)]
160pub struct SimulationReport {
161 pub iterations: usize,
163 pub successful_runs: usize,
165 pub failed_runs: usize,
167 pub metrics: SimulationMetrics,
169 pub individual_metrics: Vec<SimulationResult<SimulationMetrics>>,
171 pub seeds_used: Vec<u64>,
173 pub seeds_failing: Vec<u64>,
175 pub assertion_results: HashMap<String, AssertionStats>,
177 pub assertion_violations: Vec<String>,
179 pub coverage_violations: Vec<String>,
181 pub exploration: Option<ExplorationReport>,
183 pub assertion_details: Vec<AssertionDetail>,
185 pub bucket_summaries: Vec<BucketSiteSummary>,
187 pub convergence_timeout: bool,
189 pub saturation: Option<SaturationReport>,
192}
193
194impl SimulationReport {
195 #[must_use]
197 pub fn success_rate(&self) -> f64 {
198 if self.iterations == 0 {
199 0.0
200 } else {
201 let successful = u32::try_from(self.successful_runs).map_or(f64::INFINITY, f64::from);
203 let total = u32::try_from(self.iterations).map_or(f64::INFINITY, f64::from);
204 (successful / total) * 100.0
205 }
206 }
207
208 #[must_use]
210 pub fn average_wall_time(&self) -> Duration {
211 if self.successful_runs == 0 {
212 Duration::ZERO
213 } else {
214 let runs = u32::try_from(self.successful_runs).unwrap_or(u32::MAX);
215 self.metrics.wall_time / runs
216 }
217 }
218
219 #[must_use]
221 pub fn average_simulated_time(&self) -> Duration {
222 if self.successful_runs == 0 {
223 Duration::ZERO
224 } else {
225 let runs = u32::try_from(self.successful_runs).unwrap_or(u32::MAX);
226 self.metrics.simulated_time / runs
227 }
228 }
229
230 #[must_use]
232 pub fn average_events_processed(&self) -> f64 {
233 if self.successful_runs == 0 {
234 0.0
235 } else {
236 let events =
238 u32::try_from(self.metrics.events_processed).map_or(f64::INFINITY, f64::from);
239 let runs = u32::try_from(self.successful_runs).map_or(f64::INFINITY, f64::from);
240 events / runs
241 }
242 }
243
244 pub fn eprint(&self) {
249 super::display::eprint_report(self);
250 }
251}
252
253fn f64_to_u64_saturating(v: f64) -> u64 {
262 const TWO_POW_64: f64 = 18_446_744_073_709_551_616.0;
264 if !v.is_finite() || v <= 0.0 {
265 0
266 } else if v >= TWO_POW_64 {
267 u64::MAX
268 } else {
269 unsafe { v.round().to_int_unchecked::<u64>() }
272 }
273}
274
275fn fmt_num(n: u64) -> String {
277 let s = n.to_string();
278 let mut result = String::with_capacity(s.len() + s.len() / 3);
279 for (i, c) in s.chars().rev().enumerate() {
280 if i > 0 && i % 3 == 0 {
281 result.push(',');
282 }
283 result.push(c);
284 }
285 result.chars().rev().collect()
286}
287
288fn fmt_i64(n: i64) -> String {
290 if n < 0 {
291 format!("-{}", fmt_num(n.unsigned_abs()))
292 } else {
293 fmt_num(n.unsigned_abs())
294 }
295}
296
297fn fmt_duration(d: Duration) -> String {
299 let total_ms = d.as_millis();
300 if total_ms < 1000 {
301 format!("{total_ms}ms")
302 } else if total_ms < 60_000 {
303 format!("{:.2}s", d.as_secs_f64())
304 } else {
305 let mins = d.as_secs() / 60;
306 let secs = d.as_secs() % 60;
307 format!("{mins}m {secs:02}s")
308 }
309}
310
311fn kind_label(kind: AssertKind) -> &'static str {
313 match kind {
314 AssertKind::Always => "always",
315 AssertKind::AlwaysOrUnreachable => "always?",
316 AssertKind::Sometimes => "sometimes",
317 AssertKind::Reachable => "reachable",
318 AssertKind::Unreachable => "unreachable",
319 AssertKind::NumericAlways => "num-always",
320 AssertKind::NumericSometimes => "numeric",
321 AssertKind::BooleanSometimesAll => "frontier",
322 }
323}
324
325fn kind_sort_order(kind: AssertKind) -> u8 {
327 match kind {
328 AssertKind::Always => 0,
329 AssertKind::AlwaysOrUnreachable => 1,
330 AssertKind::Unreachable => 2,
331 AssertKind::NumericAlways => 3,
332 AssertKind::Sometimes => 4,
333 AssertKind::Reachable => 5,
334 AssertKind::NumericSometimes => 6,
335 AssertKind::BooleanSometimesAll => 7,
336 }
337}
338
339fn fmt_summary(f: &mut fmt::Formatter<'_>, report: &SimulationReport) -> fmt::Result {
344 writeln!(f, "=== Simulation Report ===")?;
345 writeln!(
346 f,
347 " Iterations: {} | Passed: {} | Failed: {} | Rate: {:.1}%",
348 report.iterations,
349 report.successful_runs,
350 report.failed_runs,
351 report.success_rate()
352 )?;
353 writeln!(f)
354}
355
356fn fmt_timing(f: &mut fmt::Formatter<'_>, report: &SimulationReport) -> fmt::Result {
357 writeln!(
358 f,
359 " Avg Wall Time: {:<14}Total: {}",
360 fmt_duration(report.average_wall_time()),
361 fmt_duration(report.metrics.wall_time)
362 )?;
363 writeln!(
364 f,
365 " Avg Sim Time: {}",
366 fmt_duration(report.average_simulated_time())
367 )?;
368 writeln!(
369 f,
370 " Avg Events: {}",
371 fmt_num(f64_to_u64_saturating(report.average_events_processed()))
372 )
373}
374
375fn fmt_saturation(f: &mut fmt::Formatter<'_>, report: &SimulationReport) -> fmt::Result {
378 let Some(sat) = &report.saturation else {
379 return Ok(());
380 };
381 writeln!(f)?;
382 let status = if report.convergence_timeout {
383 "NOT saturated"
384 } else {
385 "saturated"
386 };
387 match sat.signal {
388 SaturationSignal::CodeCoverage => writeln!(
389 f,
390 " {status}: {}/{} sometimes hit · code coverage stable at {}/{} edges for {} seeds",
391 sat.sometimes_hit,
392 sat.sometimes_total,
393 sat.edges_covered,
394 sat.edges_total,
395 sat.plateau_seeds,
396 )?,
397 SaturationSignal::AssertionCoverage => writeln!(
398 f,
399 " {status}: {}/{} sometimes hit · assertion coverage stable for {} seeds (no sancov — run via 'cargo xtask sim run' for code-coverage-driven stop)",
400 sat.sometimes_hit, sat.sometimes_total, sat.plateau_seeds,
401 )?,
402 }
403 if report.convergence_timeout {
404 writeln!(
405 f,
406 " UntilCoverageStable hit the iteration cap without saturating."
407 )?;
408 }
409 Ok(())
410}
411
412fn fmt_exploration(f: &mut fmt::Formatter<'_>, exp: &ExplorationReport) -> fmt::Result {
413 writeln!(f)?;
414 writeln!(f, "--- Exploration ---")?;
415 writeln!(
416 f,
417 " Timelines: {:<18}Bugs found: {}",
418 fmt_num(exp.total_timelines),
419 fmt_num(exp.bugs_found)
420 )?;
421 writeln!(
422 f,
423 " Fork points: {:<18}Coverage: {} / {} bits ({:.1}%)",
424 fmt_num(exp.fork_points),
425 fmt_num(u64::from(exp.coverage_bits)),
426 fmt_num(u64::from(exp.coverage_total)),
427 if exp.coverage_total > 0 {
428 (f64::from(exp.coverage_bits) / f64::from(exp.coverage_total)) * 100.0
429 } else {
430 0.0
431 }
432 )?;
433 if exp.sancov_edges_total > 0 {
434 let covered = u64::try_from(exp.sancov_edges_covered).unwrap_or(u64::MAX);
435 let total = u64::try_from(exp.sancov_edges_total).unwrap_or(u64::MAX);
436 let covered_u32 = u32::try_from(exp.sancov_edges_covered).unwrap_or(u32::MAX);
437 let total_u32 = u32::try_from(exp.sancov_edges_total).unwrap_or(u32::MAX);
438 writeln!(
439 f,
440 " Sancov: {} / {} edges ({:.1}%)",
441 fmt_num(covered),
442 fmt_num(total),
443 (f64::from(covered_u32) / f64::from(total_u32)) * 100.0
444 )?;
445 }
446 writeln!(
447 f,
448 " Energy left: {:<18}Realloc pool: {}",
449 fmt_i64(exp.energy_remaining),
450 fmt_i64(exp.realloc_pool_remaining)
451 )?;
452 for br in &exp.bug_recipes {
453 writeln!(
454 f,
455 " Bug recipe (seed={}): {}",
456 br.seed,
457 format_recipe(&br.recipe)
458 )?;
459 }
460 Ok(())
461}
462
463fn fmt_assertion_detail(f: &mut fmt::Formatter<'_>, detail: &AssertionDetail) -> fmt::Result {
464 let status_tag = match detail.status {
465 AssertionStatus::Pass => "PASS",
466 AssertionStatus::Fail => "FAIL",
467 AssertionStatus::Miss => "MISS",
468 };
469 let kind_tag = kind_label(detail.kind);
470 let quoted_msg = format!("\"{}\"", detail.msg);
471
472 match detail.kind {
473 AssertKind::Sometimes | AssertKind::Reachable => {
474 let total = detail.pass_count + detail.fail_count;
475 let rate = if total > 0 {
476 let pass_u32 = u32::try_from(detail.pass_count).unwrap_or(u32::MAX);
477 let total_u32 = u32::try_from(total).unwrap_or(u32::MAX);
478 (f64::from(pass_u32) / f64::from(total_u32)) * 100.0
479 } else {
480 0.0
481 };
482 writeln!(
483 f,
484 " {} [{:<10}] {:<34} {} / {} ({:.1}%)",
485 status_tag,
486 kind_tag,
487 quoted_msg,
488 fmt_num(detail.pass_count),
489 fmt_num(total),
490 rate
491 )
492 }
493 AssertKind::NumericSometimes | AssertKind::NumericAlways => writeln!(
494 f,
495 " {} [{:<10}] {:<34} {} pass {} fail watermark: {}",
496 status_tag,
497 kind_tag,
498 quoted_msg,
499 fmt_num(detail.pass_count),
500 fmt_num(detail.fail_count),
501 detail.watermark
502 ),
503 AssertKind::BooleanSometimesAll => writeln!(
504 f,
505 " {} [{:<10}] {:<34} {} calls frontier: {}",
506 status_tag,
507 kind_tag,
508 quoted_msg,
509 fmt_num(detail.pass_count),
510 detail.frontier
511 ),
512 _ => writeln!(
513 f,
514 " {} [{:<10}] {:<34} {} pass {} fail",
515 status_tag,
516 kind_tag,
517 quoted_msg,
518 fmt_num(detail.pass_count),
519 fmt_num(detail.fail_count)
520 ),
521 }
522}
523
524fn fmt_assertion_details(f: &mut fmt::Formatter<'_>, details: &[AssertionDetail]) -> fmt::Result {
525 if details.is_empty() {
526 return Ok(());
527 }
528 writeln!(f)?;
529 writeln!(f, "--- Assertions ({}) ---", details.len())?;
530
531 let mut sorted: Vec<&AssertionDetail> = details.iter().collect();
532 sorted.sort_by(|a, b| {
533 kind_sort_order(a.kind)
534 .cmp(&kind_sort_order(b.kind))
535 .then(a.status.cmp(&b.status))
536 .then(a.msg.cmp(&b.msg))
537 });
538
539 for detail in &sorted {
540 fmt_assertion_detail(f, detail)?;
541 }
542 Ok(())
543}
544
545impl fmt::Display for SimulationReport {
546 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
547 fmt_summary(f, self)?;
548 fmt_timing(f, self)?;
549
550 if !self.seeds_failing.is_empty() {
552 writeln!(f)?;
553 writeln!(f, " Faulty seeds: {:?}", self.seeds_failing)?;
554 }
555
556 if let Some(ref exp) = self.exploration {
557 fmt_exploration(f, exp)?;
558 }
559
560 fmt_assertion_details(f, &self.assertion_details)?;
561
562 if !self.assertion_violations.is_empty() {
564 writeln!(f)?;
565 writeln!(f, "--- Assertion Violations ---")?;
566 for v in &self.assertion_violations {
567 writeln!(f, " - {v}")?;
568 }
569 }
570
571 if !self.coverage_violations.is_empty() {
573 writeln!(f)?;
574 writeln!(f, "--- Coverage Gaps ---")?;
575 for v in &self.coverage_violations {
576 writeln!(f, " - {v}")?;
577 }
578 }
579
580 if !self.bucket_summaries.is_empty() {
582 let total_buckets: usize = self
583 .bucket_summaries
584 .iter()
585 .map(|s| s.buckets_discovered)
586 .sum();
587 writeln!(f)?;
588 writeln!(
589 f,
590 "--- Buckets ({} across {} sites) ---",
591 total_buckets,
592 self.bucket_summaries.len()
593 )?;
594 for bs in &self.bucket_summaries {
595 writeln!(
596 f,
597 " {:<34} {:>3} buckets {:>8} hits",
598 format!("\"{}\"", bs.msg),
599 bs.buckets_discovered,
600 fmt_num(bs.total_hits)
601 )?;
602 }
603 }
604
605 fmt_saturation(f, self)?;
607 if self.saturation.is_none() && self.convergence_timeout {
608 writeln!(f)?;
609 writeln!(
610 f,
611 " UntilCoverageStable hit the iteration cap without saturating."
612 )?;
613 }
614
615 if self.seeds_used.len() > 1 {
617 writeln!(f)?;
618 writeln!(f, "--- Seeds ---")?;
619 let per_seed_tl = self.exploration.as_ref().map(|e| &e.per_seed_timelines);
620 for (i, seed) in self.seeds_used.iter().enumerate() {
621 if let Some(Ok(m)) = self.individual_metrics.get(i) {
622 let tl_suffix = per_seed_tl
623 .and_then(|v| v.get(i))
624 .map(|t| format!(" timelines={}", fmt_num(*t)))
625 .unwrap_or_default();
626 writeln!(
627 f,
628 " #{:<3} seed={:<14} wall={:<10} sim={:<10} events={}{}",
629 i + 1,
630 seed,
631 fmt_duration(m.wall_time),
632 fmt_duration(m.simulated_time),
633 fmt_num(m.events_processed),
634 tl_suffix,
635 )?;
636 } else if let Some(Err(_)) = self.individual_metrics.get(i) {
637 writeln!(f, " #{:<3} seed={:<14} FAILED", i + 1, seed)?;
638 }
639 }
640 }
641
642 writeln!(f)?;
643 Ok(())
644 }
645}