use crate::priority::call_graph::{CallGraph, FunctionId};
use crate::risk::lcov::LcovData;
use serde::{Deserialize, Serialize};
use std::collections::HashSet as StdHashSet;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransitiveCoverage {
pub direct: f64,
pub transitive: f64,
pub propagated_from: Vec<FunctionId>,
pub uncovered_lines: Vec<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompleteCoverage {
pub direct_coverage: f64,
pub indirect_coverage: f64,
pub effective_coverage: f64,
pub coverage_sources: Vec<CoverageSource>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoverageSource {
pub caller: FunctionId,
pub caller_coverage: f64,
pub distance: u32,
pub contributed_coverage: f64,
}
pub fn calculate_transitive_coverage(
func_id: &FunctionId,
call_graph: &CallGraph,
coverage: &LcovData,
) -> TransitiveCoverage {
let direct = get_function_coverage(func_id, coverage);
let uncovered_lines = get_uncovered_lines(func_id, coverage);
if direct > 0.0 {
return TransitiveCoverage {
direct,
transitive: direct,
propagated_from: vec![],
uncovered_lines,
};
}
let callees = call_graph.get_callees(func_id);
if callees.is_empty() {
return TransitiveCoverage {
direct: 0.0,
transitive: 0.0,
propagated_from: vec![],
uncovered_lines,
};
}
let mut covered_callees = Vec::new();
for callee in &callees {
let callee_coverage = get_function_coverage(callee, coverage);
if callee_coverage > 0.8 {
covered_callees.push(callee.clone());
}
}
let transitive = if callees.is_empty() {
0.0
} else {
covered_callees.len() as f64 / callees.len() as f64
};
TransitiveCoverage {
direct,
transitive,
propagated_from: covered_callees,
uncovered_lines,
}
}
fn get_function_coverage(func_id: &FunctionId, coverage: &LcovData) -> f64 {
coverage
.get_function_coverage_with_line(&func_id.file, &func_id.name, func_id.line)
.unwrap_or(0.0)
}
fn get_uncovered_lines(func_id: &FunctionId, coverage: &LcovData) -> Vec<usize> {
coverage
.get_function_uncovered_lines(&func_id.file, &func_id.name, func_id.line)
.unwrap_or_default()
}
pub fn calculate_coverage_urgency(
func_id: &FunctionId,
call_graph: &CallGraph,
coverage: &LcovData,
complexity: u32,
) -> f64 {
let transitive_cov = calculate_transitive_coverage(func_id, call_graph, coverage);
let coverage_weight = 0.7; let effective_coverage = transitive_cov.direct * coverage_weight
+ transitive_cov.transitive * (1.0 - coverage_weight);
let coverage_gap = 1.0 - effective_coverage.clamp(0.0, 1.0);
let complexity_weight = if complexity == 0 {
0.5
} else {
(((complexity as f64 + 1.0).ln() / 3.0) + 0.5).min(2.0)
};
coverage_gap * complexity_weight * 10.0
}
pub fn propagate_coverage_through_graph(
call_graph: &CallGraph,
coverage: &LcovData,
) -> im::HashMap<FunctionId, TransitiveCoverage> {
let mut result = im::HashMap::new();
for func_id in call_graph.find_all_functions() {
let transitive = calculate_transitive_coverage(&func_id, call_graph, coverage);
result.insert(func_id, transitive);
}
result
}
pub fn calculate_indirect_coverage(
func_id: &FunctionId,
call_graph: &CallGraph,
coverage: &LcovData,
) -> CompleteCoverage {
let direct_coverage = get_function_coverage(func_id, coverage);
if direct_coverage >= 0.8 {
return CompleteCoverage {
direct_coverage,
indirect_coverage: 0.0,
effective_coverage: direct_coverage,
coverage_sources: vec![],
};
}
let callers = call_graph.get_callers(func_id);
if callers.is_empty() {
return CompleteCoverage {
direct_coverage,
indirect_coverage: 0.0,
effective_coverage: direct_coverage,
coverage_sources: vec![],
};
}
let sources =
analyze_caller_coverage(&callers, call_graph, coverage, 0, &mut StdHashSet::new());
let indirect_coverage = aggregate_indirect_coverage(&sources);
let effective_coverage = combine_coverages(direct_coverage, indirect_coverage);
CompleteCoverage {
direct_coverage,
indirect_coverage,
effective_coverage,
coverage_sources: sources,
}
}
fn analyze_caller_coverage(
callers: &[FunctionId],
call_graph: &CallGraph,
coverage: &LcovData,
depth: u32,
visited: &mut StdHashSet<FunctionId>,
) -> Vec<CoverageSource> {
const MAX_DEPTH: u32 = 3;
const DISTANCE_DISCOUNT: f64 = 0.7;
if depth >= MAX_DEPTH {
return vec![];
}
let mut sources = vec![];
for caller in callers {
if visited.contains(caller) {
continue;
}
visited.insert(caller.clone());
let caller_coverage = get_function_coverage(caller, coverage);
if caller_coverage >= 0.8 {
let discount = DISTANCE_DISCOUNT.powi(depth as i32);
sources.push(CoverageSource {
caller: caller.clone(),
caller_coverage,
distance: depth,
contributed_coverage: caller_coverage * discount,
});
} else if depth < MAX_DEPTH - 1 {
let upstream_callers = call_graph.get_callers(caller);
sources.extend(analyze_caller_coverage(
&upstream_callers,
call_graph,
coverage,
depth + 1,
visited,
));
}
visited.remove(caller);
}
sources
}
fn aggregate_indirect_coverage(sources: &[CoverageSource]) -> f64 {
if sources.is_empty() {
return 0.0;
}
sources
.iter()
.map(|s| s.contributed_coverage)
.fold(0.0, f64::max)
}
fn combine_coverages(direct: f64, indirect: f64) -> f64 {
direct.max(indirect)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::risk::lcov::{FunctionCoverage, LcovData};
use std::path::PathBuf;
fn create_test_coverage() -> LcovData {
let mut coverage = LcovData::default();
let funcs = vec![FunctionCoverage {
name: "test_func".to_string(),
start_line: 10,
execution_count: 5,
coverage_percentage: 50.0,
uncovered_lines: vec![],
normalized: crate::risk::lcov::NormalizedFunctionName::simple("test_func"),
}];
coverage.functions.insert(PathBuf::from("test.rs"), funcs);
coverage.build_index();
coverage
}
#[test]
fn test_direct_coverage() {
let coverage = create_test_coverage();
let graph = CallGraph::new();
let func_id = FunctionId::new(PathBuf::from("test.rs"), "test_func".to_string(), 10);
let transitive = calculate_transitive_coverage(&func_id, &graph, &coverage);
assert!(transitive.direct > 0.0);
assert_eq!(transitive.direct, transitive.transitive);
assert!(transitive.propagated_from.is_empty());
}
#[test]
fn test_transitive_coverage_with_delegation() {
let coverage = create_test_coverage();
let mut graph = CallGraph::new();
let orchestrator = FunctionId::new(PathBuf::from("orch.rs"), "orchestrate".to_string(), 1);
let worker = FunctionId::new(PathBuf::from("test.rs"), "worker".to_string(), 10);
graph.add_function(orchestrator.clone(), false, false, 2, 10);
graph.add_function(worker.clone(), false, false, 5, 30);
graph.add_call(crate::priority::call_graph::FunctionCall {
caller: orchestrator.clone(),
callee: worker.clone(),
call_type: crate::priority::call_graph::CallType::Delegate,
});
let transitive = calculate_transitive_coverage(&orchestrator, &graph, &coverage);
assert_eq!(transitive.direct, 0.0);
assert!(transitive.transitive >= 0.0);
}
#[test]
fn test_coverage_urgency() {
let coverage = create_test_coverage();
let graph = CallGraph::new();
let func_id = FunctionId::new(PathBuf::from("uncovered.rs"), "complex_func".to_string(), 1);
let urgency = calculate_coverage_urgency(&func_id, &graph, &coverage, 10);
assert!(urgency > 8.0);
let urgency = calculate_coverage_urgency(&func_id, &graph, &coverage, 2);
assert!((7.0..=10.0).contains(&urgency));
}
#[test]
fn test_coverage_urgency_gradient() {
let mut coverage = LcovData::default();
let graph = CallGraph::new();
let func_id = FunctionId::new(
PathBuf::from("gradient_test.rs"),
"test_func".to_string(),
10,
);
let complexity = 10;
let urgency_0 = calculate_coverage_urgency(&func_id, &graph, &coverage, complexity);
assert!(
urgency_0 >= 9.0,
"0% coverage should score at least 9.0, got {}",
urgency_0
);
let funcs = vec![FunctionCoverage {
name: "test_func".to_string(),
start_line: 10,
execution_count: 1,
coverage_percentage: 25.0,
uncovered_lines: vec![],
normalized: crate::risk::lcov::NormalizedFunctionName::simple("test_func"),
}];
coverage
.functions
.insert(PathBuf::from("gradient_test.rs"), funcs.clone());
coverage.build_index();
let urgency_25 = calculate_coverage_urgency(&func_id, &graph, &coverage, complexity);
assert!(
(7.0..=10.0).contains(&urgency_25),
"25% coverage should score 7.0-10.0, got {}",
urgency_25
);
let funcs = vec![FunctionCoverage {
name: "test_func".to_string(),
start_line: 10,
execution_count: 1,
coverage_percentage: 50.0,
uncovered_lines: vec![],
normalized: crate::risk::lcov::NormalizedFunctionName::simple("test_func"),
}];
coverage
.functions
.insert(PathBuf::from("gradient_test.rs"), funcs.clone());
coverage.build_index();
let urgency_50 = calculate_coverage_urgency(&func_id, &graph, &coverage, complexity);
assert!(
(5.0..=7.5).contains(&urgency_50),
"50% coverage should score 5.0-7.5, got {}",
urgency_50
);
let funcs = vec![FunctionCoverage {
name: "test_func".to_string(),
start_line: 10,
execution_count: 1,
coverage_percentage: 75.0,
uncovered_lines: vec![],
normalized: crate::risk::lcov::NormalizedFunctionName::simple("test_func"),
}];
coverage
.functions
.insert(PathBuf::from("gradient_test.rs"), funcs.clone());
coverage.build_index();
let urgency_75 = calculate_coverage_urgency(&func_id, &graph, &coverage, complexity);
assert!(
(3.0..=5.5).contains(&urgency_75),
"75% coverage should score 3.0-5.5, got {}",
urgency_75
);
let funcs = vec![FunctionCoverage {
name: "test_func".to_string(),
start_line: 10,
execution_count: 1,
coverage_percentage: 90.0,
uncovered_lines: vec![],
normalized: crate::risk::lcov::NormalizedFunctionName::simple("test_func"),
}];
coverage
.functions
.insert(PathBuf::from("gradient_test.rs"), funcs.clone());
coverage.build_index();
let urgency_90 = calculate_coverage_urgency(&func_id, &graph, &coverage, complexity);
assert!(
(1.0..=4.5).contains(&urgency_90),
"90% coverage should score 1.0-4.5, got {}",
urgency_90
);
let funcs = vec![FunctionCoverage {
name: "test_func".to_string(),
start_line: 10,
execution_count: 1,
coverage_percentage: 100.0,
uncovered_lines: vec![],
normalized: crate::risk::lcov::NormalizedFunctionName::simple("test_func"),
}];
coverage
.functions
.insert(PathBuf::from("gradient_test.rs"), funcs.clone());
coverage.build_index();
let urgency_100 = calculate_coverage_urgency(&func_id, &graph, &coverage, complexity);
assert!(
urgency_100 == 0.0,
"100% coverage should score 0.0, got {}",
urgency_100
);
assert!(
urgency_0 > urgency_25,
"Scores should decrease as coverage increases"
);
assert!(
urgency_25 > urgency_50,
"Scores should decrease as coverage increases"
);
assert!(
urgency_50 > urgency_75,
"Scores should decrease as coverage increases"
);
assert!(
urgency_75 > urgency_90,
"Scores should decrease as coverage increases"
);
assert!(
urgency_90 > urgency_100,
"Scores should decrease as coverage increases"
);
}
#[test]
fn test_indirect_coverage_single_hop() {
let mut call_graph = CallGraph::new();
let mut coverage = LcovData::default();
let func_f = FunctionId::new(PathBuf::from("test.rs"), "f".to_string(), 10);
let caller_c = FunctionId::new(PathBuf::from("test.rs"), "c".to_string(), 50);
call_graph.add_function(func_f.clone(), false, false, 5, 20);
call_graph.add_function(caller_c.clone(), false, false, 8, 40);
call_graph.add_call(crate::priority::call_graph::FunctionCall {
caller: caller_c.clone(),
callee: func_f.clone(),
call_type: crate::priority::call_graph::CallType::Direct,
});
let funcs = vec![FunctionCoverage {
name: "c".to_string(),
start_line: 50,
execution_count: 10,
coverage_percentage: 90.0,
uncovered_lines: vec![],
normalized: crate::risk::lcov::NormalizedFunctionName::simple("c"),
}];
coverage.functions.insert(PathBuf::from("test.rs"), funcs);
coverage.build_index();
let complete_coverage = calculate_indirect_coverage(&func_f, &call_graph, &coverage);
assert!(
(complete_coverage.indirect_coverage - 0.9).abs() < 0.01,
"Expected ~0.9 indirect coverage, got {}",
complete_coverage.indirect_coverage
);
assert_eq!(complete_coverage.coverage_sources.len(), 1);
assert_eq!(complete_coverage.coverage_sources[0].distance, 0);
}
#[test]
fn test_indirect_coverage_multi_hop() {
let mut call_graph = CallGraph::new();
let mut coverage = LcovData::default();
let func_f = FunctionId::new(PathBuf::from("test.rs"), "f".to_string(), 10);
let caller_c1 = FunctionId::new(PathBuf::from("test.rs"), "c1".to_string(), 50);
let caller_c2 = FunctionId::new(PathBuf::from("test.rs"), "c2".to_string(), 80);
call_graph.add_function(func_f.clone(), false, false, 5, 20);
call_graph.add_function(caller_c1.clone(), false, false, 8, 40);
call_graph.add_function(caller_c2.clone(), false, false, 10, 50);
call_graph.add_call(crate::priority::call_graph::FunctionCall {
caller: caller_c1.clone(),
callee: func_f.clone(),
call_type: crate::priority::call_graph::CallType::Direct,
});
call_graph.add_call(crate::priority::call_graph::FunctionCall {
caller: caller_c2.clone(),
callee: caller_c1.clone(),
call_type: crate::priority::call_graph::CallType::Direct,
});
let funcs = vec![FunctionCoverage {
name: "c2".to_string(),
start_line: 80,
execution_count: 10,
coverage_percentage: 95.0,
uncovered_lines: vec![],
normalized: crate::risk::lcov::NormalizedFunctionName::simple("c2"),
}];
coverage.functions.insert(PathBuf::from("test.rs"), funcs);
coverage.build_index();
let complete_coverage = calculate_indirect_coverage(&func_f, &call_graph, &coverage);
let expected = 0.95 * 0.7_f64.powi(1);
assert!(
(complete_coverage.indirect_coverage - expected).abs() < 0.01,
"Expected ~{} indirect coverage, got {}",
expected,
complete_coverage.indirect_coverage
);
assert_eq!(complete_coverage.coverage_sources.len(), 1);
assert_eq!(complete_coverage.coverage_sources[0].distance, 1);
}
#[test]
fn test_depth_limit_prevents_deep_recursion() {
let mut call_graph = CallGraph::new();
let mut coverage = LcovData::default();
let functions = [("f", 10), ("c1", 20), ("c2", 30), ("c3", 40), ("c4", 50)];
let func_ids: Vec<FunctionId> = functions
.iter()
.map(|(name, line)| FunctionId::new(PathBuf::from("test.rs"), name.to_string(), *line))
.collect();
for func_id in &func_ids {
call_graph.add_function(func_id.clone(), false, false, 5, 20);
}
for i in 0..func_ids.len() - 1 {
call_graph.add_call(crate::priority::call_graph::FunctionCall {
caller: func_ids[i + 1].clone(),
callee: func_ids[i].clone(),
call_type: crate::priority::call_graph::CallType::Direct,
});
}
let funcs = vec![FunctionCoverage {
name: "c4".to_string(),
start_line: 50,
execution_count: 10,
coverage_percentage: 100.0,
uncovered_lines: vec![],
normalized: crate::risk::lcov::NormalizedFunctionName::simple("c4"),
}];
coverage.functions.insert(PathBuf::from("test.rs"), funcs);
coverage.build_index();
let complete_coverage = calculate_indirect_coverage(&func_ids[0], &call_graph, &coverage);
assert_eq!(
complete_coverage.indirect_coverage, 0.0,
"Should not propagate beyond MAX_DEPTH"
);
assert_eq!(complete_coverage.coverage_sources.len(), 0);
}
#[test]
fn test_direct_coverage_skips_indirect() {
let call_graph = CallGraph::new();
let mut coverage = LcovData::default();
let func_f = FunctionId::new(PathBuf::from("test.rs"), "f".to_string(), 10);
let funcs = vec![FunctionCoverage {
name: "f".to_string(),
start_line: 10,
execution_count: 10,
coverage_percentage: 85.0,
uncovered_lines: vec![],
normalized: crate::risk::lcov::NormalizedFunctionName::simple("f"),
}];
coverage.functions.insert(PathBuf::from("test.rs"), funcs);
coverage.build_index();
let complete_coverage = calculate_indirect_coverage(&func_f, &call_graph, &coverage);
assert_eq!(complete_coverage.direct_coverage, 0.85);
assert_eq!(complete_coverage.indirect_coverage, 0.0);
assert_eq!(complete_coverage.effective_coverage, 0.85);
assert_eq!(complete_coverage.coverage_sources.len(), 0);
}
#[test]
fn test_no_callers_zero_indirect() {
let call_graph = CallGraph::new();
let coverage = LcovData::default();
let func_f = FunctionId::new(PathBuf::from("test.rs"), "orphan".to_string(), 10);
let complete_coverage = calculate_indirect_coverage(&func_f, &call_graph, &coverage);
assert_eq!(complete_coverage.indirect_coverage, 0.0);
assert_eq!(complete_coverage.coverage_sources.len(), 0);
}
#[test]
fn test_complexity_weighting() {
let coverage = LcovData::default(); let graph = CallGraph::new();
let func_id = FunctionId::new(
PathBuf::from("complexity_test.rs"),
"test_func".to_string(),
1,
);
let urgency_c1 = calculate_coverage_urgency(&func_id, &graph, &coverage, 1);
assert!(
(6.5..=8.0).contains(&urgency_c1),
"Complexity 1 should score 6.5-8.0, got {}",
urgency_c1
);
let urgency_c5 = calculate_coverage_urgency(&func_id, &graph, &coverage, 5);
assert!(
urgency_c5 >= 9.5,
"Complexity 5 should score at least 9.5, got {}",
urgency_c5
);
let urgency_c10 = calculate_coverage_urgency(&func_id, &graph, &coverage, 10);
assert!(
urgency_c10 >= 9.0,
"Complexity 10 should score at least 9.0, got {}",
urgency_c10
);
let urgency_c20 = calculate_coverage_urgency(&func_id, &graph, &coverage, 20);
assert!(
urgency_c20 >= 10.0,
"Complexity 20 should score at least 10.0, got {}",
urgency_c20
);
let urgency_c50 = calculate_coverage_urgency(&func_id, &graph, &coverage, 50);
assert!(
urgency_c50 >= 10.0,
"Complexity 50 should score at least 10.0, got {}",
urgency_c50
);
assert!(
urgency_c1 < urgency_c5,
"Higher complexity should have higher urgency"
);
assert!(
urgency_c5 <= urgency_c10,
"Higher complexity should have higher urgency (or be capped)"
);
assert!(
urgency_c10 <= urgency_c20,
"Higher complexity should have higher urgency (or be capped)"
);
}
}