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
147impl SimulationReport {
148 pub fn is_success(&self) -> bool {
154 self.assertion_violations.is_empty() && !self.convergence_timeout
155 }
156
157 pub fn success_rate(&self) -> f64 {
159 if self.iterations == 0 {
160 0.0
161 } else {
162 (self.successful_runs as f64 / self.iterations as f64) * 100.0
163 }
164 }
165
166 pub fn average_wall_time(&self) -> Duration {
168 if self.successful_runs == 0 {
169 Duration::ZERO
170 } else {
171 self.metrics.wall_time / self.successful_runs as u32
172 }
173 }
174
175 pub fn average_simulated_time(&self) -> Duration {
177 if self.successful_runs == 0 {
178 Duration::ZERO
179 } else {
180 self.metrics.simulated_time / self.successful_runs as u32
181 }
182 }
183
184 pub fn average_events_processed(&self) -> f64 {
186 if self.successful_runs == 0 {
187 0.0
188 } else {
189 self.metrics.events_processed as f64 / self.successful_runs as f64
190 }
191 }
192
193 pub fn eprint(&self) {
198 super::display::eprint_report(self);
199 }
200}
201
202fn fmt_num(n: u64) -> String {
208 let s = n.to_string();
209 let mut result = String::with_capacity(s.len() + s.len() / 3);
210 for (i, c) in s.chars().rev().enumerate() {
211 if i > 0 && i % 3 == 0 {
212 result.push(',');
213 }
214 result.push(c);
215 }
216 result.chars().rev().collect()
217}
218
219fn fmt_i64(n: i64) -> String {
221 if n < 0 {
222 format!("-{}", fmt_num(n.unsigned_abs()))
223 } else {
224 fmt_num(n as u64)
225 }
226}
227
228fn fmt_duration(d: Duration) -> String {
230 let total_ms = d.as_millis();
231 if total_ms < 1000 {
232 format!("{}ms", total_ms)
233 } else if total_ms < 60_000 {
234 format!("{:.2}s", d.as_secs_f64())
235 } else {
236 let mins = d.as_secs() / 60;
237 let secs = d.as_secs() % 60;
238 format!("{}m {:02}s", mins, secs)
239 }
240}
241
242fn kind_label(kind: AssertKind) -> &'static str {
244 match kind {
245 AssertKind::Always => "always",
246 AssertKind::AlwaysOrUnreachable => "always?",
247 AssertKind::Sometimes => "sometimes",
248 AssertKind::Reachable => "reachable",
249 AssertKind::Unreachable => "unreachable",
250 AssertKind::NumericAlways => "num-always",
251 AssertKind::NumericSometimes => "numeric",
252 AssertKind::BooleanSometimesAll => "frontier",
253 }
254}
255
256fn kind_sort_order(kind: AssertKind) -> u8 {
258 match kind {
259 AssertKind::Always => 0,
260 AssertKind::AlwaysOrUnreachable => 1,
261 AssertKind::Unreachable => 2,
262 AssertKind::NumericAlways => 3,
263 AssertKind::Sometimes => 4,
264 AssertKind::Reachable => 5,
265 AssertKind::NumericSometimes => 6,
266 AssertKind::BooleanSometimesAll => 7,
267 }
268}
269
270impl fmt::Display for SimulationReport {
275 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276 writeln!(f, "=== Simulation Report ===")?;
278 writeln!(
279 f,
280 " Iterations: {} | Passed: {} | Failed: {} | Rate: {:.1}%",
281 self.iterations,
282 self.successful_runs,
283 self.failed_runs,
284 self.success_rate()
285 )?;
286 writeln!(f)?;
287
288 writeln!(
290 f,
291 " Avg Wall Time: {:<14}Total: {}",
292 fmt_duration(self.average_wall_time()),
293 fmt_duration(self.metrics.wall_time)
294 )?;
295 writeln!(
296 f,
297 " Avg Sim Time: {}",
298 fmt_duration(self.average_simulated_time())
299 )?;
300 writeln!(
301 f,
302 " Avg Events: {}",
303 fmt_num(self.average_events_processed() as u64)
304 )?;
305
306 if !self.seeds_failing.is_empty() {
308 writeln!(f)?;
309 writeln!(f, " Faulty seeds: {:?}", self.seeds_failing)?;
310 }
311
312 if let Some(ref exp) = self.exploration {
314 writeln!(f)?;
315 writeln!(f, "--- Exploration ---")?;
316 writeln!(
317 f,
318 " Timelines: {:<18}Bugs found: {}",
319 fmt_num(exp.total_timelines),
320 fmt_num(exp.bugs_found)
321 )?;
322 writeln!(
323 f,
324 " Fork points: {:<18}Coverage: {} / {} bits ({:.1}%)",
325 fmt_num(exp.fork_points),
326 fmt_num(exp.coverage_bits as u64),
327 fmt_num(exp.coverage_total as u64),
328 if exp.coverage_total > 0 {
329 (exp.coverage_bits as f64 / exp.coverage_total as f64) * 100.0
330 } else {
331 0.0
332 }
333 )?;
334 if exp.sancov_edges_total > 0 {
335 writeln!(
336 f,
337 " Sancov: {} / {} edges ({:.1}%)",
338 fmt_num(exp.sancov_edges_covered as u64),
339 fmt_num(exp.sancov_edges_total as u64),
340 (exp.sancov_edges_covered as f64 / exp.sancov_edges_total as f64) * 100.0
341 )?;
342 }
343 writeln!(
344 f,
345 " Energy left: {:<18}Realloc pool: {}",
346 fmt_i64(exp.energy_remaining),
347 fmt_i64(exp.realloc_pool_remaining)
348 )?;
349 for br in &exp.bug_recipes {
350 writeln!(
351 f,
352 " Bug recipe (seed={}): {}",
353 br.seed,
354 moonpool_explorer::format_timeline(&br.recipe)
355 )?;
356 }
357 }
358
359 if !self.assertion_details.is_empty() {
361 writeln!(f)?;
362 writeln!(f, "--- Assertions ({}) ---", self.assertion_details.len())?;
363
364 let mut sorted: Vec<&AssertionDetail> = self.assertion_details.iter().collect();
365 sorted.sort_by(|a, b| {
366 kind_sort_order(a.kind)
367 .cmp(&kind_sort_order(b.kind))
368 .then(a.status.cmp(&b.status))
369 .then(a.msg.cmp(&b.msg))
370 });
371
372 for detail in &sorted {
373 let status_tag = match detail.status {
374 AssertionStatus::Pass => "PASS",
375 AssertionStatus::Fail => "FAIL",
376 AssertionStatus::Miss => "MISS",
377 };
378 let kind_tag = kind_label(detail.kind);
379 let quoted_msg = format!("\"{}\"", detail.msg);
380
381 match detail.kind {
382 AssertKind::Sometimes | AssertKind::Reachable => {
383 let total = detail.pass_count + detail.fail_count;
384 let rate = if total > 0 {
385 (detail.pass_count as f64 / total as f64) * 100.0
386 } else {
387 0.0
388 };
389 writeln!(
390 f,
391 " {} [{:<10}] {:<34} {} / {} ({:.1}%)",
392 status_tag,
393 kind_tag,
394 quoted_msg,
395 fmt_num(detail.pass_count),
396 fmt_num(total),
397 rate
398 )?;
399 }
400 AssertKind::NumericSometimes | AssertKind::NumericAlways => {
401 writeln!(
402 f,
403 " {} [{:<10}] {:<34} {} pass {} fail watermark: {}",
404 status_tag,
405 kind_tag,
406 quoted_msg,
407 fmt_num(detail.pass_count),
408 fmt_num(detail.fail_count),
409 detail.watermark
410 )?;
411 }
412 AssertKind::BooleanSometimesAll => {
413 writeln!(
414 f,
415 " {} [{:<10}] {:<34} {} calls frontier: {}",
416 status_tag,
417 kind_tag,
418 quoted_msg,
419 fmt_num(detail.pass_count),
420 detail.frontier
421 )?;
422 }
423 _ => {
424 writeln!(
426 f,
427 " {} [{:<10}] {:<34} {} pass {} fail",
428 status_tag,
429 kind_tag,
430 quoted_msg,
431 fmt_num(detail.pass_count),
432 fmt_num(detail.fail_count)
433 )?;
434 }
435 }
436 }
437 }
438
439 if !self.assertion_violations.is_empty() {
441 writeln!(f)?;
442 writeln!(f, "--- Assertion Violations ---")?;
443 for v in &self.assertion_violations {
444 writeln!(f, " - {}", v)?;
445 }
446 }
447
448 if !self.coverage_violations.is_empty() {
450 writeln!(f)?;
451 writeln!(f, "--- Coverage Gaps ---")?;
452 for v in &self.coverage_violations {
453 writeln!(f, " - {}", v)?;
454 }
455 }
456
457 if !self.bucket_summaries.is_empty() {
459 let total_buckets: usize = self
460 .bucket_summaries
461 .iter()
462 .map(|s| s.buckets_discovered)
463 .sum();
464 writeln!(f)?;
465 writeln!(
466 f,
467 "--- Buckets ({} across {} sites) ---",
468 total_buckets,
469 self.bucket_summaries.len()
470 )?;
471 for bs in &self.bucket_summaries {
472 writeln!(
473 f,
474 " {:<34} {:>3} buckets {:>8} hits",
475 format!("\"{}\"", bs.msg),
476 bs.buckets_discovered,
477 fmt_num(bs.total_hits)
478 )?;
479 }
480 }
481
482 if self.convergence_timeout {
484 writeln!(f)?;
485 writeln!(f, "--- Convergence FAILED ---")?;
486 writeln!(f, " UntilConverged hit iteration cap without converging.")?;
487 }
488
489 if self.seeds_used.len() > 1 {
491 writeln!(f)?;
492 writeln!(f, "--- Seeds ---")?;
493 let per_seed_tl = self.exploration.as_ref().map(|e| &e.per_seed_timelines);
494 for (i, seed) in self.seeds_used.iter().enumerate() {
495 if let Some(Ok(m)) = self.individual_metrics.get(i) {
496 let tl_suffix = per_seed_tl
497 .and_then(|v| v.get(i))
498 .map(|t| format!(" timelines={}", fmt_num(*t)))
499 .unwrap_or_default();
500 writeln!(
501 f,
502 " #{:<3} seed={:<14} wall={:<10} sim={:<10} events={}{}",
503 i + 1,
504 seed,
505 fmt_duration(m.wall_time),
506 fmt_duration(m.simulated_time),
507 fmt_num(m.events_processed),
508 tl_suffix,
509 )?;
510 } else if let Some(Err(_)) = self.individual_metrics.get(i) {
511 writeln!(f, " #{:<3} seed={:<14} FAILED", i + 1, seed)?;
512 }
513 }
514 }
515
516 writeln!(f)?;
517 Ok(())
518 }
519}