use crate::classifier::DefectCategory;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::Path;
use tokio::fs;
use tracing::{debug, info};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalysisMetadata {
pub organization: String,
pub analysis_date: String,
pub repositories_analyzed: usize,
pub commits_analyzed: usize,
pub analyzer_version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualitySignals {
pub avg_tdg_score: Option<f32>,
pub max_tdg_score: Option<f32>,
pub avg_complexity: Option<f32>,
pub avg_test_coverage: Option<f32>,
pub satd_instances: usize,
pub avg_lines_changed: f32,
pub avg_files_per_commit: f32,
}
impl Default for QualitySignals {
fn default() -> Self {
Self {
avg_tdg_score: None,
max_tdg_score: None,
avg_complexity: None,
avg_test_coverage: None,
satd_instances: 0,
avg_lines_changed: 0.0,
avg_files_per_commit: 0.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DefectInstance {
pub commit_hash: String,
pub message: String,
pub author: String,
pub timestamp: i64,
pub files_affected: usize,
pub lines_added: usize,
pub lines_removed: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DefectPattern {
pub category: DefectCategory,
pub frequency: usize,
pub confidence: f32,
pub quality_signals: QualitySignals,
pub examples: Vec<DefectInstance>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalysisReport {
pub version: String,
pub metadata: AnalysisMetadata,
pub defect_patterns: Vec<DefectPattern>,
}
pub struct ReportGenerator;
impl ReportGenerator {
pub fn new() -> Self {
Self
}
pub fn to_yaml(&self, report: &AnalysisReport) -> Result<String> {
debug!("Serializing report to YAML");
let yaml = serde_yaml::to_string(report)?;
Ok(yaml)
}
pub async fn write_to_file(&self, report: &AnalysisReport, path: &Path) -> Result<()> {
info!("Writing report to file: {}", path.display());
let yaml = self.to_yaml(report)?;
fs::write(path, yaml).await?;
info!("Successfully wrote report to {}", path.display());
Ok(())
}
}
impl Default for ReportGenerator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_report_generator_creation() {
let _generator = ReportGenerator::new();
let _generator_default = ReportGenerator;
}
#[test]
fn test_yaml_serialization() {
let metadata = AnalysisMetadata {
organization: "test-org".to_string(),
analysis_date: "2025-11-15T00:00:00Z".to_string(),
repositories_analyzed: 5,
commits_analyzed: 50,
analyzer_version: "0.1.0".to_string(),
};
let report = AnalysisReport {
version: "1.0".to_string(),
metadata,
defect_patterns: vec![],
};
let generator = ReportGenerator::new();
let yaml = generator.to_yaml(&report).expect("Should serialize");
assert!(yaml.contains("version: '1.0'"));
assert!(yaml.contains("organization: test-org"));
}
#[test]
fn test_yaml_with_defect_patterns() {
let metadata = AnalysisMetadata {
organization: "test-org".to_string(),
analysis_date: "2025-11-15T00:00:00Z".to_string(),
repositories_analyzed: 10,
commits_analyzed: 100,
analyzer_version: "0.1.0".to_string(),
};
let patterns = vec![
DefectPattern {
category: DefectCategory::MemorySafety,
frequency: 42,
confidence: 0.85,
quality_signals: QualitySignals {
avg_lines_changed: 45.2,
avg_files_per_commit: 2.1,
..Default::default()
},
examples: vec![DefectInstance {
commit_hash: "abc123".to_string(),
message: "fix memory leak".to_string(),
author: "test@example.com".to_string(),
timestamp: 1234567890,
files_affected: 2,
lines_added: 30,
lines_removed: 15,
}],
},
DefectPattern {
category: DefectCategory::ConcurrencyBugs,
frequency: 30,
confidence: 0.80,
quality_signals: QualitySignals {
avg_lines_changed: 67.3,
avg_files_per_commit: 3.5,
..Default::default()
},
examples: vec![DefectInstance {
commit_hash: "def456".to_string(),
message: "fix race condition".to_string(),
author: "test@example.com".to_string(),
timestamp: 1234567891,
files_affected: 4,
lines_added: 50,
lines_removed: 17,
}],
},
];
let report = AnalysisReport {
version: "1.0".to_string(),
metadata,
defect_patterns: patterns,
};
let generator = ReportGenerator::new();
let yaml = generator.to_yaml(&report).expect("Should serialize");
assert!(yaml.contains("MemorySafety"));
assert!(yaml.contains("ConcurrencyBugs"));
assert!(yaml.contains("frequency: 42"));
}
#[tokio::test]
async fn test_write_to_file() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let report_path = temp_dir.path().join("test-report.yaml");
let metadata = AnalysisMetadata {
organization: "test-org".to_string(),
analysis_date: "2025-11-15T00:00:00Z".to_string(),
repositories_analyzed: 5,
commits_analyzed: 50,
analyzer_version: "0.1.0".to_string(),
};
let report = AnalysisReport {
version: "1.0".to_string(),
metadata,
defect_patterns: vec![],
};
let generator = ReportGenerator::new();
generator
.write_to_file(&report, &report_path)
.await
.expect("Should write file");
assert!(report_path.exists());
let content = tokio::fs::read_to_string(&report_path).await.unwrap();
assert!(content.contains("test-org"));
}
#[test]
fn test_quality_signals_default() {
let signals = QualitySignals::default();
assert!(signals.avg_tdg_score.is_none());
assert!(signals.avg_complexity.is_none());
assert_eq!(signals.satd_instances, 0);
assert_eq!(signals.avg_lines_changed, 0.0);
}
#[test]
fn test_quality_signals_with_values() {
let signals = QualitySignals {
avg_tdg_score: Some(2.5),
max_tdg_score: Some(5.0),
avg_complexity: Some(8.3),
avg_test_coverage: Some(0.75),
satd_instances: 10,
avg_lines_changed: 50.5,
avg_files_per_commit: 3.2,
};
assert_eq!(signals.avg_tdg_score, Some(2.5));
assert_eq!(signals.max_tdg_score, Some(5.0));
assert_eq!(signals.satd_instances, 10);
}
#[test]
fn test_defect_instance_structure() {
let instance = DefectInstance {
commit_hash: "abc123".to_string(),
message: "fix bug".to_string(),
author: "dev@example.com".to_string(),
timestamp: 1234567890,
files_affected: 3,
lines_added: 25,
lines_removed: 10,
};
assert_eq!(instance.commit_hash, "abc123");
assert_eq!(instance.files_affected, 3);
assert_eq!(instance.lines_added, 25);
}
#[test]
fn test_defect_pattern_structure() {
let pattern = DefectPattern {
category: DefectCategory::LogicErrors,
frequency: 15,
confidence: 0.70,
quality_signals: QualitySignals::default(),
examples: vec![],
};
assert_eq!(pattern.frequency, 15);
assert_eq!(pattern.confidence, 0.70);
assert!(pattern.examples.is_empty());
}
#[test]
fn test_analysis_metadata_structure() {
let metadata = AnalysisMetadata {
organization: "my-org".to_string(),
analysis_date: "2025-11-24T12:00:00Z".to_string(),
repositories_analyzed: 20,
commits_analyzed: 500,
analyzer_version: "0.2.0".to_string(),
};
assert_eq!(metadata.organization, "my-org");
assert_eq!(metadata.repositories_analyzed, 20);
assert_eq!(metadata.commits_analyzed, 500);
}
#[test]
fn test_report_serialization_deserialization() {
let metadata = AnalysisMetadata {
organization: "test".to_string(),
analysis_date: "2025-01-01T00:00:00Z".to_string(),
repositories_analyzed: 1,
commits_analyzed: 10,
analyzer_version: "0.1.0".to_string(),
};
let report = AnalysisReport {
version: "1.0".to_string(),
metadata,
defect_patterns: vec![],
};
let json = serde_json::to_string(&report).unwrap();
let deserialized: AnalysisReport = serde_json::from_str(&json).unwrap();
assert_eq!(report.version, deserialized.version);
assert_eq!(
report.metadata.organization,
deserialized.metadata.organization
);
}
#[test]
fn test_report_generator_default() {
let generator = ReportGenerator;
let metadata = AnalysisMetadata {
organization: "test".to_string(),
analysis_date: "2025-01-01T00:00:00Z".to_string(),
repositories_analyzed: 1,
commits_analyzed: 1,
analyzer_version: "0.1.0".to_string(),
};
let report = AnalysisReport {
version: "1.0".to_string(),
metadata,
defect_patterns: vec![],
};
let yaml = generator.to_yaml(&report).expect("Should serialize");
assert!(yaml.contains("version"));
}
#[test]
fn test_empty_defect_patterns() {
let metadata = AnalysisMetadata {
organization: "empty-org".to_string(),
analysis_date: "2025-01-01T00:00:00Z".to_string(),
repositories_analyzed: 0,
commits_analyzed: 0,
analyzer_version: "0.1.0".to_string(),
};
let report = AnalysisReport {
version: "1.0".to_string(),
metadata,
defect_patterns: vec![],
};
let generator = ReportGenerator::new();
let yaml = generator.to_yaml(&report).expect("Should serialize");
assert!(yaml.contains("defect_patterns: []"));
}
}