From 78cfc16601c44fa57e876e9b54ca56f93af078b0 Mon Sep 17 00:00:00 2001 From: TH Lee Date: Sun, 23 Aug 2026 15:59:00 +0200 Subject: [PATCH 1/2] refactor: enforce reporter count invariant and fix empty failure header #42 --- src/report/reporter.rs | 56 ++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/src/report/reporter.rs b/src/report/reporter.rs index 44b2ecf..e2fcbfd 100644 --- a/src/report/reporter.rs +++ b/src/report/reporter.rs @@ -37,6 +37,22 @@ pub trait ReporterCapable: Send + Sync { ) -> bool; } +// ============================================================================ +// Count helpers +// ============================================================================ + +/// Split an evaluation list into passed/failed counts against the raw total. +/// The invariant passed <= total is enforced loudly: a silent usize wrap in +/// release would corrupt verification counts. +fn split_counts(total: usize, evaluations: &[Evaluation]) -> (usize, usize) { + let passed = evaluations.iter().filter(|e| e.passed).count(); + assert!( + passed <= total, + "reporter invariant violated: passed ({passed}) exceeds raw total ({total})" + ); + (passed, total - passed) +} + // ============================================================================ // CSV Reporter // ============================================================================ @@ -52,8 +68,7 @@ impl ReporterCapable for CsvReporter { total: usize, evaluations: &[Evaluation], ) -> bool { - let passed_count = evaluations.iter().filter(|e| e.passed).count(); - let failed_count = total - passed_count; + let (passed_count, failed_count) = split_counts(total, evaluations); // Print metadata as comments println!("# target: {}", target); @@ -110,8 +125,7 @@ impl ReporterCapable for TraceReporter { total: usize, evaluations: &[Evaluation], ) -> bool { - let passed_count = evaluations.iter().filter(|e| e.passed).count(); - let failed_count = total - passed_count; + let (passed_count, failed_count) = split_counts(total, evaluations); let started_at = chrono::Utc::now(); println!( @@ -192,8 +206,7 @@ impl ReporterCapable for TextReporter { total: usize, evaluations: &[Evaluation], ) -> bool { - let passed_count = evaluations.iter().filter(|e| e.passed).count(); - let failed_count = total - passed_count; + let (passed_count, failed_count) = split_counts(total, evaluations); println!("target: {}", target); println!("total: {}", total); @@ -202,21 +215,23 @@ impl ReporterCapable for TextReporter { println!(); if failed_count > 0 { - println!("Failures:"); - // Cap the detail listing: the full list can be gigabytes for - // sparse spaces (e.g. 432K invalid rv32imcb encodings each - // carrying the cross-mapping description). The summary lines - // above remain authoritative; the cap keeps the log usable. + // The summary lines above are authoritative; the detail listing + // is capped (the full list can be gigabytes for sparse spaces). const MAX_PRINTED_FAILURES: usize = 25; - for (i, e) in evaluations.iter().filter(|e| !e.passed).enumerate() { - if i == MAX_PRINTED_FAILURES { - println!( - " ... and {} more failures", - failed_count - MAX_PRINTED_FAILURES - ); - break; + let failed_rows: Vec<&Evaluation> = + evaluations.iter().filter(|e| !e.passed).collect(); + if !failed_rows.is_empty() { + println!("Failures:"); + for (i, e) in failed_rows.iter().enumerate() { + if i == MAX_PRINTED_FAILURES { + println!( + " ... and {} more failures", + failed_count - MAX_PRINTED_FAILURES + ); + break; + } + println!(" [FAIL] {:?} — {}", e.combination.values, e.reason); } - println!(" [FAIL] {:?} — {}", e.combination.values, e.reason); } } @@ -275,8 +290,7 @@ impl ReporterCapable for JsonReporter { total: usize, evaluations: &[Evaluation], ) -> bool { - let passed_count = evaluations.iter().filter(|e| e.passed).count(); - let failed_count = total - passed_count; + let (passed_count, failed_count) = split_counts(total, evaluations); let origin = format!("ev/{}", env!("CARGO_PKG_VERSION")); let timestamp = chrono::Utc::now().to_rfc3339(); let spec_hash = spec_hash.to_string(); From 0f76154d1b3b1a89e210bcc61b936b747bc1b203 Mon Sep 17 00:00:00 2001 From: TH Lee Date: Sun, 23 Aug 2026 16:03:24 +0200 Subject: [PATCH 2/2] fix: report remaining present failure rows in the capped list #42 --- src/report/reporter.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/report/reporter.rs b/src/report/reporter.rs index e2fcbfd..e26e93f 100644 --- a/src/report/reporter.rs +++ b/src/report/reporter.rs @@ -224,9 +224,12 @@ impl ReporterCapable for TextReporter { println!("Failures:"); for (i, e) in failed_rows.iter().enumerate() { if i == MAX_PRINTED_FAILURES { + // failed_count includes structurally absent combinations + // in the structural pipeline; the remaining present + // rows are the accurate unprinted detail. println!( " ... and {} more failures", - failed_count - MAX_PRINTED_FAILURES + failed_rows.len() - MAX_PRINTED_FAILURES ); break; }