Coverage Report

Created: 2025-09-05 15: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
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
}