#![cfg_attr(coverage_nightly, coverage(off))]
use crate::models::defect_report::{
Defect, DefectCategory, DefectReport, DefectSummary, FileHotspot, ReportMetadata, Severity,
};
use crate::services::defect_report_service::{DefectReportService, ReportFormat};
use chrono::Utc;
use std::collections::{BTreeMap, HashMap};
use std::path::PathBuf;
fn make_defect(id: &str, sev: Severity, cat: DefectCategory, file: &str) -> Defect {
Defect {
id: id.to_string(),
severity: sev,
category: cat,
file_path: PathBuf::from(file),
line_start: 10,
line_end: Some(20),
column_start: Some(1),
column_end: Some(40),
message: format!("{id} message"),
rule_id: format!("{id}-rule"),
fix_suggestion: Some(format!("Fix {id}")),
metrics: {
let mut m = HashMap::new();
m.insert("cyclomatic".to_string(), 15.0);
m.insert("cognitive".to_string(), 8.0);
m
},
}
}
fn make_report(defects: Vec<Defect>) -> DefectReport {
let mut by_severity = BTreeMap::new();
let mut by_category = BTreeMap::new();
for d in &defects {
*by_severity
.entry(format!("{:?}", d.severity).to_lowercase())
.or_insert(0) += 1;
*by_category.entry(format!("{:?}", d.category)).or_insert(0) += 1;
}
let mut file_index = BTreeMap::new();
for d in &defects {
file_index
.entry(d.file_path.clone())
.or_insert_with(Vec::new)
.push(d.id.clone());
}
DefectReport {
metadata: ReportMetadata {
tool: "pmat".to_string(),
version: "2.200.0".to_string(),
generated_at: Utc::now(),
project_root: PathBuf::from("/test/project"),
total_files_analyzed: 50,
analysis_duration_ms: 3000,
},
summary: DefectSummary {
total_defects: defects.len(),
by_severity,
by_category,
hotspot_files: vec![FileHotspot {
path: PathBuf::from("src/hot.rs"),
defect_count: 5,
severity_score: 25.0,
}],
},
defects,
file_index,
}
}
fn sample_defects() -> Vec<Defect> {
vec![
make_defect(
"CPLX-001",
Severity::High,
DefectCategory::Complexity,
"src/a.rs",
),
make_defect(
"CPLX-002",
Severity::Medium,
DefectCategory::Complexity,
"src/b.rs",
),
make_defect(
"SATD-001",
Severity::Low,
DefectCategory::TechnicalDebt,
"src/a.rs",
),
make_defect(
"DEAD-001",
Severity::Critical,
DefectCategory::DeadCode,
"tests/t.rs",
),
make_defect(
"PERF-001",
Severity::High,
DefectCategory::Performance,
"src/c.rs",
),
]
}
#[test]
fn test_defect_report_service_new() {
let svc = DefectReportService::new();
let _ = svc.generate_filename(ReportFormat::Json);
}
#[test]
fn test_defect_report_service_default() {
let svc = DefectReportService::default();
let _ = svc.generate_filename(ReportFormat::Csv);
}
#[test]
fn test_compute_summary_empty() {
let svc = DefectReportService::new();
let summary = svc.compute_summary(&[]);
assert_eq!(summary.total_defects, 0);
assert!(summary.by_severity.is_empty());
assert!(summary.by_category.is_empty());
assert!(summary.hotspot_files.is_empty());
}
#[test]
fn test_compute_summary_single_defect() {
let svc = DefectReportService::new();
let defects = vec![make_defect(
"X-001",
Severity::High,
DefectCategory::Complexity,
"src/x.rs",
)];
let summary = svc.compute_summary(&defects);
assert_eq!(summary.total_defects, 1);
assert_eq!(summary.hotspot_files.len(), 1);
assert_eq!(summary.hotspot_files[0].defect_count, 1);
}
#[test]
fn test_compute_summary_multiple_defects() {
let svc = DefectReportService::new();
let defects = sample_defects();
let summary = svc.compute_summary(&defects);
assert_eq!(summary.total_defects, 5);
assert!(summary.by_severity.values().sum::<usize>() == 5);
assert!(summary.by_category.values().sum::<usize>() == 5);
assert!(!summary.hotspot_files.is_empty());
}
#[test]
fn test_compute_summary_hotspot_sorting() {
let svc = DefectReportService::new();
let defects = vec![
make_defect(
"A-001",
Severity::Critical,
DefectCategory::Complexity,
"src/hot.rs",
),
make_defect(
"A-002",
Severity::Critical,
DefectCategory::Performance,
"src/hot.rs",
),
make_defect(
"B-001",
Severity::Low,
DefectCategory::DeadCode,
"src/cold.rs",
),
];
let summary = svc.compute_summary(&defects);
assert_eq!(summary.hotspot_files[0].path, PathBuf::from("src/hot.rs"));
assert!(summary.hotspot_files[0].severity_score > summary.hotspot_files[1].severity_score);
}
#[test]
fn test_compute_summary_hotspot_truncation() {
let svc = DefectReportService::new();
let defects: Vec<Defect> = (0..15)
.map(|i| {
make_defect(
&format!("D-{:03}", i),
Severity::Medium,
DefectCategory::Complexity,
&format!("src/file_{}.rs", i),
)
})
.collect();
let summary = svc.compute_summary(&defects);
assert!(summary.hotspot_files.len() <= 10);
}
#[test]
fn test_format_json_valid() {
let svc = DefectReportService::new();
let report = make_report(sample_defects());
let json = svc.format_json(&report).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed.is_object());
assert!(parsed["defects"].is_array());
assert_eq!(parsed["defects"].as_array().unwrap().len(), 5);
}
#[test]
fn test_format_json_empty_report() {
let svc = DefectReportService::new();
let report = make_report(vec![]);
let json = svc.format_json(&report).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["defects"].as_array().unwrap().len(), 0);
}
#[cfg(feature = "reporting")]
#[test]
fn test_format_csv_headers() {
let svc = DefectReportService::new();
let report = make_report(sample_defects());
let csv = svc.format_csv(&report).unwrap();
let first_line = csv.lines().next().unwrap();
assert!(first_line.contains("id"));
assert!(first_line.contains("severity"));
assert!(first_line.contains("category"));
assert!(first_line.contains("file_path"));
assert!(first_line.contains("message"));
}
#[cfg(feature = "reporting")]
#[test]
fn test_format_csv_row_count() {
let svc = DefectReportService::new();
let report = make_report(sample_defects());
let csv = svc.format_csv(&report).unwrap();
let line_count = csv.lines().count();
assert_eq!(line_count, 6);
}
#[cfg(feature = "reporting")]
#[test]
fn test_format_csv_empty_report() {
let svc = DefectReportService::new();
let report = make_report(vec![]);
let csv = svc.format_csv(&report).unwrap();
assert_eq!(csv.lines().count(), 1);
}
#[cfg(feature = "reporting")]
#[test]
fn test_format_csv_metrics_included() {
let svc = DefectReportService::new();
let report = make_report(sample_defects());
let csv = svc.format_csv(&report).unwrap();
let second_line = csv.lines().nth(1).unwrap();
assert!(second_line.contains("15")); }
#[cfg(not(feature = "reporting"))]
#[test]
fn test_format_csv_requires_feature() {
let svc = DefectReportService::new();
let report = make_report(sample_defects());
assert!(svc.format_csv(&report).is_err());
}
#[test]
fn test_format_markdown_structure() {
let svc = DefectReportService::new();
let report = make_report(sample_defects());
let md = svc.format_markdown(&report).unwrap();
assert!(md.contains("# Code Quality Report"));
assert!(md.contains("## Executive Summary"));
assert!(md.contains("### Severity Distribution"));
assert!(md.contains("## Detailed Findings"));
}
#[test]
fn test_format_markdown_hotspots() {
let svc = DefectReportService::new();
let report = make_report(sample_defects());
let md = svc.format_markdown(&report).unwrap();
assert!(md.contains("### Top 10 Hotspot Files"));
assert!(md.contains("| Rank | File | Defects | Severity Score |"));
}
#[test]
fn test_format_markdown_severity_bars() {
let svc = DefectReportService::new();
let report = make_report(sample_defects());
let md = svc.format_markdown(&report).unwrap();
assert!(md.contains("â–ˆ") || md.contains("â–‘"));
}
#[test]
fn test_format_markdown_fix_suggestions() {
let svc = DefectReportService::new();
let report = make_report(sample_defects());
let md = svc.format_markdown(&report).unwrap();
assert!(md.contains("Suggestion"));
}
#[test]
fn test_format_markdown_empty_report() {
let svc = DefectReportService::new();
let report = make_report(vec![]);
let md = svc.format_markdown(&report).unwrap();
assert!(md.contains("# Code Quality Report"));
assert!(md.contains("**Total Defects**: 0"));
}
#[test]
fn test_format_text_structure() {
let svc = DefectReportService::new();
let report = make_report(sample_defects());
let txt = svc.format_text(&report).unwrap();
assert!(txt.contains("CODE QUALITY REPORT"));
assert!(txt.contains("==================="));
assert!(txt.contains("SEVERITY BREAKDOWN"));
assert!(txt.contains("CATEGORY BREAKDOWN"));
assert!(txt.contains("DEFECTS"));
}
#[test]
fn test_format_text_hotspots() {
let svc = DefectReportService::new();
let report = make_report(sample_defects());
let txt = svc.format_text(&report).unwrap();
assert!(txt.contains("TOP HOTSPOT FILES"));
}
#[test]
fn test_format_text_defect_details() {
let svc = DefectReportService::new();
let report = make_report(sample_defects());
let txt = svc.format_text(&report).unwrap();
assert!(txt.contains("CPLX-001 message"));
assert!(txt.contains("Fix CPLX-001"));
}
#[test]
fn test_format_text_line_range() {
let svc = DefectReportService::new();
let report = make_report(sample_defects());
let txt = svc.format_text(&report).unwrap();
assert!(txt.contains("10-20") || txt.contains(":10"));
}
#[test]
fn test_format_text_empty_report() {
let svc = DefectReportService::new();
let report = make_report(vec![]);
let txt = svc.format_text(&report).unwrap();
assert!(txt.contains("Total Defects: 0"));
}
#[test]
fn test_generate_filename_json() {
let svc = DefectReportService::new();
let name = svc.generate_filename(ReportFormat::Json);
assert!(name.starts_with("defect-report-"));
assert!(name.ends_with(".json"));
}
#[test]
fn test_generate_filename_csv() {
let svc = DefectReportService::new();
let name = svc.generate_filename(ReportFormat::Csv);
assert!(name.ends_with(".csv"));
}
#[test]
fn test_generate_filename_markdown() {
let svc = DefectReportService::new();
let name = svc.generate_filename(ReportFormat::Markdown);
assert!(name.ends_with(".md"));
}
#[test]
fn test_generate_filename_text() {
let svc = DefectReportService::new();
let name = svc.generate_filename(ReportFormat::Text);
assert!(name.ends_with(".txt"));
}
#[test]
fn test_filter_by_pattern_no_filters() {
let report = make_report(sample_defects());
let filtered = DefectReportService::filter_by_pattern(&report, None, None, 0);
assert_eq!(filtered.defects.len(), 5);
}
#[test]
fn test_filter_by_pattern_include() {
let report = make_report(sample_defects());
let filtered =
DefectReportService::filter_by_pattern(&report, Some("src/**".to_string()), None, 0);
assert_eq!(filtered.defects.len(), 4);
assert!(filtered
.defects
.iter()
.all(|d| d.file_path.starts_with("src")));
}
#[test]
fn test_filter_by_pattern_exclude() {
let report = make_report(sample_defects());
let filtered =
DefectReportService::filter_by_pattern(&report, None, Some("tests/**".to_string()), 0);
assert_eq!(filtered.defects.len(), 4);
}
#[test]
fn test_filter_by_pattern_include_and_exclude() {
let report = make_report(sample_defects());
let filtered = DefectReportService::filter_by_pattern(
&report,
Some("src/**".to_string()),
Some("src/c.rs".to_string()),
0,
);
assert_eq!(filtered.defects.len(), 3);
}
#[test]
fn test_filter_by_pattern_recomputes_summary() {
let report = make_report(sample_defects());
let filtered =
DefectReportService::filter_by_pattern(&report, Some("src/a.rs".to_string()), None, 0);
assert_eq!(filtered.summary.total_defects, 2); assert_eq!(filtered.file_index.len(), 1);
}
#[test]
fn test_filter_by_pattern_no_matches() {
let report = make_report(sample_defects());
let filtered = DefectReportService::filter_by_pattern(
&report,
Some("nonexistent/**".to_string()),
None,
0,
);
assert_eq!(filtered.defects.len(), 0);
assert_eq!(filtered.summary.total_defects, 0);
}
#[test]
fn test_report_format_all_filename_extensions() {
let svc = DefectReportService::new();
let json_name = svc.generate_filename(ReportFormat::Json);
let csv_name = svc.generate_filename(ReportFormat::Csv);
let md_name = svc.generate_filename(ReportFormat::Markdown);
let txt_name = svc.generate_filename(ReportFormat::Text);
assert!(json_name.ends_with(".json"));
assert!(csv_name.ends_with(".csv"));
assert!(md_name.ends_with(".md"));
assert!(txt_name.ends_with(".txt"));
}