1use crate::classifier::DefectCategory;
5use anyhow::Result;
6use serde::{Deserialize, Serialize};
7use std::path::Path;
8use tokio::fs;
9use tracing::{debug, info};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct AnalysisMetadata {
14 pub organization: String,
15 pub analysis_date: String,
16 pub repositories_analyzed: usize,
17 pub commits_analyzed: usize,
18 pub analyzer_version: String,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct QualitySignals {
24 pub avg_tdg_score: Option<f32>,
26 pub max_tdg_score: Option<f32>,
28 pub avg_complexity: Option<f32>,
30 pub avg_test_coverage: Option<f32>,
32 pub satd_instances: usize,
34 pub avg_lines_changed: f32,
36 pub avg_files_per_commit: f32,
38}
39
40impl Default for QualitySignals {
41 fn default() -> Self {
42 Self {
43 avg_tdg_score: None,
44 max_tdg_score: None,
45 avg_complexity: None,
46 avg_test_coverage: None,
47 satd_instances: 0,
48 avg_lines_changed: 0.0,
49 avg_files_per_commit: 0.0,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct DefectInstance {
57 pub commit_hash: String,
58 pub message: String,
59 pub author: String,
60 pub timestamp: i64,
61 pub files_affected: usize,
63 pub lines_added: usize,
65 pub lines_removed: usize,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct DefectPattern {
73 pub category: DefectCategory,
74 pub frequency: usize,
75 pub confidence: f32,
76 pub quality_signals: QualitySignals,
78 pub examples: Vec<DefectInstance>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct AnalysisReport {
86 pub version: String,
87 pub metadata: AnalysisMetadata,
88 pub defect_patterns: Vec<DefectPattern>,
89}
90
91pub struct ReportGenerator;
94
95impl ReportGenerator {
96 pub fn new() -> Self {
105 Self
106 }
107
108 pub fn to_yaml(&self, report: &AnalysisReport) -> Result<String> {
141 debug!("Serializing report to YAML");
142 let yaml = serde_yaml::to_string(report)?;
143 Ok(yaml)
144 }
145
146 pub async fn write_to_file(&self, report: &AnalysisReport, path: &Path) -> Result<()> {
186 info!("Writing report to file: {}", path.display());
187
188 let yaml = self.to_yaml(report)?;
190
191 fs::write(path, yaml).await?;
193
194 info!("Successfully wrote report to {}", path.display());
195 Ok(())
196 }
197}
198
199impl Default for ReportGenerator {
200 fn default() -> Self {
201 Self::new()
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_report_generator_creation() {
211 let _generator = ReportGenerator::new();
212 let _generator_default = ReportGenerator;
213 }
214
215 #[test]
216 fn test_yaml_serialization() {
217 let metadata = AnalysisMetadata {
218 organization: "test-org".to_string(),
219 analysis_date: "2025-11-15T00:00:00Z".to_string(),
220 repositories_analyzed: 5,
221 commits_analyzed: 50,
222 analyzer_version: "0.1.0".to_string(),
223 };
224
225 let report = AnalysisReport {
226 version: "1.0".to_string(),
227 metadata,
228 defect_patterns: vec![],
229 };
230
231 let generator = ReportGenerator::new();
232 let yaml = generator.to_yaml(&report).expect("Should serialize");
233
234 assert!(yaml.contains("version: '1.0'"));
235 assert!(yaml.contains("organization: test-org"));
236 }
237
238 #[test]
239 fn test_yaml_with_defect_patterns() {
240 let metadata = AnalysisMetadata {
241 organization: "test-org".to_string(),
242 analysis_date: "2025-11-15T00:00:00Z".to_string(),
243 repositories_analyzed: 10,
244 commits_analyzed: 100,
245 analyzer_version: "0.1.0".to_string(),
246 };
247
248 let patterns = vec![
249 DefectPattern {
250 category: DefectCategory::MemorySafety,
251 frequency: 42,
252 confidence: 0.85,
253 quality_signals: QualitySignals {
254 avg_lines_changed: 45.2,
255 avg_files_per_commit: 2.1,
256 ..Default::default()
257 },
258 examples: vec![DefectInstance {
259 commit_hash: "abc123".to_string(),
260 message: "fix memory leak".to_string(),
261 author: "test@example.com".to_string(),
262 timestamp: 1234567890,
263 files_affected: 2,
264 lines_added: 30,
265 lines_removed: 15,
266 }],
267 },
268 DefectPattern {
269 category: DefectCategory::ConcurrencyBugs,
270 frequency: 30,
271 confidence: 0.80,
272 quality_signals: QualitySignals {
273 avg_lines_changed: 67.3,
274 avg_files_per_commit: 3.5,
275 ..Default::default()
276 },
277 examples: vec![DefectInstance {
278 commit_hash: "def456".to_string(),
279 message: "fix race condition".to_string(),
280 author: "test@example.com".to_string(),
281 timestamp: 1234567891,
282 files_affected: 4,
283 lines_added: 50,
284 lines_removed: 17,
285 }],
286 },
287 ];
288
289 let report = AnalysisReport {
290 version: "1.0".to_string(),
291 metadata,
292 defect_patterns: patterns,
293 };
294
295 let generator = ReportGenerator::new();
296 let yaml = generator.to_yaml(&report).expect("Should serialize");
297
298 assert!(yaml.contains("MemorySafety"));
299 assert!(yaml.contains("ConcurrencyBugs"));
300 assert!(yaml.contains("frequency: 42"));
301 }
302
303 #[tokio::test]
304 async fn test_write_to_file() {
305 use tempfile::TempDir;
306
307 let temp_dir = TempDir::new().unwrap();
308 let report_path = temp_dir.path().join("test-report.yaml");
309
310 let metadata = AnalysisMetadata {
311 organization: "test-org".to_string(),
312 analysis_date: "2025-11-15T00:00:00Z".to_string(),
313 repositories_analyzed: 5,
314 commits_analyzed: 50,
315 analyzer_version: "0.1.0".to_string(),
316 };
317
318 let report = AnalysisReport {
319 version: "1.0".to_string(),
320 metadata,
321 defect_patterns: vec![],
322 };
323
324 let generator = ReportGenerator::new();
325 generator
326 .write_to_file(&report, &report_path)
327 .await
328 .expect("Should write file");
329
330 assert!(report_path.exists());
331
332 let content = tokio::fs::read_to_string(&report_path).await.unwrap();
333 assert!(content.contains("test-org"));
334 }
335
336 #[test]
337 fn test_quality_signals_default() {
338 let signals = QualitySignals::default();
339 assert!(signals.avg_tdg_score.is_none());
340 assert!(signals.avg_complexity.is_none());
341 assert_eq!(signals.satd_instances, 0);
342 assert_eq!(signals.avg_lines_changed, 0.0);
343 }
344
345 #[test]
346 fn test_quality_signals_with_values() {
347 let signals = QualitySignals {
348 avg_tdg_score: Some(2.5),
349 max_tdg_score: Some(5.0),
350 avg_complexity: Some(8.3),
351 avg_test_coverage: Some(0.75),
352 satd_instances: 10,
353 avg_lines_changed: 50.5,
354 avg_files_per_commit: 3.2,
355 };
356
357 assert_eq!(signals.avg_tdg_score, Some(2.5));
358 assert_eq!(signals.max_tdg_score, Some(5.0));
359 assert_eq!(signals.satd_instances, 10);
360 }
361
362 #[test]
363 fn test_defect_instance_structure() {
364 let instance = DefectInstance {
365 commit_hash: "abc123".to_string(),
366 message: "fix bug".to_string(),
367 author: "dev@example.com".to_string(),
368 timestamp: 1234567890,
369 files_affected: 3,
370 lines_added: 25,
371 lines_removed: 10,
372 };
373
374 assert_eq!(instance.commit_hash, "abc123");
375 assert_eq!(instance.files_affected, 3);
376 assert_eq!(instance.lines_added, 25);
377 }
378
379 #[test]
380 fn test_defect_pattern_structure() {
381 let pattern = DefectPattern {
382 category: DefectCategory::LogicErrors,
383 frequency: 15,
384 confidence: 0.70,
385 quality_signals: QualitySignals::default(),
386 examples: vec![],
387 };
388
389 assert_eq!(pattern.frequency, 15);
390 assert_eq!(pattern.confidence, 0.70);
391 assert!(pattern.examples.is_empty());
392 }
393
394 #[test]
395 fn test_analysis_metadata_structure() {
396 let metadata = AnalysisMetadata {
397 organization: "my-org".to_string(),
398 analysis_date: "2025-11-24T12:00:00Z".to_string(),
399 repositories_analyzed: 20,
400 commits_analyzed: 500,
401 analyzer_version: "0.2.0".to_string(),
402 };
403
404 assert_eq!(metadata.organization, "my-org");
405 assert_eq!(metadata.repositories_analyzed, 20);
406 assert_eq!(metadata.commits_analyzed, 500);
407 }
408
409 #[test]
410 fn test_report_serialization_deserialization() {
411 let metadata = AnalysisMetadata {
412 organization: "test".to_string(),
413 analysis_date: "2025-01-01T00:00:00Z".to_string(),
414 repositories_analyzed: 1,
415 commits_analyzed: 10,
416 analyzer_version: "0.1.0".to_string(),
417 };
418
419 let report = AnalysisReport {
420 version: "1.0".to_string(),
421 metadata,
422 defect_patterns: vec![],
423 };
424
425 let json = serde_json::to_string(&report).unwrap();
426 let deserialized: AnalysisReport = serde_json::from_str(&json).unwrap();
427
428 assert_eq!(report.version, deserialized.version);
429 assert_eq!(
430 report.metadata.organization,
431 deserialized.metadata.organization
432 );
433 }
434
435 #[test]
436 fn test_report_generator_default() {
437 let generator = ReportGenerator;
438 let metadata = AnalysisMetadata {
439 organization: "test".to_string(),
440 analysis_date: "2025-01-01T00:00:00Z".to_string(),
441 repositories_analyzed: 1,
442 commits_analyzed: 1,
443 analyzer_version: "0.1.0".to_string(),
444 };
445
446 let report = AnalysisReport {
447 version: "1.0".to_string(),
448 metadata,
449 defect_patterns: vec![],
450 };
451
452 let yaml = generator.to_yaml(&report).expect("Should serialize");
453 assert!(yaml.contains("version"));
454 }
455
456 #[test]
457 fn test_empty_defect_patterns() {
458 let metadata = AnalysisMetadata {
459 organization: "empty-org".to_string(),
460 analysis_date: "2025-01-01T00:00:00Z".to_string(),
461 repositories_analyzed: 0,
462 commits_analyzed: 0,
463 analyzer_version: "0.1.0".to_string(),
464 };
465
466 let report = AnalysisReport {
467 version: "1.0".to_string(),
468 metadata,
469 defect_patterns: vec![],
470 };
471
472 let generator = ReportGenerator::new();
473 let yaml = generator.to_yaml(&report).expect("Should serialize");
474
475 assert!(yaml.contains("defect_patterns: []"));
476 }
477}