#![cfg_attr(coverage_nightly, coverage(off))]
use crate::cli::{
ComprehensiveOutputFormat, DagType, DeadCodeOutputFormat, DefectPredictionOutputFormat,
IncrementalCoverageOutputFormat, MakefileOutputFormat, ProofAnnotationOutputFormat,
PropertyTypeFilter, ProvabilityOutputFormat, QualityCheckType, QualityGateOutputFormat,
SatdOutputFormat, SatdSeverity, TdgOutputFormat, VerificationMethodFilter,
};
use crate::services::lightweight_provability_analyzer::ProofSummary;
use crate::services::makefile_linter;
use anyhow::Result;
use serde::Serialize;
use serde_json::json;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
include!("tdg.rs");
include!("makefile.rs");
include!("provability.rs");
include!("proof_coverage.rs");
include!("churn.rs");
include!("quality_gate_satd.rs");
include!("quality_gate_entry.rs");
include!("quality_gate_single_file.rs");
include!("quality_gate_project.rs");
include!("quality_gate_execute.rs");
include!("quality_gate_config.rs");
include!("quality_gate_part2a.rs");
include!("quality_gate_part2b.rs");
include!("quality_gate_part2c.rs");
include!("quality_gate_part2d.rs");
include!("quality_gate_part2e.rs");
include!("quality_gate_part2f.rs");
include!("comprehensive.rs");
include!("quality_checks.rs");
include!("string_utils.rs");
include!("satd_formatting.rs");
include!("incremental_coverage.rs");
include!("defect_report.rs");
#[cfg(all(test, feature = "broken-tests"))]
mod tests;
#[cfg(test)]
mod churn_tests {
use super::*;
use crate::models::churn::{
ChurnOutputFormat, ChurnSummary, CodeChurnAnalysis, FileChurnMetrics,
};
use chrono::Utc;
use std::collections::HashMap;
fn empty_summary() -> ChurnSummary {
ChurnSummary {
total_commits: 0,
total_files_changed: 0,
hotspot_files: Vec::new(),
stable_files: Vec::new(),
author_contributions: HashMap::new(),
mean_churn_score: 0.0,
variance_churn_score: 0.0,
stddev_churn_score: 0.0,
}
}
fn empty_analysis() -> CodeChurnAnalysis {
CodeChurnAnalysis {
generated_at: Utc::now(),
period_days: 30,
repository_root: PathBuf::from("/tmp/repo"),
files: Vec::new(),
summary: empty_summary(),
}
}
fn metric(rel_path: &str, commits: usize, score: f32) -> FileChurnMetrics {
FileChurnMetrics {
path: PathBuf::from(rel_path),
relative_path: rel_path.to_string(),
commit_count: commits,
unique_authors: vec!["alice".to_string()],
additions: 10,
deletions: 5,
churn_score: score,
last_modified: Utc::now(),
first_seen: Utc::now(),
}
}
#[test]
fn test_format_churn_as_json_round_trip() {
let mut a = empty_analysis();
a.files.push(metric("src/a.rs", 5, 0.7));
a.summary.total_commits = 5;
let s = format_churn_as_json(&a).unwrap();
let back: CodeChurnAnalysis = serde_json::from_str(&s).unwrap();
assert_eq!(back.period_days, 30);
assert_eq!(back.files.len(), 1);
assert_eq!(back.files[0].relative_path, "src/a.rs");
}
#[test]
fn test_format_churn_as_csv_emits_header_and_rows() {
let mut a = empty_analysis();
a.files.push(metric("src/a.rs", 5, 0.7));
a.files.push(metric("src/b.rs", 3, 0.3));
let csv = format_churn_as_csv(&a).unwrap();
assert!(csv.starts_with("file_path,relative_path,commit_count"));
assert!(csv.contains("src/a.rs,src/a.rs,5,1,10,5,0.700"));
assert!(csv.contains("src/b.rs,src/b.rs,3,1,10,5,0.300"));
}
#[test]
fn test_format_churn_as_csv_empty_only_header() {
let a = empty_analysis();
let csv = format_churn_as_csv(&a).unwrap();
assert_eq!(csv.lines().count(), 1);
}
#[test]
fn test_format_churn_as_summary_empty_only_header() {
let a = empty_analysis();
let s = format_churn_as_summary(&a).unwrap();
assert!(s.contains("Code Churn Analysis Summary"));
assert!(s.contains("Period:"));
assert!(s.contains("Total commits:"));
assert!(!s.contains("Top Files by Churn"));
assert!(!s.contains("Hotspot Files"));
assert!(!s.contains("Stable Files"));
assert!(!s.contains("Top Contributors"));
}
#[test]
fn test_format_churn_as_summary_with_files_includes_top_files_section() {
let mut a = empty_analysis();
a.files.push(metric("src/a.rs", 10, 0.6));
a.files.push(metric("src/b.rs", 8, 0.4));
a.files.push(metric("src/c.rs", 5, 0.1));
let s = format_churn_as_summary(&a).unwrap();
assert!(s.contains("Top Files by Churn"));
assert!(s.contains("a.rs"));
assert!(s.contains("b.rs"));
assert!(s.contains("c.rs"));
}
#[test]
fn test_format_churn_as_summary_top_files_sorts_by_commit_count_desc() {
let mut a = empty_analysis();
a.files.push(metric("src/low.rs", 1, 0.1));
a.files.push(metric("src/high.rs", 99, 0.9));
let s = format_churn_as_summary(&a).unwrap();
let high_pos = s.find("high.rs").unwrap();
let low_pos = s.find("low.rs").unwrap();
assert!(
high_pos < low_pos,
"high commit count should appear before low"
);
}
#[test]
fn test_format_churn_as_summary_emits_hotspot_section_when_present() {
let mut a = empty_analysis();
a.summary.hotspot_files = vec![PathBuf::from("src/hot.rs")];
let s = format_churn_as_summary(&a).unwrap();
assert!(s.contains("Hotspot Files (High Churn)"));
assert!(s.contains("src/hot.rs"));
}
#[test]
fn test_format_churn_as_summary_emits_stable_section_when_present() {
let mut a = empty_analysis();
a.summary.stable_files = vec![PathBuf::from("src/cold.rs")];
let s = format_churn_as_summary(&a).unwrap();
assert!(s.contains("Stable Files (Low Churn)"));
assert!(s.contains("src/cold.rs"));
}
#[test]
fn test_format_churn_as_summary_emits_top_contributors_section() {
let mut a = empty_analysis();
a.summary.author_contributions =
HashMap::from([("alice".to_string(), 10), ("bob".to_string(), 3)]);
let s = format_churn_as_summary(&a).unwrap();
assert!(s.contains("Top Contributors"));
assert!(s.contains("alice"));
let alice = s.find("alice").unwrap();
let bob = s.find("bob").unwrap();
assert!(alice < bob);
}
#[test]
fn test_format_churn_as_markdown_full_pipeline() {
let mut a = empty_analysis();
a.summary.total_commits = 10;
a.summary.total_files_changed = 5;
a.summary.hotspot_files = vec![PathBuf::from("src/hot.rs")];
a.summary.stable_files = vec![PathBuf::from("src/stable.rs")];
a.summary
.author_contributions
.insert("alice".to_string(), 5);
a.files.push(metric("src/x.rs", 3, 0.4));
let md = format_churn_as_markdown(&a).unwrap();
assert!(md.starts_with("# Code Churn Analysis Report"));
assert!(md.contains("## Summary Statistics"));
assert!(md.contains("| Total Commits | 10 |"));
assert!(md.contains("| Files Changed | 5 |"));
assert!(md.contains("| Hotspot Files | 1 |"));
assert!(md.contains("| Stable Files | 1 |"));
assert!(md.contains("| Contributing Authors | 1 |"));
assert!(md.contains("## File Churn Details"));
assert!(md.contains("src/x.rs"));
assert!(md.contains("## Author Contributions"));
assert!(md.contains("| alice |"));
assert!(md.contains("## Recommendations"));
assert!(md.contains("Review Hotspot Files"));
}
#[test]
fn test_format_churn_as_markdown_empty_skips_optional_sections() {
let a = empty_analysis();
let md = format_churn_as_markdown(&a).unwrap();
assert!(md.contains("## Summary Statistics"));
assert!(!md.contains("## File Churn Details"));
assert!(!md.contains("## Author Contributions"));
assert!(md.contains("## Recommendations"));
}
#[test]
fn test_format_churn_content_dispatches_each_format() {
let a = empty_analysis();
let json = format_churn_content(&a, ChurnOutputFormat::Json).unwrap();
let _: CodeChurnAnalysis = serde_json::from_str(&json).unwrap();
let summary = format_churn_content(&a, ChurnOutputFormat::Summary).unwrap();
assert!(summary.contains("Code Churn Analysis Summary"));
let md = format_churn_content(&a, ChurnOutputFormat::Markdown).unwrap();
assert!(md.contains("# Code Churn Analysis Report"));
let csv = format_churn_content(&a, ChurnOutputFormat::Csv).unwrap();
assert!(csv.starts_with("file_path,"));
}
#[test]
fn test_apply_churn_file_filtering_zero_keeps_all() {
let mut a = empty_analysis();
for i in 0..5 {
a.files.push(metric(&format!("f{i}"), i, 0.1));
}
apply_churn_file_filtering(&mut a, 0);
assert_eq!(a.files.len(), 5);
}
#[test]
fn test_apply_churn_file_filtering_top_files_ge_len_no_op() {
let mut a = empty_analysis();
for i in 0..3 {
a.files.push(metric(&format!("f{i}"), i, 0.1));
}
apply_churn_file_filtering(&mut a, 10);
assert_eq!(a.files.len(), 3);
}
#[test]
fn test_apply_churn_file_filtering_truncates_keeps_top_by_commit_count() {
let mut a = empty_analysis();
a.files.push(metric("low.rs", 1, 0.1));
a.files.push(metric("mid.rs", 5, 0.1));
a.files.push(metric("high.rs", 10, 0.1));
apply_churn_file_filtering(&mut a, 2);
assert_eq!(a.files.len(), 2);
let names: Vec<_> = a.files.iter().map(|f| f.relative_path.clone()).collect();
assert!(names.contains(&"high.rs".to_string()));
assert!(names.contains(&"mid.rs".to_string()));
assert!(!names.contains(&"low.rs".to_string()));
}
#[test]
fn test_write_commits_row_format() {
let mut out = String::new();
write_commits_row(&mut out, 42).unwrap();
assert_eq!(out.trim(), "| Total Commits | 42 |");
}
#[test]
fn test_write_files_changed_row_format() {
let mut out = String::new();
write_files_changed_row(&mut out, 7).unwrap();
assert_eq!(out.trim(), "| Files Changed | 7 |");
}
#[test]
fn test_write_hotspot_files_row_format() {
let mut out = String::new();
write_hotspot_files_row(&mut out, 3).unwrap();
assert_eq!(out.trim(), "| Hotspot Files | 3 |");
}
#[test]
fn test_write_stable_files_row_format() {
let mut out = String::new();
write_stable_files_row(&mut out, 2).unwrap();
assert_eq!(out.trim(), "| Stable Files | 2 |");
}
#[test]
fn test_write_authors_row_format() {
let mut out = String::new();
write_authors_row(&mut out, 5).unwrap();
assert_eq!(out.trim(), "| Contributing Authors | 5 |");
}
}