use arbtest::arbtest;
use dsfb_database::baselines::{adwin::Adwin, bocpd::Bocpd, pelt::Pelt, ChangePointDetector};
fn detectors() -> Vec<Box<dyn ChangePointDetector>> {
vec![
Box::new(Adwin::default()),
Box::new(Bocpd::default()),
Box::new(Pelt::default()),
]
}
#[test]
fn no_panic_on_empty_series() {
let series: Vec<(f64, f64)> = Vec::new();
for det in detectors() {
let cps = det.detect(&series);
assert!(
cps.is_empty(),
"{}: empty input must yield no change-points",
det.name()
);
}
}
#[test]
fn no_panic_on_single_sample() {
let series = vec![(0.0_f64, 0.0_f64)];
for det in detectors() {
let cps = det.detect(&series);
assert!(
cps.is_empty(),
"{}: single-sample input must yield no change-points",
det.name()
);
}
}
#[test]
fn no_panic_on_constant_series() {
let series: Vec<(f64, f64)> = (0..200).map(|i| (i as f64, 7.0)).collect();
for det in detectors() {
let cps = det.detect(&series);
assert!(
cps.is_empty(),
"{}: constant series must yield no change-points (got {})",
det.name(),
cps.len()
);
}
}
#[test]
fn no_panic_on_all_nan_series() {
let series: Vec<(f64, f64)> = (0..50).map(|i| (i as f64, f64::NAN)).collect();
for det in detectors() {
let _ = det.detect(&series);
}
}
#[test]
fn no_panic_on_infinite_series() {
for sign in [1.0_f64, -1.0_f64] {
let series: Vec<(f64, f64)> = (0..50).map(|i| (i as f64, sign * f64::INFINITY)).collect();
for det in detectors() {
let _ = det.detect(&series);
}
}
}
fn bounded_uniform_series(
u: &mut arbtest::arbitrary::Unstructured<'_>,
) -> arbtest::arbitrary::Result<Vec<(f64, f64)>> {
let raw_n: u8 = u.arbitrary()?;
let n = (raw_n as usize).clamp(40, 240);
let mut series = Vec::with_capacity(n);
for i in 0..n {
let raw: i8 = u.arbitrary()?;
let v = (raw as f64) / 128.0;
series.push((i as f64, v));
}
Ok(series)
}
#[test]
fn adwin_bounded_false_positive_on_stationary_uniform() {
arbtest(|u| {
let series = bounded_uniform_series(u)?;
let n = series.len();
let cps = Adwin::default().detect(&series);
assert!(
cps.len() <= n / 8 + 1,
"ADWIN: {} cps in {} stationary samples (limit {})",
cps.len(),
n,
n / 8 + 1
);
Ok(())
})
.budget_ms(100);
}
#[test]
fn pelt_bounded_false_positive_on_stationary_uniform() {
arbtest(|u| {
let series = bounded_uniform_series(u)?;
let n = series.len();
let cps = Pelt::default().detect(&series);
assert!(
cps.len() <= n / 10 + 1,
"PELT: {} cps in {} stationary samples (limit {})",
cps.len(),
n,
n / 10 + 1
);
Ok(())
})
.budget_ms(100);
}
#[test]
fn bocpd_bounded_false_positive_on_stationary_uniform() {
arbtest(|u| {
let series = bounded_uniform_series(u)?;
let n = series.len();
let cps = Bocpd::default().detect(&series);
assert!(
cps.len() <= n / 8 + 1,
"BOCPD: {} cps in {} stationary samples (limit {})",
cps.len(),
n,
n / 8 + 1
);
Ok(())
})
.budget_ms(100);
}
#[test]
fn detectors_are_deterministic_on_arbitrary_input() {
arbtest(|u| {
let series = bounded_uniform_series(u)?;
for det in detectors() {
let a = det.detect(&series);
let b = det.detect(&series);
assert_eq!(
a,
b,
"{}: detect() not deterministic (a={:?}, b={:?})",
det.name(),
a,
b
);
}
Ok(())
})
.budget_ms(100);
}