#![cfg_attr(coverage_nightly, coverage(off))]
use crate::cli::analysis_utilities::{
format_churn_as_csv, format_churn_as_markdown, format_churn_as_summary, write_churn_output,
};
use crate::models::churn::{ChurnSummary, CodeChurnAnalysis, FileChurnMetrics};
use chrono::Utc;
use std::collections::HashMap;
use std::path::PathBuf;
fn strip_ansi(s: &str) -> String {
let re = regex::Regex::new(r"\x1b\[[0-9;]*m").unwrap();
re.replace_all(s, "").to_string()
}
fn create_test_analysis() -> CodeChurnAnalysis {
CodeChurnAnalysis {
generated_at: Utc::now(),
period_days: 30,
repository_root: PathBuf::from("/test/repo"),
files: vec![
FileChurnMetrics {
path: PathBuf::from("/test/repo/src/main.rs"),
relative_path: "src/main.rs".to_string(),
commit_count: 15,
unique_authors: vec!["dev1".to_string(), "dev2".to_string()],
additions: 100,
deletions: 50,
churn_score: 0.75,
last_modified: Utc::now(),
first_seen: Utc::now(),
},
FileChurnMetrics {
path: PathBuf::from("/test/repo/src/lib.rs"),
relative_path: "src/lib.rs".to_string(),
commit_count: 8,
unique_authors: vec!["dev1".to_string()],
additions: 60,
deletions: 20,
churn_score: 0.45,
last_modified: Utc::now(),
first_seen: Utc::now(),
},
],
summary: ChurnSummary {
total_commits: 23,
total_files_changed: 2,
hotspot_files: vec![PathBuf::from("src/main.rs")],
stable_files: vec![PathBuf::from("src/lib.rs")],
author_contributions: {
let mut map = HashMap::new();
map.insert("dev1".to_string(), 15);
map.insert("dev2".to_string(), 8);
map
},
mean_churn_score: 0.6,
variance_churn_score: 0.0225,
stddev_churn_score: 0.15,
},
}
}
fn create_empty_analysis() -> CodeChurnAnalysis {
CodeChurnAnalysis {
generated_at: Utc::now(),
period_days: 7,
repository_root: PathBuf::from("/test/repo"),
files: vec![],
summary: ChurnSummary {
total_commits: 0,
total_files_changed: 0,
hotspot_files: vec![],
stable_files: vec![],
author_contributions: HashMap::new(),
mean_churn_score: 0.0,
variance_churn_score: 0.0,
stddev_churn_score: 0.0,
},
}
}
#[test]
fn test_format_churn_as_summary_with_files() {
let analysis = create_test_analysis();
let result = strip_ansi(&format_churn_as_summary(&analysis).unwrap());
assert!(result.contains("Code Churn Analysis Summary"));
assert!(result.contains("Period:"));
assert!(result.contains("30"));
assert!(result.contains("23")); assert!(result.contains("2")); }
#[test]
fn test_format_churn_as_summary_empty() {
let analysis = create_empty_analysis();
let result = strip_ansi(&format_churn_as_summary(&analysis).unwrap());
assert!(result.contains("Code Churn Analysis Summary"));
assert!(result.contains("Period:"));
assert!(result.contains("7"));
}
#[test]
fn test_format_churn_as_summary_top_files() {
let analysis = create_test_analysis();
let result = format_churn_as_summary(&analysis).unwrap();
assert!(result.contains("Top Files by Churn"));
assert!(result.contains("main.rs"));
}
#[test]
fn test_format_churn_as_summary_hotspot_files() {
let analysis = create_test_analysis();
let result = format_churn_as_summary(&analysis).unwrap();
assert!(result.contains("Hotspot Files"));
}
#[test]
fn test_format_churn_as_summary_stable_files() {
let analysis = create_test_analysis();
let result = format_churn_as_summary(&analysis).unwrap();
assert!(result.contains("Stable Files"));
}
#[test]
fn test_format_churn_as_summary_contributors() {
let analysis = create_test_analysis();
let result = format_churn_as_summary(&analysis).unwrap();
assert!(result.contains("Top Contributors"));
assert!(result.contains("dev1"));
}
#[test]
fn test_format_churn_as_markdown_with_files() {
let analysis = create_test_analysis();
let result = format_churn_as_markdown(&analysis).unwrap();
assert!(result.contains("# Code Churn Analysis Report"));
assert!(result.contains("Generated:"));
assert!(result.contains("Repository:"));
assert!(result.contains("Analysis Period: 30 days"));
}
#[test]
fn test_format_churn_as_markdown_summary_table() {
let analysis = create_test_analysis();
let result = format_churn_as_markdown(&analysis).unwrap();
assert!(result.contains("## Summary Statistics"));
assert!(result.contains("| Metric | Value |"));
assert!(result.contains("Total Commits"));
assert!(result.contains("Files Changed"));
}
#[test]
fn test_format_churn_as_markdown_file_details() {
let analysis = create_test_analysis();
let result = format_churn_as_markdown(&analysis).unwrap();
assert!(result.contains("## File Churn Details"));
assert!(result.contains("src/main.rs"));
assert!(result.contains("src/lib.rs"));
}
#[test]
fn test_format_churn_as_markdown_author_contributions() {
let analysis = create_test_analysis();
let result = format_churn_as_markdown(&analysis).unwrap();
assert!(result.contains("## Author Contributions"));
assert!(result.contains("dev1"));
}
#[test]
fn test_format_churn_as_markdown_recommendations() {
let analysis = create_test_analysis();
let result = format_churn_as_markdown(&analysis).unwrap();
assert!(result.contains("## Recommendations"));
assert!(result.contains("Review Hotspot Files"));
assert!(result.contains("Add Tests"));
}
#[test]
fn test_format_churn_as_markdown_empty() {
let analysis = create_empty_analysis();
let result = format_churn_as_markdown(&analysis).unwrap();
assert!(result.contains("# Code Churn Analysis Report"));
assert!(result.contains("## Recommendations"));
}
#[test]
fn test_format_churn_as_csv_with_files() {
let analysis = create_test_analysis();
let result = format_churn_as_csv(&analysis).unwrap();
assert!(result.contains("file_path,relative_path,commit_count,unique_authors,additions,deletions,churn_score,last_modified,first_seen"));
assert!(result.contains("src/main.rs"));
assert!(result.contains("src/lib.rs"));
assert!(result.contains("15")); assert!(result.contains("0.75")); }
#[test]
fn test_format_churn_as_csv_empty() {
let analysis = create_empty_analysis();
let result = format_churn_as_csv(&analysis).unwrap();
assert!(result.contains("file_path,relative_path"));
let lines: Vec<&str> = result.lines().collect();
assert_eq!(lines.len(), 1); }
#[test]
fn test_format_churn_as_csv_column_count() {
let analysis = create_test_analysis();
let result = format_churn_as_csv(&analysis).unwrap();
let lines: Vec<&str> = result.lines().collect();
let header_cols = lines[0].split(',').count();
let data_cols = lines[1].split(',').count();
assert_eq!(header_cols, 9); assert_eq!(data_cols, 9);
}
#[tokio::test]
async fn test_write_churn_output_to_stdout() {
let content = "Test churn output".to_string();
let result = write_churn_output(content, None).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_write_churn_output_to_file() {
let content = "Test churn output to file".to_string();
let temp_dir = std::env::temp_dir();
let output_path = temp_dir.join("test_churn_output.txt");
let result = write_churn_output(content.clone(), Some(output_path.clone())).await;
assert!(result.is_ok());
let read_content = tokio::fs::read_to_string(&output_path).await.unwrap();
assert_eq!(read_content, content);
let _ = tokio::fs::remove_file(&output_path).await;
}
#[test]
fn test_format_with_special_characters_in_paths() {
let mut analysis = create_test_analysis();
analysis.files[0].relative_path = "src/path with spaces/file.rs".to_string();
let result = format_churn_as_csv(&analysis).unwrap();
assert!(result.contains("src/path with spaces/file.rs"));
}
#[test]
fn test_format_with_zero_churn_score() {
let mut analysis = create_test_analysis();
analysis.files[0].churn_score = 0.0;
let result = format_churn_as_markdown(&analysis).unwrap();
assert!(result.contains("0.00"));
}
#[test]
fn test_format_with_high_churn_score() {
let mut analysis = create_test_analysis();
analysis.files[0].churn_score = 1.0;
let result = format_churn_as_markdown(&analysis).unwrap();
assert!(result.contains("1.00"));
}
#[test]
fn test_format_with_many_authors() {
let mut analysis = create_test_analysis();
analysis.files[0].unique_authors = vec![
"dev1".to_string(),
"dev2".to_string(),
"dev3".to_string(),
"dev4".to_string(),
"dev5".to_string(),
];
let result = format_churn_as_summary(&analysis).unwrap();
assert!(result.contains("5 authors"));
}
#[test]
fn test_format_summary_sorts_by_commit_count() {
let mut analysis = create_test_analysis();
analysis.files[0].commit_count = 5;
analysis.files[1].commit_count = 20;
let result = format_churn_as_summary(&analysis).unwrap();
let lib_pos = result.find("lib.rs");
let main_pos = result.find("main.rs");
assert!(lib_pos.is_some());
assert!(main_pos.is_some());
}
#[test]
fn test_format_markdown_sorts_by_churn_score() {
let mut analysis = create_test_analysis();
analysis.files[0].churn_score = 0.25;
analysis.files[1].churn_score = 0.95;
let result = format_churn_as_markdown(&analysis).unwrap();
assert!(result.contains("lib.rs"));
assert!(result.contains("0.95"));
}