pub mod comparison;
pub mod config;
pub mod error;
pub mod golden_trace;
pub mod output;
pub use comparison::{
compare_syscalls, compare_timing, is_timing_regression, ComparisonResult, ComparisonSummary,
MismatchType, SyscallMismatch, TimingRegression,
};
pub use config::{ValidateConfig, ValidationOutputFormat};
pub use error::{Result, ValidateError};
pub use golden_trace::{
generate_baseline, load_baseline, GoldenBaseline, PlatformInfo, SyscallTimingStats,
TimingStats, ToleranceConfig, TraceFlags, TraceHeader, TraceManifest, TraceStatistics,
TraceSyscallEntry, TRACE_MAGIC, TRACE_MAGIC_END, TRACE_VERSION,
};
pub use output::{format_json_report, format_junit_report, format_result, format_text_report};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum ValidateExitCode {
Passed = 0,
Failed = 1,
BaselineNotFound = 2,
InvalidBaseline = 3,
CommandError = 4,
ConfigError = 5,
}
impl ValidateExitCode {
pub fn code(self) -> i32 {
self as i32
}
}
fn run_generate_mode(
command: &[String],
generate_dir: &std::path::Path,
config: &ValidateConfig,
) -> ValidateExitCode {
let command_refs: Vec<&str> = command.iter().map(String::as_str).collect();
match generate_baseline(&command_refs, generate_dir, config) {
Ok(()) => {
eprintln!("Generated baseline at: {}", generate_dir.display());
ValidateExitCode::Passed
}
Err(e) => {
eprintln!("Failed to generate baseline: {e}");
ValidateExitCode::CommandError
}
}
}
fn load_baseline_with_exit_code(
baseline_dir: &std::path::Path,
) -> std::result::Result<GoldenBaseline, ValidateExitCode> {
load_baseline(baseline_dir).map_err(|e| match e {
ValidateError::BaselineNotFound { path } => {
eprintln!("Baseline not found: {}", path.display());
ValidateExitCode::BaselineNotFound
}
ValidateError::InvalidManifest { reason } => {
eprintln!("Invalid baseline: {reason}");
ValidateExitCode::InvalidBaseline
}
other => {
eprintln!("Error loading baseline: {other}");
ValidateExitCode::InvalidBaseline
}
})
}
pub fn run_validate(command: &[String], config: &ValidateConfig) -> ValidateExitCode {
if let Some(ref generate_dir) = config.generate_dir {
return run_generate_mode(command, generate_dir, config);
}
let baseline_dir = match &config.baseline_dir {
Some(dir) => dir,
None => {
eprintln!("Error: Either --baseline or --generate must be specified");
return ValidateExitCode::ConfigError;
}
};
let baseline = match load_baseline_with_exit_code(baseline_dir) {
Ok(b) => b,
Err(exit_code) => return exit_code,
};
use crate::filter::SyscallFilter;
use crate::tracer::{trace_command, TracerConfig};
let tracer_config = TracerConfig {
timing_mode: true,
statistics_mode: true,
filter: SyscallFilter::all(),
..TracerConfig::default()
};
if let Err(e) = trace_command(command, tracer_config) {
eprintln!("Command execution failed: {e}");
return ValidateExitCode::CommandError;
}
let actual_syscalls: Vec<TraceSyscallEntry> = Vec::new();
let actual_timing = std::collections::HashMap::new();
let comparison_result = match comparison::validate_against_baseline(
&baseline,
&actual_syscalls,
&actual_timing,
config.tolerance_percent,
config.strict_mode,
config.ignore_timing,
) {
Ok(r) => r,
Err(e) => {
eprintln!("Comparison error: {e}");
return ValidateExitCode::Failed;
}
};
let output = output::format_result(&comparison_result, config.output_format);
print!("{output}");
if comparison_result.passed {
ValidateExitCode::Passed
} else {
ValidateExitCode::Failed
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_exit_code_values() {
assert_eq!(ValidateExitCode::Passed.code(), 0);
assert_eq!(ValidateExitCode::Failed.code(), 1);
assert_eq!(ValidateExitCode::BaselineNotFound.code(), 2);
assert_eq!(ValidateExitCode::InvalidBaseline.code(), 3);
assert_eq!(ValidateExitCode::CommandError.code(), 4);
assert_eq!(ValidateExitCode::ConfigError.code(), 5);
}
#[test]
fn test_config_requires_baseline_or_generate() {
let config = ValidateConfig::default();
let result = run_validate(&["echo".to_string()], &config);
assert_eq!(result, ValidateExitCode::ConfigError);
}
#[test]
fn test_run_validate_baseline_not_found() {
let config = ValidateConfig::default()
.with_baseline(std::path::PathBuf::from("/nonexistent/baseline"));
let result = run_validate(&["echo".to_string()], &config);
assert_eq!(result, ValidateExitCode::BaselineNotFound);
}
#[test]
fn test_run_validate_invalid_baseline() {
let temp_dir = TempDir::new().expect("failed to create temp dir");
std::fs::write(temp_dir.path().join("manifest.json"), "invalid json")
.expect("failed to write manifest");
let config = ValidateConfig::default().with_baseline(temp_dir.path().to_path_buf());
let result = run_validate(&["echo".to_string()], &config);
assert_eq!(result, ValidateExitCode::InvalidBaseline);
}
#[test]
fn test_exit_code_clone() {
let code = ValidateExitCode::Passed;
let cloned = code;
assert_eq!(cloned.code(), 0);
}
#[test]
fn test_exit_code_debug() {
let code = ValidateExitCode::Failed;
let debug = format!("{:?}", code);
assert!(debug.contains("Failed"));
}
#[test]
fn test_exit_code_copy() {
let code = ValidateExitCode::BaselineNotFound;
let copied: ValidateExitCode = code;
assert_eq!(copied, code);
}
#[test]
fn test_run_validate_other_errors() {
let config = ValidateConfig::default()
.with_baseline(std::path::PathBuf::from("/nonexistent/path/baseline"));
let result = run_validate(&["echo".to_string()], &config);
assert_eq!(result, ValidateExitCode::BaselineNotFound);
}
#[test]
fn test_run_validate_all_exit_codes() {
assert_eq!(ValidateExitCode::Passed.code(), 0);
assert_eq!(ValidateExitCode::Failed.code(), 1);
assert_eq!(ValidateExitCode::BaselineNotFound.code(), 2);
assert_eq!(ValidateExitCode::InvalidBaseline.code(), 3);
assert_eq!(ValidateExitCode::CommandError.code(), 4);
assert_eq!(ValidateExitCode::ConfigError.code(), 5);
}
#[test]
fn test_exit_code_eq() {
assert_eq!(ValidateExitCode::Passed, ValidateExitCode::Passed);
assert_ne!(ValidateExitCode::Passed, ValidateExitCode::Failed);
}
#[test]
fn test_run_validate_empty_command() {
let config =
ValidateConfig::default().with_baseline(std::path::PathBuf::from("/nonexistent"));
let result = run_validate(&[], &config);
assert_eq!(result, ValidateExitCode::BaselineNotFound);
}
#[test]
fn test_run_validate_generate_success() {
let temp_dir = TempDir::new().expect("failed to create temp dir");
let generate_path = temp_dir.path().join("baseline");
let config = ValidateConfig::default().with_generate(generate_path.clone());
let result = run_validate(&["true".to_string()], &config);
assert_eq!(result, ValidateExitCode::Passed);
}
#[test]
fn test_run_validate_generate_failure() {
if nix::unistd::geteuid().is_root() {
return;
}
let config = ValidateConfig::default()
.with_generate(std::path::PathBuf::from("/nonexistent/deep/path/baseline"));
let result = run_validate(&["true".to_string()], &config);
assert_eq!(result, ValidateExitCode::CommandError);
}
#[test]
fn test_validate_config_with_tolerance() {
let config = ValidateConfig::default()
.with_baseline(std::path::PathBuf::from("/test"))
.set_tolerance(50.0);
assert!((config.tolerance_percent - 50.0).abs() < f32::EPSILON);
}
#[test]
fn test_validate_config_strict_mode() {
let config = ValidateConfig::default()
.with_baseline(std::path::PathBuf::from("/test"))
.with_strict_mode(true);
assert!(config.strict_mode);
}
#[test]
fn test_validate_config_ignore_timing() {
let config = ValidateConfig::default()
.with_baseline(std::path::PathBuf::from("/test"))
.with_ignore_timing(true);
assert!(config.ignore_timing);
}
#[test]
fn test_validate_config_output_format() {
use crate::validate::config::ValidationOutputFormat;
let config = ValidateConfig::default()
.with_baseline(std::path::PathBuf::from("/test"))
.with_output_format(ValidationOutputFormat::Json);
assert_eq!(config.output_format, ValidationOutputFormat::Json);
}
}