use super::comparison::{ComparisonResult, MismatchType};
use super::config::ValidationOutputFormat;
use serde::Serialize;
pub fn format_result(result: &ComparisonResult, format: ValidationOutputFormat) -> String {
match format {
ValidationOutputFormat::Text => format_text_report(result),
ValidationOutputFormat::Json => format_json_report(result),
ValidationOutputFormat::JUnit => format_junit_report(result),
}
}
#[derive(Debug, Serialize)]
pub struct JsonValidationResult {
pub status: String,
pub total_compared: usize,
pub matches: usize,
pub mismatches: usize,
pub timing_regressions: usize,
pub syscall_details: Vec<JsonSyscallMismatch>,
pub timing_details: Vec<JsonTimingRegression>,
}
#[derive(Debug, Serialize)]
pub struct JsonSyscallMismatch {
pub index: usize,
pub expected: String,
pub found: String,
pub mismatch_type: String,
}
#[derive(Debug, Serialize)]
pub struct JsonTimingRegression {
pub syscall: String,
pub baseline_ms: f64,
pub actual_ms: f64,
pub delta_percent: f64,
}
pub fn format_text_report(result: &ComparisonResult) -> String {
let mut output = String::new();
output.push_str("=== Validation Report ===\n\n");
let status = if result.passed { "PASSED" } else { "FAILED" };
output.push_str(&format!("Status: {status}\n\n"));
output.push_str("Summary:\n");
output.push_str(&format!(" Total syscalls compared: {}\n", result.summary.total_compared));
output.push_str(&format!(" Matches: {}\n", result.summary.matches));
output.push_str(&format!(" Mismatches: {}\n", result.summary.mismatches));
output.push_str(&format!(" Timing regressions: {}\n", result.summary.timing_regressions));
output.push('\n');
if !result.syscall_mismatches.is_empty() {
output.push_str("Syscall Mismatches:\n");
for mismatch in &result.syscall_mismatches {
let mismatch_type = match mismatch.mismatch_type {
MismatchType::Different => "DIFFERENT",
MismatchType::Extra => "EXTRA",
MismatchType::Missing => "MISSING",
};
output.push_str(&format!(
" [{}] Index {}: expected '{}', found '{}'\n",
mismatch_type, mismatch.index, mismatch.expected, mismatch.found
));
}
output.push('\n');
}
if !result.timing_regressions.is_empty() {
output.push_str("Timing Regressions:\n");
for regression in &result.timing_regressions {
output.push_str(&format!(
" {}: {:.2}ms -> {:.2}ms ({:+.1}%)\n",
regression.syscall,
regression.baseline_ms,
regression.actual_ms,
regression.delta_percent
));
}
output.push('\n');
}
output
}
pub fn format_json_report(result: &ComparisonResult) -> String {
let json_result = JsonValidationResult {
status: if result.passed { "passed".to_string() } else { "failed".to_string() },
total_compared: result.summary.total_compared,
matches: result.summary.matches,
mismatches: result.summary.mismatches,
timing_regressions: result.summary.timing_regressions,
syscall_details: result
.syscall_mismatches
.iter()
.map(|m| JsonSyscallMismatch {
index: m.index,
expected: m.expected.clone(),
found: m.found.clone(),
mismatch_type: match m.mismatch_type {
MismatchType::Different => "different".to_string(),
MismatchType::Extra => "extra".to_string(),
MismatchType::Missing => "missing".to_string(),
},
})
.collect(),
timing_details: result
.timing_regressions
.iter()
.map(|t| JsonTimingRegression {
syscall: t.syscall.clone(),
baseline_ms: t.baseline_ms,
actual_ms: t.actual_ms,
delta_percent: t.delta_percent,
})
.collect(),
};
serde_json::to_string_pretty(&json_result).unwrap_or_else(|_| "{}".to_string())
}
pub fn format_junit_report(result: &ComparisonResult) -> String {
let mut output = String::new();
output.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
let test_count = 1 + result.syscall_mismatches.len() + result.timing_regressions.len();
let failure_count = result.syscall_mismatches.len() + result.timing_regressions.len();
output.push_str(&format!(
"<testsuite name=\"renacer-validate\" tests=\"{test_count}\" failures=\"{failure_count}\" errors=\"0\">\n"
));
output.push_str(" <testcase name=\"validation-overall\" classname=\"renacer.validate\"");
if result.passed {
output.push_str("/>\n");
} else {
output.push_str(">\n");
output.push_str(&format!(
" <failure message=\"Validation failed: {} mismatches, {} timing regressions\"/>\n",
result.syscall_mismatches.len(),
result.timing_regressions.len()
));
output.push_str(" </testcase>\n");
}
for mismatch in &result.syscall_mismatches {
output.push_str(&format!(
" <testcase name=\"syscall-{index}\" classname=\"renacer.validate.syscalls\">\n",
index = mismatch.index
));
output.push_str(&format!(
" <failure message=\"Expected '{}', found '{}' ({})\"/>\n",
xml_escape(&mismatch.expected),
xml_escape(&mismatch.found),
match mismatch.mismatch_type {
MismatchType::Different => "different",
MismatchType::Extra => "extra",
MismatchType::Missing => "missing",
}
));
output.push_str(" </testcase>\n");
}
for regression in &result.timing_regressions {
output.push_str(&format!(
" <testcase name=\"timing-{}\" classname=\"renacer.validate.timing\">\n",
xml_escape(®ression.syscall)
));
output.push_str(&format!(
" <failure message=\"Timing regression: {:.2}ms -> {:.2}ms ({:+.1}%)\"/>\n",
regression.baseline_ms, regression.actual_ms, regression.delta_percent
));
output.push_str(" </testcase>\n");
}
output.push_str("</testsuite>\n");
output
}
fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validate::comparison::{ComparisonSummary, SyscallMismatch, TimingRegression};
#[test]
fn test_text_format_passed() {
let result = ComparisonResult::passed();
let text = format_text_report(&result);
assert!(text.contains("Status: PASSED"));
}
#[test]
fn test_text_format_failed_with_mismatches() {
let result = ComparisonResult {
passed: false,
syscall_mismatches: vec![
SyscallMismatch {
index: 0,
expected: "read".to_string(),
found: "write".to_string(),
mismatch_type: MismatchType::Different,
},
SyscallMismatch {
index: 1,
expected: "(none)".to_string(),
found: "close".to_string(),
mismatch_type: MismatchType::Extra,
},
SyscallMismatch {
index: 2,
expected: "open".to_string(),
found: "(none)".to_string(),
mismatch_type: MismatchType::Missing,
},
],
timing_regressions: vec![TimingRegression {
syscall: "read".to_string(),
baseline_ms: 0.1,
actual_ms: 0.15,
delta_percent: 50.0,
}],
summary: ComparisonSummary {
total_compared: 3,
matches: 0,
mismatches: 3,
timing_regressions: 1,
},
};
let text = format_text_report(&result);
assert!(text.contains("Status: FAILED"));
assert!(text.contains("[DIFFERENT]"));
assert!(text.contains("[EXTRA]"));
assert!(text.contains("[MISSING]"));
assert!(text.contains("Timing Regressions:"));
assert!(text.contains("read: 0.10ms -> 0.15ms (+50.0%)"));
}
#[test]
fn test_json_format_structure() {
let result = ComparisonResult::passed();
let json = format_json_report(&result);
assert!(json.contains("\"status\": \"passed\""));
}
#[test]
fn test_json_format_with_details() {
let result = ComparisonResult {
passed: false,
syscall_mismatches: vec![SyscallMismatch {
index: 5,
expected: "openat".to_string(),
found: "close".to_string(),
mismatch_type: MismatchType::Different,
}],
timing_regressions: vec![TimingRegression {
syscall: "write".to_string(),
baseline_ms: 0.5,
actual_ms: 1.0,
delta_percent: 100.0,
}],
summary: ComparisonSummary {
total_compared: 10,
matches: 9,
mismatches: 1,
timing_regressions: 1,
},
};
let json = format_json_report(&result);
assert!(json.contains("\"status\": \"failed\""));
assert!(json.contains("\"mismatch_type\": \"different\""));
assert!(json.contains("\"syscall\": \"write\""));
assert!(json.contains("\"delta_percent\": 100.0"));
}
#[test]
fn test_json_format_extra_and_missing_types() {
let result = ComparisonResult {
passed: false,
syscall_mismatches: vec![
SyscallMismatch {
index: 0,
expected: "(none)".to_string(),
found: "mmap".to_string(),
mismatch_type: MismatchType::Extra,
},
SyscallMismatch {
index: 1,
expected: "munmap".to_string(),
found: "(none)".to_string(),
mismatch_type: MismatchType::Missing,
},
],
timing_regressions: vec![],
summary: ComparisonSummary::default(),
};
let json = format_json_report(&result);
assert!(json.contains("\"mismatch_type\": \"extra\""));
assert!(json.contains("\"mismatch_type\": \"missing\""));
}
#[test]
fn test_junit_format_valid_xml() {
let result = ComparisonResult::passed();
let xml = format_junit_report(&result);
assert!(xml.starts_with("<?xml version=\"1.0\""));
assert!(xml.contains("<testsuite"));
assert!(xml.contains("</testsuite>"));
}
#[test]
fn test_junit_format_with_failures() {
let result = ComparisonResult {
passed: false,
syscall_mismatches: vec![
SyscallMismatch {
index: 0,
expected: "read".to_string(),
found: "write".to_string(),
mismatch_type: MismatchType::Different,
},
SyscallMismatch {
index: 1,
expected: "(none)".to_string(),
found: "close".to_string(),
mismatch_type: MismatchType::Extra,
},
SyscallMismatch {
index: 2,
expected: "open".to_string(),
found: "(none)".to_string(),
mismatch_type: MismatchType::Missing,
},
],
timing_regressions: vec![TimingRegression {
syscall: "read".to_string(),
baseline_ms: 0.1,
actual_ms: 0.2,
delta_percent: 100.0,
}],
summary: ComparisonSummary {
total_compared: 3,
matches: 0,
mismatches: 3,
timing_regressions: 1,
},
};
let xml = format_junit_report(&result);
assert!(xml.contains("failures=\"4\"")); assert!(xml.contains("<failure message=\"Validation failed"));
assert!(xml.contains("syscall-0"));
assert!(xml.contains("timing-read"));
assert!(xml.contains("(different)"));
assert!(xml.contains("(extra)"));
assert!(xml.contains("(missing)"));
}
#[test]
fn test_xml_escape() {
assert_eq!(xml_escape("a & b"), "a & b");
assert_eq!(xml_escape("<tag>"), "<tag>");
assert_eq!(xml_escape("\"quoted\""), ""quoted"");
assert_eq!(xml_escape("it's"), "it's");
}
#[test]
fn test_format_result_dispatch() {
let result = ComparisonResult::passed();
let text = format_result(&result, ValidationOutputFormat::Text);
assert!(text.contains("Status: PASSED"));
let json = format_result(&result, ValidationOutputFormat::Json);
assert!(json.contains("\"status\": \"passed\""));
let junit = format_result(&result, ValidationOutputFormat::JUnit);
assert!(junit.contains("<testsuite"));
}
}