use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
use thiserror::Error;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryStats {
pub allocated: usize,
pub used: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkResult {
pub test_name: String,
pub duration_ns: u64,
pub operations_count: usize,
pub memory_used: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceDelta {
pub delta_ns: i64,
pub delta_percent: f64,
}
#[derive(Error, Debug)]
pub enum RegressionError {
#[error("Failed to load baseline data: {0}")]
BaselineLoadError(String),
#[error("Failed to save baseline data: {0}")]
BaselineSaveError(String),
#[error("Performance regression detected: {0}")]
RegressionDetected(String),
#[error("Configuration error: {0}")]
ConfigError(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceThresholds {
pub startup_time_regression_percent: f64, pub execution_time_regression_percent: f64, pub memory_regression_percent: f64, pub throughput_regression_percent: f64, pub regression_count_threshold: usize, }
impl Default for PerformanceThresholds {
fn default() -> Self {
Self {
startup_time_regression_percent: 10.0, execution_time_regression_percent: 5.0, memory_regression_percent: 15.0, throughput_regression_percent: 8.0, regression_count_threshold: 1, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceBaseline {
pub test_name: String,
pub metric_type: String, pub avg_duration_ns: u64,
pub std_deviation_ns: f64,
pub operations_per_second: f64,
pub memory_stats: Option<MemoryStats>,
pub timestamp: u64,
pub sample_count: usize,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegressionDetectionResult {
pub test_name: String,
pub is_regression: bool,
pub regression_severity: RegressionSeverity,
pub current_result: BenchmarkResult,
pub baseline_result: Option<PerformanceBaseline>,
pub performance_delta: Option<PerformanceDelta>,
pub threshold: f64,
pub actual_delta_percent: f64,
pub recommendations: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum RegressionSeverity {
None,
Minor, Moderate, Severe, Critical, }
impl RegressionSeverity {
pub fn from_percentage(percent: f64) -> Self {
if percent < 5.0 {
RegressionSeverity::Minor
} else if percent < 15.0 {
RegressionSeverity::Moderate
} else {
RegressionSeverity::Severe
}
}
}
pub struct PerformanceRegressionDetector {
thresholds: PerformanceThresholds,
baselines: HashMap<String, PerformanceBaseline>,
}
impl Default for PerformanceRegressionDetector {
fn default() -> Self {
Self::new(PerformanceThresholds::default())
}
}
impl PerformanceRegressionDetector {
pub fn new(thresholds: PerformanceThresholds) -> Self {
Self {
thresholds,
baselines: HashMap::new(),
}
}
pub fn create_baseline_from_result(&self, result: &BenchmarkResult) -> PerformanceBaseline {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
PerformanceBaseline {
test_name: result.test_name.clone(),
metric_type: "execution_time".to_string(),
avg_duration_ns: result.duration_ns,
std_deviation_ns: 0.0,
operations_per_second: if result.duration_ns > 0 {
(result.operations_count as f64) / (result.duration_ns as f64 / 1_000_000_000.0)
} else {
0.0
},
memory_stats: Some(MemoryStats {
allocated: result.memory_used,
used: result.memory_used,
}),
timestamp,
sample_count: 1,
metadata: HashMap::new(),
}
}
pub fn detect_regression(&self, result: &BenchmarkResult) -> RegressionDetectionResult {
let baseline = self.baselines.get(&result.test_name);
let threshold = self.thresholds.execution_time_regression_percent;
let (is_regression, actual_delta_percent, performance_delta) =
if let Some(baseline) = baseline {
let delta_ns = result.duration_ns as i64 - baseline.avg_duration_ns as i64;
let delta_percent = if baseline.avg_duration_ns > 0 {
(delta_ns as f64 / baseline.avg_duration_ns as f64) * 100.0
} else {
0.0
};
let is_regression = delta_percent > threshold;
(
is_regression,
delta_percent,
Some(PerformanceDelta {
delta_ns,
delta_percent,
}),
)
} else {
(false, 0.0, None)
};
let regression_severity = RegressionSeverity::from_percentage(actual_delta_percent.abs());
RegressionDetectionResult {
test_name: result.test_name.clone(),
is_regression,
regression_severity,
current_result: result.clone(),
baseline_result: baseline.cloned(),
performance_delta,
threshold,
actual_delta_percent,
recommendations: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct RegressionStats {
pub total_tests: usize,
pub regressions_detected: usize,
pub minor_regressions: usize,
pub moderate_regressions: usize,
pub severe_regressions: usize,
pub critical_regressions: usize,
pub false_positive_rate: f64,
}
pub struct RegressionTestSuite {
detector: PerformanceRegressionDetector,
}
impl RegressionTestSuite {
pub fn new() -> Result<Self, RegressionError> {
Ok(Self {
detector: PerformanceRegressionDetector::default(),
})
}
pub fn run_regression_detection(&mut self, results: &[BenchmarkResult]) -> RegressionStats {
let mut stats = RegressionStats {
total_tests: results.len(),
regressions_detected: 0,
minor_regressions: 0,
moderate_regressions: 0,
severe_regressions: 0,
critical_regressions: 0,
false_positive_rate: 0.0,
};
for result in results {
let detection_result = self.detector.detect_regression(result);
if detection_result.is_regression {
stats.regressions_detected += 1;
}
match detection_result.regression_severity {
RegressionSeverity::Minor => stats.minor_regressions += 1,
RegressionSeverity::Moderate => stats.moderate_regressions += 1,
RegressionSeverity::Severe => stats.severe_regressions += 1,
RegressionSeverity::Critical => stats.critical_regressions += 1,
_ => {}
}
}
stats
}
}
pub struct DefaultTestRunner;
impl DefaultTestRunner {
pub fn run_default_tests() -> Result<BenchmarkResult, RegressionError> {
let start = SystemTime::now();
std::thread::sleep(std::time::Duration::from_millis(10));
let duration = start.elapsed().unwrap_or_default();
Ok(BenchmarkResult {
test_name: "default_test".to_string(),
duration_ns: duration.as_nanos() as u64,
operations_count: 1000,
memory_used: 1024 * 1024, })
}
}