use super::function_name_matching::{
extract_closure_parent, function_names_match, generate_function_name_variants, MatchConfidence,
};
use super::lcov::{normalize_demangled_name, strip_trailing_generics, FunctionCoverage, LcovData};
use super::path_normalization::normalize_path_components;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
#[deprecated(
since = "0.1.0",
note = "Use normalize_path_components() for better cross-platform support"
)]
pub fn normalize_path(path: &Path) -> PathBuf {
let path_str = path.to_string_lossy();
let cleaned = path_str.strip_prefix("./").unwrap_or(&path_str);
PathBuf::from(cleaned)
}
pub fn generate_name_variants(function_name: &str) -> impl Iterator<Item = String> + '_ {
function_name
.rsplit("::")
.next()
.filter(|method_name| {
function_name.contains("::") && *method_name != function_name
})
.map(|s| s.to_string())
.into_iter()
}
#[derive(Debug, Clone)]
pub struct AggregateCoverage {
pub coverage_pct: f64,
pub uncovered_lines: Vec<usize>,
pub version_count: usize,
}
impl AggregateCoverage {
fn single(func: &FunctionCoverage) -> Self {
Self {
coverage_pct: func.coverage_percentage,
uncovered_lines: func.uncovered_lines.clone(),
version_count: 1,
}
}
}
fn merge_coverage(coverages: Vec<&FunctionCoverage>) -> AggregateCoverage {
if coverages.is_empty() {
return AggregateCoverage {
coverage_pct: 0.0,
uncovered_lines: vec![],
version_count: 0,
};
}
if coverages.len() == 1 {
return AggregateCoverage {
coverage_pct: coverages[0].coverage_percentage,
uncovered_lines: coverages[0].uncovered_lines.clone(),
version_count: 1,
};
}
let mut uncovered_in_all: HashSet<usize> =
coverages[0].uncovered_lines.iter().copied().collect();
for coverage in &coverages[1..] {
let uncovered_set: HashSet<usize> = coverage.uncovered_lines.iter().copied().collect();
uncovered_in_all = uncovered_in_all
.intersection(&uncovered_set)
.copied()
.collect();
}
let mut sorted_coverages = coverages;
sorted_coverages.sort_by(|a, b| a.name.cmp(&b.name));
let version_count = sorted_coverages.len();
let avg_coverage: f64 = sorted_coverages
.iter()
.map(|c| c.coverage_percentage)
.sum::<f64>()
/ version_count as f64;
let mut uncovered_lines: Vec<usize> = uncovered_in_all.into_iter().collect();
uncovered_lines.sort_unstable();
AggregateCoverage {
coverage_pct: avg_coverage,
uncovered_lines,
version_count,
}
}
#[derive(Debug, Clone)]
pub struct CoverageIndex {
by_file: HashMap<PathBuf, HashMap<String, FunctionCoverage>>,
by_line: HashMap<PathBuf, BTreeMap<usize, FunctionCoverage>>,
file_paths: Vec<PathBuf>,
base_function_index: HashMap<(PathBuf, String), Vec<String>>,
method_name_index: HashMap<(PathBuf, String), Vec<String>>,
stats: CoverageIndexStats,
}
#[derive(Debug, Clone)]
pub struct CoverageIndexStats {
pub total_files: usize,
pub total_records: usize,
pub index_build_time: Duration,
pub estimated_memory_bytes: usize,
}
impl CoverageIndex {
pub fn empty() -> Self {
CoverageIndex {
by_file: HashMap::new(),
by_line: HashMap::new(),
file_paths: Vec::new(),
base_function_index: HashMap::new(),
method_name_index: HashMap::new(),
stats: CoverageIndexStats {
total_files: 0,
total_records: 0,
index_build_time: Duration::from_secs(0),
estimated_memory_bytes: 0,
},
}
}
pub fn from_coverage(coverage: &LcovData) -> Self {
let start = Instant::now();
let mut by_file: HashMap<PathBuf, HashMap<String, FunctionCoverage>> = HashMap::new();
let mut by_line: HashMap<PathBuf, BTreeMap<usize, FunctionCoverage>> = HashMap::new();
let mut base_function_index: HashMap<(PathBuf, String), Vec<String>> = HashMap::new();
let mut method_name_index: HashMap<(PathBuf, String), Vec<String>> = HashMap::new();
let mut total_records = 0;
for (file_path, functions) in &coverage.functions {
let mut file_functions = HashMap::new();
let mut line_map = BTreeMap::new();
for func in functions {
total_records += 1;
file_functions.insert(func.name.clone(), func.clone());
line_map.insert(func.start_line, func.clone());
let base_name_cow = strip_trailing_generics(&func.name);
let base_name = base_name_cow.as_ref();
if base_name != func.name {
base_function_index
.entry((file_path.clone(), base_name.to_string()))
.or_default()
.push(func.name.clone());
}
let method_name = &func.normalized.method_name;
if !method_name.is_empty() && method_name != &func.name {
method_name_index
.entry((file_path.clone(), method_name.clone()))
.or_default()
.push(func.name.clone());
}
}
if !file_functions.is_empty() {
by_file.insert(file_path.clone(), file_functions);
}
if !line_map.is_empty() {
by_line.insert(file_path.clone(), line_map);
}
}
let mut file_paths: Vec<PathBuf> = by_file.keys().cloned().collect();
file_paths.sort();
let index_build_time = start.elapsed();
let total_files = by_file.len();
let estimated_memory_bytes = total_records * 200 + file_paths.len() * 100;
CoverageIndex {
by_file,
by_line,
file_paths,
base_function_index,
method_name_index,
stats: CoverageIndexStats {
total_files,
total_records,
index_build_time,
estimated_memory_bytes,
},
}
}
pub fn get_function_coverage(&self, file: &Path, function_name: &str) -> Option<f64> {
log::debug!(
"Looking up coverage for function '{}' in file '{}'",
function_name,
file.display()
);
let normalized_query = normalize_demangled_name(function_name);
let query_name = &normalized_query.full_path;
if let Some(file_functions) = self.by_file.get(file) {
if let Some(f) = file_functions.get(query_name) {
log::debug!(
"✓ Found via exact match (normalized): {}% coverage",
f.coverage_percentage
);
return Some(f.coverage_percentage / 100.0);
}
if query_name != function_name {
if let Some(f) = file_functions.get(function_name) {
log::debug!(
"✓ Found via exact match (original): {}% coverage",
f.coverage_percentage
);
return Some(f.coverage_percentage / 100.0);
}
}
}
log::debug!("Exact match failed, trying path strategies...");
if let Some(result) = self.find_by_path_strategies(file, query_name) {
return Some(result.coverage_percentage / 100.0);
}
if query_name != function_name {
self.find_by_path_strategies(file, function_name)
.map(|f| f.coverage_percentage / 100.0)
} else {
None
}
}
fn find_by_path_strategies(
&self,
query_path: &Path,
function_name: &str,
) -> Option<&FunctionCoverage> {
let query_components = normalize_path_components(query_path);
log::debug!("Strategy 1: Component-based suffix matching");
for file_path in &self.file_paths {
let file_components = normalize_path_components(file_path);
if !query_components.is_empty() && query_components.len() <= file_components.len() {
let file_suffix =
&file_components[file_components.len() - query_components.len()..];
if query_components == file_suffix {
log::debug!(" Found path match: '{}'", file_path.display());
if let Some(file_functions) = self.by_file.get(file_path) {
if let Some((func, confidence)) =
self.find_best_matching_function(file_functions, function_name)
{
log::debug!(
" ✓ Matched function via {:?} confidence: query '{}' matches stored '{}': {}%",
confidence,
function_name,
func.name,
func.coverage_percentage
);
return Some(func);
}
log::debug!(" ✗ No function match in this file");
}
}
}
}
log::debug!("Strategy 2: Component-based reverse suffix matching");
for file_path in &self.file_paths {
let file_components = normalize_path_components(file_path);
if !file_components.is_empty() && file_components.len() <= query_components.len() {
let query_suffix =
&query_components[query_components.len() - file_components.len()..];
if file_components == query_suffix {
log::debug!(" Found path match: '{}'", file_path.display());
if let Some(file_functions) = self.by_file.get(file_path) {
if let Some((func, confidence)) =
self.find_best_matching_function(file_functions, function_name)
{
log::debug!(
" ✓ Matched function via {:?} confidence: query '{}' matches stored '{}': {}%",
confidence,
function_name,
func.name,
func.coverage_percentage
);
return Some(func);
}
log::debug!(" ✗ No function match in this file");
}
}
}
}
log::debug!("Strategy 3: Component-based exact equality");
for file_path in &self.file_paths {
let file_components = normalize_path_components(file_path);
if query_components == file_components {
log::debug!(" Found path match: '{}'", file_path.display());
if let Some(file_functions) = self.by_file.get(file_path) {
if let Some((func, confidence)) =
self.find_best_matching_function(file_functions, function_name)
{
log::debug!(
" ✓ Matched function via {:?} confidence: query '{}' matches stored '{}': {}%",
confidence,
function_name,
func.name,
func.coverage_percentage
);
return Some(func);
}
log::debug!(" ✗ No function match in this file");
}
}
}
log::debug!("✗ All path strategies failed");
None
}
pub fn get_function_coverage_with_line(
&self,
file: &Path,
function_name: &str,
line: usize,
) -> Option<f64> {
log::debug!(
"Attempting coverage lookup for '{}' at {}:{}",
function_name,
file.display(),
line
);
if let Some(agg) = self.get_aggregated_coverage(file, function_name) {
log::debug!(
"✓ Coverage found via aggregated match: {:.1}%",
agg.coverage_pct
);
return Some(agg.coverage_pct / 100.0);
}
let variants = generate_function_name_variants(function_name);
for variant in &variants {
log::debug!("Trying name variant: '{}'", variant);
if let Some(agg) = self.get_aggregated_coverage(file, variant) {
log::debug!(
"✓ Coverage found via name variant '{}': {:.1}%",
variant,
agg.coverage_pct
);
return Some(agg.coverage_pct / 100.0);
}
if let Some(func) = self.find_by_path_strategies(file, variant) {
log::debug!(
"✓ Coverage found via name variant '{}' with path strategies: {:.1}%",
variant,
func.coverage_percentage
);
return Some(func.coverage_percentage / 100.0);
}
}
if let Some(parent) = extract_closure_parent(function_name) {
log::debug!("Trying closure parent: '{}'", parent);
if let Some(agg) = self.get_aggregated_coverage(file, &parent) {
log::debug!(
"✓ Coverage found via closure parent '{}': {:.1}%",
parent,
agg.coverage_pct
);
return Some(agg.coverage_pct / 100.0);
}
if let Some(func) = self.find_by_path_strategies(file, &parent) {
log::debug!(
"✓ Coverage found via closure parent '{}' with path strategies: {:.1}%",
parent,
func.coverage_percentage
);
return Some(func.coverage_percentage / 100.0);
}
}
log::debug!("Trying line-based lookup with tolerance ±2");
match self.find_function_by_line(file, line, 2) {
Some(f) => {
log::debug!(
"✓ Coverage found via line-based fallback: matched '{}' at line {}, coverage {:.1}%",
f.name,
f.start_line,
f.coverage_percentage
);
return Some(f.coverage_percentage / 100.0);
}
None => {
if !self.by_line.contains_key(file) {
log::trace!(
"File '{}' not found in line-based index (has {} files indexed)",
file.display(),
self.by_line.len()
);
} else {
let line_map = &self.by_line[file];
log::debug!(
"Line-based lookup failed: file has {} indexed functions, searched for line {} with ±2 tolerance",
line_map.len(),
line
);
let min_line = line.saturating_sub(5);
let max_line = line.saturating_add(5);
let nearby_lines: Vec<usize> = line_map
.range(min_line..=max_line)
.map(|(l, _)| *l)
.collect();
if !nearby_lines.is_empty() {
log::debug!("Nearby indexed lines (±5): {:?}", nearby_lines);
}
}
}
}
log::debug!("Trying path matching strategies");
match self.find_by_path_strategies(file, function_name) {
Some(f) => {
log::debug!(
"✓ Coverage found via path matching: {:.1}%",
f.coverage_percentage
);
Some(f.coverage_percentage / 100.0)
}
None => {
log::trace!(
"✗ No coverage found for '{}' at {}:{} after all strategies",
function_name,
file.display(),
line
);
None
}
}
}
pub fn get_function_uncovered_lines(
&self,
file: &Path,
function_name: &str,
line: usize,
) -> Option<Vec<usize>> {
if let Some(file_functions) = self.by_file.get(file) {
if let Some(func) = file_functions.get(function_name) {
return Some(func.uncovered_lines.clone());
}
}
if let Some(func) = self.find_by_path_strategies(file, function_name) {
return Some(func.uncovered_lines.clone());
}
self.find_function_by_line(file, line, 2)
.map(|f| f.uncovered_lines.clone())
}
fn find_function_by_line(
&self,
file: &Path,
target_line: usize,
tolerance: usize,
) -> Option<&FunctionCoverage> {
let line_map = self.by_line.get(file)?;
let min_line = target_line.saturating_sub(tolerance);
let max_line = target_line.saturating_add(tolerance);
log::trace!(
"Searching line-based index: target={}, range={}..={}, index_size={}",
target_line,
min_line,
max_line,
line_map.len()
);
let result = line_map
.range(min_line..=max_line)
.min_by_key(|(line, _)| line.abs_diff(target_line))
.map(|(_, func)| func);
if let Some(func) = result {
log::trace!(
"Found function '{}' at line {} (distance: {})",
func.name,
func.start_line,
func.start_line.abs_diff(target_line)
);
} else {
log::trace!("No function found within tolerance range");
}
result
}
pub fn stats(&self) -> &CoverageIndexStats {
&self.stats
}
fn get_aggregated_coverage(
&self,
file: &Path,
function_name: &str,
) -> Option<AggregateCoverage> {
if let Some(file_functions) = self.by_file.get(file) {
if let Some(exact) = file_functions.get(function_name) {
return Some(AggregateCoverage::single(exact));
}
}
if let Some(matching_functions) = self
.method_name_index
.get(&(file.to_path_buf(), function_name.to_string()))
{
let coverages: Vec<&FunctionCoverage> = matching_functions
.iter()
.filter_map(|name| self.by_file.get(file).and_then(|funcs| funcs.get(name)))
.collect();
if !coverages.is_empty() {
log::debug!(
"✓ Found {} matching functions via method_name_index for '{}'",
coverages.len(),
function_name
);
return Some(merge_coverage(coverages));
}
}
if let Some(versions) = self
.base_function_index
.get(&(file.to_path_buf(), function_name.to_string()))
{
let coverages: Vec<&FunctionCoverage> = versions
.iter()
.filter_map(|name| self.by_file.get(file).and_then(|funcs| funcs.get(name)))
.collect();
if !coverages.is_empty() {
return Some(merge_coverage(coverages));
}
}
None
}
fn find_best_matching_function<'a>(
&self,
file_functions: &'a HashMap<String, FunctionCoverage>,
query_name: &str,
) -> Option<(&'a FunctionCoverage, MatchConfidence)> {
let mut best_match = None;
let mut best_confidence = MatchConfidence::None;
let mut sorted_functions: Vec<_> = file_functions.iter().collect();
sorted_functions.sort_by(|a, b| a.0.cmp(b.0));
for (lcov_name, func) in sorted_functions {
let (matches, confidence) = function_names_match(query_name, lcov_name);
if matches && confidence > best_confidence {
best_match = Some(func);
best_confidence = confidence;
if confidence == MatchConfidence::High {
break;
}
}
}
best_match.map(|func| (func, best_confidence))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::risk::lcov::NormalizedFunctionName;
fn create_test_function_coverage(
name: &str,
start_line: usize,
execution_count: u64,
coverage_percentage: f64,
uncovered_lines: Vec<usize>,
) -> FunctionCoverage {
FunctionCoverage {
name: name.to_string(),
start_line,
execution_count,
coverage_percentage,
uncovered_lines,
normalized: NormalizedFunctionName {
full_path: name.to_string(),
method_name: name.to_string(),
original: name.to_string(),
},
}
}
fn create_test_coverage() -> LcovData {
let mut coverage = LcovData::default();
let test_functions = vec![
create_test_function_coverage("func_a", 10, 5, 100.0, vec![]),
create_test_function_coverage("func_b", 20, 3, 75.0, vec![22, 24]),
create_test_function_coverage("func_c", 30, 0, 0.0, vec![30, 31, 32, 33]),
];
coverage
.functions
.insert(PathBuf::from("test.rs"), test_functions);
coverage
}
#[test]
fn test_index_build() {
let coverage = create_test_coverage();
let index = CoverageIndex::from_coverage(&coverage);
assert_eq!(index.stats.total_files, 1);
assert_eq!(index.stats.total_records, 3);
assert!(index.stats.index_build_time < Duration::from_millis(10));
}
#[test]
fn test_exact_function_lookup() {
let coverage = create_test_coverage();
let index = CoverageIndex::from_coverage(&coverage);
assert_eq!(
index.get_function_coverage(Path::new("test.rs"), "func_a"),
Some(1.0) );
assert_eq!(
index.get_function_coverage(Path::new("test.rs"), "func_b"),
Some(0.75) );
assert_eq!(
index.get_function_coverage(Path::new("test.rs"), "func_c"),
Some(0.0)
);
}
#[test]
fn test_function_not_found() {
let coverage = create_test_coverage();
let index = CoverageIndex::from_coverage(&coverage);
assert_eq!(
index.get_function_coverage(Path::new("test.rs"), "nonexistent"),
None
);
assert_eq!(
index.get_function_coverage(Path::new("other.rs"), "func_a"),
None
);
}
#[test]
fn test_line_based_lookup() {
let coverage = create_test_coverage();
let index = CoverageIndex::from_coverage(&coverage);
assert_eq!(
index.get_function_coverage_with_line(Path::new("test.rs"), "unknown", 10),
Some(1.0)
);
assert_eq!(
index.get_function_coverage_with_line(Path::new("test.rs"), "unknown", 11),
Some(1.0) );
assert_eq!(
index.get_function_coverage_with_line(Path::new("test.rs"), "unknown", 21),
Some(0.75)
);
}
#[test]
fn test_uncovered_lines() {
let coverage = create_test_coverage();
let index = CoverageIndex::from_coverage(&coverage);
assert_eq!(
index.get_function_uncovered_lines(Path::new("test.rs"), "func_a", 10),
Some(vec![])
);
assert_eq!(
index.get_function_uncovered_lines(Path::new("test.rs"), "func_b", 20),
Some(vec![22, 24])
);
assert_eq!(
index.get_function_uncovered_lines(Path::new("test.rs"), "func_c", 30),
Some(vec![30, 31, 32, 33])
);
}
#[test]
fn test_empty_coverage() {
let coverage = LcovData::default();
let index = CoverageIndex::from_coverage(&coverage);
assert_eq!(index.stats.total_files, 0);
assert_eq!(index.stats.total_records, 0);
assert_eq!(
index.get_function_coverage(Path::new("test.rs"), "func"),
None
);
}
#[test]
fn test_multiple_files() {
let mut coverage = LcovData::default();
coverage.functions.insert(
PathBuf::from("file1.rs"),
vec![create_test_function_coverage("func1", 5, 10, 100.0, vec![])],
);
coverage.functions.insert(
PathBuf::from("file2.rs"),
vec![create_test_function_coverage(
"func2",
15,
0,
0.0,
vec![15, 16],
)],
);
let index = CoverageIndex::from_coverage(&coverage);
assert_eq!(index.stats.total_files, 2);
assert_eq!(index.stats.total_records, 2);
assert_eq!(
index.get_function_coverage(Path::new("file1.rs"), "func1"),
Some(1.0)
);
assert_eq!(
index.get_function_coverage(Path::new("file2.rs"), "func2"),
Some(0.0)
);
}
#[test]
fn test_merge_coverage_intersection() {
let cov1 = create_test_function_coverage("func", 10, 5, 70.0, vec![10, 20, 30]);
let cov2 = create_test_function_coverage("func", 10, 3, 80.0, vec![20, 40]);
let agg = merge_coverage(vec![&cov1, &cov2]);
assert_eq!(agg.version_count, 2);
assert_eq!(agg.coverage_pct, 75.0); assert_eq!(agg.uncovered_lines.len(), 1);
assert!(agg.uncovered_lines.contains(&20));
assert!(!agg.uncovered_lines.contains(&10)); assert!(!agg.uncovered_lines.contains(&40)); }
#[test]
fn test_merge_coverage_all_covered_in_some() {
let cov1 = create_test_function_coverage("func", 10, 5, 50.0, vec![10, 20]);
let cov2 = create_test_function_coverage("func", 10, 3, 50.0, vec![30, 40]);
let agg = merge_coverage(vec![&cov1, &cov2]);
assert_eq!(agg.uncovered_lines.len(), 0);
}
#[test]
fn test_merge_coverage_single() {
let cov = create_test_function_coverage("func", 10, 5, 75.0, vec![10, 20]);
let agg = merge_coverage(vec![&cov]);
assert_eq!(agg.version_count, 1);
assert_eq!(agg.coverage_pct, 75.0);
assert_eq!(agg.uncovered_lines, vec![10, 20]);
}
#[test]
fn test_merge_coverage_empty() {
let agg = merge_coverage(vec![]);
assert_eq!(agg.version_count, 0);
assert_eq!(agg.coverage_pct, 0.0);
assert_eq!(agg.uncovered_lines.len(), 0);
}
#[test]
fn test_monomorphized_function_indexing() {
use crate::risk::lcov::NormalizedFunctionName;
let mut coverage = LcovData::default();
coverage.functions.insert(
PathBuf::from("test.rs"),
vec![
FunctionCoverage {
name: "Type::method::<WorkflowExecutor>".to_string(),
start_line: 10,
execution_count: 5,
coverage_percentage: 70.0,
uncovered_lines: vec![10, 20, 30],
normalized: NormalizedFunctionName {
full_path: "Type::method".to_string(),
method_name: "method".to_string(),
original: "Type::method::<WorkflowExecutor>".to_string(),
},
},
FunctionCoverage {
name: "Type::method::<MockExecutor>".to_string(),
start_line: 10,
execution_count: 3,
coverage_percentage: 80.0,
uncovered_lines: vec![20, 40],
normalized: NormalizedFunctionName {
full_path: "Type::method".to_string(),
method_name: "method".to_string(),
original: "Type::method::<MockExecutor>".to_string(),
},
},
],
);
let index = CoverageIndex::from_coverage(&coverage);
let agg = index.get_aggregated_coverage(Path::new("test.rs"), "Type::method");
assert!(agg.is_some());
let agg = agg.unwrap();
assert_eq!(agg.version_count, 2);
assert_eq!(agg.coverage_pct, 75.0); assert_eq!(agg.uncovered_lines, vec![20]);
}
#[test]
fn test_generate_name_variants_trait_method() {
let variants: Vec<String> =
generate_name_variants("RecursiveMatchDetector::visit_expr").collect();
assert_eq!(variants, vec!["visit_expr"]);
}
#[test]
fn test_generate_name_variants_nested_path() {
let variants: Vec<String> = generate_name_variants("crate::module::Type::method").collect();
assert_eq!(variants, vec!["method"]);
}
#[test]
fn test_generate_name_variants_simple_function() {
let variants: Vec<String> = generate_name_variants("simple_function").collect();
assert_eq!(variants.len(), 0); }
#[test]
fn test_generate_name_variants_single_segment() {
let variants: Vec<String> = generate_name_variants("main").collect();
assert_eq!(variants.len(), 0); }
#[test]
fn test_trait_method_coverage_match_by_method_name() {
use crate::risk::lcov::NormalizedFunctionName;
let mut coverage = LcovData::default();
coverage.functions.insert(
PathBuf::from("src/complexity/recursive_detector.rs"),
vec![FunctionCoverage {
name: "visit_expr".to_string(),
start_line: 177,
execution_count: 3507,
coverage_percentage: 90.2,
uncovered_lines: vec![200, 205],
normalized: NormalizedFunctionName {
full_path: "visit_expr".to_string(),
method_name: "visit_expr".to_string(),
original: "visit_expr".to_string(),
},
}],
);
let index = CoverageIndex::from_coverage(&coverage);
let coverage = index.get_function_coverage_with_line(
Path::new("src/complexity/recursive_detector.rs"),
"RecursiveMatchDetector::visit_expr",
177,
);
assert!(coverage.is_some());
assert_eq!(coverage.unwrap(), 0.902); }
#[test]
fn test_trait_method_coverage_no_regression_exact_match() {
use crate::risk::lcov::NormalizedFunctionName;
let mut coverage = LcovData::default();
coverage.functions.insert(
PathBuf::from("src/test.rs"),
vec![FunctionCoverage {
name: "MyType::my_method".to_string(),
start_line: 10,
execution_count: 100,
coverage_percentage: 95.0,
uncovered_lines: vec![15],
normalized: NormalizedFunctionName {
full_path: "MyType::my_method".to_string(),
method_name: "my_method".to_string(),
original: "MyType::my_method".to_string(),
},
}],
);
let index = CoverageIndex::from_coverage(&coverage);
let coverage = index.get_function_coverage_with_line(
Path::new("src/test.rs"),
"MyType::my_method",
10,
);
assert!(coverage.is_some());
assert_eq!(coverage.unwrap(), 0.95);
}
#[test]
fn test_trait_method_coverage_method_name_conflict() {
use crate::risk::lcov::NormalizedFunctionName;
let mut coverage = LcovData::default();
coverage.functions.insert(
PathBuf::from("src/test.rs"),
vec![
FunctionCoverage {
name: "TypeA::process".to_string(),
start_line: 10,
execution_count: 50,
coverage_percentage: 80.0,
uncovered_lines: vec![12],
normalized: NormalizedFunctionName {
full_path: "TypeA::process".to_string(),
method_name: "process".to_string(),
original: "TypeA::process".to_string(),
},
},
FunctionCoverage {
name: "TypeB::process".to_string(),
start_line: 30,
execution_count: 75,
coverage_percentage: 90.0,
uncovered_lines: vec![35],
normalized: NormalizedFunctionName {
full_path: "TypeB::process".to_string(),
method_name: "process".to_string(),
original: "TypeB::process".to_string(),
},
},
],
);
let index = CoverageIndex::from_coverage(&coverage);
let coverage_a =
index.get_function_coverage_with_line(Path::new("src/test.rs"), "TypeA::process", 10);
assert_eq!(coverage_a.unwrap(), 0.80);
let coverage_b =
index.get_function_coverage_with_line(Path::new("src/test.rs"), "TypeB::process", 30);
assert_eq!(coverage_b.unwrap(), 0.90);
}
#[test]
fn test_line_based_fallback_exact_match() {
let mut coverage = LcovData::default();
coverage.functions.insert(
PathBuf::from("test.rs"),
vec![create_test_function_coverage("foo", 100, 10, 85.0, vec![])],
);
let index = CoverageIndex::from_coverage(&coverage);
let result = index.get_function_coverage_with_line(
Path::new("test.rs"),
"WRONG_NAME", 100, );
assert!(
result.is_some(),
"Line-based fallback should find function at exact line"
);
assert_eq!(result.unwrap(), 0.85);
}
#[test]
fn test_line_based_fallback_within_tolerance() {
let mut coverage = LcovData::default();
coverage.functions.insert(
PathBuf::from("test.rs"),
vec![create_test_function_coverage("foo", 100, 10, 85.0, vec![])],
);
let index = CoverageIndex::from_coverage(&coverage);
for line in 98..=102 {
let result =
index.get_function_coverage_with_line(Path::new("test.rs"), "WRONG_NAME", line);
assert!(
result.is_some(),
"Line {} should match function at 100 with ±2 tolerance",
line
);
assert_eq!(result.unwrap(), 0.85);
}
}
#[test]
fn test_line_based_fallback_outside_tolerance() {
let mut coverage = LcovData::default();
coverage.functions.insert(
PathBuf::from("test.rs"),
vec![create_test_function_coverage("foo", 100, 10, 85.0, vec![])],
);
let index = CoverageIndex::from_coverage(&coverage);
let result = index.get_function_coverage_with_line(Path::new("test.rs"), "WRONG_NAME", 97);
assert!(
result.is_none(),
"Line 97 should be outside ±2 tolerance of line 100"
);
let result = index.get_function_coverage_with_line(Path::new("test.rs"), "WRONG_NAME", 103);
assert!(
result.is_none(),
"Line 103 should be outside ±2 tolerance of line 100"
);
}
#[test]
fn test_line_based_fallback_chooses_closest() {
let mut coverage = LcovData::default();
coverage.functions.insert(
PathBuf::from("test.rs"),
vec![
create_test_function_coverage("func_at_100", 100, 10, 80.0, vec![]),
create_test_function_coverage("func_at_105", 105, 10, 90.0, vec![]),
],
);
let index = CoverageIndex::from_coverage(&coverage);
let result = index.get_function_coverage_with_line(Path::new("test.rs"), "WRONG_NAME", 102);
assert!(result.is_some());
assert_eq!(
result.unwrap(),
0.80,
"Should match function at line 100 (closer)"
);
let result = index.get_function_coverage_with_line(Path::new("test.rs"), "WRONG_NAME", 104);
assert!(result.is_some());
assert_eq!(
result.unwrap(),
0.90,
"Should match function at line 105 (closer)"
);
}
#[test]
fn test_line_based_fallback_boundary_conditions() {
let mut coverage = LcovData::default();
coverage.functions.insert(
PathBuf::from("test.rs"),
vec![
create_test_function_coverage("func_at_0", 0, 10, 70.0, vec![]),
create_test_function_coverage(
"func_at_usize_max",
usize::MAX - 5,
10,
75.0,
vec![],
),
],
);
let index = CoverageIndex::from_coverage(&coverage);
let result = index.get_function_coverage_with_line(Path::new("test.rs"), "WRONG_NAME", 0);
assert!(result.is_some());
assert_eq!(result.unwrap(), 0.70);
let result = index.get_function_coverage_with_line(
Path::new("test.rs"),
"WRONG_NAME",
usize::MAX - 4,
);
assert!(result.is_some());
assert_eq!(result.unwrap(), 0.75);
}
#[test]
fn test_line_index_populated_for_all_functions() {
let mut coverage = LcovData::default();
let test_functions = vec![
create_test_function_coverage("func_a", 10, 5, 100.0, vec![]),
create_test_function_coverage("func_b", 20, 3, 75.0, vec![22, 24]),
create_test_function_coverage("func_c", 30, 0, 0.0, vec![30, 31, 32, 33]),
];
coverage
.functions
.insert(PathBuf::from("test.rs"), test_functions);
let index = CoverageIndex::from_coverage(&coverage);
let file = Path::new("test.rs");
assert!(
index.by_line.contains_key(file),
"File should be in line index"
);
let line_map = &index.by_line[file];
assert_eq!(line_map.len(), 3, "All 3 functions should be in line index");
assert!(
line_map.contains_key(&10),
"Function at line 10 should be indexed"
);
assert!(
line_map.contains_key(&20),
"Function at line 20 should be indexed"
);
assert!(
line_map.contains_key(&30),
"Function at line 30 should be indexed"
);
}
#[test]
fn test_line_based_fallback_with_trait_method() {
use crate::risk::lcov::NormalizedFunctionName;
let mut coverage = LcovData::default();
coverage.functions.insert(
PathBuf::from("src/test.rs"),
vec![FunctionCoverage {
name: "visit_expr".to_string(),
start_line: 177,
execution_count: 3507,
coverage_percentage: 90.2,
uncovered_lines: vec![200, 205],
normalized: NormalizedFunctionName {
full_path: "visit_expr".to_string(),
method_name: "visit_expr".to_string(),
original: "visit_expr".to_string(),
},
}],
);
let index = CoverageIndex::from_coverage(&coverage);
let coverage = index.get_function_coverage_with_line(
Path::new("src/test.rs"),
"SomeType::visit_expr", 177,
);
assert!(
coverage.is_some(),
"Line-based fallback should find coverage when name doesn't match exactly"
);
assert_eq!(coverage.unwrap(), 0.902);
}
#[test]
fn test_line_based_fallback_empty_file() {
let coverage = LcovData::default();
let index = CoverageIndex::from_coverage(&coverage);
let result =
index.get_function_coverage_with_line(Path::new("nonexistent.rs"), "func", 100);
assert!(result.is_none(), "Should return None for non-existent file");
}
#[test]
fn test_tolerance_calculation_inclusive_range() {
let mut coverage = LcovData::default();
coverage.functions.insert(
PathBuf::from("test.rs"),
vec![
create_test_function_coverage("func_98", 98, 10, 70.0, vec![]),
create_test_function_coverage("func_100", 100, 10, 80.0, vec![]),
create_test_function_coverage("func_102", 102, 10, 90.0, vec![]),
],
);
let index = CoverageIndex::from_coverage(&coverage);
let result = index.find_function_by_line(Path::new("test.rs"), 100, 2);
assert!(result.is_some());
assert_eq!(
result.unwrap().name,
"func_100",
"Should find closest function at 100"
);
let result = index.find_function_by_line(Path::new("test.rs"), 99, 2);
assert!(result.is_some());
}
}