fn create_defect_report_from_predictions(
predictions: Vec<(String, crate::services::defect_probability::DefectScore)>,
) -> Result<DefectPredictionReport> {
use crate::services::defect_probability::RiskLevel;
let mut high_risk_files = 0;
let mut medium_risk_files = 0;
let mut low_risk_files = 0;
let file_predictions: Vec<FilePrediction> = predictions
.iter()
.map(|(file_path, score)| {
match score.risk_level {
RiskLevel::High => high_risk_files += 1,
RiskLevel::Medium => medium_risk_files += 1,
RiskLevel::Low => low_risk_files += 1,
}
let factors: Vec<String> = score
.contributing_factors
.iter()
.map(|(factor, contribution)| format!("{}: {:.1}%", factor, contribution * 100.0))
.collect();
FilePrediction {
file_path: file_path.clone(),
risk_score: score.probability,
risk_level: format!("{:?}", score.risk_level),
factors,
}
})
.collect();
Ok(DefectPredictionReport {
total_files: predictions.len(),
high_risk_files,
medium_risk_files,
low_risk_files,
file_predictions,
})
}
#[derive(Debug, Serialize)]
pub struct DefectPredictionReport {
pub total_files: usize,
pub high_risk_files: usize,
pub medium_risk_files: usize,
pub low_risk_files: usize,
pub file_predictions: Vec<FilePrediction>,
}
#[derive(Debug, Serialize)]
pub struct FilePrediction {
pub file_path: String,
pub risk_score: f32,
pub risk_level: String,
pub factors: Vec<String>,
}
pub fn format_defect_summary(report: &DefectPredictionReport, top_files: usize) -> Result<String> {
use std::fmt::Write;
let mut output = String::new();
writeln!(&mut output, "# Defect Prediction Analysis\n")?;
format_defect_summary_stats(&mut output, report)?;
if !report.file_predictions.is_empty() {
format_defect_top_files(&mut output, report, top_files)?;
}
Ok(output)
}
fn format_defect_summary_stats(output: &mut String, report: &DefectPredictionReport) -> Result<()> {
use std::fmt::Write;
writeln!(output, "## Summary")?;
writeln!(output, "- Total files analyzed: {}", report.total_files)?;
writeln!(output, "- High risk files: {}", report.high_risk_files)?;
writeln!(output, "- Medium risk files: {}", report.medium_risk_files)?;
writeln!(output, "- Low risk files: {}\n", report.low_risk_files)?;
Ok(())
}
fn format_defect_top_files(
output: &mut String,
report: &DefectPredictionReport,
top_files: usize,
) -> Result<()> {
use std::fmt::Write;
writeln!(output, "## Top Files by Defect Risk\n")?;
let files_to_show = if top_files == 0 { 10 } else { top_files };
for (i, prediction) in report
.file_predictions
.iter()
.take(files_to_show)
.enumerate()
{
format_defect_prediction_entry(output, i + 1, prediction)?;
}
Ok(())
}
fn format_defect_prediction_entry(
output: &mut String,
index: usize,
prediction: &FilePrediction,
) -> Result<()> {
use std::fmt::Write;
let filename = extract_filename_from_prediction(prediction);
writeln!(
output,
"{}. `{}` - {:.1}% risk ({})",
index,
filename,
prediction.risk_score * 100.0,
prediction.risk_level
)?;
Ok(())
}
fn extract_filename_from_prediction(prediction: &FilePrediction) -> &str {
std::path::Path::new(&prediction.file_path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(&prediction.file_path)
}
fn format_defect_full(report: &DefectPredictionReport, top_files: usize) -> Result<String> {
crate::cli::defect_formatter::format_defect_report(report, "full", top_files)
}
fn format_defect_sarif(report: &DefectPredictionReport) -> Result<String> {
crate::cli::defect_formatter::format_defect_report(report, "sarif", 0)
}
fn format_defect_csv(report: &DefectPredictionReport) -> Result<String> {
crate::cli::defect_formatter::format_defect_report(report, "csv", 0)
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod defect_report_tests {
use super::*;
fn create_test_prediction(file: &str, score: f32, level: &str) -> FilePrediction {
FilePrediction {
file_path: file.to_string(),
risk_score: score,
risk_level: level.to_string(),
factors: vec!["test factor".to_string()],
}
}
fn create_test_report() -> DefectPredictionReport {
DefectPredictionReport {
total_files: 3,
high_risk_files: 1,
medium_risk_files: 1,
low_risk_files: 1,
file_predictions: vec![
create_test_prediction("src/high.rs", 0.9, "High"),
create_test_prediction("src/medium.rs", 0.5, "Medium"),
create_test_prediction("src/low.rs", 0.2, "Low"),
],
}
}
#[test]
fn test_extract_filename_from_prediction() {
let pred = create_test_prediction("src/services/context.rs", 0.5, "Medium");
assert_eq!(extract_filename_from_prediction(&pred), "context.rs");
}
#[test]
fn test_extract_filename_simple_path() {
let pred = create_test_prediction("main.rs", 0.5, "Medium");
assert_eq!(extract_filename_from_prediction(&pred), "main.rs");
}
#[test]
fn test_format_defect_prediction_entry() {
let pred = create_test_prediction("src/test.rs", 0.85, "High");
let mut output = String::new();
format_defect_prediction_entry(&mut output, 1, &pred).unwrap();
assert!(output.contains("1. `test.rs`"));
assert!(output.contains("85.0% risk"));
assert!(output.contains("High"));
}
#[test]
fn test_format_defect_summary_stats() {
let report = create_test_report();
let mut output = String::new();
format_defect_summary_stats(&mut output, &report).unwrap();
assert!(output.contains("## Summary"));
assert!(output.contains("Total files analyzed: 3"));
assert!(output.contains("High risk files: 1"));
assert!(output.contains("Medium risk files: 1"));
assert!(output.contains("Low risk files: 1"));
}
#[test]
fn test_format_defect_top_files() {
let report = create_test_report();
let mut output = String::new();
format_defect_top_files(&mut output, &report, 2).unwrap();
assert!(output.contains("## Top Files by Defect Risk"));
assert!(output.contains("high.rs"));
assert!(output.contains("medium.rs"));
}
#[test]
fn test_format_defect_top_files_default_count() {
let report = create_test_report();
let mut output = String::new();
format_defect_top_files(&mut output, &report, 0).unwrap();
assert!(output.contains("high.rs"));
assert!(output.contains("medium.rs"));
assert!(output.contains("low.rs"));
}
#[test]
fn test_format_defect_summary() {
let report = create_test_report();
let output = format_defect_summary(&report, 2).unwrap();
assert!(output.contains("# Defect Prediction Analysis"));
assert!(output.contains("## Summary"));
assert!(output.contains("## Top Files by Defect Risk"));
}
#[test]
fn test_format_defect_summary_empty_predictions() {
let report = DefectPredictionReport {
total_files: 0,
high_risk_files: 0,
medium_risk_files: 0,
low_risk_files: 0,
file_predictions: vec![],
};
let output = format_defect_summary(&report, 5).unwrap();
assert!(output.contains("# Defect Prediction Analysis"));
assert!(output.contains("Total files analyzed: 0"));
assert!(!output.contains("## Top Files by Defect Risk"));
}
#[test]
fn test_defect_prediction_report_struct() {
let report = create_test_report();
assert_eq!(report.total_files, 3);
assert_eq!(report.high_risk_files, 1);
assert_eq!(report.file_predictions.len(), 3);
}
#[test]
fn test_file_prediction_struct() {
let pred = create_test_prediction("path/to/file.rs", 0.75, "High");
assert_eq!(pred.file_path, "path/to/file.rs");
assert!((pred.risk_score - 0.75).abs() < 0.001);
assert_eq!(pred.risk_level, "High");
assert_eq!(pred.factors.len(), 1);
}
}