use super::result::ValidationResult;
pub trait ValidationStrategy: Send + Sync {
fn name(&self) -> &str;
fn evaluate(&self, baseline: f64, current: f64, sample_count: usize) -> ValidationResult;
}
#[derive(Debug, Clone, Default)]
pub struct NoRegression {
epsilon: f64,
}
impl NoRegression {
pub fn new() -> Self {
Self { epsilon: 1e-9 }
}
pub fn with_epsilon(mut self, epsilon: f64) -> Self {
self.epsilon = epsilon;
self
}
}
impl ValidationStrategy for NoRegression {
fn name(&self) -> &str {
"no_regression"
}
fn evaluate(&self, baseline: f64, current: f64, sample_count: usize) -> ValidationResult {
if current >= baseline - self.epsilon {
ValidationResult::pass(baseline, current, self.name(), sample_count)
} else {
ValidationResult::fail(
baseline,
current,
self.name(),
format!(
"regression detected: {:.1}% < baseline {:.1}%",
current * 100.0,
baseline * 100.0
),
sample_count,
)
}
}
}
#[derive(Debug, Clone)]
pub struct Improvement {
threshold: f64,
epsilon: f64,
}
impl Improvement {
pub fn new(threshold: f64) -> Self {
Self {
threshold,
epsilon: 1e-9,
}
}
pub fn ten_percent() -> Self {
Self::new(1.1)
}
pub fn five_percent() -> Self {
Self::new(1.05)
}
}
impl ValidationStrategy for Improvement {
fn name(&self) -> &str {
"improvement"
}
fn evaluate(&self, baseline: f64, current: f64, sample_count: usize) -> ValidationResult {
let required = baseline * self.threshold;
if current >= required - self.epsilon {
ValidationResult::pass(baseline, current, self.name(), sample_count)
} else {
let actual_improvement = if baseline > 0.0 {
(current / baseline - 1.0) * 100.0
} else {
0.0
};
let required_improvement = (self.threshold - 1.0) * 100.0;
ValidationResult::fail(
baseline,
current,
self.name(),
format!(
"insufficient improvement: {:.1}% (need {:.0}%, got {:.1}%)",
current * 100.0,
required_improvement,
actual_improvement
),
sample_count,
)
}
}
}
#[derive(Debug, Clone)]
pub struct Absolute {
threshold: f64,
epsilon: f64,
}
impl Absolute {
pub fn new(threshold: f64) -> Self {
Self {
threshold,
epsilon: 1e-9,
}
}
pub fn eighty_percent() -> Self {
Self::new(0.8)
}
pub fn ninety_percent() -> Self {
Self::new(0.9)
}
}
impl ValidationStrategy for Absolute {
fn name(&self) -> &str {
"absolute"
}
fn evaluate(&self, baseline: f64, current: f64, sample_count: usize) -> ValidationResult {
if current >= self.threshold - self.epsilon {
ValidationResult::pass(baseline, current, self.name(), sample_count)
} else {
ValidationResult::fail(
baseline,
current,
self.name(),
format!(
"below threshold: {:.1}% < required {:.1}%",
current * 100.0,
self.threshold * 100.0
),
sample_count,
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_regression_pass() {
let strategy = NoRegression::new();
let result = strategy.evaluate(0.7, 0.7, 100);
assert!(result.passed);
let result = strategy.evaluate(0.7, 0.8, 100);
assert!(result.passed);
let result = strategy.evaluate(1.0, 1.0, 100);
assert!(result.passed);
}
#[test]
fn test_no_regression_fail() {
let strategy = NoRegression::new();
let result = strategy.evaluate(0.7, 0.65, 100);
assert!(!result.passed);
assert!(result.failure_reason.unwrap().contains("regression"));
}
#[test]
fn test_improvement_pass() {
let strategy = Improvement::ten_percent();
let result = strategy.evaluate(0.7, 0.8, 100);
assert!(result.passed);
let result = strategy.evaluate(0.7, 0.77, 100);
assert!(result.passed);
}
#[test]
fn test_improvement_fail() {
let strategy = Improvement::ten_percent();
let result = strategy.evaluate(0.7, 0.72, 100);
assert!(!result.passed);
assert!(result.failure_reason.unwrap().contains("insufficient"));
}
#[test]
fn test_absolute_pass() {
let strategy = Absolute::eighty_percent();
let result = strategy.evaluate(0.5, 0.85, 100);
assert!(result.passed);
let result = strategy.evaluate(0.9, 0.8, 100);
assert!(result.passed);
}
#[test]
fn test_absolute_fail() {
let strategy = Absolute::eighty_percent();
let result = strategy.evaluate(0.9, 0.75, 100);
assert!(!result.passed);
assert!(result.failure_reason.unwrap().contains("below threshold"));
}
}