Skip to main content

moonpool_sim/runner/
report.rs

1//! Simulation metrics and reporting.
2//!
3//! This module provides types for collecting and reporting simulation results.
4
5use 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/// Core metrics collected during a simulation run.
15#[derive(Debug, Clone, PartialEq)]
16pub struct SimulationMetrics {
17    /// Wall-clock time taken for the simulation
18    pub wall_time: Duration,
19    /// Simulated logical time elapsed
20    pub simulated_time: Duration,
21    /// Number of events processed
22    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/// A captured bug recipe with its root seed for deterministic replay.
36#[derive(Debug, Clone, PartialEq)]
37pub struct BugRecipe {
38    /// The root seed that was active when this bug was discovered.
39    pub seed: u64,
40    /// Fork path: `(rng_call_count, child_seed)` pairs.
41    pub recipe: Vec<(u64, u64)>,
42}
43
44/// Report from fork-based exploration.
45#[derive(Debug, Clone)]
46pub struct ExplorationReport {
47    /// Total timelines explored across all forks.
48    pub total_timelines: u64,
49    /// Total fork points triggered.
50    pub fork_points: u64,
51    /// Number of bugs found (children exiting with code 42).
52    pub bugs_found: u64,
53    /// Bug recipes captured during exploration (one per seed that found bugs).
54    pub bug_recipes: Vec<BugRecipe>,
55    /// Remaining global energy after exploration.
56    pub energy_remaining: i64,
57    /// Energy in the reallocation pool.
58    pub realloc_pool_remaining: i64,
59    /// Number of bits set in the explored coverage map.
60    pub coverage_bits: u32,
61    /// Total number of bits in the coverage map (8192).
62    pub coverage_total: u32,
63}
64
65/// Pass/fail/miss status for an assertion in the report.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
67pub enum AssertionStatus {
68    /// Assertion contract violated (always-type failed, or unreachable reached).
69    Fail,
70    /// Coverage assertion never satisfied (sometimes/reachable never triggered).
71    Miss,
72    /// Assertion contract satisfied.
73    Pass,
74}
75
76/// Detailed information about a single assertion slot.
77#[derive(Debug, Clone)]
78pub struct AssertionDetail {
79    /// Human-readable assertion message.
80    pub msg: String,
81    /// The kind of assertion.
82    pub kind: AssertKind,
83    /// Number of times the assertion passed.
84    pub pass_count: u64,
85    /// Number of times the assertion failed.
86    pub fail_count: u64,
87    /// Best watermark value (for numeric assertions).
88    pub watermark: i64,
89    /// Frontier value (for BooleanSometimesAll).
90    pub frontier: u8,
91    /// Computed status based on kind and counts.
92    pub status: AssertionStatus,
93}
94
95/// Summary of one `assert_sometimes_each!` site (grouped by msg).
96#[derive(Debug, Clone)]
97pub struct BucketSiteSummary {
98    /// Assertion message identifying the site.
99    pub msg: String,
100    /// Number of unique identity-key buckets discovered.
101    pub buckets_discovered: usize,
102    /// Total hit count across all buckets.
103    pub total_hits: u64,
104}
105
106/// Comprehensive report of a simulation run with statistical analysis.
107#[derive(Debug, Clone)]
108pub struct SimulationReport {
109    /// Number of iterations executed
110    pub iterations: usize,
111    /// Number of successful runs
112    pub successful_runs: usize,
113    /// Number of failed runs
114    pub failed_runs: usize,
115    /// Aggregated metrics across all runs
116    pub metrics: SimulationMetrics,
117    /// Individual metrics for each iteration
118    pub individual_metrics: Vec<SimulationResult<SimulationMetrics>>,
119    /// Seeds used for each iteration
120    pub seeds_used: Vec<u64>,
121    /// failed seeds
122    pub seeds_failing: Vec<u64>,
123    /// Aggregated assertion results across all iterations
124    pub assertion_results: HashMap<String, AssertionStats>,
125    /// Always-type assertion violations (definite bugs).
126    pub assertion_violations: Vec<String>,
127    /// Coverage assertion violations (sometimes/reachable not satisfied).
128    pub coverage_violations: Vec<String>,
129    /// Exploration report (present when fork-based exploration was enabled).
130    pub exploration: Option<ExplorationReport>,
131    /// Per-assertion detailed breakdown from shared memory slots.
132    pub assertion_details: Vec<AssertionDetail>,
133    /// Per-site summaries of `assert_sometimes_each!` buckets.
134    pub bucket_summaries: Vec<BucketSiteSummary>,
135}
136
137impl SimulationReport {
138    /// Calculate the success rate as a percentage.
139    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    /// Get the average wall time per iteration.
148    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    /// Get the average simulated time per iteration.
157    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    /// Get the average number of events processed per iteration.
166    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
175// ---------------------------------------------------------------------------
176// Display helpers
177// ---------------------------------------------------------------------------
178
179/// Format a number with comma separators (e.g., 123456 -> "123,456").
180fn 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
192/// Format a duration as a human-readable string.
193fn 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
206/// Short human-readable label for an assertion kind.
207fn 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
220/// Sort key for grouping assertion kinds in display.
221fn 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
234// ---------------------------------------------------------------------------
235// Display impl
236// ---------------------------------------------------------------------------
237
238impl fmt::Display for SimulationReport {
239    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240        // === Header ===
241        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        // === Timing ===
253        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        // === Faulty Seeds ===
271        if !self.seeds_failing.is_empty() {
272            writeln!(f)?;
273            writeln!(f, "  Faulty seeds: {:?}", self.seeds_failing)?;
274        }
275
276        // === Exploration ===
277        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        // === Assertion Details ===
314        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                        // Always, AlwaysOrUnreachable, Unreachable
379                        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        // === Assertion Violations ===
394        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        // === Coverage Gaps ===
403        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        // === Buckets ===
412        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        // === Per-Seed Metrics ===
437        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}