use std::cmp::Ordering;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use crate::services::complexity::FileComplexityMetrics;
pub trait FileRanker: Send + Sync {
type Metric: PartialOrd + Clone + Send + Sync;
fn compute_score(&self, file_path: &Path) -> Self::Metric;
fn format_ranking_entry(&self, file: &str, metric: &Self::Metric, rank: usize) -> String;
fn ranking_type(&self) -> &'static str;
}
pub struct RankingEngine<R: FileRanker> {
ranker: R,
cache: Arc<RwLock<HashMap<String, R::Metric>>>,
}
impl<R: FileRanker> RankingEngine<R> {
pub fn new(ranker: R) -> Self {
Self {
ranker,
cache: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn rank_files(&self, files: &[PathBuf], limit: usize) -> Vec<(String, R::Metric)> {
if files.is_empty() || limit == 0 {
return Vec::new();
}
let mut scores: Vec<_> = files
.par_iter()
.filter_map(|f| {
if !f.exists() || !f.is_file() {
return None;
}
let file_str = f.to_string_lossy().to_string();
if let Ok(cache) = self.cache.read() {
if let Some(cached_score) = cache.get(&file_str) {
return Some((file_str, cached_score.clone()));
}
}
let score = self.ranker.compute_score(f);
if let Ok(mut cache) = self.cache.write() {
cache.insert(file_str.clone(), score.clone());
}
Some((file_str, score))
})
.collect();
scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal));
scores.truncate(limit);
scores
}
pub fn format_rankings_table(&self, rankings: &[(String, R::Metric)]) -> String {
if rankings.is_empty() {
return format!(
"## Top {} Files\n\nNo files found.\n",
self.ranker.ranking_type()
);
}
let mut output = format!(
"## Top {} {} Files\n\n",
rankings.len(),
self.ranker.ranking_type()
);
for (i, (file, metric)) in rankings.iter().enumerate() {
output.push_str(&self.ranker.format_ranking_entry(file, metric, i + 1));
output.push('\n');
}
output.push('\n');
output
}
pub fn format_rankings_json(&self, rankings: &[(String, R::Metric)]) -> serde_json::Value {
serde_json::json!({
"analysis_type": self.ranker.ranking_type(),
"timestamp": chrono::Utc::now().to_rfc3339(),
"top_files": {
"requested": rankings.len(),
"returned": rankings.len(),
},
"rankings": rankings.iter().enumerate().map(|(i, (file, _))| {
serde_json::json!({
"rank": i + 1,
"file": file,
})
}).collect::<Vec<_>>()
})
}
pub fn clear_cache(&self) {
if let Ok(mut cache) = self.cache.write() {
cache.clear();
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CompositeComplexityScore {
pub cyclomatic_max: u32,
pub cognitive_avg: f64,
pub halstead_effort: f64,
pub function_count: usize,
pub total_score: f64,
}
impl Default for CompositeComplexityScore {
fn default() -> Self {
Self {
cyclomatic_max: 0,
cognitive_avg: 0.0,
halstead_effort: 0.0,
function_count: 0,
total_score: 0.0,
}
}
}
impl PartialEq for CompositeComplexityScore {
fn eq(&self, other: &Self) -> bool {
(self.total_score - other.total_score).abs() < f64::EPSILON
}
}
impl PartialOrd for CompositeComplexityScore {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.total_score.partial_cmp(&other.total_score)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ChurnScore {
pub commit_count: usize,
pub unique_authors: usize,
pub lines_changed: usize,
pub recency_weight: f64,
pub score: f64,
}
impl Default for ChurnScore {
fn default() -> Self {
Self {
commit_count: 0,
unique_authors: 0,
lines_changed: 0,
recency_weight: 0.0,
score: 0.0,
}
}
}
impl PartialEq for ChurnScore {
fn eq(&self, other: &Self) -> bool {
(self.score - other.score).abs() < f64::EPSILON
}
}
impl PartialOrd for ChurnScore {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.score.partial_cmp(&other.score)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DuplicationScore {
pub exact_clones: usize,
pub renamed_clones: usize,
pub gapped_clones: usize,
pub semantic_clones: usize,
pub duplication_ratio: f64,
pub score: f64,
}
impl Default for DuplicationScore {
fn default() -> Self {
Self {
exact_clones: 0,
renamed_clones: 0,
gapped_clones: 0,
semantic_clones: 0,
duplication_ratio: 0.0,
score: 0.0,
}
}
}
impl PartialEq for DuplicationScore {
fn eq(&self, other: &Self) -> bool {
(self.score - other.score).abs() < f64::EPSILON
}
}
impl PartialOrd for DuplicationScore {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.score.partial_cmp(&other.score)
}
}
#[must_use]
pub fn rank_files_vectorized(scores: &[f32], limit: usize) -> Vec<usize> {
let mut indices: Vec<usize> = (0..scores.len()).collect();
if scores.len() > 1024 {
indices.par_sort_unstable_by(|&a, &b| {
scores[b].partial_cmp(&scores[a]).unwrap_or(Ordering::Equal)
});
} else {
indices.sort_by(|&a, &b| scores[b].partial_cmp(&scores[a]).unwrap_or(Ordering::Equal));
}
indices.truncate(limit);
indices
}
pub struct ComplexityRanker {
pub cyclomatic_weight: f64,
pub cognitive_weight: f64,
pub function_count_weight: f64,
}
impl Default for ComplexityRanker {
fn default() -> Self {
Self {
cyclomatic_weight: 0.4,
cognitive_weight: 0.4,
function_count_weight: 0.2,
}
}
}
impl ComplexityRanker {
#[must_use]
pub fn new(cyclomatic_weight: f64, cognitive_weight: f64, function_count_weight: f64) -> Self {
Self {
cyclomatic_weight,
cognitive_weight,
function_count_weight,
}
}
fn calculate_composite_score(
&self,
metrics: &FileComplexityMetrics,
) -> CompositeComplexityScore {
let all_functions: Vec<_> = metrics
.functions
.iter()
.chain(metrics.classes.iter().flat_map(|c| &c.methods))
.collect();
let function_count = all_functions.len();
if function_count == 0 {
return CompositeComplexityScore::default();
}
let cyclomatic_max = all_functions
.iter()
.map(|f| u32::from(f.metrics.cyclomatic))
.max()
.unwrap_or(0);
let cognitive_total: u32 = all_functions
.iter()
.map(|f| u32::from(f.metrics.cognitive))
.sum();
let cognitive_avg = f64::from(cognitive_total) / function_count as f64;
let halstead_effort = f64::from(all_functions
.iter()
.map(|f| u32::from(f.metrics.lines) * 10) .sum::<u32>());
let normalized_cyclomatic = f64::from(cyclomatic_max).min(50.0) / 50.0; let normalized_cognitive = cognitive_avg.min(100.0) / 100.0; let normalized_function_count = (function_count as f64).min(100.0) / 100.0;
let total_score = (self.cyclomatic_weight * normalized_cyclomatic * 100.0)
+ (self.cognitive_weight * normalized_cognitive * 100.0)
+ (self.function_count_weight * normalized_function_count * 50.0);
CompositeComplexityScore {
cyclomatic_max,
cognitive_avg,
halstead_effort,
function_count,
total_score,
}
}
}
impl FileRanker for ComplexityRanker {
type Metric = CompositeComplexityScore;
fn compute_score(&self, file_path: &Path) -> Self::Metric {
if let Some(ext) = file_path.extension().and_then(|s| s.to_str()) {
match ext {
"rs" => {
if let Ok(metadata) = std::fs::metadata(file_path) {
let size_score = (metadata.len() as f64 / 1000.0).min(100.0);
CompositeComplexityScore {
total_score: size_score,
function_count: (size_score / 10.0) as usize,
cyclomatic_max: (size_score / 5.0) as u32,
cognitive_avg: size_score / 3.0,
halstead_effort: size_score * 10.0,
}
} else {
CompositeComplexityScore::default()
}
}
"ts" | "tsx" | "js" | "jsx" => {
if let Ok(metadata) = std::fs::metadata(file_path) {
let size_score = (metadata.len() as f64 / 1200.0).min(100.0);
CompositeComplexityScore {
total_score: size_score * 0.9, function_count: (size_score / 12.0) as usize,
cyclomatic_max: (size_score / 6.0) as u32,
cognitive_avg: size_score / 4.0,
halstead_effort: size_score * 8.0,
}
} else {
CompositeComplexityScore::default()
}
}
"py" => {
if let Ok(metadata) = std::fs::metadata(file_path) {
let size_score = (metadata.len() as f64 / 800.0).min(100.0);
CompositeComplexityScore {
total_score: size_score * 1.1, function_count: (size_score / 8.0) as usize,
cyclomatic_max: (size_score / 4.0) as u32,
cognitive_avg: size_score / 2.5,
halstead_effort: size_score * 12.0,
}
} else {
CompositeComplexityScore::default()
}
}
_ => CompositeComplexityScore::default(),
}
} else {
CompositeComplexityScore::default()
}
}
fn format_ranking_entry(&self, file: &str, metric: &Self::Metric, rank: usize) -> String {
format!(
"| {:>4} | {:<50} | {:>9} | {:>14} | {:>13.1} | {:>11.1} | {:>11.1} |",
rank,
file,
metric.function_count,
metric.cyclomatic_max,
metric.cognitive_avg,
metric.halstead_effort,
metric.total_score
)
}
fn ranking_type(&self) -> &'static str {
"Complexity"
}
}
#[must_use]
pub fn rank_files_by_complexity(
file_metrics: &[FileComplexityMetrics],
limit: usize,
ranker: &ComplexityRanker,
) -> Vec<(String, CompositeComplexityScore)> {
let mut rankings: Vec<_> = file_metrics
.iter()
.map(|metrics| {
let score = ranker.calculate_composite_score(metrics);
(metrics.path.clone(), score)
})
.collect();
rankings.sort_by(|a, b| {
b.1.total_score
.partial_cmp(&a.1.total_score)
.unwrap_or(Ordering::Equal)
});
if limit > 0 {
rankings.truncate(limit);
}
rankings
}
#[cfg(test)]
mod tests {
use super::*;
use crate::services::complexity::{ClassComplexity, ComplexityMetrics, FunctionComplexity};
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use tempfile::TempDir;
struct MockRanker;
impl FileRanker for MockRanker {
type Metric = f64;
fn compute_score(&self, file_path: &Path) -> Self::Metric {
file_path.to_string_lossy().len() as f64
}
fn format_ranking_entry(&self, file: &str, metric: &Self::Metric, rank: usize) -> String {
format!("| {rank:>4} | {file} | {metric:.1} |")
}
fn ranking_type(&self) -> &'static str {
"Mock"
}
}
fn create_test_file_metrics() -> FileComplexityMetrics {
FileComplexityMetrics {
path: "test.rs".to_string(),
total_complexity: ComplexityMetrics::new(23, 37, 4, 50),
functions: vec![
FunctionComplexity {
name: "test_func".to_string(),
line_start: 1,
line_end: 10,
metrics: ComplexityMetrics::new(5, 8, 2, 10),
},
FunctionComplexity {
name: "complex_func".to_string(),
line_start: 20,
line_end: 50,
metrics: ComplexityMetrics::new(15, 25, 4, 30),
},
],
classes: vec![ClassComplexity {
name: "TestClass".to_string(),
line_start: 60,
line_end: 100,
metrics: ComplexityMetrics::new(3, 4, 1, 10),
methods: vec![FunctionComplexity {
name: "method".to_string(),
line_start: 65,
line_end: 75,
metrics: ComplexityMetrics::new(3, 4, 1, 10),
}],
}],
}
}
#[tokio::test]
async fn test_empty_file_list() {
let ranker = MockRanker;
let engine = RankingEngine::new(ranker);
let result = engine.rank_files(&[], 5).await;
assert_eq!(result.len(), 0);
}
#[tokio::test]
async fn test_limit_exceeds_files() {
let files = vec![PathBuf::from("a.rs"), PathBuf::from("b.rs")];
let ranker = MockRanker;
let engine = RankingEngine::new(ranker);
let result = engine.rank_files(&files, 10).await;
assert_eq!(result.len(), 0); }
#[test]
fn test_vectorized_ranking() {
let scores = vec![1.0, 5.0, 3.0, 2.0, 4.0];
let ranked = rank_files_vectorized(&scores, 3);
assert_eq!(ranked, vec![1, 4, 2]);
}
#[test]
fn test_composite_complexity_score_ordering() {
let score1 = CompositeComplexityScore {
total_score: 10.0,
..Default::default()
};
let score2 = CompositeComplexityScore {
total_score: 5.0,
..Default::default()
};
assert!(score1 > score2);
}
#[test]
fn test_composite_complexity_score_default() {
let score = CompositeComplexityScore::default();
assert_eq!(score.cyclomatic_max, 0);
assert_eq!(score.cognitive_avg, 0.0);
assert_eq!(score.halstead_effort, 0.0);
assert_eq!(score.function_count, 0);
assert_eq!(score.total_score, 0.0);
}
#[test]
fn test_composite_complexity_score_equality() {
let score1 = CompositeComplexityScore {
total_score: 10.0,
..Default::default()
};
let score2 = CompositeComplexityScore {
total_score: 10.0,
..Default::default()
};
let score3 = CompositeComplexityScore {
total_score: 15.0,
..Default::default()
};
assert_eq!(score1, score2);
assert_ne!(score1, score3);
}
#[test]
fn test_churn_score_default_and_ordering() {
let score1 = ChurnScore::default();
let score2 = ChurnScore {
score: 10.0,
..Default::default()
};
assert_eq!(score1.commit_count, 0);
assert_eq!(score1.score, 0.0);
assert!(score2 > score1);
}
#[test]
fn test_duplication_score_default_and_ordering() {
let score1 = DuplicationScore::default();
let score2 = DuplicationScore {
score: 5.0,
exact_clones: 2,
duplication_ratio: 0.3,
..Default::default()
};
assert_eq!(score1.exact_clones, 0);
assert_eq!(score1.duplication_ratio, 0.0);
assert!(score2 > score1);
}
#[test]
fn test_vectorized_ranking_small_dataset() {
let scores = vec![3.0, 1.0, 4.0, 2.0];
let ranked = rank_files_vectorized(&scores, 2);
assert_eq!(ranked, vec![2, 0]); }
#[test]
fn test_vectorized_ranking_large_dataset() {
let scores: Vec<f32> = (0..2000).map(|i| i as f32).collect();
let ranked = rank_files_vectorized(&scores, 5);
assert_eq!(ranked, vec![1999, 1998, 1997, 1996, 1995]);
}
#[test]
fn test_vectorized_ranking_empty() {
let scores = vec![];
let ranked = rank_files_vectorized(&scores, 5);
assert_eq!(ranked.len(), 0);
}
#[test]
fn test_complexity_ranker_default() {
let ranker = ComplexityRanker::default();
assert_eq!(ranker.cyclomatic_weight, 0.4);
assert_eq!(ranker.cognitive_weight, 0.4);
assert_eq!(ranker.function_count_weight, 0.2);
assert_eq!(ranker.ranking_type(), "Complexity");
}
#[test]
fn test_complexity_ranker_new() {
let ranker = ComplexityRanker::new(0.5, 0.3, 0.2);
assert_eq!(ranker.cyclomatic_weight, 0.5);
assert_eq!(ranker.cognitive_weight, 0.3);
assert_eq!(ranker.function_count_weight, 0.2);
}
#[test]
fn test_complexity_ranker_calculate_composite_score() {
let ranker = ComplexityRanker::default();
let metrics = create_test_file_metrics();
let score = ranker.calculate_composite_score(&metrics);
assert_eq!(score.function_count, 3); assert_eq!(score.cyclomatic_max, 15); assert!((score.cognitive_avg - 12.333333333333334).abs() < 0.001); assert!(score.total_score > 0.0);
}
#[test]
fn test_complexity_ranker_calculate_composite_score_empty() {
let ranker = ComplexityRanker::default();
let metrics = FileComplexityMetrics {
path: "empty.rs".to_string(),
total_complexity: ComplexityMetrics::default(),
functions: vec![],
classes: vec![],
};
let score = ranker.calculate_composite_score(&metrics);
assert_eq!(score, CompositeComplexityScore::default());
}
#[tokio::test]
async fn test_ranking_engine_with_temp_files() {
let temp_dir = TempDir::new().unwrap();
let file1 = temp_dir.path().join("small.rs");
let file2 = temp_dir.path().join("large.rs");
{
use std::io::BufWriter;
let f1 = File::create(&file1).unwrap();
let mut writer = BufWriter::new(f1);
writeln!(writer, "fn small() {{}}").unwrap();
}
{
use std::io::BufWriter;
let f2 = File::create(&file2).unwrap();
let mut writer = BufWriter::new(f2);
writeln!(writer, "fn large() {{ // This is a much longer file").unwrap();
for _ in 0..100 {
writeln!(writer, " println!(\"line\");").unwrap();
}
writeln!(writer, "}}").unwrap();
}
let ranker = ComplexityRanker::default();
let engine = RankingEngine::new(ranker);
let files = vec![file1, file2];
let rankings = engine.rank_files(&files, 2).await;
assert_eq!(rankings.len(), 2);
assert!(rankings[0].1.total_score >= rankings[1].1.total_score);
}
#[tokio::test]
async fn test_ranking_engine_zero_limit() {
let ranker = ComplexityRanker::default();
let engine = RankingEngine::new(ranker);
let files = vec![PathBuf::from("test.rs")];
let rankings = engine.rank_files(&files, 0).await;
assert_eq!(rankings.len(), 0);
}
#[tokio::test]
async fn test_ranking_engine_cache() {
let temp_dir = TempDir::new().unwrap();
let file1 = temp_dir.path().join("test.rs");
let mut f1 = File::create(&file1).unwrap();
writeln!(f1, "fn test() {{}}").unwrap();
let ranker = ComplexityRanker::default();
let engine = RankingEngine::new(ranker);
let files = vec![file1.clone()];
let rankings1 = engine.rank_files(&files, 1).await;
let rankings2 = engine.rank_files(&files, 1).await;
assert_eq!(rankings1.len(), 1);
assert_eq!(rankings2.len(), 1);
assert_eq!(rankings1[0].1.total_score, rankings2[0].1.total_score);
engine.clear_cache();
let rankings3 = engine.rank_files(&files, 1).await;
assert_eq!(rankings3.len(), 1);
}
#[test]
fn test_ranking_engine_format_rankings_table_empty() {
let ranker = ComplexityRanker::default();
let engine = RankingEngine::new(ranker);
let rankings = vec![];
let output = engine.format_rankings_table(&rankings);
assert!(output.contains("No files found"));
}
#[test]
fn test_ranking_engine_format_rankings_table() {
let ranker = ComplexityRanker::default();
let engine = RankingEngine::new(ranker);
let rankings = vec![
(
"test1.rs".to_string(),
CompositeComplexityScore {
total_score: 10.0,
function_count: 5,
cyclomatic_max: 8,
cognitive_avg: 12.0,
halstead_effort: 150.0,
},
),
(
"test2.rs".to_string(),
CompositeComplexityScore {
total_score: 5.0,
function_count: 2,
cyclomatic_max: 3,
cognitive_avg: 4.0,
halstead_effort: 50.0,
},
),
];
let output = engine.format_rankings_table(&rankings);
assert!(output.contains("Top 2 Complexity Files"));
assert!(output.contains("test1.rs"));
assert!(output.contains("test2.rs"));
assert!(output.contains("10.0"));
assert!(output.contains("5.0"));
}
#[test]
fn test_ranking_engine_format_rankings_json() {
let ranker = ComplexityRanker::default();
let engine = RankingEngine::new(ranker);
let rankings = vec![(
"test1.rs".to_string(),
CompositeComplexityScore {
total_score: 10.0,
..Default::default()
},
)];
let json = engine.format_rankings_json(&rankings);
assert_eq!(json["analysis_type"], "Complexity");
assert_eq!(json["top_files"]["requested"], 1);
assert_eq!(json["rankings"][0]["rank"], 1);
assert_eq!(json["rankings"][0]["file"], "test1.rs");
}
#[test]
fn test_complexity_ranker_compute_score_rust_file() {
let temp_dir = TempDir::new().unwrap();
let rust_file = temp_dir.path().join("test.rs");
let mut f = File::create(&rust_file).unwrap();
writeln!(f, "fn test() {{ println!(\"hello\"); }}").unwrap();
let ranker = ComplexityRanker::default();
let score = ranker.compute_score(&rust_file);
assert!(score.total_score > 0.0);
}
#[test]
fn test_complexity_ranker_compute_score_javascript_file() {
let temp_dir = TempDir::new().unwrap();
let js_file = temp_dir.path().join("test.js");
let mut f = File::create(&js_file).unwrap();
writeln!(f, "function test() {{ console.log('hello'); }}").unwrap();
let ranker = ComplexityRanker::default();
let score = ranker.compute_score(&js_file);
assert!(score.total_score > 0.0);
}
#[test]
fn test_complexity_ranker_compute_score_python_file() {
let temp_dir = TempDir::new().unwrap();
let py_file = temp_dir.path().join("test.py");
let mut f = File::create(&py_file).unwrap();
writeln!(f, "def test():\n print('hello')").unwrap();
let ranker = ComplexityRanker::default();
let score = ranker.compute_score(&py_file);
assert!(score.total_score > 0.0);
}
#[test]
fn test_complexity_ranker_compute_score_unknown_file() {
let temp_dir = TempDir::new().unwrap();
let unknown_file = temp_dir.path().join("test.txt");
let mut f = File::create(&unknown_file).unwrap();
writeln!(f, "hello world").unwrap();
let ranker = ComplexityRanker::default();
let score = ranker.compute_score(&unknown_file);
assert_eq!(score, CompositeComplexityScore::default());
}
#[test]
fn test_complexity_ranker_compute_score_nonexistent_file() {
let ranker = ComplexityRanker::default();
let score = ranker.compute_score(Path::new("/nonexistent/file.rs"));
assert_eq!(score, CompositeComplexityScore::default());
}
#[test]
fn test_complexity_ranker_format_ranking_entry() {
let ranker = ComplexityRanker::default();
let metric = CompositeComplexityScore {
total_score: 42.5,
function_count: 10,
cyclomatic_max: 15,
cognitive_avg: 8.7,
halstead_effort: 123.4,
};
let output = ranker.format_ranking_entry("test.rs", &metric, 1);
assert!(output.contains("1"));
assert!(output.contains("test.rs"));
assert!(output.contains("42.5"));
assert!(output.contains("10"));
assert!(output.contains("15"));
assert!(output.contains("8.7"));
assert!(output.contains("123.4"));
}
#[test]
fn test_rank_files_by_complexity() {
let metrics = vec![
FileComplexityMetrics {
path: "simple.rs".to_string(),
total_complexity: ComplexityMetrics::new(1, 1, 0, 5),
functions: vec![FunctionComplexity {
name: "simple".to_string(),
line_start: 1,
line_end: 5,
metrics: ComplexityMetrics::new(1, 1, 0, 5),
}],
classes: vec![],
},
create_test_file_metrics(), ];
let ranker = ComplexityRanker::default();
let rankings = rank_files_by_complexity(&metrics, 2, &ranker);
assert_eq!(rankings.len(), 2);
assert_eq!(rankings[0].0, "test.rs");
assert_eq!(rankings[1].0, "simple.rs");
assert!(rankings[0].1.total_score > rankings[1].1.total_score);
}
#[test]
fn test_rank_files_by_complexity_with_limit() {
let metrics = vec![create_test_file_metrics()];
let ranker = ComplexityRanker::default();
let rankings = rank_files_by_complexity(&metrics, 0, &ranker);
assert_eq!(rankings.len(), 1);
let rankings_limited = rank_files_by_complexity(&metrics, 1, &ranker);
assert_eq!(rankings_limited.len(), 1);
}
#[test]
fn test_rank_files_by_complexity_empty() {
let metrics = vec![];
let ranker = ComplexityRanker::default();
let rankings = rank_files_by_complexity(&metrics, 5, &ranker);
assert_eq!(rankings.len(), 0);
}
#[tokio::test]
async fn test_ranking_engine_with_nonexistent_files() {
let ranker = ComplexityRanker::default();
let engine = RankingEngine::new(ranker);
let files = vec![
PathBuf::from("/nonexistent/file1.rs"),
PathBuf::from("/nonexistent/file2.rs"),
];
let rankings = engine.rank_files(&files, 5).await;
assert_eq!(rankings.len(), 0); }
#[tokio::test]
async fn test_ranking_engine_mixed_existing_nonexistent() {
let temp_dir = TempDir::new().unwrap();
let existing_file = temp_dir.path().join("exists.rs");
let mut f = File::create(&existing_file).unwrap();
writeln!(f, "fn test() {{}}").unwrap();
let ranker = ComplexityRanker::default();
let engine = RankingEngine::new(ranker);
let files = vec![existing_file, PathBuf::from("/nonexistent/file.rs")];
let rankings = engine.rank_files(&files, 5).await;
assert_eq!(rankings.len(), 1); }
struct TestRanker {
score_multiplier: f64,
}
impl FileRanker for TestRanker {
type Metric = f64;
fn compute_score(&self, file_path: &Path) -> Self::Metric {
file_path.to_string_lossy().len() as f64 * self.score_multiplier
}
fn format_ranking_entry(&self, file: &str, metric: &Self::Metric, rank: usize) -> String {
format!("{rank}. {file} ({metric})")
}
fn ranking_type(&self) -> &'static str {
"Test"
}
}
#[tokio::test]
async fn test_custom_ranker() {
let temp_dir = TempDir::new().unwrap();
let file1 = temp_dir.path().join("a.rs");
let file2 = temp_dir.path().join("longer_name.rs");
File::create(&file1).unwrap();
File::create(&file2).unwrap();
let ranker = TestRanker {
score_multiplier: 2.0,
};
let engine = RankingEngine::new(ranker);
let files = vec![file1, file2];
let rankings = engine.rank_files(&files, 2).await;
assert_eq!(rankings.len(), 2);
assert!(rankings[0].0.contains("longer_name"));
assert!(rankings[0].1 > rankings[1].1);
}
#[test]
fn test_all_score_types_partial_ord() {
let comp1 = CompositeComplexityScore {
total_score: 5.0,
..Default::default()
};
let comp2 = CompositeComplexityScore {
total_score: 10.0,
..Default::default()
};
assert!(comp1 < comp2);
let churn1 = ChurnScore {
score: 3.0,
..Default::default()
};
let churn2 = ChurnScore {
score: 7.0,
..Default::default()
};
assert!(churn1 < churn2);
let dup1 = DuplicationScore {
score: 2.0,
..Default::default()
};
let dup2 = DuplicationScore {
score: 8.0,
..Default::default()
};
assert!(dup1 < dup2);
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}