use super::error::{Result, ValidateError};
use super::golden_trace::{GoldenBaseline, SyscallTimingStats, TraceSyscallEntry};
#[derive(Debug, Clone)]
pub struct ComparisonResult {
pub passed: bool,
pub syscall_mismatches: Vec<SyscallMismatch>,
pub timing_regressions: Vec<TimingRegression>,
pub summary: ComparisonSummary,
}
impl ComparisonResult {
pub fn passed() -> Self {
Self {
passed: true,
syscall_mismatches: Vec::new(),
timing_regressions: Vec::new(),
summary: ComparisonSummary::default(),
}
}
pub fn failed(mismatches: Vec<SyscallMismatch>, regressions: Vec<TimingRegression>) -> Self {
Self {
passed: false,
syscall_mismatches: mismatches,
timing_regressions: regressions,
summary: ComparisonSummary::default(),
}
}
}
#[derive(Debug, Clone)]
pub struct SyscallMismatch {
pub index: usize,
pub expected: String,
pub found: String,
pub mismatch_type: MismatchType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MismatchType {
Different,
Extra,
Missing,
}
#[derive(Debug, Clone)]
pub struct TimingRegression {
pub syscall: String,
pub baseline_ms: f64,
pub actual_ms: f64,
pub delta_percent: f64,
}
#[derive(Debug, Clone, Default)]
pub struct ComparisonSummary {
pub total_compared: usize,
pub matches: usize,
pub mismatches: usize,
pub timing_regressions: usize,
}
pub fn compare_syscalls(
baseline: &[TraceSyscallEntry],
actual: &[TraceSyscallEntry],
strict: bool,
) -> Result<ComparisonResult> {
let mut mismatches = Vec::new();
if baseline.len() != actual.len() && strict {
return Err(ValidateError::SyscallCountMismatch {
baseline: baseline.len(),
current: actual.len(),
});
}
let compare_len = baseline.len().min(actual.len());
for i in 0..compare_len {
if baseline[i].syscall_nr != actual[i].syscall_nr {
mismatches.push(SyscallMismatch {
index: i,
expected: format!("syscall_{}", baseline[i].syscall_nr),
found: format!("syscall_{}", actual[i].syscall_nr),
mismatch_type: MismatchType::Different,
});
}
}
for (i, entry) in actual.iter().enumerate().skip(compare_len) {
mismatches.push(SyscallMismatch {
index: i,
expected: "(none)".to_string(),
found: format!("syscall_{}", entry.syscall_nr),
mismatch_type: MismatchType::Extra,
});
}
for (i, entry) in baseline.iter().enumerate().skip(compare_len) {
mismatches.push(SyscallMismatch {
index: i,
expected: format!("syscall_{}", entry.syscall_nr),
found: "(none)".to_string(),
mismatch_type: MismatchType::Missing,
});
}
let summary = ComparisonSummary {
total_compared: compare_len,
matches: compare_len
- mismatches.iter().filter(|m| m.mismatch_type == MismatchType::Different).count(),
mismatches: mismatches.len(),
timing_regressions: 0,
};
if mismatches.is_empty() {
Ok(ComparisonResult {
passed: true,
syscall_mismatches: Vec::new(),
timing_regressions: Vec::new(),
summary,
})
} else {
Ok(ComparisonResult {
passed: false,
syscall_mismatches: mismatches,
timing_regressions: Vec::new(),
summary,
})
}
}
pub fn compare_timing(
baseline_stats: &std::collections::HashMap<String, SyscallTimingStats>,
actual_stats: &std::collections::HashMap<String, SyscallTimingStats>,
tolerance_percent: f32,
) -> Vec<TimingRegression> {
let mut regressions = Vec::new();
for (syscall, baseline) in baseline_stats {
if let Some(actual) = actual_stats.get(syscall) {
if let Some(regression) =
is_timing_regression(syscall, baseline.mean_ns, actual.mean_ns, tolerance_percent)
{
regressions.push(regression);
}
}
}
regressions
}
pub fn is_timing_regression(
syscall: &str,
baseline_ns: u64,
actual_ns: u64,
tolerance_percent: f32,
) -> Option<TimingRegression> {
if baseline_ns == 0 {
return None;
}
let baseline_ms = baseline_ns as f64 / 1_000_000.0;
let actual_ms = actual_ns as f64 / 1_000_000.0;
let delta_percent = ((actual_ns as f64 - baseline_ns as f64) / baseline_ns as f64) * 100.0;
if delta_percent > f64::from(tolerance_percent) {
Some(TimingRegression {
syscall: syscall.to_string(),
baseline_ms,
actual_ms,
delta_percent,
})
} else {
None
}
}
pub fn validate_against_baseline(
baseline: &GoldenBaseline,
actual_syscalls: &[TraceSyscallEntry],
actual_timing: &std::collections::HashMap<String, SyscallTimingStats>,
tolerance_percent: f32,
strict: bool,
ignore_timing: bool,
) -> Result<ComparisonResult> {
let mut result = compare_syscalls(&baseline.syscalls, actual_syscalls, strict)?;
if !ignore_timing {
let timing_regressions =
compare_timing(&baseline.timing.by_syscall, actual_timing, tolerance_percent);
if !timing_regressions.is_empty() {
result.passed = false;
result.timing_regressions = timing_regressions;
}
}
result.summary.timing_regressions = result.timing_regressions.len();
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_compare_identical_sequences() {
let baseline = vec![
TraceSyscallEntry::simple(1, "read", 100),
TraceSyscallEntry::simple(2, "write", 200),
];
let actual = vec![
TraceSyscallEntry::simple(1, "read", 100),
TraceSyscallEntry::simple(2, "write", 200),
];
let result =
compare_syscalls(&baseline, &actual, false).expect("comparison should succeed");
assert!(result.passed);
assert!(result.syscall_mismatches.is_empty());
}
#[test]
fn test_compare_different_sequences() {
let baseline = vec![TraceSyscallEntry::simple(1, "read", 100)];
let actual = vec![TraceSyscallEntry::simple(2, "write", 100)];
let result =
compare_syscalls(&baseline, &actual, false).expect("comparison should succeed");
assert!(!result.passed);
assert_eq!(result.syscall_mismatches.len(), 1);
assert_eq!(result.syscall_mismatches[0].mismatch_type, MismatchType::Different);
}
#[test]
fn test_compare_strict_mode_count_mismatch() {
let baseline = vec![
TraceSyscallEntry::simple(1, "read", 100),
TraceSyscallEntry::simple(2, "write", 200),
];
let actual = vec![TraceSyscallEntry::simple(1, "read", 100)];
let result = compare_syscalls(&baseline, &actual, true);
assert!(result.is_err());
let result =
compare_syscalls(&baseline, &actual, false).expect("comparison should succeed");
assert!(!result.passed);
assert_eq!(result.syscall_mismatches.len(), 1); }
#[test]
fn test_compare_extra_syscalls() {
let baseline = vec![TraceSyscallEntry::simple(1, "read", 100)];
let actual = vec![
TraceSyscallEntry::simple(1, "read", 100),
TraceSyscallEntry::simple(2, "write", 200),
TraceSyscallEntry::simple(3, "close", 50),
];
let result =
compare_syscalls(&baseline, &actual, false).expect("comparison should succeed");
assert!(!result.passed);
assert_eq!(result.syscall_mismatches.len(), 2); assert!(result.syscall_mismatches.iter().all(|m| m.mismatch_type == MismatchType::Extra));
}
#[test]
fn test_compare_missing_syscalls() {
let baseline = vec![
TraceSyscallEntry::simple(1, "read", 100),
TraceSyscallEntry::simple(2, "write", 200),
TraceSyscallEntry::simple(3, "close", 50),
];
let actual = vec![TraceSyscallEntry::simple(1, "read", 100)];
let result =
compare_syscalls(&baseline, &actual, false).expect("comparison should succeed");
assert!(!result.passed);
assert_eq!(result.syscall_mismatches.len(), 2); assert!(result.syscall_mismatches.iter().all(|m| m.mismatch_type == MismatchType::Missing));
}
#[test]
fn test_compare_empty_sequences() {
let baseline: Vec<TraceSyscallEntry> = vec![];
let actual: Vec<TraceSyscallEntry> = vec![];
let result =
compare_syscalls(&baseline, &actual, false).expect("comparison should succeed");
assert!(result.passed);
assert!(result.syscall_mismatches.is_empty());
}
#[test]
fn test_timing_within_tolerance() {
let result = is_timing_regression("read", 100_000, 105_000, 10.0);
assert!(result.is_none()); }
#[test]
fn test_timing_exceeds_tolerance() {
let result = is_timing_regression("read", 100_000, 120_000, 10.0);
assert!(result.is_some()); let regression = result.expect("regression should be detected");
assert!((regression.delta_percent - 20.0).abs() < 0.1);
}
#[test]
fn test_timing_zero_baseline() {
let result = is_timing_regression("read", 0, 100_000, 10.0);
assert!(result.is_none()); }
#[test]
fn test_timing_at_exactly_tolerance() {
let result = is_timing_regression("read", 100_000, 110_000, 10.0);
assert!(result.is_none()); }
#[test]
fn test_timing_just_over_tolerance() {
let result = is_timing_regression("read", 100_000, 110_001, 10.0);
assert!(result.is_some()); }
#[test]
fn test_compare_timing_map() {
let mut baseline_stats = HashMap::new();
baseline_stats.insert(
"read".to_string(),
SyscallTimingStats {
count: 10,
total_ns: 1_000_000,
mean_ns: 100_000,
std_ns: 10_000,
min_ns: 80_000,
max_ns: 120_000,
p50_ns: 100_000,
p95_ns: 115_000,
p99_ns: 118_000,
},
);
let mut actual_stats = HashMap::new();
actual_stats.insert(
"read".to_string(),
SyscallTimingStats {
count: 10,
total_ns: 1_200_000,
mean_ns: 120_000, std_ns: 10_000,
min_ns: 100_000,
max_ns: 140_000,
p50_ns: 120_000,
p95_ns: 135_000,
p99_ns: 138_000,
},
);
let regressions = compare_timing(&baseline_stats, &actual_stats, 10.0);
assert_eq!(regressions.len(), 1);
assert_eq!(regressions[0].syscall, "read");
}
#[test]
fn test_compare_timing_no_regression() {
let mut baseline_stats = HashMap::new();
baseline_stats.insert(
"read".to_string(),
SyscallTimingStats {
count: 10,
total_ns: 1_000_000,
mean_ns: 100_000,
std_ns: 10_000,
min_ns: 80_000,
max_ns: 120_000,
p50_ns: 100_000,
p95_ns: 115_000,
p99_ns: 118_000,
},
);
let mut actual_stats = HashMap::new();
actual_stats.insert(
"read".to_string(),
SyscallTimingStats {
count: 10,
total_ns: 1_050_000,
mean_ns: 105_000, std_ns: 10_000,
min_ns: 85_000,
max_ns: 125_000,
p50_ns: 105_000,
p95_ns: 120_000,
p99_ns: 123_000,
},
);
let regressions = compare_timing(&baseline_stats, &actual_stats, 10.0);
assert!(regressions.is_empty());
}
#[test]
fn test_compare_timing_missing_syscall() {
let mut baseline_stats = HashMap::new();
baseline_stats.insert(
"read".to_string(),
SyscallTimingStats {
count: 10,
total_ns: 1_000_000,
mean_ns: 100_000,
std_ns: 10_000,
min_ns: 80_000,
max_ns: 120_000,
p50_ns: 100_000,
p95_ns: 115_000,
p99_ns: 118_000,
},
);
let actual_stats = HashMap::new();
let regressions = compare_timing(&baseline_stats, &actual_stats, 10.0);
assert!(regressions.is_empty()); }
#[test]
fn test_validate_against_baseline_with_timing() {
use super::super::golden_trace::{
GoldenBaseline, PlatformInfo, TimingStats, ToleranceConfig, TraceManifest,
TraceStatistics,
};
let baseline = GoldenBaseline {
manifest: TraceManifest {
version: "1.0.0".to_string(),
renacer_version: "0.1.0".to_string(),
created_at: "2025-01-01T00:00:00Z".to_string(),
platform: PlatformInfo {
os: "linux".to_string(),
arch: "x86_64".to_string(),
kernel: "6.0.0".to_string(),
},
command: vec!["echo".to_string(), "test".to_string()],
statistics: TraceStatistics {
total_syscalls: 2,
total_duration_ms: 0.3,
unique_syscalls: 2,
},
tolerance: ToleranceConfig::default(),
},
syscalls: vec![
TraceSyscallEntry::simple(1, "read", 100_000),
TraceSyscallEntry::simple(2, "write", 200_000),
],
timing: TimingStats {
total_duration_ns: 300_000,
syscall_count: 2,
by_syscall: {
let mut map = HashMap::new();
map.insert(
"read".to_string(),
SyscallTimingStats {
count: 1,
total_ns: 100_000,
mean_ns: 100_000,
std_ns: 0,
min_ns: 100_000,
max_ns: 100_000,
p50_ns: 100_000,
p95_ns: 100_000,
p99_ns: 100_000,
},
);
map
},
},
};
let actual_syscalls = vec![
TraceSyscallEntry::simple(1, "read", 100_000),
TraceSyscallEntry::simple(2, "write", 200_000),
];
let mut actual_timing = HashMap::new();
actual_timing.insert(
"read".to_string(),
SyscallTimingStats {
count: 1,
total_ns: 150_000,
mean_ns: 150_000, std_ns: 0,
min_ns: 150_000,
max_ns: 150_000,
p50_ns: 150_000,
p95_ns: 150_000,
p99_ns: 150_000,
},
);
let result = validate_against_baseline(
&baseline,
&actual_syscalls,
&actual_timing,
10.0,
false,
false,
)
.expect("validation should succeed");
assert!(!result.passed);
assert_eq!(result.timing_regressions.len(), 1);
}
#[test]
fn test_validate_against_baseline_ignore_timing() {
use super::super::golden_trace::{
GoldenBaseline, PlatformInfo, TimingStats, ToleranceConfig, TraceManifest,
TraceStatistics,
};
let baseline = GoldenBaseline {
manifest: TraceManifest {
version: "1.0.0".to_string(),
renacer_version: "0.1.0".to_string(),
created_at: "2025-01-01T00:00:00Z".to_string(),
platform: PlatformInfo {
os: "linux".to_string(),
arch: "x86_64".to_string(),
kernel: "6.0.0".to_string(),
},
command: vec!["echo".to_string(), "test".to_string()],
statistics: TraceStatistics {
total_syscalls: 1,
total_duration_ms: 0.1,
unique_syscalls: 1,
},
tolerance: ToleranceConfig::default(),
},
syscalls: vec![TraceSyscallEntry::simple(1, "read", 100_000)],
timing: TimingStats {
total_duration_ns: 100_000,
syscall_count: 1,
by_syscall: {
let mut map = HashMap::new();
map.insert(
"read".to_string(),
SyscallTimingStats {
count: 1,
total_ns: 100_000,
mean_ns: 100_000,
std_ns: 0,
min_ns: 100_000,
max_ns: 100_000,
p50_ns: 100_000,
p95_ns: 100_000,
p99_ns: 100_000,
},
);
map
},
},
};
let actual_syscalls = vec![TraceSyscallEntry::simple(1, "read", 100_000)];
let mut actual_timing = HashMap::new();
actual_timing.insert(
"read".to_string(),
SyscallTimingStats {
count: 1,
total_ns: 1_000_000,
mean_ns: 1_000_000, std_ns: 0,
min_ns: 1_000_000,
max_ns: 1_000_000,
p50_ns: 1_000_000,
p95_ns: 1_000_000,
p99_ns: 1_000_000,
},
);
let result = validate_against_baseline(
&baseline,
&actual_syscalls,
&actual_timing,
10.0,
false,
true,
)
.expect("validation should succeed");
assert!(result.passed); assert!(result.timing_regressions.is_empty());
}
#[test]
fn test_comparison_result_constructors() {
let passed = ComparisonResult::passed();
assert!(passed.passed);
assert!(passed.syscall_mismatches.is_empty());
let failed = ComparisonResult::failed(
vec![SyscallMismatch {
index: 0,
expected: "read".to_string(),
found: "write".to_string(),
mismatch_type: MismatchType::Different,
}],
vec![],
);
assert!(!failed.passed);
assert_eq!(failed.syscall_mismatches.len(), 1);
}
#[test]
fn test_mismatch_type_equality() {
assert_eq!(MismatchType::Different, MismatchType::Different);
assert_eq!(MismatchType::Extra, MismatchType::Extra);
assert_eq!(MismatchType::Missing, MismatchType::Missing);
assert_ne!(MismatchType::Different, MismatchType::Extra);
}
#[test]
fn test_comparison_summary_default() {
let summary = ComparisonSummary::default();
assert_eq!(summary.total_compared, 0);
assert_eq!(summary.matches, 0);
assert_eq!(summary.mismatches, 0);
assert_eq!(summary.timing_regressions, 0);
}
#[test]
fn test_summary_counts() {
let baseline = vec![
TraceSyscallEntry::simple(1, "read", 100),
TraceSyscallEntry::simple(2, "write", 200),
TraceSyscallEntry::simple(3, "close", 50),
];
let actual = vec![
TraceSyscallEntry::simple(1, "read", 100),
TraceSyscallEntry::simple(99, "other", 200), TraceSyscallEntry::simple(3, "close", 50),
];
let result =
compare_syscalls(&baseline, &actual, false).expect("comparison should succeed");
assert_eq!(result.summary.total_compared, 3);
assert_eq!(result.summary.matches, 2); assert_eq!(result.summary.mismatches, 1); }
}