use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[cfg(test)]
use proptest::prelude::*;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TDGScore {
pub value: f64,
pub components: TDGComponents,
pub severity: TDGSeverity,
pub percentile: f64,
pub confidence: f64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub struct TDGComponents {
pub complexity: f64,
pub churn: f64,
pub coupling: f64,
pub domain_risk: f64,
pub duplication: f64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum TDGSeverity {
Normal,
Warning,
Critical,
}
impl From<f64> for TDGSeverity {
fn from(value: f64) -> Self {
if value > 2.5 {
TDGSeverity::Critical
} else if value > 1.5 {
TDGSeverity::Warning
} else {
TDGSeverity::Normal
}
}
}
impl TDGSeverity {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
TDGSeverity::Normal => "normal",
TDGSeverity::Warning => "warning",
TDGSeverity::Critical => "critical",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TDGConfig {
pub complexity_weight: f64,
pub churn_weight: f64,
pub coupling_weight: f64,
pub domain_risk_weight: f64,
pub duplication_weight: f64,
pub critical_threshold: f64,
pub warning_threshold: f64,
}
impl Default for TDGConfig {
fn default() -> Self {
Self {
complexity_weight: 0.30,
churn_weight: 0.35,
coupling_weight: 0.15,
domain_risk_weight: 0.10,
duplication_weight: 0.10,
critical_threshold: 2.5,
warning_threshold: 1.5,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TDGSummary {
pub total_files: usize,
pub critical_files: usize,
pub warning_files: usize,
pub average_tdg: f64,
pub p95_tdg: f64,
pub p99_tdg: f64,
pub estimated_debt_hours: f64,
pub hotspots: Vec<TDGHotspot>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TDGHotspot {
pub path: String,
pub tdg_score: f64,
pub primary_factor: String,
pub estimated_hours: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TDGAnalysis {
pub score: TDGScore,
pub explanation: String,
pub recommendations: Vec<TDGRecommendation>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TDGRecommendation {
pub recommendation_type: RecommendationType,
pub action: String,
pub expected_reduction: f64,
pub estimated_hours: f64,
pub priority: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RecommendationType {
ReduceComplexity,
StabilizeChurn,
ReduceCoupling,
AddressDomainRisk,
RemoveDuplication,
SplitFile,
AddTests,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TDGDistribution {
pub buckets: Vec<TDGBucket>,
pub total_files: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TDGBucket {
pub min: f64,
pub max: f64,
pub count: usize,
pub percentage: f64,
}
#[cfg(test)]
mod property_tests {
use super::*;
prop_compose! {
fn valid_tdg_score()(
value in 0.0..10.0,
complexity in 0.0..5.0,
churn in 0.0..5.0,
coupling in 0.0..5.0,
domain_risk in 0.0..5.0,
duplication in 0.0..5.0,
percentile in 0.0..100.0,
confidence in 0.0..1.0
) -> TDGScore {
TDGScore {
value,
components: TDGComponents {
complexity,
churn,
coupling,
domain_risk,
duplication,
},
severity: if value > 2.5 { TDGSeverity::Critical }
else if value > 1.5 { TDGSeverity::Warning }
else { TDGSeverity::Normal },
percentile,
confidence,
}
}
}
proptest! {
#[test]
fn tdg_score_roundtrip_serialization(score in valid_tdg_score()) {
let json = serde_json::to_string(&score)?;
let deserialized: TDGScore = serde_json::from_str(&json)?;
prop_assert_eq!(score, deserialized);
}
#[test]
fn tdg_severity_matches_value(score in valid_tdg_score()) {
match score.severity {
TDGSeverity::Normal => prop_assert!(score.value <= 1.5),
TDGSeverity::Warning => prop_assert!(score.value > 1.5 && score.value <= 2.5),
TDGSeverity::Critical => prop_assert!(score.value > 2.5),
}
}
#[test]
fn tdg_percentile_in_valid_range(score in valid_tdg_score()) {
prop_assert!(score.percentile >= 0.0 && score.percentile <= 100.0);
}
#[test]
fn tdg_confidence_in_valid_range(score in valid_tdg_score()) {
prop_assert!(score.confidence >= 0.0 && score.confidence <= 1.0);
}
#[test]
fn tdg_components_non_negative(score in valid_tdg_score()) {
prop_assert!(score.components.complexity >= 0.0);
prop_assert!(score.components.churn >= 0.0);
prop_assert!(score.components.coupling >= 0.0);
prop_assert!(score.components.domain_risk >= 0.0);
prop_assert!(score.components.duplication >= 0.0);
}
#[test]
fn tdg_severity_ordering_consistent(value in 0.0..10.0) {
let severity = if value > 2.5 { TDGSeverity::Critical }
else if value > 1.5 { TDGSeverity::Warning }
else { TDGSeverity::Normal };
match severity {
TDGSeverity::Normal => prop_assert!(value <= 1.5),
TDGSeverity::Warning => prop_assert!(value > 1.5 && value <= 2.5),
TDGSeverity::Critical => prop_assert!(value > 2.5),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tdg_severity_from_value() {
assert_eq!(TDGSeverity::from(0.5), TDGSeverity::Normal);
assert_eq!(TDGSeverity::from(1.5), TDGSeverity::Normal);
assert_eq!(TDGSeverity::from(1.6), TDGSeverity::Warning);
assert_eq!(TDGSeverity::from(2.5), TDGSeverity::Warning);
assert_eq!(TDGSeverity::from(2.6), TDGSeverity::Critical);
assert_eq!(TDGSeverity::from(5.0), TDGSeverity::Critical);
}
#[test]
fn test_tdg_config_default() {
let config = TDGConfig::default();
let total_weight = config.complexity_weight
+ config.churn_weight
+ config.coupling_weight
+ config.domain_risk_weight
+ config.duplication_weight;
assert!((total_weight - 1.0).abs() < f64::EPSILON);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SatdItem {
pub file_path: PathBuf,
pub line_number: usize,
pub comment_text: String,
pub debt_type: String,
pub severity: SatdSeverity,
pub confidence: f64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum SatdSeverity {
Low,
Medium,
High,
Critical,
}
#[cfg(test)]
mod new_tests {
use super::*;
#[test]
fn test_tdg_score_creation() {
let components = TDGComponents {
complexity: 1.5,
churn: 0.8,
coupling: 0.3,
domain_risk: 0.2,
duplication: 0.4,
};
let score = TDGScore {
value: 3.2,
components,
severity: TDGSeverity::Warning,
percentile: 75.0,
confidence: 0.9,
};
assert_eq!(score.value, 3.2);
assert_eq!(score.components.complexity, 1.5);
assert_eq!(score.severity, TDGSeverity::Warning);
assert_eq!(score.percentile, 75.0);
assert_eq!(score.confidence, 0.9);
}
#[test]
fn test_tdg_severity_ordering() {
assert_eq!(TDGSeverity::Normal, TDGSeverity::Normal);
assert_eq!(TDGSeverity::Warning, TDGSeverity::Warning);
assert_eq!(TDGSeverity::Critical, TDGSeverity::Critical);
assert_eq!(TDGSeverity::Normal, TDGSeverity::Normal);
assert_ne!(TDGSeverity::Normal, TDGSeverity::Critical);
}
#[test]
fn test_tdg_components_equality() {
let comp1 = TDGComponents {
complexity: 1.0,
churn: 2.0,
coupling: 3.0,
domain_risk: 4.0,
duplication: 5.0,
};
let comp2 = TDGComponents {
complexity: 1.0,
churn: 2.0,
coupling: 3.0,
domain_risk: 4.0,
duplication: 5.0,
};
assert_eq!(comp1, comp2);
}
#[test]
fn test_tdg_summary_creation() {
let summary = TDGSummary {
total_files: 95,
critical_files: 10,
warning_files: 20,
average_tdg: 2.5,
p95_tdg: 4.5,
p99_tdg: 4.9,
estimated_debt_hours: 120.0,
hotspots: vec![],
};
assert_eq!(summary.total_files, 95);
assert_eq!(summary.critical_files, 10);
assert_eq!(summary.warning_files, 20);
assert_eq!(summary.average_tdg, 2.5);
assert_eq!(summary.p95_tdg, 4.5);
assert_eq!(summary.p99_tdg, 4.9);
assert_eq!(summary.estimated_debt_hours, 120.0);
}
#[test]
fn test_tdg_hotspot() {
let hotspot = TDGHotspot {
path: "src/complex.rs".to_string(),
tdg_score: 4.5,
primary_factor: "High complexity and churn".to_string(),
estimated_hours: 8.0,
};
assert_eq!(hotspot.path, "src/complex.rs");
assert_eq!(hotspot.tdg_score, 4.5);
assert_eq!(hotspot.primary_factor, "High complexity and churn");
assert_eq!(hotspot.estimated_hours, 8.0);
}
#[test]
fn test_tdg_analysis() {
let analysis = TDGAnalysis {
score: TDGScore {
value: 2.5,
components: TDGComponents {
complexity: 0.8,
churn: 0.5,
coupling: 0.4,
domain_risk: 0.3,
duplication: 0.5,
},
severity: TDGSeverity::Warning,
percentile: 75.0,
confidence: 0.95,
},
explanation: "Test explanation".to_string(),
recommendations: vec![],
};
assert_eq!(analysis.score.value, 2.5);
assert_eq!(analysis.score.percentile, 75.0);
assert!(analysis.recommendations.is_empty());
}
#[test]
fn test_recommendation_type() {
assert_eq!(
RecommendationType::ReduceComplexity,
RecommendationType::ReduceComplexity
);
assert_ne!(
RecommendationType::ReduceComplexity,
RecommendationType::StabilizeChurn
);
let rec = TDGRecommendation {
recommendation_type: RecommendationType::ReduceComplexity,
action: "Refactor complex function into smaller units".to_string(),
expected_reduction: 0.5,
estimated_hours: 4.0,
priority: 3,
};
assert_eq!(rec.priority, 3);
assert_eq!(rec.action, "Refactor complex function into smaller units");
assert_eq!(rec.expected_reduction, 0.5);
}
#[test]
fn test_tdg_distribution() {
let dist = TDGDistribution {
buckets: vec![
TDGBucket {
min: 0.0,
max: 1.5,
count: 50,
percentage: 50.0,
},
TDGBucket {
min: 1.5,
max: 3.0,
count: 30,
percentage: 30.0,
},
TDGBucket {
min: 3.0,
max: 5.0,
count: 20,
percentage: 20.0,
},
],
total_files: 100,
};
assert_eq!(dist.buckets.len(), 3);
assert_eq!(dist.buckets[0].count, 50);
assert_eq!(dist.buckets[0].percentage, 50.0);
assert_eq!(dist.total_files, 100);
}
#[test]
fn test_tdg_config() {
let config = TDGConfig {
complexity_weight: 0.30,
churn_weight: 0.35,
coupling_weight: 0.15,
domain_risk_weight: 0.10,
duplication_weight: 0.10,
critical_threshold: 2.5,
warning_threshold: 1.5,
};
assert_eq!(config.complexity_weight, 0.30);
assert_eq!(config.churn_weight, 0.35);
assert_eq!(config.critical_threshold, 2.5);
assert_eq!(config.warning_threshold, 1.5);
}
#[test]
fn test_satd_item_creation() {
let item = SatdItem {
file_path: PathBuf::from("lib.rs"),
line_number: 123,
comment_text: "TODO: Fix this hack".to_string(),
debt_type: "TODO".to_string(),
severity: SatdSeverity::Medium,
confidence: 0.95,
};
assert_eq!(item.file_path, PathBuf::from("lib.rs"));
assert_eq!(item.line_number, 123);
assert_eq!(item.comment_text, "TODO: Fix this hack");
assert_eq!(item.debt_type, "TODO");
assert_eq!(item.severity, SatdSeverity::Medium);
assert_eq!(item.confidence, 0.95);
}
#[test]
fn test_satd_severity_ordering() {
assert!(SatdSeverity::Low < SatdSeverity::Medium);
assert!(SatdSeverity::Medium < SatdSeverity::High);
assert!(SatdSeverity::High < SatdSeverity::Critical);
let mut severities = vec![
SatdSeverity::High,
SatdSeverity::Low,
SatdSeverity::Critical,
SatdSeverity::Medium,
];
severities.sort();
assert_eq!(
severities,
vec![
SatdSeverity::Low,
SatdSeverity::Medium,
SatdSeverity::High,
SatdSeverity::Critical,
]
);
}
#[test]
fn test_serialization_roundtrip() {
let original = TDGScore {
value: 3.14,
components: TDGComponents {
complexity: 1.1,
churn: 2.2,
coupling: 3.3,
domain_risk: 4.4,
duplication: 5.5,
},
severity: TDGSeverity::Critical,
percentile: 90.0,
confidence: 0.99,
};
let json = serde_json::to_string(&original).unwrap();
let deserialized: TDGScore = serde_json::from_str(&json).unwrap();
assert_eq!(original, deserialized);
assert_eq!(original.value, deserialized.value);
assert_eq!(original.components, deserialized.components);
assert_eq!(original.severity, deserialized.severity);
}
}