/home/noah/src/ruchy/src/quality/instrumentation.rs
Line | Count | Source |
1 | | //! Code instrumentation for coverage tracking |
2 | | //! |
3 | | //! [RUCHY-206] Instrument Ruchy code for runtime coverage collection |
4 | | |
5 | | use std::collections::{HashMap, HashSet}; |
6 | | use anyhow::Result; |
7 | | |
8 | | /// Runtime coverage collector |
9 | | pub struct CoverageInstrumentation { |
10 | | /// Map of file -> set of executed line numbers |
11 | | pub executed_lines: HashMap<String, HashSet<usize>>, |
12 | | /// Map of file -> set of executed function names |
13 | | pub executed_functions: HashMap<String, HashSet<String>>, |
14 | | /// Map of file -> branch execution counts |
15 | | pub executed_branches: HashMap<String, HashMap<String, usize>>, |
16 | | } |
17 | | |
18 | | impl CoverageInstrumentation { |
19 | 3 | pub fn new() -> Self { |
20 | 3 | Self { |
21 | 3 | executed_lines: HashMap::new(), |
22 | 3 | executed_functions: HashMap::new(), |
23 | 3 | executed_branches: HashMap::new(), |
24 | 3 | } |
25 | 3 | } |
26 | | |
27 | | /// Mark a line as executed |
28 | 3 | pub fn mark_line_executed(&mut self, file: &str, line: usize) { |
29 | 3 | self.executed_lines |
30 | 3 | .entry(file.to_string()) |
31 | 3 | .or_default() |
32 | 3 | .insert(line); |
33 | 3 | } |
34 | | |
35 | | /// Mark a function as executed |
36 | 3 | pub fn mark_function_executed(&mut self, file: &str, function: &str) { |
37 | 3 | self.executed_functions |
38 | 3 | .entry(file.to_string()) |
39 | 3 | .or_default() |
40 | 3 | .insert(function.to_string()); |
41 | 3 | } |
42 | | |
43 | | /// Mark a branch as executed |
44 | 1 | pub fn mark_branch_executed(&mut self, file: &str, branch_id: &str) { |
45 | 1 | *self.executed_branches |
46 | 1 | .entry(file.to_string()) |
47 | 1 | .or_default() |
48 | 1 | .entry(branch_id.to_string()) |
49 | 1 | .or_default() += 1; |
50 | 1 | } |
51 | | |
52 | | /// Get executed lines for a file |
53 | 2 | pub fn get_executed_lines(&self, file: &str) -> Option<&HashSet<usize>> { |
54 | 2 | self.executed_lines.get(file) |
55 | 2 | } |
56 | | |
57 | | /// Get executed functions for a file |
58 | 2 | pub fn get_executed_functions(&self, file: &str) -> Option<&HashSet<String>> { |
59 | 2 | self.executed_functions.get(file) |
60 | 2 | } |
61 | | |
62 | | /// Get branch execution counts for a file |
63 | 1 | pub fn get_executed_branches(&self, file: &str) -> Option<&HashMap<String, usize>> { |
64 | 1 | self.executed_branches.get(file) |
65 | 1 | } |
66 | | |
67 | | /// Merge coverage data from another instrumentation instance |
68 | 1 | pub fn merge(&mut self, other: &CoverageInstrumentation) { |
69 | | // Merge executed lines |
70 | 2 | for (file1 , lines1 ) in &other.executed_lines { |
71 | 1 | let entry = self.executed_lines.entry(file.clone()).or_default(); |
72 | 2 | for line1 in lines { |
73 | 1 | entry.insert(*line); |
74 | 1 | } |
75 | | } |
76 | | |
77 | | // Merge executed functions |
78 | 2 | for (file1 , functions1 ) in &other.executed_functions { |
79 | 1 | let entry = self.executed_functions.entry(file.clone()).or_default(); |
80 | 2 | for function1 in functions { |
81 | 1 | entry.insert(function.clone()); |
82 | 1 | } |
83 | | } |
84 | | |
85 | | // Merge branch counts |
86 | 1 | for (file0 , branches0 ) in &other.executed_branches { |
87 | 0 | let entry = self.executed_branches.entry(file.clone()).or_default(); |
88 | 0 | for (branch_id, count) in branches { |
89 | 0 | *entry.entry(branch_id.clone()).or_default() += count; |
90 | 0 | } |
91 | | } |
92 | 1 | } |
93 | | } |
94 | | |
95 | | impl Default for CoverageInstrumentation { |
96 | 0 | fn default() -> Self { |
97 | 0 | Self::new() |
98 | 0 | } |
99 | | } |
100 | | |
101 | | /// Add instrumentation to Ruchy source code |
102 | 0 | pub fn instrument_source(source: &str, file_path: &str) -> Result<String> { |
103 | 0 | let lines: Vec<&str> = source.lines().collect(); |
104 | 0 | let mut instrumented = String::new(); |
105 | | |
106 | | // Add coverage initialization at the top |
107 | 0 | instrumented.push_str(&format!( |
108 | 0 | "// Coverage instrumentation for {file_path}\n" |
109 | 0 | )); |
110 | 0 | instrumented.push_str("let __coverage = CoverageInstrumentation::new();\n\n"); |
111 | | |
112 | 0 | for (line_num, line) in lines.iter().enumerate() { |
113 | 0 | let actual_line_num = line_num + 1; |
114 | 0 | let trimmed = line.trim(); |
115 | | |
116 | | // Skip empty lines and comments |
117 | 0 | if trimmed.is_empty() || trimmed.starts_with("//") { |
118 | 0 | instrumented.push_str(line); |
119 | 0 | instrumented.push('\n'); |
120 | 0 | continue; |
121 | 0 | } |
122 | | |
123 | | // Add line execution tracking before executable statements |
124 | 0 | if is_executable_line(trimmed) { |
125 | 0 | instrumented.push_str(&format!( |
126 | 0 | "__coverage.mark_line_executed(\"{file_path}\", {actual_line_num});\n" |
127 | 0 | )); |
128 | 0 | } |
129 | | |
130 | | // Instrument function declarations |
131 | 0 | if trimmed.starts_with("fn ") || trimmed.starts_with("fun ") { |
132 | 0 | let function_name = extract_function_name(trimmed); |
133 | 0 | instrumented.push_str(&format!( |
134 | 0 | "__coverage.mark_function_executed(\"{file_path}\", \"{function_name}\");\n" |
135 | 0 | )); |
136 | 0 | } |
137 | | |
138 | | // Add the original line |
139 | 0 | instrumented.push_str(line); |
140 | 0 | instrumented.push('\n'); |
141 | | } |
142 | | |
143 | 0 | Ok(instrumented) |
144 | 0 | } |
145 | | |
146 | | /// Check if a line contains executable code (not just declarations) |
147 | 9 | fn is_executable_line(line: &str) -> bool { |
148 | 9 | let trimmed = line.trim(); |
149 | | |
150 | | // First check if it's a control flow statement (these are executable even with {) |
151 | 9 | if trimmed.starts_with("if ") || |
152 | 8 | trimmed.starts_with("while ") || |
153 | 8 | trimmed.starts_with("for ") || |
154 | 8 | trimmed.starts_with("match ") { |
155 | 1 | return true; |
156 | 8 | } |
157 | | |
158 | | // Skip function signatures, struct definitions, etc. |
159 | 8 | if trimmed.starts_with("fn ") || |
160 | 7 | trimmed.starts_with("fun ") || |
161 | 7 | trimmed.starts_with("struct ") || |
162 | 6 | trimmed.starts_with("enum ") || |
163 | 6 | trimmed.starts_with("use ") || |
164 | 5 | trimmed.starts_with("mod ") || |
165 | 5 | trimmed.starts_with("#[") || |
166 | 5 | (trimmed.ends_with('{') && !trimmed.contains('=')0 ) { |
167 | 3 | return false; |
168 | 5 | } |
169 | | |
170 | | // Consider lines with statements/expressions as executable |
171 | 5 | trimmed.contains('=') || |
172 | 4 | trimmed.contains("println") || |
173 | 3 | trimmed.contains("assert") || |
174 | 3 | trimmed.contains("return") |
175 | 9 | } |
176 | | |
177 | | /// Extract function name from function declaration |
178 | 3 | fn extract_function_name(line: &str) -> String { |
179 | 3 | let parts: Vec<&str> = line.split_whitespace().collect(); |
180 | 3 | if parts.len() >= 2 { |
181 | 3 | parts[1].split('(').next().unwrap_or("unknown").to_string() |
182 | | } else { |
183 | 0 | "unknown".to_string() |
184 | | } |
185 | 3 | } |
186 | | |
187 | | #[cfg(test)] |
188 | | mod tests { |
189 | | use super::*; |
190 | | |
191 | | #[test] |
192 | 1 | fn test_coverage_instrumentation() { |
193 | 1 | let mut coverage = CoverageInstrumentation::new(); |
194 | | |
195 | 1 | coverage.mark_line_executed("test.ruchy", 5); |
196 | 1 | coverage.mark_function_executed("test.ruchy", "main"); |
197 | 1 | coverage.mark_branch_executed("test.ruchy", "if_1"); |
198 | | |
199 | 1 | assert!(coverage.get_executed_lines("test.ruchy").unwrap().contains(&5)); |
200 | 1 | assert!(coverage.get_executed_functions("test.ruchy").unwrap().contains("main")); |
201 | 1 | assert_eq!(coverage.get_executed_branches("test.ruchy").unwrap().get("if_1"), Some(&1)); |
202 | 1 | } |
203 | | |
204 | | #[test] |
205 | 1 | fn test_is_executable_line() { |
206 | 1 | assert!(is_executable_line("let x = 5;")); |
207 | 1 | assert!(is_executable_line("println(\"hello\");")); |
208 | 1 | assert!(is_executable_line("return x + 1;")); |
209 | 1 | assert!(is_executable_line("if x > 0 {")); |
210 | | |
211 | 1 | assert!(!is_executable_line("fn main() {")); |
212 | 1 | assert!(!is_executable_line("struct Point {")); |
213 | 1 | assert!(!is_executable_line("use std::collections::HashMap;")); |
214 | 1 | assert!(!is_executable_line("// comment")); |
215 | 1 | assert!(!is_executable_line("")); |
216 | 1 | } |
217 | | |
218 | | #[test] |
219 | 1 | fn test_extract_function_name() { |
220 | 1 | assert_eq!(extract_function_name("fn main() {"), "main"); |
221 | 1 | assert_eq!(extract_function_name("fun test_function(x: i32) -> i32 {"), "test_function"); |
222 | 1 | assert_eq!(extract_function_name("fn add(a: i32, b: i32) -> i32 {"), "add"); |
223 | 1 | } |
224 | | |
225 | | #[test] |
226 | 1 | fn test_merge_coverage() { |
227 | 1 | let mut coverage1 = CoverageInstrumentation::new(); |
228 | 1 | coverage1.mark_line_executed("test.ruchy", 1); |
229 | 1 | coverage1.mark_function_executed("test.ruchy", "func1"); |
230 | | |
231 | 1 | let mut coverage2 = CoverageInstrumentation::new(); |
232 | 1 | coverage2.mark_line_executed("test.ruchy", 2); |
233 | 1 | coverage2.mark_function_executed("test.ruchy", "func2"); |
234 | | |
235 | 1 | coverage1.merge(&coverage2); |
236 | | |
237 | 1 | let lines = coverage1.get_executed_lines("test.ruchy").unwrap(); |
238 | 1 | assert!(lines.contains(&1)); |
239 | 1 | assert!(lines.contains(&2)); |
240 | | |
241 | 1 | let functions = coverage1.get_executed_functions("test.ruchy").unwrap(); |
242 | 1 | assert!(functions.contains("func1")); |
243 | 1 | assert!(functions.contains("func2")); |
244 | 1 | } |
245 | | } |