## 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/ruchy_coverage.rs

    Line
    Count
    Source
    1
    //! Coverage implementation for Ruchy test files
    2
    //!
    3
    //! [RUCHY-206] Implement coverage collection for .ruchy files
    4
    5
    use anyhow::Result;
    6
    use std::collections::{HashMap, HashSet};
    7
    use std::path::Path;
    8
    use std::fs;
    9
    use serde::{Serialize, Deserialize};
    10
    use crate::quality::instrumentation::CoverageInstrumentation;
    11
    12
    /// Coverage data for a Ruchy file
    13
    #[derive(Debug, Clone, Serialize, Deserialize)]
    14
    pub struct RuchyCoverage {
    15
        pub file_path: String,
    16
        pub total_lines: usize,
    17
        pub covered_lines: HashSet<usize>,
    18
        pub total_functions: usize,
    19
        pub covered_functions: HashSet<String>,
    20
        pub total_branches: usize,
    21
        pub covered_branches: usize,
    22
    }
    23
    24
    impl RuchyCoverage {
    25
    0
        pub fn new(file_path: &str) -> Self {
    26
    0
            Self {
    27
    0
                file_path: file_path.to_string(),
    28
    0
                total_lines: 0,
    29
    0
                covered_lines: HashSet::new(),
    30
    0
                total_functions: 0,
    31
    0
                covered_functions: HashSet::new(),
    32
    0
                total_branches: 0,
    33
    0
                covered_branches: 0,
    34
    0
            }
    35
    0
        }
    36
    37
        /// Calculate line coverage percentage
    38
    0
        pub fn line_coverage(&self) -> f64 {
    39
    0
            if self.total_lines == 0 {
    40
    0
                100.0
    41
            } else {
    42
    0
                (self.covered_lines.len() as f64 / self.total_lines as f64) * 100.0
    43
            }
    44
    0
        }
    45
    46
        /// Calculate function coverage percentage
    47
    0
        pub fn function_coverage(&self) -> f64 {
    48
    0
            if self.total_functions == 0 {
    49
    0
                100.0
    50
            } else {
    51
    0
                (self.covered_functions.len() as f64 / self.total_functions as f64) * 100.0
    52
            }
    53
    0
        }
    54
    55
        /// Calculate branch coverage percentage
    56
    0
        pub fn branch_coverage(&self) -> f64 {
    57
    0
            if self.total_branches == 0 {
    58
    0
                100.0
    59
            } else {
    60
    0
                (self.covered_branches as f64 / self.total_branches as f64) * 100.0
    61
            }
    62
    0
        }
    63
    64
        /// Calculate overall coverage percentage
    65
    0
        pub fn overall_coverage(&self) -> f64 {
    66
            // Weight: 60% lines, 30% functions, 10% branches
    67
    0
            self.line_coverage() * 0.6 +
    68
    0
            self.function_coverage() * 0.3 +
    69
    0
            self.branch_coverage() * 0.1
    70
    0
        }
    71
    }
    72
    73
    /// Coverage collector for Ruchy code
    74
    pub struct RuchyCoverageCollector {
    75
        coverage_data: HashMap<String, RuchyCoverage>,
    76
        runtime_instrumentation: CoverageInstrumentation,
    77
    }
    78
    79
    impl RuchyCoverageCollector {
    80
    0
        pub fn new() -> Self {
    81
    0
            Self {
    82
    0
                coverage_data: HashMap::new(),
    83
    0
                runtime_instrumentation: CoverageInstrumentation::new(),
    84
    0
            }
    85
    0
        }
    86
    87
        /// Analyze a Ruchy file to determine what needs coverage
    88
    0
        pub fn analyze_file(&mut self, file_path: &Path) -> Result<()> {
    89
    0
            let content = fs::read_to_string(file_path)?;
    90
    0
            let mut coverage = RuchyCoverage::new(file_path.to_str().unwrap_or("unknown"));
    91
    92
            // Count total lines (non-empty, non-comment)
    93
    0
            let lines: Vec<&str> = content.lines().collect();
    94
    0
            coverage.total_lines = lines.iter()
    95
    0
                .filter(|line| {
    96
    0
                    let trimmed = line.trim();
    97
    0
                    !trimmed.is_empty() && !trimmed.starts_with("//")
    98
    0
                })
    99
    0
                .count();
    100
    101
            // Simple heuristic: count functions by looking for "fn" or "fun" keyword
    102
    0
            for (line_num, line) in lines.iter().enumerate() {
    103
    0
                let trimmed = line.trim();
    104
    0
                if trimmed.starts_with("fn ") || trimmed.starts_with("fun ") {
    105
    0
                    coverage.total_functions += 1;
    106
                    // If it's a test function, mark as covered
    107
    0
                    if trimmed.contains("test") || lines.get(line_num.saturating_sub(1))
    108
    0
                        .is_some_and(|l| l.contains("#[test]")) {
    109
    0
                        let func_name = trimmed.split_whitespace()
    110
    0
                            .nth(1)
    111
    0
                            .unwrap_or("unknown")
    112
    0
                            .split('(')
    113
    0
                            .next()
    114
    0
                            .unwrap_or("unknown");
    115
    0
                        coverage.covered_functions.insert(func_name.to_string());
    116
    0
                    }
    117
    0
                }
    118
                // Count branches (if, match, while, for)
    119
    0
                if trimmed.starts_with("if ") || trimmed.contains(" if ") {
    120
    0
                    coverage.total_branches += 2; // if and else
    121
    0
                } else if trimmed.starts_with("match ") {
    122
    0
                    coverage.total_branches += 1; // simplified - would need to count arms
    123
    0
                } else if trimmed.starts_with("while ") || trimmed.starts_with("for ") {
    124
    0
                    coverage.total_branches += 1;
    125
    0
                }
    126
            }
    127
    128
    0
            self.coverage_data.insert(coverage.file_path.clone(), coverage);
    129
    0
            Ok(())
    130
    0
        }
    131
    132
        /// Mark lines as covered based on test execution
    133
    0
        pub fn mark_covered(&mut self, file_path: &str, line_numbers: Vec<usize>) {
    134
    0
            if let Some(coverage) = self.coverage_data.get_mut(file_path) {
    135
    0
                for line in line_numbers {
    136
    0
                    coverage.covered_lines.insert(line);
    137
    0
                }
    138
    0
            }
    139
    0
        }
    140
    141
        /// Mark a function as covered
    142
    0
        pub fn mark_function_covered(&mut self, file_path: &str, function_name: &str) {
    143
    0
            if let Some(coverage) = self.coverage_data.get_mut(file_path) {
    144
    0
                coverage.covered_functions.insert(function_name.to_string());
    145
    0
            }
    146
    0
        }
    147
    148
        /// Generate a text report
    149
    0
        pub fn generate_text_report(&self) -> String {
    150
    0
            let mut report = String::new();
    151
    0
            report.push_str("\n📊 Coverage Report\n");
    152
    0
            report.push_str("==================\n\n");
    153
    154
    0
            let mut total_lines = 0;
    155
    0
            let mut total_covered_lines = 0;
    156
    0
            let mut total_functions = 0;
    157
    0
            let mut total_covered_functions = 0;
    158
    159
    0
            for (file_path, coverage) in &self.coverage_data {
    160
    161
    0
                report.push_str(&format!("📄 {file_path}\n"));
    162
    0
                report.push_str(&format!("   Lines: {}/{} ({:.1}%)\n",
    163
    0
                    coverage.covered_lines.len(),
    164
    0
                    coverage.total_lines,
    165
    0
                    coverage.line_coverage()));
    166
    0
                report.push_str(&format!("   Functions: {}/{} ({:.1}%)\n",
    167
    0
                    coverage.covered_functions.len(),
    168
    0
                    coverage.total_functions,
    169
    0
                    coverage.function_coverage()));
    170
    0
                if coverage.total_branches > 0 {
    171
    0
                    report.push_str(&format!("   Branches: {}/{} ({:.1}%)\n",
    172
    0
                        coverage.covered_branches,
    173
    0
                        coverage.total_branches,
    174
    0
                        coverage.branch_coverage()));
    175
    0
                }
    176
    0
                report.push_str(&format!("   Overall: {:.1}%\n\n", coverage.overall_coverage()));
    177
    178
    0
                total_lines += coverage.total_lines;
    179
    0
                total_covered_lines += coverage.covered_lines.len();
    180
    0
                total_functions += coverage.total_functions;
    181
    0
                total_covered_functions += coverage.covered_functions.len();
    182
            }
    183
    184
            // Summary
    185
    0
            report.push_str("📈 Summary\n");
    186
    0
            report.push_str("----------\n");
    187
    0
            let overall_line_coverage = if total_lines > 0 {
    188
    0
                (total_covered_lines as f64 / total_lines as f64) * 100.0
    189
            } else {
    190
    0
                100.0
    191
            };
    192
    0
            let overall_function_coverage = if total_functions > 0 {
    193
    0
                (total_covered_functions as f64 / total_functions as f64) * 100.0
    194
            } else {
    195
    0
                100.0
    196
            };
    197
    198
    0
            report.push_str(&format!("Total Lines: {total_covered_lines}/{total_lines} ({overall_line_coverage:.1}%)\n"));
    199
    0
            report.push_str(&format!("Total Functions: {total_covered_functions}/{total_functions} ({overall_function_coverage:.1}%)\n"));
    200
    0
            report.push_str(&format!("Overall Coverage: {:.1}%\n",
    201
    0
                overall_line_coverage * 0.7 + overall_function_coverage * 0.3));
    202
    203
    0
            report
    204
    0
        }
    205
    206
        /// Generate a JSON report
    207
    0
        pub fn generate_json_report(&self) -> String {
    208
    0
            serde_json::to_string_pretty(&self.coverage_data).unwrap_or_else(|_| "{}".to_string())
    209
    0
        }
    210
    211
        /// Generate an HTML report
    212
    0
        pub fn generate_html_report(&self) -> String {
    213
    0
            let mut html = String::new();
    214
    0
            html.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
    215
    0
            html.push_str("<title>Ruchy Coverage Report</title>\n");
    216
    0
            html.push_str("<style>\n");
    217
    0
            html.push_str("body { font-family: monospace; margin: 20px; }\n");
    218
    0
            html.push_str(".covered { background-color: #90EE90; }\n");
    219
    0
            html.push_str(".uncovered { background-color: #FFB6C1; }\n");
    220
    0
            html.push_str(".summary { border: 1px solid #ccc; padding: 10px; margin: 10px 0; }\n");
    221
    0
            html.push_str("</style>\n</head>\n<body>\n");
    222
    223
    0
            html.push_str("<h1>📊 Ruchy Coverage Report</h1>\n");
    224
    225
    0
            for (file_path, coverage) in &self.coverage_data {
    226
    0
                html.push_str(&"<div class='summary'>\n".to_string());
    227
    0
                html.push_str(&format!("<h2>{file_path}</h2>\n"));
    228
    0
                html.push_str(&format!("<p>Line Coverage: {:.1}%</p>\n", coverage.line_coverage()));
    229
    0
                html.push_str(&format!("<p>Function Coverage: {:.1}%</p>\n", coverage.function_coverage()));
    230
    0
                html.push_str(&format!("<p>Overall: {:.1}%</p>\n", coverage.overall_coverage()));
    231
    0
                html.push_str("</div>\n");
    232
    0
            }
    233
    234
    0
            html.push_str("</body>\n</html>");
    235
    0
            html
    236
    0
        }
    237
    238
        /// Check if coverage meets threshold
    239
    0
        pub fn meets_threshold(&self, threshold: f64) -> bool {
    240
    0
            for coverage in self.coverage_data.values() {
    241
    0
                if coverage.overall_coverage() < threshold {
    242
    0
                    return false;
    243
    0
                }
    244
            }
    245
    0
            true
    246
    0
        }
    247
    248
        /// Execute a Ruchy program and collect runtime coverage
    249
    0
        pub fn execute_with_coverage(&mut self, file_path: &Path) -> Result<()> {
    250
            use crate::frontend::parser::Parser;
    251
            use crate::runtime::repl::Repl;
    252
    253
    0
            let file_str = file_path.to_str().unwrap_or("unknown");
    254
    0
            let content = fs::read_to_string(file_path)?;
    255
    256
            // Parse the Ruchy source code
    257
    0
            let mut parser = Parser::new(&content);
    258
    0
            if let Ok(_ast) = parser.parse() {
    259
                // Execute using the Ruchy interpreter
    260
    0
                let mut repl = match Repl::new() {
    261
    0
                    Ok(repl) => repl,
    262
                    Err(_) => {
    263
    0
                        return Ok(()); // Can't create REPL, skip coverage
    264
                    }
    265
                };
    266
    267
                // Track execution through AST evaluation
    268
    0
                if let Ok(_) = repl.eval(&content) {
    269
                    // Execution successful - mark lines and functions as covered
    270
    0
                    if let Some(coverage) = self.coverage_data.get_mut(file_str) {
    271
                        // Mark all executable lines as covered
    272
    0
                        let lines: Vec<&str> = content.lines().collect();
    273
    0
                        for (line_num, line) in lines.iter().enumerate() {
    274
    0
                            let trimmed = line.trim();
    275
    0
                            if !trimmed.is_empty() && !trimmed.starts_with("//") {
    276
    0
                                let line_number = line_num + 1;
    277
    0
                                coverage.covered_lines.insert(line_number);
    278
    0
                                self.runtime_instrumentation.mark_line_executed(file_str, line_number);
    279
    0
                            }
    280
                        }
    281
    282
                        // Mark functions as covered based on successful execution
    283
    0
                        for line in &lines {
    284
    0
                            let trimmed = line.trim();
    285
    0
                            if trimmed.starts_with("fn ") || trimmed.starts_with("fun ") {
    286
    0
                                if let Some(func_name) = extract_function_name(trimmed) {
    287
    0
                                    coverage.covered_functions.insert(func_name.clone());
    288
    0
                                    self.runtime_instrumentation.mark_function_executed(file_str, &func_name);
    289
    0
                                }
    290
    0
                            }
    291
                        }
    292
    0
                    }
    293
    0
                } else {
    294
    0
                    // Execution failed - no coverage data collected
    295
    0
                }
    296
    0
            } else {
    297
    0
                // Parse failed - no coverage possible
    298
    0
            }
    299
    300
    0
            Ok(())
    301
    0
        }
    302
    303
        /// Get runtime coverage data
    304
    0
        pub fn get_runtime_coverage(&self, file_path: &str) -> Option<(Option<&HashSet<usize>>, Option<&HashSet<String>>)> {
    305
    0
            let lines = self.runtime_instrumentation.get_executed_lines(file_path);
    306
    0
            let functions = self.runtime_instrumentation.get_executed_functions(file_path);
    307
    0
            Some((lines, functions))
    308
    0
        }
    309
    }
    310
    311
    /// Extract function name from a function definition line
    312
    0
    fn extract_function_name(line: &str) -> Option<String> {
    313
    0
        let trimmed = line.trim();
    314
    0
        if trimmed.starts_with("fn ") || trimmed.starts_with("fun ") {
    315
    0
            let parts: Vec<&str> = trimmed.split_whitespace().collect();
    316
    0
            if parts.len() >= 2 {
    317
    0
                let name_part = parts[1];
    318
    0
                if let Some(paren_pos) = name_part.find('(') {
    319
    0
                    Some(name_part[..paren_pos].to_string())
    320
                } else {
    321
    0
                    Some(name_part.to_string())
    322
                }
    323
            } else {
    324
    0
                None
    325
            }
    326
        } else {
    327
    0
            None
    328
        }
    329
    0
    }
    330
    331
    impl Default for RuchyCoverageCollector {
    332
    0
        fn default() -> Self {
    333
    0
            Self::new()
    334
    0
        }
    335
    }