1use serde::{Deserialize, Serialize};
6use std::fmt::Write as FmtWrite;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum TestOutcome {
11 Passed,
13 Falsified,
15 Skipped,
17 Error,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct FalsificationReport {
24 pub spec_name: String,
26 pub results: Vec<TestResult>,
28 pub summary: FalsificationSummary,
30 pub generated_at: String,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct TestResult {
37 pub id: String,
39 pub name: String,
41 pub category: String,
43 pub points: u32,
45 pub outcome: TestOutcome,
47 pub error: Option<String>,
49 pub evidence: Vec<String>,
51}
52
53#[derive(Debug, Clone, Default, Serialize, Deserialize)]
55pub struct FalsificationSummary {
56 pub total_tests: usize,
58 pub total_points: u32,
60 pub passed: usize,
62 pub falsified: usize,
64 pub skipped: usize,
66 pub errors: usize,
68 pub falsification_rate: f64,
70 pub points_by_category: std::collections::HashMap<String, CategoryStats>,
72}
73
74#[derive(Debug, Clone, Default, Serialize, Deserialize)]
76pub struct CategoryStats {
77 pub total: u32,
78 pub passed: u32,
79 pub falsified: u32,
80}
81
82impl FalsificationReport {
83 pub fn new(spec_name: String) -> Self {
85 Self {
86 spec_name,
87 results: Vec::new(),
88 summary: FalsificationSummary::default(),
89 generated_at: chrono::Utc::now().to_rfc3339(),
90 }
91 }
92
93 pub fn add_result(&mut self, result: TestResult) {
95 self.results.push(result);
96 }
97
98 pub fn finalize(&mut self) {
100 self.summary.total_tests = self.results.len();
101 self.summary.total_points = self.results.iter().map(|r| r.points).sum();
102
103 for result in &self.results {
104 match result.outcome {
105 TestOutcome::Passed => self.summary.passed += 1,
106 TestOutcome::Falsified => self.summary.falsified += 1,
107 TestOutcome::Skipped => self.summary.skipped += 1,
108 TestOutcome::Error => self.summary.errors += 1,
109 }
110
111 let entry = self.summary.points_by_category.entry(result.category.clone()).or_default();
112 entry.total += result.points;
113 match result.outcome {
114 TestOutcome::Passed => entry.passed += result.points,
115 TestOutcome::Falsified => entry.falsified += result.points,
116 _ => {}
117 }
118 }
119
120 let executed = self.summary.passed + self.summary.falsified;
121 if executed > 0 {
122 self.summary.falsification_rate = (self.summary.falsified as f64) / (executed as f64);
123 }
124 }
125
126 pub fn format_markdown(&self) -> String {
128 let mut out = String::new();
129
130 writeln!(out, "# Falsification Report: {}", self.spec_name).ok();
131 writeln!(out).ok();
132 writeln!(out, "**Generated**: {}", self.generated_at).ok();
133 writeln!(out, "**Total Points**: {}", self.summary.total_points).ok();
134 writeln!(out, "**Falsifications Found**: {} (target: 5-15%)", self.summary.falsified).ok();
135 writeln!(out).ok();
136
137 writeln!(out, "## Summary").ok();
138 writeln!(out).ok();
139 writeln!(out, "| Category | Points | Passed | Failed | Pass Rate |").ok();
140 writeln!(out, "|----------|--------|--------|--------|-----------|").ok();
141
142 for (category, stats) in &self.summary.points_by_category {
143 let pass_rate = if stats.total > 0 {
144 (stats.passed as f64 / stats.total as f64) * 100.0
145 } else {
146 0.0
147 };
148 writeln!(
149 out,
150 "| {} | {} | {} | {} | {:.0}% |",
151 category, stats.total, stats.passed, stats.falsified, pass_rate
152 )
153 .ok();
154 }
155 writeln!(out).ok();
156
157 let verdict =
159 if self.summary.falsification_rate >= 0.05 && self.summary.falsification_rate <= 0.15 {
160 "Healthy falsification rate - specification is well-tested"
161 } else if self.summary.falsification_rate < 0.05 {
162 "Low falsification rate - consider more edge cases"
163 } else {
164 "High falsification rate - specification needs hardening"
165 };
166
167 writeln!(
168 out,
169 "**Verdict**: {:.1}% falsification rate - {}",
170 self.summary.falsification_rate * 100.0,
171 verdict
172 )
173 .ok();
174 writeln!(out).ok();
175
176 if self.summary.falsified > 0 {
178 writeln!(out, "## Falsifications (Failures = Success!)").ok();
179 writeln!(out).ok();
180
181 for result in &self.results {
182 if result.outcome == TestOutcome::Falsified {
183 writeln!(out, "### {}: {}", result.id, result.name).ok();
184 writeln!(out, "**Status**: FALSIFIED").ok();
185 writeln!(out, "**Points**: {}", result.points).ok();
186 if let Some(err) = &result.error {
187 writeln!(out, "**Details**: {}", err).ok();
188 }
189 for evidence in &result.evidence {
190 writeln!(out, "- {}", evidence).ok();
191 }
192 writeln!(out).ok();
193 }
194 }
195 }
196
197 out
198 }
199
200 pub fn format_json(&self) -> String {
202 serde_json::to_string_pretty(self).unwrap_or_else(|_| "{}".to_string())
203 }
204
205 pub fn format_text(&self) -> String {
207 let mut out = String::new();
208
209 writeln!(out, "FALSIFICATION REPORT: {}", self.spec_name).ok();
210 writeln!(out, "{}", "=".repeat(60)).ok();
211 writeln!(out).ok();
212
213 writeln!(out, "Total Points: {}", self.summary.total_points).ok();
214 writeln!(
215 out,
216 "Falsifications: {} ({:.1}%)",
217 self.summary.falsified,
218 self.summary.falsification_rate * 100.0
219 )
220 .ok();
221 writeln!(out).ok();
222
223 for result in &self.results {
224 let status = match result.outcome {
225 TestOutcome::Passed => "PASS",
226 TestOutcome::Falsified => "FAIL",
227 TestOutcome::Skipped => "SKIP",
228 TestOutcome::Error => "ERR",
229 };
230 writeln!(out, "[{}] {}: {} ({} pts)", status, result.id, result.name, result.points)
231 .ok();
232 }
233
234 out
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn test_report_creation() {
244 let report = FalsificationReport::new("test-spec".to_string());
245 assert_eq!(report.spec_name, "test-spec");
246 }
247
248 #[test]
249 fn test_report_finalize() {
250 let mut report = FalsificationReport::new("test".to_string());
251
252 report.add_result(TestResult {
253 id: "BC-001".to_string(),
254 name: "Test 1".to_string(),
255 category: "boundary".to_string(),
256 points: 5,
257 outcome: TestOutcome::Passed,
258 error: None,
259 evidence: vec![],
260 });
261
262 report.add_result(TestResult {
263 id: "BC-002".to_string(),
264 name: "Test 2".to_string(),
265 category: "boundary".to_string(),
266 points: 5,
267 outcome: TestOutcome::Falsified,
268 error: Some("Found edge case".to_string()),
269 evidence: vec!["Input: empty".to_string()],
270 });
271
272 report.finalize();
273
274 assert_eq!(report.summary.total_tests, 2);
275 assert_eq!(report.summary.passed, 1);
276 assert_eq!(report.summary.falsified, 1);
277 assert!((report.summary.falsification_rate - 0.5).abs() < 0.01);
278 }
279
280 #[test]
281 fn test_format_markdown() {
282 let mut report = FalsificationReport::new("test".to_string());
283 report.add_result(TestResult {
284 id: "BC-001".to_string(),
285 name: "Empty input".to_string(),
286 category: "boundary".to_string(),
287 points: 5,
288 outcome: TestOutcome::Passed,
289 error: None,
290 evidence: vec![],
291 });
292 report.finalize();
293
294 let md = report.format_markdown();
295 assert!(md.contains("Falsification Report"));
296 assert!(md.contains("boundary"));
297 }
298
299 #[test]
300 fn test_format_json() {
301 let mut report = FalsificationReport::new("json-test".to_string());
302 report.add_result(TestResult {
303 id: "BC-001".to_string(),
304 name: "Test".to_string(),
305 category: "boundary".to_string(),
306 points: 5,
307 outcome: TestOutcome::Passed,
308 error: None,
309 evidence: vec![],
310 });
311 report.finalize();
312
313 let json = report.format_json();
314 assert!(json.contains("json-test"));
315 assert!(json.contains("BC-001"));
316 assert!(json.contains("boundary"));
317 }
318
319 #[test]
320 fn test_format_text() {
321 let mut report = FalsificationReport::new("text-test".to_string());
322 report.add_result(TestResult {
323 id: "BC-001".to_string(),
324 name: "Test".to_string(),
325 category: "boundary".to_string(),
326 points: 5,
327 outcome: TestOutcome::Passed,
328 error: None,
329 evidence: vec![],
330 });
331 report.add_result(TestResult {
332 id: "BC-002".to_string(),
333 name: "Test 2".to_string(),
334 category: "boundary".to_string(),
335 points: 5,
336 outcome: TestOutcome::Falsified,
337 error: None,
338 evidence: vec![],
339 });
340 report.finalize();
341
342 let text = report.format_text();
343 assert!(text.contains("FALSIFICATION REPORT"));
344 assert!(text.contains("text-test"));
345 assert!(text.contains("[PASS]"));
346 assert!(text.contains("[FAIL]"));
347 }
348
349 #[test]
350 fn test_test_outcome_equality() {
351 assert_eq!(TestOutcome::Passed, TestOutcome::Passed);
352 assert_eq!(TestOutcome::Falsified, TestOutcome::Falsified);
353 assert_eq!(TestOutcome::Skipped, TestOutcome::Skipped);
354 assert_eq!(TestOutcome::Error, TestOutcome::Error);
355 assert_ne!(TestOutcome::Passed, TestOutcome::Falsified);
356 }
357
358 #[test]
359 fn test_falsification_summary_default() {
360 let summary = FalsificationSummary::default();
361 assert_eq!(summary.total_tests, 0);
362 assert_eq!(summary.total_points, 0);
363 assert_eq!(summary.passed, 0);
364 assert_eq!(summary.falsified, 0);
365 assert_eq!(summary.skipped, 0);
366 assert_eq!(summary.errors, 0);
367 assert!((summary.falsification_rate - 0.0).abs() < f64::EPSILON);
368 }
369
370 #[test]
371 fn test_category_stats_default() {
372 let stats = CategoryStats::default();
373 assert_eq!(stats.total, 0);
374 assert_eq!(stats.passed, 0);
375 assert_eq!(stats.falsified, 0);
376 }
377
378 #[test]
379 fn test_report_with_all_outcomes() {
380 let mut report = FalsificationReport::new("all-outcomes".to_string());
381
382 report.add_result(TestResult {
383 id: "T-001".to_string(),
384 name: "Passed".to_string(),
385 category: "test".to_string(),
386 points: 1,
387 outcome: TestOutcome::Passed,
388 error: None,
389 evidence: vec![],
390 });
391 report.add_result(TestResult {
392 id: "T-002".to_string(),
393 name: "Falsified".to_string(),
394 category: "test".to_string(),
395 points: 2,
396 outcome: TestOutcome::Falsified,
397 error: None,
398 evidence: vec![],
399 });
400 report.add_result(TestResult {
401 id: "T-003".to_string(),
402 name: "Skipped".to_string(),
403 category: "test".to_string(),
404 points: 3,
405 outcome: TestOutcome::Skipped,
406 error: None,
407 evidence: vec![],
408 });
409 report.add_result(TestResult {
410 id: "T-004".to_string(),
411 name: "Error".to_string(),
412 category: "test".to_string(),
413 points: 4,
414 outcome: TestOutcome::Error,
415 error: Some("Infra issue".to_string()),
416 evidence: vec![],
417 });
418
419 report.finalize();
420
421 assert_eq!(report.summary.total_tests, 4);
422 assert_eq!(report.summary.passed, 1);
423 assert_eq!(report.summary.falsified, 1);
424 assert_eq!(report.summary.skipped, 1);
425 assert_eq!(report.summary.errors, 1);
426 }
427
428 #[test]
429 fn test_report_format_text_status_codes() {
430 let mut report = FalsificationReport::new("status".to_string());
431 report.add_result(TestResult {
432 id: "T-001".to_string(),
433 name: "Skip".to_string(),
434 category: "test".to_string(),
435 points: 1,
436 outcome: TestOutcome::Skipped,
437 error: None,
438 evidence: vec![],
439 });
440 report.add_result(TestResult {
441 id: "T-002".to_string(),
442 name: "Err".to_string(),
443 category: "test".to_string(),
444 points: 1,
445 outcome: TestOutcome::Error,
446 error: None,
447 evidence: vec![],
448 });
449 report.finalize();
450
451 let text = report.format_text();
452 assert!(text.contains("[SKIP]"));
453 assert!(text.contains("[ERR]"));
454 }
455
456 #[test]
457 fn test_markdown_with_falsifications() {
458 let mut report = FalsificationReport::new("falsify-test".to_string());
459 report.add_result(TestResult {
460 id: "BC-001".to_string(),
461 name: "Edge case".to_string(),
462 category: "boundary".to_string(),
463 points: 5,
464 outcome: TestOutcome::Falsified,
465 error: Some("Assertion failed".to_string()),
466 evidence: vec!["Input: empty".to_string(), "Expected: error".to_string()],
467 });
468 report.finalize();
469
470 let md = report.format_markdown();
471 assert!(md.contains("Falsifications (Failures = Success!)"));
472 assert!(md.contains("BC-001"));
473 assert!(md.contains("FALSIFIED"));
474 assert!(md.contains("Assertion failed"));
475 assert!(md.contains("Input: empty"));
476 }
477
478 #[test]
479 fn test_markdown_healthy_rate() {
480 let mut report = FalsificationReport::new("healthy".to_string());
481 for i in 0..19 {
482 report.add_result(TestResult {
483 id: format!("T-{:03}", i),
484 name: format!("Test {}", i),
485 category: "test".to_string(),
486 points: 1,
487 outcome: TestOutcome::Passed,
488 error: None,
489 evidence: vec![],
490 });
491 }
492 report.add_result(TestResult {
493 id: "T-019".to_string(),
494 name: "Falsified".to_string(),
495 category: "test".to_string(),
496 points: 1,
497 outcome: TestOutcome::Falsified,
498 error: None,
499 evidence: vec![],
500 });
501 report.finalize();
502
503 let md = report.format_markdown();
505 assert!(md.contains("well-tested"));
506 }
507
508 #[test]
509 fn test_markdown_low_falsification_rate() {
510 let mut report = FalsificationReport::new("low".to_string());
511 for i in 0..100 {
512 report.add_result(TestResult {
513 id: format!("T-{:03}", i),
514 name: format!("Test {}", i),
515 category: "test".to_string(),
516 points: 1,
517 outcome: TestOutcome::Passed,
518 error: None,
519 evidence: vec![],
520 });
521 }
522 report.finalize();
523
524 let md = report.format_markdown();
525 assert!(md.contains("more edge cases"));
526 }
527}