use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
lazy_static::lazy_static! {
static ref TEST_RESULTS: Mutex<BTreeMap<String, TestResult>> = Mutex::new(BTreeMap::new());
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct TestResult {
passed: bool,
expected_to_fail: bool,
}
pub fn record_test_result(test_name: &str, passed: bool, expected_to_fail: bool) {
let mut results = TEST_RESULTS.lock().unwrap_or_else(|e| e.into_inner());
results.insert(
test_name.to_string(),
TestResult {
passed,
expected_to_fail,
},
);
}
#[cfg(test)]
#[ctor::dtor]
fn generate_test_report() {
if let Err(e) = create_test_summary_report() {
eprintln!("Warning: Failed to generate test summary report: {}", e);
}
}
fn create_test_summary_report() -> Result<(), Box<dyn std::error::Error>> {
let output_path = PathBuf::from("docs/test-summary.md");
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)?;
}
let (categories, total_rules, implemented_rules, total_tests) = collect_rule_data()?;
let mut report = String::new();
report.push_str("# CERT C Rules Test Summary\n\n");
render_overview(&mut report, total_rules, implemented_rules, total_tests);
render_toc(&mut report, &categories);
render_detail_sections(&mut report, &categories);
render_summary_table(&mut report, &categories);
fs::write(&output_path, report)?;
println!("Generated test summary report: {}", output_path.display());
Ok(())
}
#[allow(clippy::type_complexity)]
fn collect_rule_data(
) -> Result<(BTreeMap<String, Vec<RuleInfo>>, usize, usize, usize), Box<dyn std::error::Error>> {
let cert_c_dir = PathBuf::from("src/rules/cert_c");
let mut categories: BTreeMap<String, Vec<RuleInfo>> = BTreeMap::new();
let mut total_rules = 0;
let mut implemented_rules = 0;
let mut total_tests = 0;
for category_entry in fs::read_dir(&cert_c_dir)? {
let category_entry = category_entry?;
let category_path = category_entry.path();
if !category_path.is_dir() {
continue;
}
let category_name = category_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("Unknown")
.to_string();
if category_name.starts_with('.') {
continue;
}
for rule_entry in fs::read_dir(&category_path)? {
let rule_entry = rule_entry?;
let rule_path = rule_entry.path();
if !rule_path.is_dir() {
continue;
}
let rule_id = rule_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("Unknown")
.to_string();
let toml_path = rule_path.join(format!("{}.toml", rule_id));
let (title, description, is_enabled) = parse_rule_toml(&toml_path);
let tests_dir = rule_path.join("tests");
let mut fail_tests = collect_test_cases(&tests_dir.join("fail"), &rule_id, "fail")?;
let mut pass_tests = collect_test_cases(&tests_dir.join("pass"), &rule_id, "pass")?;
fail_tests.sort_by(|a, b| a.name.cmp(&b.name));
pass_tests.sort_by(|a, b| a.name.cmp(&b.name));
total_tests += fail_tests.len() + pass_tests.len();
total_rules += 1;
if is_enabled {
implemented_rules += 1;
}
let rule_info = RuleInfo {
id: rule_id,
category: category_name.clone(),
title,
description,
is_enabled,
fail_tests,
pass_tests,
};
categories
.entry(category_name.clone())
.or_default()
.push(rule_info);
}
}
Ok((categories, total_rules, implemented_rules, total_tests))
}
fn parse_rule_toml(toml_path: &Path) -> (String, String, bool) {
let Ok(content) = fs::read_to_string(toml_path) else {
return ("No title".to_string(), "No description".to_string(), false);
};
let title = content
.lines()
.find(|line| line.trim().starts_with("title = "))
.and_then(|line| line.split('=').nth(1))
.map(|s| s.trim().trim_matches('"').to_string())
.unwrap_or_else(|| "No title".to_string());
let description = if let Some(desc_start) = content.find("description = \"\"\"") {
let desc_content = &content[desc_start + 18..];
if let Some(desc_end) = desc_content.find("\"\"\"") {
desc_content[..desc_end].trim().to_string()
} else {
"No description".to_string()
}
} else {
"No description".to_string()
};
let enabled = content.contains("enabled = true");
(title, description, enabled)
}
fn collect_test_cases(
dir: &Path,
rule_id: &str,
test_type: &str,
) -> Result<Vec<TestCaseInfo>, Box<dyn std::error::Error>> {
let mut tests = Vec::new();
if !dir.exists() {
return Ok(tests);
}
let results = TEST_RESULTS.lock().unwrap_or_else(|e| e.into_inner());
for test_file in fs::read_dir(dir)? {
let test_file = test_file?;
if test_file.path().extension().is_some_and(|e| e == "c") {
let file_name = test_file.file_name().to_string_lossy().to_string();
let func_name = test_func_name(rule_id, test_type, &file_name);
let result = results.get(&func_name).cloned();
tests.push(TestCaseInfo {
name: file_name,
result,
});
}
}
Ok(tests)
}
fn test_func_name(rule_id: &str, test_type: &str, file_name: &str) -> String {
let test_name = file_name.trim_end_matches(".c");
format!(
"test_{}_{}_{}",
rule_id.to_lowercase().replace("-", "_"),
test_type,
test_name.replace("-", "_").replace(".", "_")
)
}
fn rule_status(rule: &RuleInfo, total: usize) -> (&'static str, &'static str) {
if rule.is_enabled {
("✅", "Implemented")
} else if total > 0 {
("🔶", "Not Implemented (has tests)")
} else {
("⚫", "Not Implemented (no tests)")
}
}
fn render_overview(
report: &mut String,
total_rules: usize,
implemented_rules: usize,
total_tests: usize,
) {
report.push_str("## Overview\n\n");
report.push_str(&format!("- **Total Rules:** {}\n", total_rules));
report.push_str(&format!(
"- **Implemented Rules:** {} ({:.1}%)\n",
implemented_rules,
(implemented_rules as f64 / total_rules as f64) * 100.0
));
report.push_str(&format!("- **Total Test Cases:** {}\n", total_tests));
report.push_str(&format!(
"- **Average Tests per Rule:** {:.1}\n\n",
total_tests as f64 / total_rules as f64
));
}
fn render_toc(report: &mut String, categories: &BTreeMap<String, Vec<RuleInfo>>) {
report.push_str("## Table of Contents\n\n");
for (category, rules) in categories {
let implemented_count = rules.iter().filter(|r| r.is_enabled).count();
report.push_str(&format!(
"- [{}](#category-{}) ({} implemented / {} total)\n",
category,
category.to_lowercase(),
implemented_count,
rules.len()
));
for rule in rules {
let (passed, total, not_run) = calculate_rule_metrics(rule);
let (status_icon, status_text) = rule_status(rule, total);
let pass_rate = if total > 0 {
format!(
"{}/{} ({:.1}%)",
passed,
total,
(passed as f64 / total as f64) * 100.0
)
} else {
"0/0 (N/A)".to_string()
};
let not_run_str = if not_run > 0 {
format!(" [{} not run]", not_run)
} else {
String::new()
};
report.push_str(&format!(
" - {} [{}](#rule-{}) - {}: Pass {}{}\n",
status_icon,
rule.id,
rule.id.to_lowercase().replace("-", ""),
status_text,
pass_rate,
not_run_str
));
}
}
report.push('\n');
}
fn render_detail_sections(report: &mut String, categories: &BTreeMap<String, Vec<RuleInfo>>) {
for (category, rules) in categories {
report.push_str(&format!("## Category: {}\n\n", category));
report.push_str(&format!(
"<a id=\"category-{}\"></a>\n\n",
category.to_lowercase()
));
let implemented_count = rules.iter().filter(|r| r.is_enabled).count();
report.push_str(&format!(
"**Implementation Status:** {} / {} rules ({:.1}%)\n\n",
implemented_count,
rules.len(),
(implemented_count as f64 / rules.len() as f64) * 100.0
));
for rule in rules {
render_rule_detail(report, rule);
}
}
}
fn render_rule_detail(report: &mut String, rule: &RuleInfo) {
let (passed, total, not_run) = calculate_rule_metrics(rule);
let test_count = rule.fail_tests.len() + rule.pass_tests.len();
let (status_icon, status_text) = rule_status(rule, total);
report.push_str(&format!(
"### {} {} - {}\n\n",
status_icon, rule.id, status_text
));
report.push_str(&format!(
"<a id=\"rule-{}\"></a>\n\n",
rule.id.to_lowercase().replace("-", "")
));
report.push_str(&format!("**Title:** {}\n\n", rule.title));
report.push_str(&format!("**Description:** {}\n\n", rule.description));
report.push_str(&format!(
"**Test Coverage:** {} tests ({} fail, {} pass)\n\n",
test_count,
rule.fail_tests.len(),
rule.pass_tests.len()
));
if total > 0 {
let pass_rate = (passed as f64 / total as f64) * 100.0;
report.push_str(&format!(
"**Test Results:** {}/{} passed ({:.1}%)",
passed, total, pass_rate
));
if not_run > 0 {
report.push_str(&format!(", {} not run", not_run));
}
report.push_str("\n\n");
}
render_test_list(
report,
rule,
&rule.fail_tests,
"fail",
"#### Fail Tests (Should Detect Violations)\n\n",
);
render_test_list(
report,
rule,
&rule.pass_tests,
"pass",
"#### Pass Tests (Should NOT Detect Violations)\n\n",
);
report.push_str("---\n\n");
}
fn render_test_list(
report: &mut String,
rule: &RuleInfo,
tests: &[TestCaseInfo],
test_type: &str,
header: &str,
) {
if tests.is_empty() {
return;
}
report.push_str(header);
for test in tests {
let func_name = test_func_name(&rule.id, test_type, &test.name);
let status = match &test.result {
Some(result) if result.passed => "✅ PASS",
Some(_) => "❌ FAIL",
None => "⏭️ NOT RUN",
};
report.push_str(&format!("- {} `{}` → `{}`\n", status, test.name, func_name));
}
report.push('\n');
}
fn render_summary_table(report: &mut String, categories: &BTreeMap<String, Vec<RuleInfo>>) {
report.push_str("## Summary by Category\n\n");
report.push_str("| Category | Rules | Implemented | Tests | Avg Tests/Rule |\n");
report.push_str("|----------|-------|-------------|-------|----------------|\n");
for (category, rules) in categories {
let implemented = rules.iter().filter(|r| r.is_enabled).count();
let tests: usize = rules
.iter()
.map(|r| r.fail_tests.len() + r.pass_tests.len())
.sum();
let avg = tests as f64 / rules.len() as f64;
report.push_str(&format!(
"| {} | {} | {} | {} | {:.1} |\n",
category,
rules.len(),
implemented,
tests,
avg
));
}
report.push('\n');
}
fn calculate_rule_metrics(rule: &RuleInfo) -> (usize, usize, usize) {
let mut passed = 0;
let mut total = 0;
let mut not_run = 0;
for test in &rule.fail_tests {
total += 1;
if let Some(result) = &test.result {
if result.passed {
passed += 1;
}
} else {
not_run += 1;
}
}
for test in &rule.pass_tests {
total += 1;
if let Some(result) = &test.result {
if result.passed {
passed += 1;
}
} else {
not_run += 1;
}
}
(passed, total, not_run)
}
#[derive(Debug, Clone)]
struct TestCaseInfo {
name: String,
result: Option<TestResult>,
}
#[derive(Debug)]
#[allow(dead_code)]
struct RuleInfo {
id: String,
category: String,
title: String,
description: String,
is_enabled: bool,
fail_tests: Vec<TestCaseInfo>,
pass_tests: Vec<TestCaseInfo>,
}
include!(concat!(env!("OUT_DIR"), "/integration_tests.rs"));