use serde::{Deserialize, Serialize};
use std::fmt::Write as FmtWrite;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TestOutcome {
Passed,
Falsified,
Skipped,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FalsificationReport {
pub spec_name: String,
pub results: Vec<TestResult>,
pub summary: FalsificationSummary,
pub generated_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestResult {
pub id: String,
pub name: String,
pub category: String,
pub points: u32,
pub outcome: TestOutcome,
pub error: Option<String>,
pub evidence: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FalsificationSummary {
pub total_tests: usize,
pub total_points: u32,
pub passed: usize,
pub falsified: usize,
pub skipped: usize,
pub errors: usize,
pub falsification_rate: f64,
pub points_by_category: std::collections::HashMap<String, CategoryStats>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CategoryStats {
pub total: u32,
pub passed: u32,
pub falsified: u32,
}
impl FalsificationReport {
pub fn new(spec_name: String) -> Self {
Self {
spec_name,
results: Vec::new(),
summary: FalsificationSummary::default(),
generated_at: chrono::Utc::now().to_rfc3339(),
}
}
pub fn add_result(&mut self, result: TestResult) {
self.results.push(result);
}
pub fn finalize(&mut self) {
self.summary.total_tests = self.results.len();
self.summary.total_points = self.results.iter().map(|r| r.points).sum();
for result in &self.results {
match result.outcome {
TestOutcome::Passed => self.summary.passed += 1,
TestOutcome::Falsified => self.summary.falsified += 1,
TestOutcome::Skipped => self.summary.skipped += 1,
TestOutcome::Error => self.summary.errors += 1,
}
let entry = self.summary.points_by_category.entry(result.category.clone()).or_default();
entry.total += result.points;
match result.outcome {
TestOutcome::Passed => entry.passed += result.points,
TestOutcome::Falsified => entry.falsified += result.points,
_ => {}
}
}
let executed = self.summary.passed + self.summary.falsified;
if executed > 0 {
self.summary.falsification_rate = (self.summary.falsified as f64) / (executed as f64);
}
}
pub fn format_markdown(&self) -> String {
let mut out = String::new();
writeln!(out, "# Falsification Report: {}", self.spec_name).ok();
writeln!(out).ok();
writeln!(out, "**Generated**: {}", self.generated_at).ok();
writeln!(out, "**Total Points**: {}", self.summary.total_points).ok();
writeln!(out, "**Falsifications Found**: {} (target: 5-15%)", self.summary.falsified).ok();
writeln!(out).ok();
writeln!(out, "## Summary").ok();
writeln!(out).ok();
writeln!(out, "| Category | Points | Passed | Failed | Pass Rate |").ok();
writeln!(out, "|----------|--------|--------|--------|-----------|").ok();
for (category, stats) in &self.summary.points_by_category {
let pass_rate = if stats.total > 0 {
(stats.passed as f64 / stats.total as f64) * 100.0
} else {
0.0
};
writeln!(
out,
"| {} | {} | {} | {} | {:.0}% |",
category, stats.total, stats.passed, stats.falsified, pass_rate
)
.ok();
}
writeln!(out).ok();
let verdict =
if self.summary.falsification_rate >= 0.05 && self.summary.falsification_rate <= 0.15 {
"Healthy falsification rate - specification is well-tested"
} else if self.summary.falsification_rate < 0.05 {
"Low falsification rate - consider more edge cases"
} else {
"High falsification rate - specification needs hardening"
};
writeln!(
out,
"**Verdict**: {:.1}% falsification rate - {}",
self.summary.falsification_rate * 100.0,
verdict
)
.ok();
writeln!(out).ok();
if self.summary.falsified > 0 {
writeln!(out, "## Falsifications (Failures = Success!)").ok();
writeln!(out).ok();
for result in &self.results {
if result.outcome == TestOutcome::Falsified {
writeln!(out, "### {}: {}", result.id, result.name).ok();
writeln!(out, "**Status**: FALSIFIED").ok();
writeln!(out, "**Points**: {}", result.points).ok();
if let Some(err) = &result.error {
writeln!(out, "**Details**: {}", err).ok();
}
for evidence in &result.evidence {
writeln!(out, "- {}", evidence).ok();
}
writeln!(out).ok();
}
}
}
out
}
pub fn format_json(&self) -> String {
serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
}
pub fn format_text(&self) -> String {
let mut out = String::new();
writeln!(out, "FALSIFICATION REPORT: {}", self.spec_name).ok();
writeln!(out, "{}", "=".repeat(60)).ok();
writeln!(out).ok();
writeln!(out, "Total Points: {}", self.summary.total_points).ok();
writeln!(
out,
"Falsifications: {} ({:.1}%)",
self.summary.falsified,
self.summary.falsification_rate * 100.0
)
.ok();
writeln!(out).ok();
for result in &self.results {
let status = match result.outcome {
TestOutcome::Passed => "PASS",
TestOutcome::Falsified => "FAIL",
TestOutcome::Skipped => "SKIP",
TestOutcome::Error => "ERR",
};
writeln!(out, "[{}] {}: {} ({} pts)", status, result.id, result.name, result.points)
.ok();
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_report_creation() {
let report = FalsificationReport::new("test-spec".to_string());
assert_eq!(report.spec_name, "test-spec");
}
#[test]
fn test_report_finalize() {
let mut report = FalsificationReport::new("test".to_string());
report.add_result(TestResult {
id: "BC-001".to_string(),
name: "Test 1".to_string(),
category: "boundary".to_string(),
points: 5,
outcome: TestOutcome::Passed,
error: None,
evidence: vec![],
});
report.add_result(TestResult {
id: "BC-002".to_string(),
name: "Test 2".to_string(),
category: "boundary".to_string(),
points: 5,
outcome: TestOutcome::Falsified,
error: Some("Found edge case".to_string()),
evidence: vec!["Input: empty".to_string()],
});
report.finalize();
assert_eq!(report.summary.total_tests, 2);
assert_eq!(report.summary.passed, 1);
assert_eq!(report.summary.falsified, 1);
assert!((report.summary.falsification_rate - 0.5).abs() < 0.01);
}
#[test]
fn test_format_markdown() {
let mut report = FalsificationReport::new("test".to_string());
report.add_result(TestResult {
id: "BC-001".to_string(),
name: "Empty input".to_string(),
category: "boundary".to_string(),
points: 5,
outcome: TestOutcome::Passed,
error: None,
evidence: vec![],
});
report.finalize();
let md = report.format_markdown();
assert!(md.contains("Falsification Report"));
assert!(md.contains("boundary"));
}
#[test]
fn test_format_json() {
let mut report = FalsificationReport::new("json-test".to_string());
report.add_result(TestResult {
id: "BC-001".to_string(),
name: "Test".to_string(),
category: "boundary".to_string(),
points: 5,
outcome: TestOutcome::Passed,
error: None,
evidence: vec![],
});
report.finalize();
let json = report.format_json();
assert!(json.contains("json-test"));
assert!(json.contains("BC-001"));
assert!(json.contains("boundary"));
}
#[test]
fn test_format_text() {
let mut report = FalsificationReport::new("text-test".to_string());
report.add_result(TestResult {
id: "BC-001".to_string(),
name: "Test".to_string(),
category: "boundary".to_string(),
points: 5,
outcome: TestOutcome::Passed,
error: None,
evidence: vec![],
});
report.add_result(TestResult {
id: "BC-002".to_string(),
name: "Test 2".to_string(),
category: "boundary".to_string(),
points: 5,
outcome: TestOutcome::Falsified,
error: None,
evidence: vec![],
});
report.finalize();
let text = report.format_text();
assert!(text.contains("FALSIFICATION REPORT"));
assert!(text.contains("text-test"));
assert!(text.contains("[PASS]"));
assert!(text.contains("[FAIL]"));
}
#[test]
fn test_test_outcome_equality() {
assert_eq!(TestOutcome::Passed, TestOutcome::Passed);
assert_eq!(TestOutcome::Falsified, TestOutcome::Falsified);
assert_eq!(TestOutcome::Skipped, TestOutcome::Skipped);
assert_eq!(TestOutcome::Error, TestOutcome::Error);
assert_ne!(TestOutcome::Passed, TestOutcome::Falsified);
}
#[test]
fn test_falsification_summary_default() {
let summary = FalsificationSummary::default();
assert_eq!(summary.total_tests, 0);
assert_eq!(summary.total_points, 0);
assert_eq!(summary.passed, 0);
assert_eq!(summary.falsified, 0);
assert_eq!(summary.skipped, 0);
assert_eq!(summary.errors, 0);
assert!((summary.falsification_rate - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_category_stats_default() {
let stats = CategoryStats::default();
assert_eq!(stats.total, 0);
assert_eq!(stats.passed, 0);
assert_eq!(stats.falsified, 0);
}
#[test]
fn test_report_with_all_outcomes() {
let mut report = FalsificationReport::new("all-outcomes".to_string());
report.add_result(TestResult {
id: "T-001".to_string(),
name: "Passed".to_string(),
category: "test".to_string(),
points: 1,
outcome: TestOutcome::Passed,
error: None,
evidence: vec![],
});
report.add_result(TestResult {
id: "T-002".to_string(),
name: "Falsified".to_string(),
category: "test".to_string(),
points: 2,
outcome: TestOutcome::Falsified,
error: None,
evidence: vec![],
});
report.add_result(TestResult {
id: "T-003".to_string(),
name: "Skipped".to_string(),
category: "test".to_string(),
points: 3,
outcome: TestOutcome::Skipped,
error: None,
evidence: vec![],
});
report.add_result(TestResult {
id: "T-004".to_string(),
name: "Error".to_string(),
category: "test".to_string(),
points: 4,
outcome: TestOutcome::Error,
error: Some("Infra issue".to_string()),
evidence: vec![],
});
report.finalize();
assert_eq!(report.summary.total_tests, 4);
assert_eq!(report.summary.passed, 1);
assert_eq!(report.summary.falsified, 1);
assert_eq!(report.summary.skipped, 1);
assert_eq!(report.summary.errors, 1);
}
#[test]
fn test_report_format_text_status_codes() {
let mut report = FalsificationReport::new("status".to_string());
report.add_result(TestResult {
id: "T-001".to_string(),
name: "Skip".to_string(),
category: "test".to_string(),
points: 1,
outcome: TestOutcome::Skipped,
error: None,
evidence: vec![],
});
report.add_result(TestResult {
id: "T-002".to_string(),
name: "Err".to_string(),
category: "test".to_string(),
points: 1,
outcome: TestOutcome::Error,
error: None,
evidence: vec![],
});
report.finalize();
let text = report.format_text();
assert!(text.contains("[SKIP]"));
assert!(text.contains("[ERR]"));
}
#[test]
fn test_markdown_with_falsifications() {
let mut report = FalsificationReport::new("falsify-test".to_string());
report.add_result(TestResult {
id: "BC-001".to_string(),
name: "Edge case".to_string(),
category: "boundary".to_string(),
points: 5,
outcome: TestOutcome::Falsified,
error: Some("Assertion failed".to_string()),
evidence: vec!["Input: empty".to_string(), "Expected: error".to_string()],
});
report.finalize();
let md = report.format_markdown();
assert!(md.contains("Falsifications (Failures = Success!)"));
assert!(md.contains("BC-001"));
assert!(md.contains("FALSIFIED"));
assert!(md.contains("Assertion failed"));
assert!(md.contains("Input: empty"));
}
#[test]
fn test_markdown_healthy_rate() {
let mut report = FalsificationReport::new("healthy".to_string());
for i in 0..19 {
report.add_result(TestResult {
id: format!("T-{:03}", i),
name: format!("Test {}", i),
category: "test".to_string(),
points: 1,
outcome: TestOutcome::Passed,
error: None,
evidence: vec![],
});
}
report.add_result(TestResult {
id: "T-019".to_string(),
name: "Falsified".to_string(),
category: "test".to_string(),
points: 1,
outcome: TestOutcome::Falsified,
error: None,
evidence: vec![],
});
report.finalize();
let md = report.format_markdown();
assert!(md.contains("well-tested"));
}
#[test]
fn test_markdown_low_falsification_rate() {
let mut report = FalsificationReport::new("low".to_string());
for i in 0..100 {
report.add_result(TestResult {
id: format!("T-{:03}", i),
name: format!("Test {}", i),
category: "test".to_string(),
points: 1,
outcome: TestOutcome::Passed,
error: None,
evidence: vec![],
});
}
report.finalize();
let md = report.format_markdown();
assert!(md.contains("more edge cases"));
}
}