use crate::regression::config::RegressionConfig;
use crate::regression::noise_filter::filter_noisy_syscalls;
use crate::regression::statistics::{compare_distributions, StatisticalTest};
use anyhow::Result;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RegressionVerdict {
NoRegression,
Regression {
regressed_syscalls: Vec<String>,
filtered_count: usize,
},
InsufficientData { reason: String },
}
#[derive(Debug, Clone)]
pub struct RegressionAssessment {
pub verdict: RegressionVerdict,
pub tests: HashMap<String, StatisticalTest>,
pub filtered_syscalls: Vec<String>,
pub config: RegressionConfig,
}
impl RegressionAssessment {
pub fn to_report_string(&self) -> String {
let mut report = String::new();
match &self.verdict {
RegressionVerdict::NoRegression => {
report.push_str("✅ NO REGRESSION DETECTED\n\n");
report.push_str(&format!("Statistical tests performed: {}\n", self.tests.len()));
report.push_str(&format!(
"Significance level: {} ({}% confidence)\n",
self.config.significance_level,
(1.0 - self.config.significance_level) * 100.0
));
}
RegressionVerdict::Regression { regressed_syscalls, filtered_count } => {
report.push_str(&format!(
"❌ REGRESSION DETECTED ({} syscalls)\n\n",
regressed_syscalls.len()
));
report
.push_str(&format!("Regressed syscalls: {}\n", regressed_syscalls.join(", ")));
report.push_str(&format!("Filtered noisy syscalls: {filtered_count}\n"));
}
RegressionVerdict::InsufficientData { reason } => {
report.push_str("⚠️ INSUFFICIENT DATA\n\n");
report.push_str(&format!("Reason: {reason}\n"));
}
}
if !self.filtered_syscalls.is_empty() {
report.push_str(&format!(
"\n🔇 Filtered noisy syscalls ({}):\n",
self.filtered_syscalls.len()
));
for name in &self.filtered_syscalls {
report.push_str(&format!(" - {name}\n"));
}
}
if !self.tests.is_empty() {
report.push_str("\n📊 Statistical Tests:\n");
for (name, test) in &self.tests {
report.push_str(&format!(
" {} (p={:.4}, baseline_median={:.1}, current_median={:.1})\n",
name, test.pvalue, test.baseline_median, test.current_median
));
}
}
report
}
}
pub fn assess_regression(
baseline: &HashMap<String, Vec<f32>>,
current: &HashMap<String, Vec<f32>>,
config: &RegressionConfig,
) -> Result<RegressionAssessment> {
config.validate().map_err(|e| anyhow::anyhow!(e))?;
let (stable_baseline, filtered_syscalls) = if config.enable_noise_filtering {
filter_noisy_syscalls(baseline, config.noise_threshold)
} else {
(baseline.clone(), Vec::new())
};
let mut tests = HashMap::new();
let mut regressed_syscalls = Vec::new();
for (name, baseline_samples) in &stable_baseline {
let Some(current_samples) = current.get(name) else {
continue; };
if baseline_samples.len() < config.min_sample_size
|| current_samples.len() < config.min_sample_size
{
continue;
}
match compare_distributions(baseline_samples, current_samples) {
Ok(test) => {
if test.pvalue < config.significance_level as f32 {
regressed_syscalls.push(name.clone());
}
tests.insert(name.clone(), test);
}
Err(e) => {
tracing::warn!("Failed to compare distributions for {}: {}", name, e);
}
}
}
let verdict = if tests.is_empty() {
RegressionVerdict::InsufficientData {
reason: format!(
"No syscalls passed noise filtering and sample size requirements \
(min_sample_size={}, filtered={})",
config.min_sample_size,
filtered_syscalls.len()
),
}
} else if regressed_syscalls.is_empty() {
RegressionVerdict::NoRegression
} else {
RegressionVerdict::Regression {
regressed_syscalls,
filtered_count: filtered_syscalls.len(),
}
};
Ok(RegressionAssessment { verdict, tests, filtered_syscalls, config: config.clone() })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_assess_regression_no_regression() {
let mut baseline = HashMap::new();
baseline.insert("mmap".to_string(), vec![10.0, 11.0, 10.0, 12.0, 10.0]);
let mut current = HashMap::new();
current.insert("mmap".to_string(), vec![10.0, 11.0, 10.0, 13.0, 10.0]);
let config = RegressionConfig::default();
let assessment = assess_regression(&baseline, ¤t, &config).expect("test");
assert_eq!(assessment.verdict, RegressionVerdict::NoRegression);
assert_eq!(assessment.tests.len(), 1);
}
#[test]
fn test_assess_regression_regression_detected() {
let mut baseline = HashMap::new();
baseline.insert("mmap".to_string(), vec![10.0, 11.0, 10.0, 12.0, 10.0]);
let mut current = HashMap::new();
current.insert("mmap".to_string(), vec![50.0, 52.0, 51.0, 53.0, 50.0]);
let config = RegressionConfig::default();
let assessment = assess_regression(&baseline, ¤t, &config).expect("test");
match assessment.verdict {
RegressionVerdict::Regression { ref regressed_syscalls, .. } => {
assert!(regressed_syscalls.contains(&"mmap".to_string()));
}
_ => panic!("Expected Regression verdict"),
}
}
#[test]
fn test_assess_regression_filters_noisy() {
let mut baseline = HashMap::new();
baseline.insert("mmap".to_string(), vec![10.0, 11.0, 10.0, 12.0, 10.0]);
baseline.insert("socket".to_string(), vec![5.0, 50.0, 3.0, 45.0, 2.0]);
let mut current = HashMap::new();
current.insert("mmap".to_string(), vec![10.0, 11.0, 10.0, 13.0, 10.0]);
current.insert("socket".to_string(), vec![6.0, 51.0, 4.0, 46.0, 3.0]);
let config = RegressionConfig::default();
let assessment = assess_regression(&baseline, ¤t, &config).expect("test");
assert!(assessment.filtered_syscalls.contains(&"socket".to_string()));
assert_eq!(assessment.tests.len(), 1);
assert!(assessment.tests.contains_key("mmap"));
}
#[test]
fn test_assess_regression_insufficient_data() {
let mut baseline = HashMap::new();
baseline.insert("mmap".to_string(), vec![10.0]);
let mut current = HashMap::new();
current.insert("mmap".to_string(), vec![10.0]);
let config = RegressionConfig::default();
let assessment = assess_regression(&baseline, ¤t, &config).expect("test");
match assessment.verdict {
RegressionVerdict::InsufficientData { .. } => {
}
_ => panic!("Expected InsufficientData verdict"),
}
}
#[test]
fn test_report_string_no_regression() {
let verdict = RegressionVerdict::NoRegression;
let assessment = RegressionAssessment {
verdict,
tests: HashMap::new(),
filtered_syscalls: vec![],
config: RegressionConfig::default(),
};
let report = assessment.to_report_string();
assert!(report.contains("NO REGRESSION DETECTED"));
}
#[test]
fn test_report_string_regression() {
let verdict = RegressionVerdict::Regression {
regressed_syscalls: vec!["mmap".to_string()],
filtered_count: 1,
};
let assessment = RegressionAssessment {
verdict,
tests: HashMap::new(),
filtered_syscalls: vec!["socket".to_string()],
config: RegressionConfig::default(),
};
let report = assessment.to_report_string();
assert!(report.contains("REGRESSION DETECTED"));
assert!(report.contains("mmap"));
assert!(report.contains("socket"));
}
}