## 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
    }