vtcode_core/code/code_quality/metrics/
coverage.rs

1use std::fs;
2use std::path::Path;
3use walkdir::WalkDir;
4
5/// Coverage analysis results
6#[derive(Debug, Clone)]
7pub struct CoverageResult {
8    pub line_coverage: f64,
9    pub branch_coverage: f64,
10    pub function_coverage: f64,
11    pub total_lines: usize,
12    pub covered_lines: usize,
13}
14
15/// Coverage analyzer for test coverage metrics
16pub struct CoverageAnalyzer;
17
18impl CoverageAnalyzer {
19    pub fn new() -> Self {
20        Self
21    }
22
23    /// Analyze test coverage for a project
24    pub fn analyze_project(&self, project_path: &Path) -> CoverageResult {
25        let report_path = std::env::var("COVERAGE_REPORT_PATH")
26            .map(|p| project_path.join(p))
27            .unwrap_or_else(|_| project_path.join("coverage/lcov.info"));
28
29        if let Ok(report) = fs::read_to_string(&report_path) {
30            let mut total_lines = 0;
31            let mut covered_lines = 0;
32            for line in report.lines() {
33                if let Some(data) = line.strip_prefix("DA:") {
34                    let parts: Vec<&str> = data.split(',').collect();
35                    if parts.len() == 2 {
36                        total_lines += 1;
37                        if parts[1].trim() != "0" {
38                            covered_lines += 1;
39                        }
40                    }
41                }
42            }
43
44            let line_coverage = if total_lines > 0 {
45                (covered_lines as f64 / total_lines as f64) * 100.0
46            } else {
47                100.0
48            };
49            let branch_coverage = (line_coverage * 0.8).min(100.0);
50            let function_coverage = (line_coverage * 0.9).min(100.0);
51
52            return CoverageResult {
53                line_coverage,
54                branch_coverage,
55                function_coverage,
56                total_lines,
57                covered_lines,
58            };
59        }
60
61        let mut total_lines = 0;
62        let mut covered_lines = 0;
63        let mut _total_files = 0;
64        let mut _test_files = 0;
65
66        for entry in WalkDir::new(project_path)
67            .follow_links(true)
68            .into_iter()
69            .filter_map(|e| e.ok())
70        {
71            if entry.file_type().is_file() {
72                if let Some(ext) = entry.path().extension() {
73                    let source_extensions = ["rs", "js", "ts", "py", "java", "cpp", "c", "go"];
74                    if source_extensions.contains(&ext.to_str().unwrap_or("")) {
75                        _total_files += 1;
76                        if let Ok(content) = fs::read_to_string(entry.path()) {
77                            let lines = content.lines().count();
78                            total_lines += lines;
79                            if self.has_test_file(entry.path()) {
80                                covered_lines += lines;
81                            }
82                        }
83                    }
84                }
85                if let Some(file_name) = entry.path().file_name().and_then(|n| n.to_str()) {
86                    if file_name.contains("test") || file_name.contains("spec") {
87                        _test_files += 1;
88                    }
89                }
90            }
91        }
92
93        let line_coverage = if total_lines > 0 {
94            (covered_lines as f64 / total_lines as f64) * 100.0
95        } else {
96            100.0
97        };
98        let branch_coverage = (line_coverage * 0.8).min(100.0);
99        let function_coverage = (line_coverage * 0.9).min(100.0);
100
101        CoverageResult {
102            line_coverage,
103            branch_coverage,
104            function_coverage,
105            total_lines,
106            covered_lines,
107        }
108    }
109
110    /// Check if a source file has a corresponding test file
111    fn has_test_file(&self, source_path: &Path) -> bool {
112        if let Some(file_name) = source_path.file_name().and_then(|n| n.to_str()) {
113            if let Some(parent) = source_path.parent() {
114                // Look for test files in the same directory
115                for entry in WalkDir::new(parent)
116                    .max_depth(1)
117                    .into_iter()
118                    .filter_map(|e| e.ok())
119                {
120                    if entry.file_type().is_file() {
121                        if let Some(test_file_name) =
122                            entry.path().file_name().and_then(|n| n.to_str())
123                        {
124                            if (test_file_name.contains("test") || test_file_name.contains("spec"))
125                                && test_file_name
126                                    .contains(file_name.split('.').next().unwrap_or(""))
127                            {
128                                return true;
129                            }
130                        }
131                    }
132                }
133            }
134        }
135        false
136    }
137}