use crate::coverage::CoverageReport;
use crate::result::ProbarResult;
use std::collections::BTreeMap;
use std::path::Path;
#[derive(Debug)]
pub struct LcovFormatter<'a> {
report: &'a CoverageReport,
test_name: Option<String>,
}
impl<'a> LcovFormatter<'a> {
#[must_use]
pub fn new(report: &'a CoverageReport) -> Self {
Self {
report,
test_name: report.session_name().map(String::from),
}
}
#[must_use]
pub fn with_test_name(mut self, name: impl Into<String>) -> Self {
self.test_name = Some(name.into());
self
}
#[must_use]
pub fn generate(&self) -> String {
use std::fmt::Write;
let mut output = String::new();
if let Some(ref name) = self.test_name {
let _ = writeln!(output, "TN:{name}");
} else {
output.push_str("TN:\n");
}
let files = self.group_by_file();
for (file, blocks) in &files {
let _ = writeln!(output, "SF:{file}");
let functions = Self::extract_functions(blocks);
let mut functions_hit = 0;
for (func_name, (line, count)) in &functions {
let _ = writeln!(output, "FN:{line},{func_name}");
let _ = writeln!(output, "FNDA:{count},{func_name}");
if *count > 0 {
functions_hit += 1;
}
}
let _ = writeln!(output, "FNF:{}", functions.len());
let _ = writeln!(output, "FNH:{functions_hit}");
let lines = Self::extract_lines(blocks);
let mut lines_hit = 0;
for (line, count) in &lines {
let _ = writeln!(output, "DA:{line},{count}");
if *count > 0 {
lines_hit += 1;
}
}
let _ = writeln!(output, "LF:{}", lines.len());
let _ = writeln!(output, "LH:{lines_hit}");
output.push_str("end_of_record\n");
}
output
}
pub fn save(&self, path: &Path) -> ProbarResult<()> {
let content = self.generate();
std::fs::write(path, content)?;
Ok(())
}
fn group_by_file(&self) -> BTreeMap<String, Vec<(u32, u64, Option<String>)>> {
let mut files: BTreeMap<String, Vec<(u32, u64, Option<String>)>> = BTreeMap::new();
for block in self.report.block_coverages() {
let file = block.source_location.as_ref().map_or_else(
|| "unknown".to_string(),
|loc| {
loc.split(':').next().unwrap_or("unknown").to_string()
},
);
let line = block.source_location.as_ref().map_or(0, |loc| {
loc.split(':')
.nth(1)
.and_then(|l| l.parse().ok())
.unwrap_or(0)
});
files
.entry(file)
.or_default()
.push((line, block.hit_count, block.function_name));
}
files
}
fn extract_functions(blocks: &[(u32, u64, Option<String>)]) -> BTreeMap<String, (u32, u64)> {
let mut functions = BTreeMap::new();
for (line, count, func_name) in blocks {
if let Some(ref name) = func_name {
let entry = functions.entry(name.clone()).or_insert((*line, 0));
entry.1 += count;
}
}
functions
}
fn extract_lines(blocks: &[(u32, u64, Option<String>)]) -> BTreeMap<u32, u64> {
let mut lines = BTreeMap::new();
for (line, count, _) in blocks {
if *line > 0 {
*lines.entry(*line).or_insert(0) += count;
}
}
lines
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::coverage::BlockId;
fn create_test_report() -> CoverageReport {
let mut report = CoverageReport::new(5);
report.set_session_name("test_session");
report.record_hits(BlockId::new(0), 10);
report.record_hits(BlockId::new(1), 5);
report.record_hits(BlockId::new(2), 0);
report.record_hits(BlockId::new(3), 3);
report.record_hits(BlockId::new(4), 0);
report.set_source_location(BlockId::new(0), "src/game.rs:10");
report.set_source_location(BlockId::new(1), "src/game.rs:15");
report.set_source_location(BlockId::new(2), "src/game.rs:20");
report.set_source_location(BlockId::new(3), "src/player.rs:5");
report.set_source_location(BlockId::new(4), "src/player.rs:10");
report.set_function_name(BlockId::new(0), "main");
report.set_function_name(BlockId::new(1), "main");
report.set_function_name(BlockId::new(2), "update");
report.set_function_name(BlockId::new(3), "move_player");
report.set_function_name(BlockId::new(4), "move_player");
report
}
#[test]
fn test_lcov_formatter_new() {
let report = CoverageReport::new(10);
let formatter = LcovFormatter::new(&report);
assert!(formatter.test_name.is_none());
}
#[test]
fn test_lcov_formatter_with_test_name() {
let report = CoverageReport::new(10);
let formatter = LcovFormatter::new(&report).with_test_name("my_test");
assert_eq!(formatter.test_name, Some("my_test".to_string()));
}
#[test]
fn test_generate_empty_report() {
let report = CoverageReport::new(0);
let formatter = LcovFormatter::new(&report);
let output = formatter.generate();
assert!(output.contains("TN:"));
}
#[test]
fn test_generate_with_test_name() {
let report = create_test_report();
let formatter = LcovFormatter::new(&report);
let output = formatter.generate();
assert!(output.contains("TN:test_session"));
}
#[test]
fn test_generate_contains_source_files() {
let report = create_test_report();
let formatter = LcovFormatter::new(&report);
let output = formatter.generate();
assert!(output.contains("SF:src/game.rs"));
assert!(output.contains("SF:src/player.rs"));
}
#[test]
fn test_generate_contains_functions() {
let report = create_test_report();
let formatter = LcovFormatter::new(&report);
let output = formatter.generate();
assert!(output.contains("FN:"));
assert!(output.contains("FNDA:"));
assert!(output.contains("FNF:"));
assert!(output.contains("FNH:"));
}
#[test]
fn test_generate_contains_line_data() {
let report = create_test_report();
let formatter = LcovFormatter::new(&report);
let output = formatter.generate();
assert!(output.contains("DA:"));
assert!(output.contains("LF:"));
assert!(output.contains("LH:"));
}
#[test]
fn test_generate_contains_end_of_record() {
let report = create_test_report();
let formatter = LcovFormatter::new(&report);
let output = formatter.generate();
assert!(output.contains("end_of_record"));
}
#[test]
fn test_generate_line_hit_counts() {
let report = create_test_report();
let formatter = LcovFormatter::new(&report);
let output = formatter.generate();
assert!(output.contains("DA:10,10"));
assert!(output.contains("DA:15,5"));
}
#[test]
fn test_save_creates_file() {
let report = create_test_report();
let formatter = LcovFormatter::new(&report);
let temp_dir = tempfile::tempdir().unwrap();
let path = temp_dir.path().join("coverage.lcov");
formatter.save(&path).unwrap();
assert!(path.exists());
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("TN:"));
}
#[test]
fn test_group_by_file() {
let report = create_test_report();
let formatter = LcovFormatter::new(&report);
let files = formatter.group_by_file();
assert!(files.contains_key("src/game.rs"));
assert!(files.contains_key("src/player.rs"));
assert_eq!(files.len(), 2);
}
#[test]
fn test_custom_test_name_overrides_session() {
let report = create_test_report();
let formatter = LcovFormatter::new(&report).with_test_name("custom_name");
let output = formatter.generate();
assert!(output.contains("TN:custom_name"));
assert!(!output.contains("TN:test_session"));
}
}