use super::diagnostics::{track_match_attempt, track_match_success, track_match_zero};
use super::types::{FunctionCoverage, LcovData};
use crate::risk::function_name_matching::{
find_matching_function, MatchConfidence, MatchableFunction,
};
use crate::risk::path_normalization::{find_matching_path, MatchStrategy};
use rayon::prelude::*;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
type FunctionCoverageMatch = (f64, MatchConfidence);
fn coverage_debug_mode() -> bool {
std::env::var("DEBTMAP_COVERAGE_DEBUG").is_ok()
}
fn track_coverage_attempt(debug_mode: bool) {
if debug_mode {
track_match_attempt();
}
}
fn sorted_coverage_paths(functions: &HashMap<PathBuf, Vec<FunctionCoverage>>) -> Vec<PathBuf> {
let mut available_paths: Vec<PathBuf> = functions.keys().cloned().collect();
available_paths.sort();
available_paths
}
fn zero_coverage_result(debug_mode: bool) -> Option<f64> {
if debug_mode {
track_match_zero();
Some(0.0)
} else {
None
}
}
fn log_path_match(
debug_mode: bool,
file: &Path,
function_name: &str,
path_match: Option<(&PathBuf, MatchStrategy)>,
available_path_count: usize,
) {
if !debug_mode {
return;
}
match path_match {
Some((_matched_path, strategy)) => eprintln!(
"[COVERAGE] {}::{} Path:✓ Strategy:{:?}",
file.display(),
function_name,
strategy
),
None => eprintln!(
"[COVERAGE] {}::{} Path:✗ (not found in {} paths)",
file.display(),
function_name,
available_path_count
),
}
}
fn find_function_coverage(
function_name: &str,
functions: &[FunctionCoverage],
) -> Option<FunctionCoverageMatch> {
let matchable_funcs: Vec<MatchableFunction<&FunctionCoverage>> = functions
.iter()
.map(|f| MatchableFunction {
name: f.name.clone(),
data: f,
})
.collect();
find_matching_function(function_name, &matchable_funcs).map(|(matched_func, confidence)| {
(matched_func.data.coverage_percentage / 100.0, confidence)
})
}
fn log_function_match(
debug_mode: bool,
function_match: Option<FunctionCoverageMatch>,
function_count: usize,
) {
if !debug_mode {
return;
}
match function_match {
Some((coverage, confidence)) => eprintln!(
"[COVERAGE] Func:✓ Confidence:{:?} Coverage:{:.1}%",
confidence,
coverage * 100.0
),
None => eprintln!(
"[COVERAGE] Func:✗ (not found in {} functions)",
function_count
),
}
}
fn track_coverage_result(debug_mode: bool, coverage: f64) {
if !debug_mode {
return;
}
if coverage > 0.0 {
track_match_success();
} else {
track_match_zero();
}
}
impl LcovData {
pub fn with_loc_counter(mut self, loc_counter: crate::metrics::LocCounter) -> Self {
self.loc_counter = Some(loc_counter);
self
}
pub fn loc_counter(&self) -> Option<&crate::metrics::LocCounter> {
self.loc_counter.as_ref()
}
pub fn recalculate_with_loc_counter(&mut self) {
if let Some(counter) = &self.loc_counter {
let files: Vec<PathBuf> = self.functions.keys().cloned().collect();
let mut total_code_lines = 0;
for file in &files {
if counter.should_include(file) {
if let Ok(count) = counter.count_file(file) {
total_code_lines += count.code_lines;
log::debug!(
"LOC counter: {} has {} code lines",
file.display(),
count.code_lines
);
}
}
}
log::debug!(
"Recalculated total_lines using LocCounter: {} (was {})",
total_code_lines,
self.total_lines
);
self.total_lines = total_code_lines;
}
}
pub fn get_function_coverage(&self, file: &Path, function_name: &str) -> Option<f64> {
self.coverage_index
.get_function_coverage(file, function_name)
}
pub fn get_function_coverage_with_line(
&self,
file: &Path,
function_name: &str,
line: usize,
) -> Option<f64> {
self.coverage_index
.get_function_coverage_with_line(file, function_name, line)
}
pub fn get_function_coverage_with_bounds(
&self,
file: &Path,
function_name: &str,
_start_line: usize,
_end_line: usize,
) -> Option<f64> {
let debug_mode = coverage_debug_mode();
track_coverage_attempt(debug_mode);
let available_paths = sorted_coverage_paths(&self.functions);
let path_match = find_matching_path(file, &available_paths);
log_path_match(
debug_mode,
file,
function_name,
path_match,
available_paths.len(),
);
let (matched_path, _path_strategy) = match path_match {
Some(path_match) => path_match,
None => return zero_coverage_result(debug_mode),
};
let functions = self.functions.get(matched_path)?;
let function_match = find_function_coverage(function_name, functions);
log_function_match(debug_mode, function_match, functions.len());
let (coverage, _confidence) = match function_match {
Some(function_match) => function_match,
None => return zero_coverage_result(debug_mode),
};
track_coverage_result(debug_mode, coverage);
Some(coverage)
}
pub fn get_overall_coverage(&self) -> f64 {
if self.total_lines == 0 {
0.0
} else {
(self.lines_hit as f64 / self.total_lines as f64) * 100.0
}
}
pub fn get_file_coverage(&self, file: &Path) -> Option<f64> {
self.functions.get(file).map(|funcs| {
if funcs.is_empty() {
0.0
} else {
let sum: f64 = funcs.par_iter().map(|f| f.coverage_percentage).sum();
sum / funcs.len() as f64 / 100.0 }
})
}
pub fn get_function_uncovered_lines(
&self,
file: &Path,
function_name: &str,
line: usize,
) -> Option<Vec<usize>> {
self.coverage_index
.get_function_uncovered_lines(file, function_name, line)
}
pub fn batch_get_function_coverage(
&self,
queries: &[(PathBuf, String, usize)], ) -> Vec<Option<f64>> {
queries
.par_iter()
.map(|(file, function_name, line)| {
self.get_function_coverage_with_line(file, function_name, *line)
})
.collect()
}
pub fn get_all_file_coverages(&self) -> HashMap<PathBuf, f64> {
self.functions
.par_iter()
.map(|(path, funcs)| {
let coverage = if funcs.is_empty() {
0.0
} else {
let sum: f64 = funcs.par_iter().map(|f| f.coverage_percentage).sum();
sum / funcs.len() as f64 / 100.0 };
(path.clone(), coverage)
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::risk::lcov::types::NormalizedFunctionName;
use std::sync::Arc;
fn create_test_lcov_data() -> LcovData {
let mut functions = HashMap::new();
functions.insert(
PathBuf::from("/path/to/file.rs"),
vec![
FunctionCoverage {
name: "fully_covered".to_string(),
start_line: 10,
execution_count: 10,
coverage_percentage: 100.0,
uncovered_lines: vec![],
normalized: NormalizedFunctionName::simple("fully_covered"),
},
FunctionCoverage {
name: "partially_covered".to_string(),
start_line: 20,
execution_count: 5,
coverage_percentage: 50.0,
uncovered_lines: vec![22, 23],
normalized: NormalizedFunctionName::simple("partially_covered"),
},
FunctionCoverage {
name: "not_covered".to_string(),
start_line: 30,
execution_count: 0,
coverage_percentage: 0.0,
uncovered_lines: vec![30, 31, 32],
normalized: NormalizedFunctionName::simple("not_covered"),
},
],
);
let mut data = LcovData {
functions,
total_lines: 10,
lines_hit: 5,
loc_counter: None,
coverage_index: Arc::new(crate::risk::coverage_index::CoverageIndex::empty()),
};
data.build_index();
data
}
#[test]
fn test_get_function_coverage() {
let data = create_test_lcov_data();
let file_path = PathBuf::from("/path/to/file.rs");
let coverage = data.get_function_coverage(&file_path, "fully_covered");
assert_eq!(coverage, Some(1.0));
let coverage = data.get_function_coverage(&file_path, "partially_covered");
assert_eq!(coverage, Some(0.5));
let coverage = data.get_function_coverage(&file_path, "not_covered");
assert_eq!(coverage, Some(0.0));
let coverage = data.get_function_coverage(&file_path, "nonexistent");
assert_eq!(coverage, None);
}
#[test]
fn test_get_function_coverage_with_line() {
let data = create_test_lcov_data();
let file_path = PathBuf::from("/path/to/file.rs");
let coverage = data.get_function_coverage_with_line(&file_path, "unknown_name", 10);
assert_eq!(coverage, Some(1.0));
let coverage = data.get_function_coverage_with_line(&file_path, "unknown_name", 21);
assert_eq!(coverage, Some(0.5));
}
#[test]
fn test_get_overall_coverage() {
let data = create_test_lcov_data();
assert_eq!(data.get_overall_coverage(), 50.0);
}
#[test]
fn test_get_overall_coverage_empty() {
let data = LcovData::new();
assert_eq!(data.get_overall_coverage(), 0.0);
}
#[test]
fn test_get_file_coverage() {
let data = create_test_lcov_data();
let file_path = PathBuf::from("/path/to/file.rs");
let coverage = data.get_file_coverage(&file_path);
assert!(coverage.is_some());
assert!((coverage.unwrap() - 0.5).abs() < 0.01);
}
#[test]
fn test_get_file_coverage_nonexistent() {
let data = create_test_lcov_data();
let file_path = PathBuf::from("/nonexistent/file.rs");
let coverage = data.get_file_coverage(&file_path);
assert!(coverage.is_none());
}
#[test]
fn test_batch_get_function_coverage() {
let data = create_test_lcov_data();
let queries = vec![
(
PathBuf::from("/path/to/file.rs"),
"fully_covered".to_string(),
10,
),
(
PathBuf::from("/path/to/file.rs"),
"not_covered".to_string(),
30,
),
(PathBuf::from("/nonexistent/file.rs"), "func".to_string(), 1),
];
let results = data.batch_get_function_coverage(&queries);
assert_eq!(results.len(), 3);
assert_eq!(results[0], Some(1.0));
assert_eq!(results[1], Some(0.0));
assert_eq!(results[2], None);
}
#[test]
fn test_get_all_file_coverages() {
let data = create_test_lcov_data();
let coverages = data.get_all_file_coverages();
assert_eq!(coverages.len(), 1);
let file_coverage = coverages.get(&PathBuf::from("/path/to/file.rs")).unwrap();
assert!((file_coverage - 0.5).abs() < 0.01);
}
}