use super::coverage::process_function_coverage_parallel;
use super::demangle::demangle_function_name;
use super::normalize::normalize_demangled_name;
use super::types::{FunctionCoverage, LcovData, NormalizedFunctionName};
use std::collections::HashMap;
use std::path::PathBuf;
pub(crate) struct LcovParserState {
pub data: LcovData,
pub current_file: Option<PathBuf>,
pub file_functions: HashMap<String, FunctionCoverage>,
pub file_lines: HashMap<usize, u64>,
pub file_count: usize,
}
impl LcovParserState {
pub fn new() -> Self {
Self {
data: LcovData::default(),
current_file: None,
file_functions: HashMap::new(),
file_lines: HashMap::new(),
file_count: 0,
}
}
}
impl Default for LcovParserState {
fn default() -> Self {
Self::new()
}
}
pub fn create_function_coverage(
normalized: NormalizedFunctionName,
start_line: u32,
) -> FunctionCoverage {
FunctionCoverage {
name: normalized.full_path.clone(),
start_line: start_line as usize,
execution_count: 0,
coverage_percentage: 0.0,
uncovered_lines: Vec::new(),
normalized,
}
}
pub fn finalize_file_functions(
file_functions: &mut HashMap<String, FunctionCoverage>,
) -> Vec<FunctionCoverage> {
let mut funcs: Vec<FunctionCoverage> = file_functions.drain().map(|(_, v)| v).collect();
funcs.sort_by(|a, b| {
a.start_line
.cmp(&b.start_line)
.then_with(|| a.name.cmp(&b.name))
});
funcs
}
pub(crate) fn handle_source_file(state: &mut LcovParserState, path: PathBuf) {
if let Some(file) = state.current_file.take() {
if !state.file_functions.is_empty() {
let funcs = finalize_file_functions(&mut state.file_functions);
if let Some(existing_funcs) = state.data.functions.get_mut(&file) {
for new_func in funcs {
if let Some(existing) =
existing_funcs.iter_mut().find(|f| f.name == new_func.name)
{
existing.execution_count =
existing.execution_count.max(new_func.execution_count);
} else {
existing_funcs.push(new_func);
}
}
existing_funcs.sort_by(|a, b| {
a.start_line
.cmp(&b.start_line)
.then_with(|| a.name.cmp(&b.name))
});
} else {
state.data.functions.insert(file, funcs);
}
}
}
state.current_file = Some(path);
state.file_functions.clear();
state.file_lines.clear();
}
pub(crate) fn handle_function_name(state: &mut LcovParserState, start_line: u32, name: String) {
let demangled = demangle_function_name(&name);
let normalized = normalize_demangled_name(&demangled);
state
.file_functions
.entry(normalized.full_path.clone())
.or_insert_with(|| create_function_coverage(normalized, start_line));
}
pub(crate) fn handle_function_data(state: &mut LcovParserState, name: String, count: u64) {
let demangled = demangle_function_name(&name);
let normalized = normalize_demangled_name(&demangled);
if let Some(func) = state.file_functions.get_mut(&normalized.full_path) {
func.execution_count = func.execution_count.max(count);
if func.coverage_percentage == 0.0 && count > 0 {
func.coverage_percentage = 100.0;
}
}
}
pub(crate) fn handle_line_data(state: &mut LcovParserState, line: u32, count: u64) {
state.file_lines.insert(line as usize, count);
}
pub(crate) fn handle_lines_found(state: &mut LcovParserState, found: u32) {
state.data.total_lines += found as usize;
}
pub(crate) fn handle_lines_hit(state: &mut LcovParserState, hit: u32) {
state.data.lines_hit += hit as usize;
}
pub(crate) fn handle_end_of_record(state: &mut LcovParserState) {
process_function_coverage_parallel(&mut state.file_functions, &state.file_lines);
if let Some(file) = state.current_file.take() {
if !state.file_functions.is_empty() {
let funcs = finalize_file_functions(&mut state.file_functions);
if let Some(existing_funcs) = state.data.functions.get_mut(&file) {
for new_func in funcs {
if let Some(existing) =
existing_funcs.iter_mut().find(|f| f.name == new_func.name)
{
existing.execution_count =
existing.execution_count.max(new_func.execution_count);
} else {
existing_funcs.push(new_func);
}
}
existing_funcs.sort_by(|a, b| {
a.start_line
.cmp(&b.start_line)
.then_with(|| a.name.cmp(&b.name))
});
} else {
state.data.functions.insert(file, funcs);
}
}
}
state.file_functions.clear();
state.file_lines.clear();
state.file_count += 1;
}
pub(crate) fn handle_incomplete_file(state: &mut LcovParserState) {
if let Some(file) = state.current_file.take() {
if !state.file_functions.is_empty() {
let funcs = finalize_file_functions(&mut state.file_functions);
if let Some(existing_funcs) = state.data.functions.get_mut(&file) {
for new_func in funcs {
if let Some(existing) =
existing_funcs.iter_mut().find(|f| f.name == new_func.name)
{
existing.execution_count =
existing.execution_count.max(new_func.execution_count);
} else {
existing_funcs.push(new_func);
}
}
existing_funcs.sort_by(|a, b| {
a.start_line
.cmp(&b.start_line)
.then_with(|| a.name.cmp(&b.name))
});
} else {
state.data.functions.insert(file, funcs);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_function_coverage() {
let normalized = NormalizedFunctionName {
full_path: "module::func".to_string(),
method_name: "func".to_string(),
original: "module::func".to_string(),
};
let coverage = create_function_coverage(normalized, 42);
assert_eq!(coverage.name, "module::func");
assert_eq!(coverage.start_line, 42);
assert_eq!(coverage.execution_count, 0);
assert_eq!(coverage.coverage_percentage, 0.0);
assert!(coverage.uncovered_lines.is_empty());
}
#[test]
fn test_finalize_file_functions_sorts_by_line() {
let mut functions = HashMap::new();
functions.insert(
"func_c".to_string(),
FunctionCoverage {
name: "func_c".to_string(),
start_line: 30,
execution_count: 1,
coverage_percentage: 100.0,
uncovered_lines: vec![],
normalized: NormalizedFunctionName::simple("func_c"),
},
);
functions.insert(
"func_a".to_string(),
FunctionCoverage {
name: "func_a".to_string(),
start_line: 10,
execution_count: 1,
coverage_percentage: 100.0,
uncovered_lines: vec![],
normalized: NormalizedFunctionName::simple("func_a"),
},
);
functions.insert(
"func_b".to_string(),
FunctionCoverage {
name: "func_b".to_string(),
start_line: 20,
execution_count: 1,
coverage_percentage: 100.0,
uncovered_lines: vec![],
normalized: NormalizedFunctionName::simple("func_b"),
},
);
let sorted = finalize_file_functions(&mut functions);
assert_eq!(sorted.len(), 3);
assert_eq!(sorted[0].start_line, 10);
assert_eq!(sorted[1].start_line, 20);
assert_eq!(sorted[2].start_line, 30);
assert!(functions.is_empty(), "HashMap should be drained");
}
#[test]
fn test_handle_function_name_deduplicates() {
let mut state = LcovParserState::new();
state.current_file = Some(PathBuf::from("test.rs"));
handle_function_name(&mut state, 10, "my_func".to_string());
handle_function_name(&mut state, 10, "my_func".to_string());
assert_eq!(state.file_functions.len(), 1);
}
#[test]
fn test_handle_function_data_updates_execution_count() {
let mut state = LcovParserState::new();
state.current_file = Some(PathBuf::from("test.rs"));
handle_function_name(&mut state, 10, "my_func".to_string());
handle_function_data(&mut state, "my_func".to_string(), 5);
let func = state.file_functions.get("my_func").unwrap();
assert_eq!(func.execution_count, 5);
assert_eq!(func.coverage_percentage, 100.0);
}
#[test]
fn test_handle_function_data_keeps_max_count() {
let mut state = LcovParserState::new();
state.current_file = Some(PathBuf::from("test.rs"));
handle_function_name(&mut state, 10, "my_func".to_string());
handle_function_data(&mut state, "my_func".to_string(), 3);
handle_function_data(&mut state, "my_func".to_string(), 7);
handle_function_data(&mut state, "my_func".to_string(), 5);
let func = state.file_functions.get("my_func").unwrap();
assert_eq!(
func.execution_count, 7,
"Should keep maximum execution count"
);
}
#[test]
fn test_handle_line_data_tracks_lines() {
let mut state = LcovParserState::new();
handle_line_data(&mut state, 10, 5);
handle_line_data(&mut state, 20, 0);
handle_line_data(&mut state, 30, 3);
assert_eq!(state.file_lines.len(), 3);
assert_eq!(state.file_lines.get(&10), Some(&5));
assert_eq!(state.file_lines.get(&20), Some(&0));
assert_eq!(state.file_lines.get(&30), Some(&3));
}
#[test]
fn test_handle_source_file_transitions() {
let mut state = LcovParserState::new();
handle_source_file(&mut state, PathBuf::from("file1.rs"));
handle_function_name(&mut state, 10, "func1".to_string());
handle_function_data(&mut state, "func1".to_string(), 1);
handle_source_file(&mut state, PathBuf::from("file2.rs"));
assert!(state
.data
.functions
.contains_key(&PathBuf::from("file1.rs")));
assert_eq!(state.current_file, Some(PathBuf::from("file2.rs")));
assert!(state.file_functions.is_empty());
}
#[test]
fn test_handle_incomplete_file() {
let mut state = LcovParserState::new();
state.current_file = Some(PathBuf::from("incomplete.rs"));
handle_function_name(&mut state, 10, "orphan_func".to_string());
handle_function_data(&mut state, "orphan_func".to_string(), 2);
handle_incomplete_file(&mut state);
assert!(state
.data
.functions
.contains_key(&PathBuf::from("incomplete.rs")));
let funcs = state
.data
.functions
.get(&PathBuf::from("incomplete.rs"))
.unwrap();
assert_eq!(funcs.len(), 1);
assert_eq!(funcs[0].name, "orphan_func");
}
#[test]
fn test_handle_lines_found_accumulates() {
let mut state = LcovParserState::new();
handle_lines_found(&mut state, 100);
handle_lines_found(&mut state, 50);
assert_eq!(state.data.total_lines, 150);
}
#[test]
fn test_handle_lines_hit_accumulates() {
let mut state = LcovParserState::new();
handle_lines_hit(&mut state, 80);
handle_lines_hit(&mut state, 40);
assert_eq!(state.data.lines_hit, 120);
}
#[test]
fn test_parser_state_initial_values() {
let state = LcovParserState::new();
assert!(state.current_file.is_none());
assert!(state.file_functions.is_empty());
assert!(state.file_lines.is_empty());
assert_eq!(state.file_count, 0);
assert_eq!(state.data.total_lines, 0);
assert_eq!(state.data.lines_hit, 0);
}
}