#![cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use crate::models::churn::{
ChurnOutputFormat, ChurnSummary, CodeChurnAnalysis, FileChurnMetrics,
};
use crate::services::git_analysis::GitAnalysisService;
use chrono::Utc;
use std::collections::HashMap;
use std::path::PathBuf;
use tempfile::TempDir;
fn create_test_analysis() -> CodeChurnAnalysis {
let files = vec![
FileChurnMetrics {
path: PathBuf::from("/test/repo/src/main.rs"),
relative_path: "src/main.rs".to_string(),
commit_count: 25,
unique_authors: vec!["alice".to_string(), "bob".to_string()],
additions: 300,
deletions: 150,
churn_score: 0.85,
last_modified: Utc::now(),
first_seen: Utc::now() - chrono::Duration::days(30),
},
FileChurnMetrics {
path: PathBuf::from("/test/repo/src/lib.rs"),
relative_path: "src/lib.rs".to_string(),
commit_count: 5,
unique_authors: vec!["alice".to_string()],
additions: 50,
deletions: 10,
churn_score: 0.15,
last_modified: Utc::now() - chrono::Duration::days(20),
first_seen: Utc::now() - chrono::Duration::days(30),
},
];
let mut author_contributions = HashMap::new();
author_contributions.insert("alice".to_string(), 2);
author_contributions.insert("bob".to_string(), 1);
let summary = ChurnSummary {
total_commits: 30,
total_files_changed: 2,
hotspot_files: vec![files[0].path.clone()],
stable_files: vec![files[1].path.clone()],
author_contributions,
mean_churn_score: 0.0,
variance_churn_score: 0.0,
stddev_churn_score: 0.0,
};
CodeChurnAnalysis {
generated_at: Utc::now(),
period_days: 30,
repository_root: PathBuf::from("/test/repo"),
files,
summary,
}
}
#[test]
fn test_churn_score_calculation() {
let mut metric = FileChurnMetrics {
path: PathBuf::from("test.rs"),
relative_path: "test.rs".to_string(),
commit_count: 10,
unique_authors: vec![],
additions: 100,
deletions: 50,
churn_score: 0.0,
last_modified: Utc::now(),
first_seen: Utc::now(),
};
metric.calculate_churn_score(20, 300);
assert!(metric.churn_score > 0.0 && metric.churn_score <= 1.0);
assert!((metric.churn_score - 0.5).abs() < 0.01); }
#[test]
fn test_output_format_parsing() {
assert!(matches!(
"json".parse::<ChurnOutputFormat>().unwrap(),
ChurnOutputFormat::Json
));
assert!(matches!(
"markdown".parse::<ChurnOutputFormat>().unwrap(),
ChurnOutputFormat::Markdown
));
assert!(matches!(
"csv".parse::<ChurnOutputFormat>().unwrap(),
ChurnOutputFormat::Csv
));
assert!(matches!(
"summary".parse::<ChurnOutputFormat>().unwrap(),
ChurnOutputFormat::Summary
));
assert!("invalid".parse::<ChurnOutputFormat>().is_err());
}
#[test]
fn test_format_churn_summary() {
use crate::handlers::tools::format_churn_summary;
let analysis = create_test_analysis();
let summary = format_churn_summary(&analysis);
assert!(summary.contains("Code Churn Analysis"));
assert!(summary.contains("Period: 30 days"));
assert!(summary.contains("Total files changed: 2"));
assert!(summary.contains("Total commits: 30"));
assert!(summary.contains("Hotspot Files"));
assert!(summary.contains("src/main.rs"));
assert!(summary.contains("Stable Files"));
assert!(summary.contains("src/lib.rs"));
}
#[test]
fn test_format_churn_markdown() {
use crate::handlers::tools::format_churn_as_markdown;
let analysis = create_test_analysis();
let markdown = format_churn_as_markdown(&analysis);
assert!(markdown.contains("# Code Churn Analysis Report"));
assert!(markdown.contains("**Repository:**"));
assert!(markdown.contains("## Summary"));
assert!(markdown.contains("## Top 10 Files by Churn Score"));
assert!(markdown.contains("| File | Commits | Changes | Churn Score | Authors |"));
assert!(markdown.contains("src/main.rs"));
assert!(markdown.contains("| 25 |"));
assert!(markdown.contains("| 0.85 |"));
}
#[test]
fn test_format_churn_csv() {
use crate::handlers::tools::format_churn_as_csv;
let analysis = create_test_analysis();
let csv = format_churn_as_csv(&analysis);
assert!(csv.contains(
"file_path,commits,additions,deletions,churn_score,unique_authors,last_modified"
));
assert!(csv.contains("src/main.rs,25,300,150,0.850,2,"));
assert!(csv.contains("src/lib.rs,5,50,10,0.150,1,"));
}
#[test]
fn test_no_git_repository_error() {
let temp_dir = TempDir::new().unwrap();
let result = GitAnalysisService::analyze_code_churn(temp_dir.path(), 30);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("No git repository found"));
}
#[test]
fn test_parse_commit_line() {
use crate::services::git_analysis::GitAnalysisService;
let temp_dir = TempDir::new().unwrap();
let repo_path = temp_dir.path();
std::process::Command::new("git")
.arg("init")
.current_dir(repo_path)
.output()
.expect("Failed to init git repo");
std::process::Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(repo_path)
.output()
.expect("Failed to configure git email");
std::process::Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(repo_path)
.output()
.expect("Failed to configure git name");
std::fs::write(repo_path.join("test.txt"), "Hello").unwrap();
std::process::Command::new("git")
.args(["add", "test.txt"])
.current_dir(repo_path)
.output()
.expect("Failed to add file");
std::process::Command::new("git")
.args(["commit", "--no-verify", "-m", "Initial commit"])
.current_dir(repo_path)
.output()
.expect("Failed to commit");
let result = GitAnalysisService::analyze_code_churn(repo_path, 30);
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.period_days, 30);
assert_eq!(analysis.files.len(), 1);
assert_eq!(analysis.files[0].relative_path, "test.txt");
assert_eq!(analysis.files[0].commit_count, 1);
assert!(analysis.files[0]
.unique_authors
.contains(&"Test User".to_string()));
}
#[test]
fn test_multiple_commits_and_files() {
let temp_dir = TempDir::new().unwrap();
let repo_path = temp_dir.path();
std::process::Command::new("git")
.arg("init")
.current_dir(repo_path)
.output()
.expect("Failed to init git repo");
std::process::Command::new("git")
.args(["config", "user.email", "test@example.com"])
.current_dir(repo_path)
.output()
.expect("Failed to configure git email");
std::process::Command::new("git")
.args(["config", "user.name", "Test User"])
.current_dir(repo_path)
.output()
.expect("Failed to configure git name");
std::fs::write(repo_path.join("file1.txt"), "Content1").unwrap();
std::process::Command::new("git")
.args(["add", "file1.txt"])
.current_dir(repo_path)
.output()
.unwrap();
std::process::Command::new("git")
.args(["commit", "--no-verify", "-m", "Add file1"])
.current_dir(repo_path)
.output()
.unwrap();
std::fs::write(repo_path.join("file1.txt"), "Content1 Modified").unwrap();
std::fs::write(repo_path.join("file2.txt"), "Content2").unwrap();
std::process::Command::new("git")
.args(["add", "."])
.current_dir(repo_path)
.output()
.unwrap();
std::process::Command::new("git")
.args(["commit", "--no-verify", "-m", "Modify file1 and add file2"])
.current_dir(repo_path)
.output()
.unwrap();
let result = GitAnalysisService::analyze_code_churn(repo_path, 30);
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.summary.total_files_changed, 2);
assert_eq!(analysis.summary.total_commits, 3);
let file1 = analysis
.files
.iter()
.find(|f| f.relative_path == "file1.txt");
assert!(file1.is_some());
assert_eq!(file1.unwrap().commit_count, 2);
let file2 = analysis
.files
.iter()
.find(|f| f.relative_path == "file2.txt");
assert!(file2.is_some());
assert_eq!(file2.unwrap().commit_count, 1);
}
#[test]
fn test_churn_score_edge_cases() {
let mut metric = FileChurnMetrics {
path: PathBuf::from("test.rs"),
relative_path: "test.rs".to_string(),
commit_count: 0,
unique_authors: vec![],
additions: 0,
deletions: 0,
churn_score: 0.0,
last_modified: Utc::now(),
first_seen: Utc::now(),
};
metric.calculate_churn_score(0, 0);
assert_eq!(metric.churn_score, 0.0);
metric.commit_count = 5;
metric.additions = 100;
metric.deletions = 50;
metric.calculate_churn_score(0, 0);
assert_eq!(metric.churn_score, 0.0); }
#[test]
fn test_empty_repository() {
let temp_dir = TempDir::new().unwrap();
let repo_path = temp_dir.path();
std::process::Command::new("git")
.arg("init")
.current_dir(repo_path)
.output()
.expect("Failed to init git repo");
let result = GitAnalysisService::analyze_code_churn(repo_path, 30);
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.files.len(), 0);
assert_eq!(analysis.summary.total_files_changed, 0);
assert_eq!(analysis.summary.total_commits, 0);
assert!(analysis.summary.hotspot_files.is_empty());
assert!(analysis.summary.stable_files.is_empty());
}
}