use anyhow::Result;
use std::collections::{HashMap, HashSet};
pub struct CoverageInstrumentation {
pub executed_lines: HashMap<String, HashSet<usize>>,
pub executed_functions: HashMap<String, HashSet<String>>,
pub executed_branches: HashMap<String, HashMap<String, usize>>,
}
impl CoverageInstrumentation {
pub fn new() -> Self {
Self {
executed_lines: HashMap::new(),
executed_functions: HashMap::new(),
executed_branches: HashMap::new(),
}
}
pub fn mark_line_executed(&mut self, file: &str, line: usize) {
self.executed_lines
.entry(file.to_string())
.or_default()
.insert(line);
}
pub fn mark_function_executed(&mut self, file: &str, function: &str) {
self.executed_functions
.entry(file.to_string())
.or_default()
.insert(function.to_string());
}
pub fn mark_branch_executed(&mut self, file: &str, branch_id: &str) {
*self
.executed_branches
.entry(file.to_string())
.or_default()
.entry(branch_id.to_string())
.or_default() += 1;
}
pub fn get_executed_lines(&self, file: &str) -> Option<&HashSet<usize>> {
self.executed_lines.get(file)
}
pub fn get_executed_functions(&self, file: &str) -> Option<&HashSet<String>> {
self.executed_functions.get(file)
}
pub fn get_executed_branches(&self, file: &str) -> Option<&HashMap<String, usize>> {
self.executed_branches.get(file)
}
pub fn merge(&mut self, other: &CoverageInstrumentation) {
for (file, lines) in &other.executed_lines {
let entry = self.executed_lines.entry(file.clone()).or_default();
for line in lines {
entry.insert(*line);
}
}
for (file, functions) in &other.executed_functions {
let entry = self.executed_functions.entry(file.clone()).or_default();
for function in functions {
entry.insert(function.clone());
}
}
for (file, branches) in &other.executed_branches {
let entry = self.executed_branches.entry(file.clone()).or_default();
for (branch_id, count) in branches {
*entry.entry(branch_id.clone()).or_default() += count;
}
}
}
}
impl Default for CoverageInstrumentation {
fn default() -> Self {
Self::new()
}
}
pub fn instrument_source(source: &str, file_path: &str) -> Result<String> {
let lines: Vec<&str> = source.lines().collect();
let mut instrumented = String::new();
instrumented.push_str(&format!("// Coverage instrumentation for {file_path}\n"));
instrumented.push_str("let __coverage = CoverageInstrumentation::new();\n\n");
for (line_num, line) in lines.iter().enumerate() {
let actual_line_num = line_num + 1;
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with("//") {
instrumented.push_str(line);
instrumented.push('\n');
continue;
}
if is_executable_line(trimmed) {
instrumented.push_str(&format!(
"__coverage.mark_line_executed(\"{file_path}\", {actual_line_num});\n"
));
}
if trimmed.starts_with("fn ") || trimmed.starts_with("fun ") {
let function_name = extract_function_name(trimmed);
instrumented.push_str(&format!(
"__coverage.mark_function_executed(\"{file_path}\", \"{function_name}\");\n"
));
}
instrumented.push_str(line);
instrumented.push('\n');
}
Ok(instrumented)
}
fn is_executable_line(line: &str) -> bool {
let trimmed = line.trim();
if is_control_flow_statement(trimmed) {
return true;
}
if is_declaration_statement(trimmed) {
return false;
}
if is_block_start(trimmed) {
return false;
}
is_executable_statement(trimmed)
}
fn is_control_flow_statement(trimmed: &str) -> bool {
trimmed.starts_with("if ")
|| trimmed.starts_with("while ")
|| trimmed.starts_with("for ")
|| trimmed.starts_with("match ")
}
fn is_declaration_statement(trimmed: &str) -> bool {
trimmed.starts_with("fn ")
|| trimmed.starts_with("fun ")
|| trimmed.starts_with("struct ")
|| trimmed.starts_with("enum ")
|| trimmed.starts_with("use ")
|| trimmed.starts_with("mod ")
|| trimmed.starts_with("#[")
}
fn is_block_start(trimmed: &str) -> bool {
trimmed.ends_with('{') && !trimmed.contains('=')
}
fn is_executable_statement(trimmed: &str) -> bool {
trimmed.contains('=')
|| trimmed.contains("println")
|| trimmed.contains("assert")
|| trimmed.contains("return")
}
fn extract_function_name(line: &str) -> String {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
parts[1].split('(').next().unwrap_or("unknown").to_string()
} else {
"unknown".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_coverage_instrumentation() {
let mut coverage = CoverageInstrumentation::new();
coverage.mark_line_executed("test.ruchy", 5);
coverage.mark_function_executed("test.ruchy", "main");
coverage.mark_branch_executed("test.ruchy", "if_1");
assert!(coverage
.get_executed_lines("test.ruchy")
.expect("operation should succeed in test")
.contains(&5));
assert!(coverage
.get_executed_functions("test.ruchy")
.expect("operation should succeed in test")
.contains("main"));
assert_eq!(
coverage
.get_executed_branches("test.ruchy")
.expect("operation should succeed in test")
.get("if_1"),
Some(&1)
);
}
#[test]
fn test_is_executable_line() {
assert!(is_executable_line("let x = 5;"));
assert!(is_executable_line("println(\"hello\");"));
assert!(is_executable_line("return x + 1;"));
assert!(is_executable_line("if x > 0 {"));
assert!(!is_executable_line("fn main() {"));
assert!(!is_executable_line("struct Point {"));
assert!(!is_executable_line(
"use std::collections::HashMap;
#[cfg(test)]
use proptest::prelude::*;
"
));
assert!(!is_executable_line("// comment"));
assert!(!is_executable_line(""));
}
#[test]
fn test_extract_function_name() {
assert_eq!(extract_function_name("fn main() {"), "main");
assert_eq!(
extract_function_name("fun test_function(x: i32) -> i32 {"),
"test_function"
);
assert_eq!(
extract_function_name("fn add(a: i32, b: i32) -> i32 {"),
"add"
);
}
#[test]
fn test_merge_coverage() {
let mut coverage1 = CoverageInstrumentation::new();
coverage1.mark_line_executed("test.ruchy", 1);
coverage1.mark_function_executed("test.ruchy", "func1");
let mut coverage2 = CoverageInstrumentation::new();
coverage2.mark_line_executed("test.ruchy", 2);
coverage2.mark_function_executed("test.ruchy", "func2");
coverage1.merge(&coverage2);
let lines = coverage1
.get_executed_lines("test.ruchy")
.expect("operation should succeed in test");
assert!(lines.contains(&1));
assert!(lines.contains(&2));
let functions = coverage1
.get_executed_functions("test.ruchy")
.expect("operation should succeed in test");
assert!(functions.contains("func1"));
assert!(functions.contains("func2"));
}
}
#[cfg(test)]
mod property_tests_instrumentation {
use proptest::proptest;
proptest! {
#[test]
fn test_new_never_panics(input: String) {
let _input = if input.len() > 100 { &input[..100] } else { &input[..] };
let _ = std::panic::catch_unwind(|| {
});
}
}
}
#[cfg(test)]
mod coverage_tests {
use super::*;
#[test]
fn test_coverage_instrumentation_default() {
let coverage = CoverageInstrumentation::default();
assert!(coverage.executed_lines.is_empty());
assert!(coverage.executed_functions.is_empty());
assert!(coverage.executed_branches.is_empty());
}
#[test]
fn test_get_executed_lines_none() {
let coverage = CoverageInstrumentation::new();
assert!(coverage.get_executed_lines("nonexistent.rs").is_none());
}
#[test]
fn test_get_executed_functions_none() {
let coverage = CoverageInstrumentation::new();
assert!(coverage.get_executed_functions("nonexistent.rs").is_none());
}
#[test]
fn test_get_executed_branches_none() {
let coverage = CoverageInstrumentation::new();
assert!(coverage.get_executed_branches("nonexistent.rs").is_none());
}
#[test]
fn test_mark_line_executed_multiple_files() {
let mut coverage = CoverageInstrumentation::new();
coverage.mark_line_executed("file1.rs", 1);
coverage.mark_line_executed("file2.rs", 2);
coverage.mark_line_executed("file1.rs", 3);
let file1_lines = coverage.get_executed_lines("file1.rs").unwrap();
assert!(file1_lines.contains(&1));
assert!(file1_lines.contains(&3));
assert_eq!(file1_lines.len(), 2);
let file2_lines = coverage.get_executed_lines("file2.rs").unwrap();
assert!(file2_lines.contains(&2));
}
#[test]
fn test_mark_branch_executed_increments() {
let mut coverage = CoverageInstrumentation::new();
coverage.mark_branch_executed("test.rs", "branch_1");
coverage.mark_branch_executed("test.rs", "branch_1");
coverage.mark_branch_executed("test.rs", "branch_1");
let branches = coverage.get_executed_branches("test.rs").unwrap();
assert_eq!(branches.get("branch_1"), Some(&3));
}
#[test]
fn test_merge_branch_counts_accumulate() {
let mut coverage1 = CoverageInstrumentation::new();
coverage1.mark_branch_executed("test.rs", "branch_1");
coverage1.mark_branch_executed("test.rs", "branch_1");
let mut coverage2 = CoverageInstrumentation::new();
coverage2.mark_branch_executed("test.rs", "branch_1");
coverage2.mark_branch_executed("test.rs", "branch_2");
coverage1.merge(&coverage2);
let branches = coverage1.get_executed_branches("test.rs").unwrap();
assert_eq!(branches.get("branch_1"), Some(&3));
assert_eq!(branches.get("branch_2"), Some(&1));
}
#[test]
fn test_instrument_source_basic() {
let source = "let x = 5;\nprintln(x);";
let result = instrument_source(source, "test.ruchy").unwrap();
assert!(result.contains("Coverage instrumentation"));
assert!(result.contains("CoverageInstrumentation::new()"));
}
#[test]
fn test_instrument_source_empty_lines() {
let source = "\n\n// comment\n";
let result = instrument_source(source, "test.ruchy").unwrap();
assert!(result.contains("// comment"));
}
#[test]
fn test_instrument_source_function() {
let source = "fn main() { println(\"hello\"); }";
let result = instrument_source(source, "test.ruchy").unwrap();
assert!(result.contains("mark_function_executed"));
}
#[test]
fn test_instrument_source_fun_keyword() {
let source = "fun add(a, b) { a + b }";
let result = instrument_source(source, "test.ruchy").unwrap();
assert!(result.contains("mark_function_executed"));
assert!(result.contains("\"add\""));
}
#[test]
fn test_is_control_flow_statement_while() {
assert!(is_control_flow_statement("while x > 0 {"));
}
#[test]
fn test_is_control_flow_statement_for() {
assert!(is_control_flow_statement("for i in 0..10 {"));
}
#[test]
fn test_is_control_flow_statement_match() {
assert!(is_control_flow_statement("match x {"));
}
#[test]
fn test_is_declaration_statement_mod() {
assert!(is_declaration_statement("mod tests;"));
}
#[test]
fn test_is_declaration_statement_attribute() {
assert!(is_declaration_statement("#[derive(Debug)]"));
}
#[test]
fn test_is_block_start() {
assert!(is_block_start("impl Foo {"));
assert!(!is_block_start("let x = Foo {"));
}
#[test]
fn test_is_executable_statement_assert() {
assert!(is_executable_statement("assert!(x > 0);"));
}
#[test]
fn test_extract_function_name_short() {
assert_eq!(extract_function_name("fn"), "unknown");
}
#[test]
fn test_merge_empty() {
let mut coverage1 = CoverageInstrumentation::new();
let coverage2 = CoverageInstrumentation::new();
coverage1.merge(&coverage2);
assert!(coverage1.executed_lines.is_empty());
}
#[test]
fn test_merge_multiple_files() {
let mut coverage1 = CoverageInstrumentation::new();
coverage1.mark_line_executed("file1.rs", 1);
let mut coverage2 = CoverageInstrumentation::new();
coverage2.mark_line_executed("file2.rs", 1);
coverage2.mark_function_executed("file2.rs", "test");
coverage1.merge(&coverage2);
assert!(coverage1.get_executed_lines("file1.rs").is_some());
assert!(coverage1.get_executed_lines("file2.rs").is_some());
assert!(coverage1.get_executed_functions("file2.rs").is_some());
}
}