use std::collections::HashMap;
use std::fmt;
use std::time::Duration;
use moonpool_explorer::AssertKind;
use crate::SimulationResult;
use crate::chaos::AssertionStats;
#[derive(Debug, Clone, PartialEq)]
pub struct SimulationMetrics {
pub wall_time: Duration,
pub simulated_time: Duration,
pub events_processed: u64,
}
impl Default for SimulationMetrics {
fn default() -> Self {
Self {
wall_time: Duration::ZERO,
simulated_time: Duration::ZERO,
events_processed: 0,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BugRecipe {
pub seed: u64,
pub recipe: Vec<(u64, u64)>,
}
#[derive(Debug, Clone)]
pub struct ExplorationReport {
pub total_timelines: u64,
pub fork_points: u64,
pub bugs_found: u64,
pub bug_recipes: Vec<BugRecipe>,
pub energy_remaining: i64,
pub realloc_pool_remaining: i64,
pub coverage_bits: u32,
pub coverage_total: u32,
pub sancov_edges_total: usize,
pub sancov_edges_covered: usize,
pub converged: bool,
pub per_seed_timelines: Vec<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum AssertionStatus {
Fail,
Miss,
Pass,
}
#[derive(Debug, Clone)]
pub struct AssertionDetail {
pub msg: String,
pub kind: AssertKind,
pub pass_count: u64,
pub fail_count: u64,
pub watermark: i64,
pub frontier: u8,
pub status: AssertionStatus,
}
#[derive(Debug, Clone)]
pub struct BucketSiteSummary {
pub msg: String,
pub buckets_discovered: usize,
pub total_hits: u64,
}
#[derive(Debug, Clone)]
pub struct SimulationReport {
pub iterations: usize,
pub successful_runs: usize,
pub failed_runs: usize,
pub metrics: SimulationMetrics,
pub individual_metrics: Vec<SimulationResult<SimulationMetrics>>,
pub seeds_used: Vec<u64>,
pub seeds_failing: Vec<u64>,
pub assertion_results: HashMap<String, AssertionStats>,
pub assertion_violations: Vec<String>,
pub coverage_violations: Vec<String>,
pub exploration: Option<ExplorationReport>,
pub assertion_details: Vec<AssertionDetail>,
pub bucket_summaries: Vec<BucketSiteSummary>,
pub convergence_timeout: bool,
}
impl SimulationReport {
#[must_use]
pub fn success_rate(&self) -> f64 {
if self.iterations == 0 {
0.0
} else {
let successful = u32::try_from(self.successful_runs).map_or(f64::INFINITY, f64::from);
let total = u32::try_from(self.iterations).map_or(f64::INFINITY, f64::from);
(successful / total) * 100.0
}
}
#[must_use]
pub fn average_wall_time(&self) -> Duration {
if self.successful_runs == 0 {
Duration::ZERO
} else {
let runs = u32::try_from(self.successful_runs).unwrap_or(u32::MAX);
self.metrics.wall_time / runs
}
}
#[must_use]
pub fn average_simulated_time(&self) -> Duration {
if self.successful_runs == 0 {
Duration::ZERO
} else {
let runs = u32::try_from(self.successful_runs).unwrap_or(u32::MAX);
self.metrics.simulated_time / runs
}
}
#[must_use]
pub fn average_events_processed(&self) -> f64 {
if self.successful_runs == 0 {
0.0
} else {
let events =
u32::try_from(self.metrics.events_processed).map_or(f64::INFINITY, f64::from);
let runs = u32::try_from(self.successful_runs).map_or(f64::INFINITY, f64::from);
events / runs
}
}
pub fn eprint(&self) {
super::display::eprint_report(self);
}
}
fn f64_to_u64_saturating(v: f64) -> u64 {
const TWO_POW_64: f64 = 18_446_744_073_709_551_616.0;
if !v.is_finite() || v <= 0.0 {
0
} else if v >= TWO_POW_64 {
u64::MAX
} else {
unsafe { v.round().to_int_unchecked::<u64>() }
}
}
fn fmt_num(n: u64) -> String {
let s = n.to_string();
let mut result = String::with_capacity(s.len() + s.len() / 3);
for (i, c) in s.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
result.push(',');
}
result.push(c);
}
result.chars().rev().collect()
}
fn fmt_i64(n: i64) -> String {
if n < 0 {
format!("-{}", fmt_num(n.unsigned_abs()))
} else {
fmt_num(n.unsigned_abs())
}
}
fn fmt_duration(d: Duration) -> String {
let total_ms = d.as_millis();
if total_ms < 1000 {
format!("{total_ms}ms")
} else if total_ms < 60_000 {
format!("{:.2}s", d.as_secs_f64())
} else {
let mins = d.as_secs() / 60;
let secs = d.as_secs() % 60;
format!("{mins}m {secs:02}s")
}
}
fn kind_label(kind: AssertKind) -> &'static str {
match kind {
AssertKind::Always => "always",
AssertKind::AlwaysOrUnreachable => "always?",
AssertKind::Sometimes => "sometimes",
AssertKind::Reachable => "reachable",
AssertKind::Unreachable => "unreachable",
AssertKind::NumericAlways => "num-always",
AssertKind::NumericSometimes => "numeric",
AssertKind::BooleanSometimesAll => "frontier",
}
}
fn kind_sort_order(kind: AssertKind) -> u8 {
match kind {
AssertKind::Always => 0,
AssertKind::AlwaysOrUnreachable => 1,
AssertKind::Unreachable => 2,
AssertKind::NumericAlways => 3,
AssertKind::Sometimes => 4,
AssertKind::Reachable => 5,
AssertKind::NumericSometimes => 6,
AssertKind::BooleanSometimesAll => 7,
}
}
fn fmt_summary(f: &mut fmt::Formatter<'_>, report: &SimulationReport) -> fmt::Result {
writeln!(f, "=== Simulation Report ===")?;
writeln!(
f,
" Iterations: {} | Passed: {} | Failed: {} | Rate: {:.1}%",
report.iterations,
report.successful_runs,
report.failed_runs,
report.success_rate()
)?;
writeln!(f)
}
fn fmt_timing(f: &mut fmt::Formatter<'_>, report: &SimulationReport) -> fmt::Result {
writeln!(
f,
" Avg Wall Time: {:<14}Total: {}",
fmt_duration(report.average_wall_time()),
fmt_duration(report.metrics.wall_time)
)?;
writeln!(
f,
" Avg Sim Time: {}",
fmt_duration(report.average_simulated_time())
)?;
writeln!(
f,
" Avg Events: {}",
fmt_num(f64_to_u64_saturating(report.average_events_processed()))
)
}
fn fmt_exploration(f: &mut fmt::Formatter<'_>, exp: &ExplorationReport) -> fmt::Result {
writeln!(f)?;
writeln!(f, "--- Exploration ---")?;
writeln!(
f,
" Timelines: {:<18}Bugs found: {}",
fmt_num(exp.total_timelines),
fmt_num(exp.bugs_found)
)?;
writeln!(
f,
" Fork points: {:<18}Coverage: {} / {} bits ({:.1}%)",
fmt_num(exp.fork_points),
fmt_num(u64::from(exp.coverage_bits)),
fmt_num(u64::from(exp.coverage_total)),
if exp.coverage_total > 0 {
(f64::from(exp.coverage_bits) / f64::from(exp.coverage_total)) * 100.0
} else {
0.0
}
)?;
if exp.sancov_edges_total > 0 {
let covered = u64::try_from(exp.sancov_edges_covered).unwrap_or(u64::MAX);
let total = u64::try_from(exp.sancov_edges_total).unwrap_or(u64::MAX);
let covered_u32 = u32::try_from(exp.sancov_edges_covered).unwrap_or(u32::MAX);
let total_u32 = u32::try_from(exp.sancov_edges_total).unwrap_or(u32::MAX);
writeln!(
f,
" Sancov: {} / {} edges ({:.1}%)",
fmt_num(covered),
fmt_num(total),
(f64::from(covered_u32) / f64::from(total_u32)) * 100.0
)?;
}
writeln!(
f,
" Energy left: {:<18}Realloc pool: {}",
fmt_i64(exp.energy_remaining),
fmt_i64(exp.realloc_pool_remaining)
)?;
for br in &exp.bug_recipes {
writeln!(
f,
" Bug recipe (seed={}): {}",
br.seed,
moonpool_explorer::format_timeline(&br.recipe)
)?;
}
Ok(())
}
fn fmt_assertion_detail(f: &mut fmt::Formatter<'_>, detail: &AssertionDetail) -> fmt::Result {
let status_tag = match detail.status {
AssertionStatus::Pass => "PASS",
AssertionStatus::Fail => "FAIL",
AssertionStatus::Miss => "MISS",
};
let kind_tag = kind_label(detail.kind);
let quoted_msg = format!("\"{}\"", detail.msg);
match detail.kind {
AssertKind::Sometimes | AssertKind::Reachable => {
let total = detail.pass_count + detail.fail_count;
let rate = if total > 0 {
let pass_u32 = u32::try_from(detail.pass_count).unwrap_or(u32::MAX);
let total_u32 = u32::try_from(total).unwrap_or(u32::MAX);
(f64::from(pass_u32) / f64::from(total_u32)) * 100.0
} else {
0.0
};
writeln!(
f,
" {} [{:<10}] {:<34} {} / {} ({:.1}%)",
status_tag,
kind_tag,
quoted_msg,
fmt_num(detail.pass_count),
fmt_num(total),
rate
)
}
AssertKind::NumericSometimes | AssertKind::NumericAlways => writeln!(
f,
" {} [{:<10}] {:<34} {} pass {} fail watermark: {}",
status_tag,
kind_tag,
quoted_msg,
fmt_num(detail.pass_count),
fmt_num(detail.fail_count),
detail.watermark
),
AssertKind::BooleanSometimesAll => writeln!(
f,
" {} [{:<10}] {:<34} {} calls frontier: {}",
status_tag,
kind_tag,
quoted_msg,
fmt_num(detail.pass_count),
detail.frontier
),
_ => writeln!(
f,
" {} [{:<10}] {:<34} {} pass {} fail",
status_tag,
kind_tag,
quoted_msg,
fmt_num(detail.pass_count),
fmt_num(detail.fail_count)
),
}
}
fn fmt_assertion_details(f: &mut fmt::Formatter<'_>, details: &[AssertionDetail]) -> fmt::Result {
if details.is_empty() {
return Ok(());
}
writeln!(f)?;
writeln!(f, "--- Assertions ({}) ---", details.len())?;
let mut sorted: Vec<&AssertionDetail> = details.iter().collect();
sorted.sort_by(|a, b| {
kind_sort_order(a.kind)
.cmp(&kind_sort_order(b.kind))
.then(a.status.cmp(&b.status))
.then(a.msg.cmp(&b.msg))
});
for detail in &sorted {
fmt_assertion_detail(f, detail)?;
}
Ok(())
}
impl fmt::Display for SimulationReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt_summary(f, self)?;
fmt_timing(f, self)?;
if !self.seeds_failing.is_empty() {
writeln!(f)?;
writeln!(f, " Faulty seeds: {:?}", self.seeds_failing)?;
}
if let Some(ref exp) = self.exploration {
fmt_exploration(f, exp)?;
}
fmt_assertion_details(f, &self.assertion_details)?;
if !self.assertion_violations.is_empty() {
writeln!(f)?;
writeln!(f, "--- Assertion Violations ---")?;
for v in &self.assertion_violations {
writeln!(f, " - {v}")?;
}
}
if !self.coverage_violations.is_empty() {
writeln!(f)?;
writeln!(f, "--- Coverage Gaps ---")?;
for v in &self.coverage_violations {
writeln!(f, " - {v}")?;
}
}
if !self.bucket_summaries.is_empty() {
let total_buckets: usize = self
.bucket_summaries
.iter()
.map(|s| s.buckets_discovered)
.sum();
writeln!(f)?;
writeln!(
f,
"--- Buckets ({} across {} sites) ---",
total_buckets,
self.bucket_summaries.len()
)?;
for bs in &self.bucket_summaries {
writeln!(
f,
" {:<34} {:>3} buckets {:>8} hits",
format!("\"{}\"", bs.msg),
bs.buckets_discovered,
fmt_num(bs.total_hits)
)?;
}
}
if self.convergence_timeout {
writeln!(f)?;
writeln!(f, "--- Convergence FAILED ---")?;
writeln!(f, " UntilConverged hit iteration cap without converging.")?;
}
if self.seeds_used.len() > 1 {
writeln!(f)?;
writeln!(f, "--- Seeds ---")?;
let per_seed_tl = self.exploration.as_ref().map(|e| &e.per_seed_timelines);
for (i, seed) in self.seeds_used.iter().enumerate() {
if let Some(Ok(m)) = self.individual_metrics.get(i) {
let tl_suffix = per_seed_tl
.and_then(|v| v.get(i))
.map(|t| format!(" timelines={}", fmt_num(*t)))
.unwrap_or_default();
writeln!(
f,
" #{:<3} seed={:<14} wall={:<10} sim={:<10} events={}{}",
i + 1,
seed,
fmt_duration(m.wall_time),
fmt_duration(m.simulated_time),
fmt_num(m.events_processed),
tl_suffix,
)?;
} else if let Some(Err(_)) = self.individual_metrics.get(i) {
writeln!(f, " #{:<3} seed={:<14} FAILED", i + 1, seed)?;
}
}
}
writeln!(f)?;
Ok(())
}
}