use crate::errors::AnalysisError;
use std::path::Path;
pub trait FileSystem: Send + Sync {
fn read_to_string(&self, path: &Path) -> Result<String, AnalysisError>;
fn write(&self, path: &Path, content: &str) -> Result<(), AnalysisError>;
fn exists(&self, path: &Path) -> bool;
fn is_file(&self, path: &Path) -> bool;
fn is_dir(&self, path: &Path) -> bool;
fn read_bytes(&self, path: &Path) -> Result<Vec<u8>, AnalysisError>;
}
pub trait CoverageLoader: Send + Sync {
fn load_lcov(&self, path: &Path) -> Result<CoverageData, AnalysisError>;
fn load_cobertura(&self, path: &Path) -> Result<CoverageData, AnalysisError>;
}
#[derive(Debug, Clone, Default)]
pub struct CoverageData {
file_coverage: std::collections::HashMap<std::path::PathBuf, FileCoverage>,
}
#[derive(Debug, Clone, Default)]
pub struct FileCoverage {
line_hits: std::collections::HashMap<usize, u64>,
pub total_lines: usize,
pub hit_lines: usize,
}
impl CoverageData {
pub fn new() -> Self {
Self::default()
}
pub fn get_file_coverage(&self, path: &Path) -> Option<f64> {
self.file_coverage.get(path).map(|fc| {
if fc.total_lines == 0 {
100.0
} else {
(fc.hit_lines as f64 / fc.total_lines as f64) * 100.0
}
})
}
pub fn get_function_coverage(
&self,
file_path: &Path,
start_line: usize,
end_line: usize,
) -> Option<f64> {
self.file_coverage.get(file_path).map(|fc| {
let lines_in_range: Vec<_> = fc
.line_hits
.iter()
.filter(|(&line, _)| line >= start_line && line <= end_line)
.collect();
if lines_in_range.is_empty() {
return 0.0;
}
let hit_count = lines_in_range.iter().filter(|(_, &hits)| hits > 0).count();
(hit_count as f64 / lines_in_range.len() as f64) * 100.0
})
}
pub fn add_file_coverage(&mut self, path: std::path::PathBuf, coverage: FileCoverage) {
self.file_coverage.insert(path, coverage);
}
pub fn has_file(&self, path: &Path) -> bool {
self.file_coverage.contains_key(path)
}
pub fn files(&self) -> impl Iterator<Item = &std::path::PathBuf> {
self.file_coverage.keys()
}
}
impl FileCoverage {
pub fn new() -> Self {
Self::default()
}
pub fn add_line(&mut self, line: usize, hits: u64) {
self.line_hits.insert(line, hits);
self.total_lines += 1;
if hits > 0 {
self.hit_lines += 1;
}
}
pub fn get_line_hits(&self, line: usize) -> Option<u64> {
self.line_hits.get(&line).copied()
}
}
pub trait Cache: Send + Sync {
fn get(&self, key: &str) -> Option<Vec<u8>>;
fn set(&self, key: &str, value: &[u8]) -> Result<(), AnalysisError>;
fn invalidate(&self, key: &str) -> Result<(), AnalysisError>;
fn clear(&self) -> Result<(), AnalysisError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_coverage_data_empty() {
let data = CoverageData::new();
assert!(data
.get_file_coverage(Path::new("nonexistent.rs"))
.is_none());
}
#[test]
fn test_coverage_data_with_file() {
let mut data = CoverageData::new();
let mut file_coverage = FileCoverage::new();
file_coverage.add_line(1, 5);
file_coverage.add_line(2, 0);
file_coverage.add_line(3, 3);
data.add_file_coverage("test.rs".into(), file_coverage);
let coverage = data.get_file_coverage(Path::new("test.rs")).unwrap();
assert!((coverage - 66.67).abs() < 1.0);
}
#[test]
fn test_function_coverage() {
let mut data = CoverageData::new();
let mut file_coverage = FileCoverage::new();
file_coverage.add_line(1, 5);
file_coverage.add_line(2, 5);
file_coverage.add_line(3, 5);
file_coverage.add_line(4, 0);
file_coverage.add_line(5, 0);
file_coverage.add_line(10, 1);
file_coverage.add_line(11, 1);
file_coverage.add_line(12, 1);
data.add_file_coverage("test.rs".into(), file_coverage);
let func_a = data
.get_function_coverage(Path::new("test.rs"), 1, 5)
.unwrap();
assert!((func_a - 60.0).abs() < 0.1);
let func_b = data
.get_function_coverage(Path::new("test.rs"), 10, 12)
.unwrap();
assert!((func_b - 100.0).abs() < 0.1);
}
#[test]
fn test_file_coverage_line_access() {
let mut fc = FileCoverage::new();
fc.add_line(1, 5);
fc.add_line(2, 0);
assert_eq!(fc.get_line_hits(1), Some(5));
assert_eq!(fc.get_line_hits(2), Some(0));
assert_eq!(fc.get_line_hits(3), None);
}
}