use crate::errors::AnalysisError;
use crate::io::traits::{Cache, CoverageData, CoverageLoader, FileCoverage, FileSystem};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::sync::RwLock;
#[derive(Debug, Default, Clone)]
pub struct RealFileSystem;
impl RealFileSystem {
pub fn new() -> Self {
Self
}
}
impl FileSystem for RealFileSystem {
fn read_to_string(&self, path: &Path) -> Result<String, AnalysisError> {
fs::read_to_string(path)
.map_err(|e| AnalysisError::io_with_path(format!("Failed to read file: {}", e), path))
}
fn write(&self, path: &Path, content: &str) -> Result<(), AnalysisError> {
fs::write(path, content)
.map_err(|e| AnalysisError::io_with_path(format!("Failed to write file: {}", e), path))
}
fn exists(&self, path: &Path) -> bool {
path.exists()
}
fn is_file(&self, path: &Path) -> bool {
path.is_file()
}
fn is_dir(&self, path: &Path) -> bool {
path.is_dir()
}
fn read_bytes(&self, path: &Path) -> Result<Vec<u8>, AnalysisError> {
fs::read(path)
.map_err(|e| AnalysisError::io_with_path(format!("Failed to read file: {}", e), path))
}
}
#[derive(Debug, Default, Clone)]
pub struct RealCoverageLoader;
impl RealCoverageLoader {
pub fn new() -> Self {
Self
}
}
impl CoverageLoader for RealCoverageLoader {
fn load_lcov(&self, path: &Path) -> Result<CoverageData, AnalysisError> {
let content = fs::read_to_string(path).map_err(|e| {
AnalysisError::coverage_with_path(format!("Failed to read LCOV file: {}", e), path)
})?;
parse_lcov_content(&content, path)
}
fn load_cobertura(&self, path: &Path) -> Result<CoverageData, AnalysisError> {
Err(AnalysisError::coverage_with_path(
"Cobertura format not yet implemented",
path,
))
}
}
#[derive(Debug, PartialEq)]
enum LcovLine {
SourceFile(std::path::PathBuf),
LineData { line: usize, hits: u64 },
EndOfRecord,
Unknown,
}
fn parse_lcov_line(line: &str) -> LcovLine {
let line = line.trim();
if let Some(sf) = line.strip_prefix("SF:") {
LcovLine::SourceFile(sf.into())
} else if let Some(da) = line.strip_prefix("DA:") {
parse_line_data(da)
} else if line == "end_of_record" {
LcovLine::EndOfRecord
} else {
LcovLine::Unknown
}
}
fn parse_line_data(da: &str) -> LcovLine {
let mut parts = da.split(',');
let parsed = parts
.next()
.and_then(|line_str| line_str.parse::<usize>().ok())
.zip(
parts
.next()
.and_then(|hits_str| hits_str.parse::<u64>().ok()),
);
match parsed {
Some((line, hits)) => LcovLine::LineData { line, hits },
None => LcovLine::Unknown,
}
}
struct LcovParserState {
data: CoverageData,
current_file: Option<std::path::PathBuf>,
current_coverage: Option<FileCoverage>,
}
impl LcovParserState {
fn new() -> Self {
Self {
data: CoverageData::new(),
current_file: None,
current_coverage: None,
}
}
fn process(mut self, line: LcovLine) -> Self {
match line {
LcovLine::SourceFile(path) => {
self.finalize_current();
self.current_file = Some(path);
self.current_coverage = Some(FileCoverage::new());
}
LcovLine::LineData { line, hits } => {
if let Some(ref mut coverage) = self.current_coverage {
coverage.add_line(line, hits);
}
}
LcovLine::EndOfRecord => {
self.finalize_current();
}
LcovLine::Unknown => {}
}
self
}
fn finalize_current(&mut self) {
if let (Some(path), Some(coverage)) =
(self.current_file.take(), self.current_coverage.take())
{
self.data.add_file_coverage(path, coverage);
}
}
fn finish(mut self) -> CoverageData {
self.finalize_current();
self.data
}
}
fn parse_lcov_content(content: &str, source_path: &Path) -> Result<CoverageData, AnalysisError> {
let data = content
.lines()
.map(parse_lcov_line)
.fold(LcovParserState::new(), LcovParserState::process)
.finish();
validate_coverage_data(data, content, source_path)
}
fn validate_coverage_data(
data: CoverageData,
content: &str,
source_path: &Path,
) -> Result<CoverageData, AnalysisError> {
if data.files().next().is_none() && !content.is_empty() {
return Err(AnalysisError::coverage_with_path(
"No coverage data found in LCOV file",
source_path,
));
}
Ok(data)
}
#[derive(Debug, Default)]
pub struct MemoryCache {
data: RwLock<HashMap<String, Vec<u8>>>,
}
impl MemoryCache {
pub fn new() -> Self {
Self {
data: RwLock::new(HashMap::new()),
}
}
}
impl Cache for MemoryCache {
fn get(&self, key: &str) -> Option<Vec<u8>> {
self.data.read().ok()?.get(key).cloned()
}
fn set(&self, key: &str, value: &[u8]) -> Result<(), AnalysisError> {
self.data
.write()
.map_err(|e| AnalysisError::io(format!("Cache write lock failed: {}", e)))?
.insert(key.to_string(), value.to_vec());
Ok(())
}
fn invalidate(&self, key: &str) -> Result<(), AnalysisError> {
self.data
.write()
.map_err(|e| AnalysisError::io(format!("Cache write lock failed: {}", e)))?
.remove(key);
Ok(())
}
fn clear(&self) -> Result<(), AnalysisError> {
self.data
.write()
.map_err(|e| AnalysisError::io(format!("Cache write lock failed: {}", e)))?
.clear();
Ok(())
}
}
#[derive(Debug, Default, Clone)]
pub struct NoOpCache;
impl NoOpCache {
pub fn new() -> Self {
Self
}
}
impl Cache for NoOpCache {
fn get(&self, _key: &str) -> Option<Vec<u8>> {
None
}
fn set(&self, _key: &str, _value: &[u8]) -> Result<(), AnalysisError> {
Ok(())
}
fn invalidate(&self, _key: &str) -> Result<(), AnalysisError> {
Ok(())
}
fn clear(&self) -> Result<(), AnalysisError> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_real_filesystem_read_write() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
let fs = RealFileSystem::new();
assert!(!fs.exists(&file_path));
assert!(!fs.is_file(&file_path));
fs.write(&file_path, "Hello, World!").unwrap();
assert!(fs.exists(&file_path));
assert!(fs.is_file(&file_path));
let content = fs.read_to_string(&file_path).unwrap();
assert_eq!(content, "Hello, World!");
}
#[test]
fn test_real_filesystem_read_nonexistent() {
let fs = RealFileSystem::new();
let result = fs.read_to_string(Path::new("/nonexistent/path/file.txt"));
assert!(result.is_err());
}
#[test]
fn test_real_filesystem_is_dir() {
let temp_dir = TempDir::new().unwrap();
let fs = RealFileSystem::new();
assert!(fs.is_dir(temp_dir.path()));
assert!(!fs.is_file(temp_dir.path()));
}
#[test]
fn test_parse_lcov_content() {
let lcov_content = r#"
SF:src/main.rs
DA:1,5
DA:2,5
DA:3,0
end_of_record
SF:src/lib.rs
DA:1,1
DA:2,0
end_of_record
"#;
let data = parse_lcov_content(lcov_content, Path::new("test.lcov")).unwrap();
let main_coverage = data.get_file_coverage(Path::new("src/main.rs")).unwrap();
assert!((main_coverage - 66.67).abs() < 1.0);
let lib_coverage = data.get_file_coverage(Path::new("src/lib.rs")).unwrap();
assert!((lib_coverage - 50.0).abs() < 0.1);
}
#[test]
fn test_parse_lcov_empty() {
let data = parse_lcov_content("", Path::new("test.lcov")).unwrap();
assert!(data.files().next().is_none());
}
#[test]
fn test_memory_cache_operations() {
let cache = MemoryCache::new();
assert!(cache.get("key1").is_none());
cache.set("key1", b"value1").unwrap();
assert_eq!(cache.get("key1"), Some(b"value1".to_vec()));
cache.invalidate("key1").unwrap();
assert!(cache.get("key1").is_none());
cache.set("key1", b"value1").unwrap();
cache.set("key2", b"value2").unwrap();
cache.clear().unwrap();
assert!(cache.get("key1").is_none());
assert!(cache.get("key2").is_none());
}
#[test]
fn test_noop_cache() {
let cache = NoOpCache::new();
cache.set("key1", b"value1").unwrap();
assert!(cache.get("key1").is_none());
cache.invalidate("key1").unwrap();
cache.clear().unwrap();
}
#[test]
fn test_parse_lcov_line_source_file() {
let line = parse_lcov_line("SF:src/main.rs");
assert_eq!(line, LcovLine::SourceFile("src/main.rs".into()));
}
#[test]
fn test_parse_lcov_line_line_data() {
let line = parse_lcov_line("DA:42,5");
assert_eq!(line, LcovLine::LineData { line: 42, hits: 5 });
}
#[test]
fn test_parse_lcov_line_end_of_record() {
let line = parse_lcov_line("end_of_record");
assert_eq!(line, LcovLine::EndOfRecord);
}
#[test]
fn test_parse_lcov_line_unknown() {
assert_eq!(parse_lcov_line(""), LcovLine::Unknown);
assert_eq!(parse_lcov_line(" "), LcovLine::Unknown);
assert_eq!(parse_lcov_line("# comment"), LcovLine::Unknown);
assert_eq!(parse_lcov_line("TN:test"), LcovLine::Unknown);
}
#[test]
fn test_parse_lcov_line_whitespace_handling() {
let line = parse_lcov_line(" SF:src/lib.rs ");
assert_eq!(line, LcovLine::SourceFile("src/lib.rs".into()));
}
#[test]
fn test_parse_line_data_invalid() {
assert_eq!(parse_line_data("42"), LcovLine::Unknown);
assert_eq!(parse_line_data("abc,5"), LcovLine::Unknown);
assert_eq!(parse_line_data("42,abc"), LcovLine::Unknown);
assert_eq!(parse_line_data(""), LcovLine::Unknown);
}
#[test]
fn test_parse_lcov_content_without_end_of_record() {
let lcov_content = "SF:src/main.rs\nDA:1,5\nDA:2,3";
let data = parse_lcov_content(lcov_content, Path::new("test.lcov")).unwrap();
let coverage = data.get_file_coverage(Path::new("src/main.rs")).unwrap();
assert!((coverage - 100.0).abs() < 0.1); }
#[test]
fn test_parse_lcov_content_invalid_format() {
let result = parse_lcov_content("garbage\nmore garbage", Path::new("test.lcov"));
assert!(result.is_err());
}
}