#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComplexityTier {
Simple, Moderate, High, Extreme, }
impl ComplexityTier {
pub fn from_cyclomatic(cyclo: u32) -> Self {
match cyclo {
0..=10 => ComplexityTier::Simple,
11..=30 => ComplexityTier::Moderate,
31..=50 => ComplexityTier::High,
_ => ComplexityTier::Extreme,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TestRecommendation {
pub count: u32,
pub formula_used: String,
pub rationale: String,
}
pub fn calculate_tests_needed(
cyclomatic: u32,
coverage_percent: f64,
tier: Option<ComplexityTier>,
) -> TestRecommendation {
let tier = tier.unwrap_or_else(|| ComplexityTier::from_cyclomatic(cyclomatic));
let coverage_gap = 1.0 - coverage_percent;
if coverage_percent >= 1.0 {
return TestRecommendation {
count: 0,
formula_used: "fully_covered".to_string(),
rationale: "Function has full coverage".to_string(),
};
}
let (count, formula, rationale) = match tier {
ComplexityTier::Simple => {
let tests = (cyclomatic as f64 * coverage_gap).ceil() as u32;
let tests = tests.max(2); (
tests,
format!(
"cyclomatic × coverage_gap = {} × {:.2} = {}",
cyclomatic, coverage_gap, tests
),
"Simple functions: one test per execution path".to_string(),
)
}
ComplexityTier::Moderate => {
let ideal_tests = (cyclomatic as f64).sqrt() * 1.5 + 2.0;
let current_tests = ideal_tests * coverage_percent;
let needed = (ideal_tests - current_tests).ceil() as u32;
(
needed,
format!(
"sqrt(cyclo) × 1.5 + 2 - current = sqrt({}) × 1.5 + 2 - {:.1} = {}",
cyclomatic, current_tests, needed
),
"Moderate functions: tests cover overlapping paths via shared conditions"
.to_string(),
)
}
ComplexityTier::High => {
let tests = (cyclomatic as f64 * coverage_gap).ceil() as u32;
let tests = tests.max(3); (
tests,
format!(
"cyclomatic × coverage_gap = {} × {:.2} = {}",
cyclomatic, coverage_gap, tests
),
"High complexity: linear formula (conservative approach for independent paths)"
.to_string(),
)
}
ComplexityTier::Extreme => {
let structural_tests = ((cyclomatic as f64).sqrt() * 1.5 + 2.0).ceil() as u32;
let property_tests = 3; (
structural_tests + property_tests,
format!(
"{} structural + {} property-based test suites",
structural_tests, property_tests
),
"Extreme complexity: combine structural and property-based testing".to_string(),
)
}
};
TestRecommendation {
count,
formula_used: formula,
rationale,
}
}
fn extract_test_count(text: &str) -> Option<u32> {
use regex::Regex;
let patterns = vec![
r"(?i)add\s+(\d+)\s+tests?",
r"(?i)write\s+(\d+)\s+tests?",
r"(?i)need\s+(\d+)\s+tests?",
r"(?i)(\d+)\s+tests?\s+(?:to|for)",
];
for pattern_str in patterns {
if let Ok(re) = Regex::new(pattern_str) {
if let Some(captures) = re.captures(text) {
if let Some(count_str) = captures.get(1) {
if let Ok(count) = count_str.as_str().parse::<u32>() {
return Some(count);
}
}
}
}
}
None
}
#[cfg(debug_assertions)]
pub fn validate_recommendation_consistency(action: &str, steps: &[String]) -> Result<(), String> {
let action_count = extract_test_count(action);
let step_counts: Vec<u32> = steps.iter().filter_map(|s| extract_test_count(s)).collect();
if action_count.is_none() && step_counts.is_empty() {
return Ok(());
}
if action_count.is_some() && step_counts.is_empty() {
return Err(format!(
"ACTION mentions {} tests but steps don't mention any test count",
action_count.unwrap()
));
}
if action_count.is_none() && !step_counts.is_empty() {
return Err(format!(
"Steps mention {} tests but ACTION doesn't mention any test count",
step_counts[0]
));
}
if let Some(action_num) = action_count {
for step_num in step_counts {
if action_num != step_num {
return Err(format!(
"Test count mismatch: ACTION says {} tests but steps say {} tests\n\
ACTION: {}\n\
This is the bug from spec 109 - all test counts must be consistent!",
action_num, step_num, action
));
}
}
}
Ok(())
}
#[cfg(not(debug_assertions))]
pub fn validate_recommendation_consistency(_action: &str, _steps: &[String]) -> Result<(), String> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_complexity_tier_from_cyclomatic() {
assert_eq!(ComplexityTier::from_cyclomatic(5), ComplexityTier::Simple);
assert_eq!(ComplexityTier::from_cyclomatic(10), ComplexityTier::Simple);
assert_eq!(
ComplexityTier::from_cyclomatic(11),
ComplexityTier::Moderate
);
assert_eq!(
ComplexityTier::from_cyclomatic(30),
ComplexityTier::Moderate
);
assert_eq!(ComplexityTier::from_cyclomatic(31), ComplexityTier::High);
assert_eq!(ComplexityTier::from_cyclomatic(50), ComplexityTier::High);
assert_eq!(ComplexityTier::from_cyclomatic(51), ComplexityTier::Extreme);
}
#[test]
fn test_simple_function_linear_calculation() {
let result = calculate_tests_needed(5, 0.6, None);
assert_eq!(result.count, 2); assert!(result.formula_used.contains("cyclomatic × coverage_gap"));
assert!(result.rationale.contains("Simple functions"));
}
#[test]
fn test_moderate_function_sqrt_calculation() {
let result = calculate_tests_needed(20, 0.5, None);
assert!(result.count >= 4 && result.count <= 5);
assert!(result.formula_used.contains("sqrt"));
assert!(result.rationale.contains("Moderate functions"));
}
#[test]
fn test_extreme_complexity_case_cyclo_33() {
let result = calculate_tests_needed(33, 0.661, None);
assert_eq!(
result.count, 12,
"Bug fix: cyclo=33 with 66.1% coverage should need ~11-12 tests, not 3"
);
assert!(result.formula_used.contains("cyclomatic × coverage_gap"));
assert!(result.rationale.contains("High complexity"));
}
#[test]
fn test_all_tiers_produce_consistent_results() {
let test_cases = vec![
(5, 0.8, ComplexityTier::Simple),
(15, 0.6, ComplexityTier::Moderate),
(33, 0.661, ComplexityTier::High),
(60, 0.5, ComplexityTier::Extreme),
];
for (cyclo, coverage, tier) in test_cases {
let result1 = calculate_tests_needed(cyclo, coverage, Some(tier));
let result2 = calculate_tests_needed(cyclo, coverage, Some(tier));
assert_eq!(
result1.count, result2.count,
"Non-deterministic calculation for cyclo={}",
cyclo
);
assert_eq!(result1.formula_used, result2.formula_used);
assert_eq!(result1.rationale, result2.rationale);
}
}
#[test]
fn test_full_coverage_returns_zero() {
let result = calculate_tests_needed(20, 1.0, None);
assert_eq!(result.count, 0);
assert_eq!(result.formula_used, "fully_covered");
assert!(result.rationale.contains("full coverage"));
}
#[test]
fn test_minimum_two_tests_for_simple() {
let result = calculate_tests_needed(2, 0.0, Some(ComplexityTier::Simple));
assert!(
result.count >= 2,
"Should always recommend at least 2 tests for simple functions"
);
}
#[test]
fn test_zero_coverage_simple() {
let result = calculate_tests_needed(5, 0.0, None);
assert_eq!(result.count, 5); }
#[test]
fn test_zero_coverage_moderate() {
let result = calculate_tests_needed(20, 0.0, None);
assert!(result.count >= 8 && result.count <= 9);
}
#[test]
fn test_zero_coverage_high() {
let result = calculate_tests_needed(35, 0.0, None);
assert_eq!(result.count, 35);
}
#[test]
fn test_extreme_complexity_recommends_property_testing() {
let result = calculate_tests_needed(60, 0.5, None);
assert!(result.rationale.contains("property-based"));
assert!(result.formula_used.contains("property-based test suites"));
}
#[test]
fn test_boundary_at_tier_transitions() {
let simple_10 = calculate_tests_needed(10, 0.5, None);
let moderate_11 = calculate_tests_needed(11, 0.5, None);
assert_eq!(simple_10.count, 5);
assert!(moderate_11.count >= 3 && moderate_11.count <= 4);
let moderate_30 = calculate_tests_needed(30, 0.5, None);
let high_31 = calculate_tests_needed(31, 0.5, None);
assert!(moderate_30.count >= 4 && moderate_30.count <= 6);
assert_eq!(high_31.count, 16);
}
#[test]
fn test_high_coverage_small_gap() {
let result = calculate_tests_needed(20, 0.95, None);
assert!(result.count <= 1);
}
#[test]
#[cfg(debug_assertions)]
fn test_validate_consistency_matching_counts() {
let action = "Add 11 tests for 34% coverage gap";
let steps = vec![
"Step 1: Analyze uncovered branches".to_string(),
"Step 2: Write 11 tests to cover critical paths".to_string(),
];
assert!(validate_recommendation_consistency(action, &steps).is_ok());
}
#[test]
#[cfg(debug_assertions)]
fn test_validate_consistency_mismatched_counts() {
let action = "Add 3 tests for 34% coverage gap";
let steps = vec![
"Step 1: Analyze branches".to_string(),
"Step 2: Write 11 tests to cover uncovered branches".to_string(),
];
let result = validate_recommendation_consistency(action, &steps);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("3 tests"));
assert!(err.contains("11 tests"));
assert!(err.contains("spec 109"));
}
#[test]
#[cfg(debug_assertions)]
fn test_validate_consistency_no_counts() {
let action = "Refactor for better maintainability";
let steps = vec!["Step 1: Extract helper functions".to_string()];
assert!(validate_recommendation_consistency(action, &steps).is_ok());
}
#[test]
#[cfg(debug_assertions)]
fn test_validate_consistency_action_only() {
let action = "Add 5 tests for coverage";
let steps = vec!["Step 1: Refactor the function".to_string()];
let result = validate_recommendation_consistency(action, &steps);
assert!(result.is_err());
assert!(result.unwrap_err().contains("ACTION mentions 5 tests"));
}
#[test]
#[cfg(debug_assertions)]
fn test_validate_consistency_steps_only() {
let action = "Improve test coverage";
let steps = vec!["Step 1: Write 8 tests for edge cases".to_string()];
let result = validate_recommendation_consistency(action, &steps);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Steps mention 8 tests"));
}
#[test]
fn test_extract_test_count_various_patterns() {
assert_eq!(extract_test_count("Add 11 tests for coverage"), Some(11));
assert_eq!(extract_test_count("Write 5 tests to cover"), Some(5));
assert_eq!(extract_test_count("Need 3 tests for this"), Some(3));
assert_eq!(extract_test_count("15 tests to cover branches"), Some(15));
assert_eq!(extract_test_count("Add 1 test for edge case"), Some(1));
assert_eq!(extract_test_count("No tests mentioned here"), None);
}
}