/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 | 0 | pub fn new() -> Self { |
20 | 0 | Self { |
21 | 0 | executed_lines: HashMap::new(), |
22 | 0 | executed_functions: HashMap::new(), |
23 | 0 | executed_branches: HashMap::new(), |
24 | 0 | } |
25 | 0 | } |
26 | | |
27 | | /// Mark a line as executed |
28 | 0 | pub fn mark_line_executed(&mut self, file: &str, line: usize) { |
29 | 0 | self.executed_lines |
30 | 0 | .entry(file.to_string()) |
31 | 0 | .or_default() |
32 | 0 | .insert(line); |
33 | 0 | } |
34 | | |
35 | | /// Mark a function as executed |
36 | 0 | pub fn mark_function_executed(&mut self, file: &str, function: &str) { |
37 | 0 | self.executed_functions |
38 | 0 | .entry(file.to_string()) |
39 | 0 | .or_default() |
40 | 0 | .insert(function.to_string()); |
41 | 0 | } |
42 | | |
43 | | /// Mark a branch as executed |
44 | 0 | pub fn mark_branch_executed(&mut self, file: &str, branch_id: &str) { |
45 | 0 | *self.executed_branches |
46 | 0 | .entry(file.to_string()) |
47 | 0 | .or_default() |
48 | 0 | .entry(branch_id.to_string()) |
49 | 0 | .or_default() += 1; |
50 | 0 | } |
51 | | |
52 | | /// Get executed lines for a file |
53 | 0 | pub fn get_executed_lines(&self, file: &str) -> Option<&HashSet<usize>> { |
54 | 0 | self.executed_lines.get(file) |
55 | 0 | } |
56 | | |
57 | | /// Get executed functions for a file |
58 | 0 | pub fn get_executed_functions(&self, file: &str) -> Option<&HashSet<String>> { |
59 | 0 | self.executed_functions.get(file) |
60 | 0 | } |
61 | | |
62 | | /// Get branch execution counts for a file |
63 | 0 | pub fn get_executed_branches(&self, file: &str) -> Option<&HashMap<String, usize>> { |
64 | 0 | self.executed_branches.get(file) |
65 | 0 | } |
66 | | |
67 | | /// Merge coverage data from another instrumentation instance |
68 | 0 | pub fn merge(&mut self, other: &CoverageInstrumentation) { |
69 | | // Merge executed lines |
70 | 0 | for (file, lines) in &other.executed_lines { |
71 | 0 | let entry = self.executed_lines.entry(file.clone()).or_default(); |
72 | 0 | for line in lines { |
73 | 0 | entry.insert(*line); |
74 | 0 | } |
75 | | } |
76 | | |
77 | | // Merge executed functions |
78 | 0 | for (file, functions) in &other.executed_functions { |
79 | 0 | let entry = self.executed_functions.entry(file.clone()).or_default(); |
80 | 0 | for function in functions { |
81 | 0 | entry.insert(function.clone()); |
82 | 0 | } |
83 | | } |
84 | | |
85 | | // Merge branch counts |
86 | 0 | for (file, branches) 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 | 0 | } |
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 | 0 | fn is_executable_line(line: &str) -> bool { |
148 | 0 | let trimmed = line.trim(); |
149 | | |
150 | | // Check control flow (complexity: 4) |
151 | 0 | if is_control_flow_statement(trimmed) { |
152 | 0 | return true; |
153 | 0 | } |
154 | | |
155 | | // Check declarations (complexity: 3) |
156 | 0 | if is_declaration_statement(trimmed) { |
157 | 0 | return false; |
158 | 0 | } |
159 | | |
160 | | // Check block starts (complexity: 2) |
161 | 0 | if is_block_start(trimmed) { |
162 | 0 | return false; |
163 | 0 | } |
164 | | |
165 | | // Check executable statements (complexity: 1) |
166 | 0 | is_executable_statement(trimmed) |
167 | 0 | } |
168 | | |
169 | | /// Check if line is a control flow statement (complexity: 4) |
170 | 0 | fn is_control_flow_statement(trimmed: &str) -> bool { |
171 | 0 | trimmed.starts_with("if ") || |
172 | 0 | trimmed.starts_with("while ") || |
173 | 0 | trimmed.starts_with("for ") || |
174 | 0 | trimmed.starts_with("match ") |
175 | 0 | } |
176 | | |
177 | | /// Check if line is a declaration (complexity: 7) |
178 | 0 | fn is_declaration_statement(trimmed: &str) -> bool { |
179 | 0 | trimmed.starts_with("fn ") || |
180 | 0 | trimmed.starts_with("fun ") || |
181 | 0 | trimmed.starts_with("struct ") || |
182 | 0 | trimmed.starts_with("enum ") || |
183 | 0 | trimmed.starts_with("use ") || |
184 | 0 | trimmed.starts_with("mod ") || |
185 | 0 | trimmed.starts_with("#[") |
186 | 0 | } |
187 | | |
188 | | /// Check if line starts a block (complexity: 2) |
189 | 0 | fn is_block_start(trimmed: &str) -> bool { |
190 | 0 | trimmed.ends_with('{') && !trimmed.contains('=') |
191 | 0 | } |
192 | | |
193 | | /// Check if line contains executable statement (complexity: 4) |
194 | 0 | fn is_executable_statement(trimmed: &str) -> bool { |
195 | 0 | trimmed.contains('=') || |
196 | 0 | trimmed.contains("println") || |
197 | 0 | trimmed.contains("assert") || |
198 | 0 | trimmed.contains("return") |
199 | 0 | } |
200 | | |
201 | | /// Extract function name from function declaration |
202 | 0 | fn extract_function_name(line: &str) -> String { |
203 | 0 | let parts: Vec<&str> = line.split_whitespace().collect(); |
204 | 0 | if parts.len() >= 2 { |
205 | 0 | parts[1].split('(').next().unwrap_or("unknown").to_string() |
206 | | } else { |
207 | 0 | "unknown".to_string() |
208 | | } |
209 | 0 | } |
210 | | |
211 | | #[cfg(test)] |
212 | | mod tests { |
213 | | use super::*; |
214 | | |
215 | | #[test] |
216 | | fn test_coverage_instrumentation() { |
217 | | let mut coverage = CoverageInstrumentation::new(); |
218 | | |
219 | | coverage.mark_line_executed("test.ruchy", 5); |
220 | | coverage.mark_function_executed("test.ruchy", "main"); |
221 | | coverage.mark_branch_executed("test.ruchy", "if_1"); |
222 | | |
223 | | assert!(coverage.get_executed_lines("test.ruchy").unwrap().contains(&5)); |
224 | | assert!(coverage.get_executed_functions("test.ruchy").unwrap().contains("main")); |
225 | | assert_eq!(coverage.get_executed_branches("test.ruchy").unwrap().get("if_1"), Some(&1)); |
226 | | } |
227 | | |
228 | | #[test] |
229 | | fn test_is_executable_line() { |
230 | | assert!(is_executable_line("let x = 5;")); |
231 | | assert!(is_executable_line("println(\"hello\");")); |
232 | | assert!(is_executable_line("return x + 1;")); |
233 | | assert!(is_executable_line("if x > 0 {")); |
234 | | |
235 | | assert!(!is_executable_line("fn main() {")); |
236 | | assert!(!is_executable_line("struct Point {")); |
237 | | assert!(!is_executable_line("use std::collections::HashMap;")); |
238 | | assert!(!is_executable_line("// comment")); |
239 | | assert!(!is_executable_line("")); |
240 | | } |
241 | | |
242 | | #[test] |
243 | | fn test_extract_function_name() { |
244 | | assert_eq!(extract_function_name("fn main() {"), "main"); |
245 | | assert_eq!(extract_function_name("fun test_function(x: i32) -> i32 {"), "test_function"); |
246 | | assert_eq!(extract_function_name("fn add(a: i32, b: i32) -> i32 {"), "add"); |
247 | | } |
248 | | |
249 | | #[test] |
250 | | fn test_merge_coverage() { |
251 | | let mut coverage1 = CoverageInstrumentation::new(); |
252 | | coverage1.mark_line_executed("test.ruchy", 1); |
253 | | coverage1.mark_function_executed("test.ruchy", "func1"); |
254 | | |
255 | | let mut coverage2 = CoverageInstrumentation::new(); |
256 | | coverage2.mark_line_executed("test.ruchy", 2); |
257 | | coverage2.mark_function_executed("test.ruchy", "func2"); |
258 | | |
259 | | coverage1.merge(&coverage2); |
260 | | |
261 | | let lines = coverage1.get_executed_lines("test.ruchy").unwrap(); |
262 | | assert!(lines.contains(&1)); |
263 | | assert!(lines.contains(&2)); |
264 | | |
265 | | let functions = coverage1.get_executed_functions("test.ruchy").unwrap(); |
266 | | assert!(functions.contains("func1")); |
267 | | assert!(functions.contains("func2")); |
268 | | } |
269 | | } |