Coverage Report

Created: 2025-09-08 21:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}