extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
use rand::seq::SliceRandom;
use rand::SeedableRng;
use rand_xoshiro::Xoshiro256PlusPlus;
use crate::result::{PreflightCategory, PreflightSeverity, PreflightWarningInfo};
use crate::statistics::compute_deciles_inplace;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
pub enum SanityWarning {
BrokenHarness {
variance_ratio: f64,
},
InsufficientSamples {
available: usize,
required: usize,
},
}
impl SanityWarning {
pub fn is_result_undermining(&self) -> bool {
matches!(self, SanityWarning::BrokenHarness { .. })
}
pub fn severity(&self) -> PreflightSeverity {
match self {
SanityWarning::BrokenHarness { .. } => PreflightSeverity::ResultUndermining,
SanityWarning::InsufficientSamples { .. } => PreflightSeverity::Informational,
}
}
pub fn description(&self) -> String {
match self {
SanityWarning::BrokenHarness { variance_ratio } => {
alloc::format!(
"The baseline samples showed {:.1}x the expected variation between \
random subsets. This may indicate mutable state captured in \
your test closure, or severe environmental interference. \
(If you're intentionally testing with identical inputs for \
FPR validation, this warning is expected and can be ignored.)",
variance_ratio
)
}
SanityWarning::InsufficientSamples {
available,
required,
} => {
alloc::format!(
"Insufficient samples for sanity check: {} available, {} required. \
Skipping Fixed-vs-Fixed validation.",
available,
required
)
}
}
}
pub fn guidance(&self) -> Option<String> {
match self {
SanityWarning::BrokenHarness { .. } => {
Some("Ensure baseline/sample closures don't share mutable state.".into())
}
SanityWarning::InsufficientSamples { .. } => None,
}
}
pub fn to_warning_info(&self) -> PreflightWarningInfo {
match self.guidance() {
Some(guidance) => PreflightWarningInfo::with_guidance(
PreflightCategory::Sanity,
self.severity(),
self.description(),
guidance,
),
None => PreflightWarningInfo::new(
PreflightCategory::Sanity,
self.severity(),
self.description(),
),
}
}
}
const MIN_SAMPLES_FOR_SANITY: usize = 1000;
const NOISE_MULTIPLIER: f64 = 5.0;
pub fn sanity_check(
fixed_samples: &[f64],
timer_resolution_ns: f64,
seed: u64,
) -> Option<SanityWarning> {
if fixed_samples.len() < MIN_SAMPLES_FOR_SANITY {
return Some(SanityWarning::InsufficientSamples {
available: fixed_samples.len(),
required: MIN_SAMPLES_FOR_SANITY,
});
}
let mut indices: Vec<usize> = (0..fixed_samples.len()).collect();
let mut rng = Xoshiro256PlusPlus::seed_from_u64(seed);
indices.shuffle(&mut rng);
let mid = indices.len() / 2;
let mut first_half: Vec<f64> = indices[..mid].iter().map(|&i| fixed_samples[i]).collect();
let mut second_half: Vec<f64> = indices[mid..].iter().map(|&i| fixed_samples[i]).collect();
let q_first = compute_deciles_inplace(&mut first_half);
let q_second = compute_deciles_inplace(&mut second_half);
let max_diff = (0..9)
.map(|i| (q_first[i] - q_second[i]).abs())
.fold(0.0_f64, f64::max);
let mut all_samples: Vec<f64> = fixed_samples.to_vec();
all_samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
let q25_idx = all_samples.len() / 4;
let q75_idx = 3 * all_samples.len() / 4;
let iqr = all_samples[q75_idx] - all_samples[q25_idx];
let n = fixed_samples.len() as f64;
let expected_noise = iqr / crate::math::sqrt(n);
let noise_based_threshold = NOISE_MULTIPLIER * expected_noise;
let iqr_floor = 0.4 * iqr;
let quantization_floor = 2.0 * timer_resolution_ns;
let threshold = noise_based_threshold.max(iqr_floor).max(quantization_floor);
if max_diff > threshold && threshold > 0.0 {
let variance_ratio = max_diff / expected_noise;
Some(SanityWarning::BrokenHarness { variance_ratio })
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::Rng;
const TEST_SEED: u64 = 12345;
#[test]
fn test_insufficient_samples() {
let samples = alloc::vec![1.0; 100];
let result = sanity_check(&samples, 1.0, TEST_SEED);
assert!(matches!(
result,
Some(SanityWarning::InsufficientSamples { .. })
));
}
#[test]
fn test_identical_samples_pass() {
let samples: Vec<f64> = (0..2000).map(|i| 100.0 + (i % 10) as f64).collect();
let result = sanity_check(&samples, 1.0, TEST_SEED);
assert!(
!matches!(result, Some(SanityWarning::BrokenHarness { .. })),
"Identical samples should not trigger broken harness warning"
);
}
#[test]
fn test_severity() {
let broken = SanityWarning::BrokenHarness {
variance_ratio: 5.0,
};
assert_eq!(broken.severity(), PreflightSeverity::ResultUndermining);
assert!(broken.is_result_undermining());
let insufficient = SanityWarning::InsufficientSamples {
available: 100,
required: 1000,
};
assert_eq!(insufficient.severity(), PreflightSeverity::Informational);
assert!(!insufficient.is_result_undermining());
}
#[test]
fn test_single_outlier_not_detected() {
let mut samples = Vec::with_capacity(2000);
samples.push(5000.0);
for i in 1..2000 {
samples.push(50.0 + (i % 5) as f64);
}
let result = sanity_check(&samples, 1.0, TEST_SEED);
assert!(
!matches!(result, Some(SanityWarning::BrokenHarness { .. })),
"Single outlier should not trigger (outlier removal handles this), got {:?}",
result
);
}
#[test]
fn test_broken_harness_repeated_cold_starts() {
let mut samples = Vec::with_capacity(2000);
for i in 0..2000 {
let base = if i % 10 == 0 { 2000.0 } else { 100.0 };
let noise = (i % 5) as f64;
samples.push(base + noise);
}
let result = sanity_check(&samples, 1.0, TEST_SEED);
assert!(
matches!(result, Some(SanityWarning::BrokenHarness { .. })),
"10% cold starts should affect D90 and trigger warning, got {:?}",
result
);
}
#[test]
fn test_broken_harness_alternating_state() {
let mut samples = Vec::with_capacity(2000);
for i in 0..2000 {
let base = if i % 2 == 0 { 100.0 } else { 500.0 };
let noise = (i % 7) as f64;
samples.push(base + noise);
}
let result = sanity_check(&samples, 1.0, TEST_SEED);
assert!(
matches!(result, Some(SanityWarning::BrokenHarness { .. })),
"Alternating state pattern should trigger warning, got {:?}",
result
);
}
#[test]
fn test_moderate_noise_does_not_trigger() {
let mut rng = Xoshiro256PlusPlus::seed_from_u64(99999);
let samples: Vec<f64> = (0..2000)
.map(|_| {
let base = 500.0;
let noise = (rng.random::<f64>() - 0.5) * 100.0;
base + noise
})
.collect();
let result = sanity_check(&samples, 1.0, TEST_SEED);
assert!(
!matches!(result, Some(SanityWarning::BrokenHarness { .. })),
"Moderate noise should not trigger broken harness warning, got {:?}",
result
);
}
}