vtcode_core/code/code_quality/metrics/
coverage.rs1use std::fs;
2use std::path::Path;
3use walkdir::WalkDir;
4
5#[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
15pub struct CoverageAnalyzer;
17
18impl CoverageAnalyzer {
19 pub fn new() -> Self {
20 Self
21 }
22
23 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 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 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}