#![allow(clippy::unwrap_used, clippy::panic)]
#![allow(clippy::cast_precision_loss, clippy::float_cmp)]
use anomstream_core::{ForestBuilder, TenantForestPool, ThresholdedForestBuilder};
#[test]
fn update_indexed_returns_fresh_indices() {
let mut f = ForestBuilder::<2>::new()
.num_trees(50)
.sample_size(64)
.seed(1)
.build()
.unwrap();
let idx_a = f.update_indexed([0.0, 0.0]).unwrap();
let idx_b = f.update_indexed([1.0, 1.0]).unwrap();
assert_ne!(idx_a, idx_b);
}
#[test]
fn delete_removes_point_from_trees() {
let mut f = ForestBuilder::<2>::new()
.num_trees(50)
.sample_size(64)
.seed(2)
.build()
.unwrap();
let idx = f.update_indexed([7.0, 11.0]).unwrap();
for i in 0_u32..32 {
let v = f64::from(i) * 0.01;
f.update([v, v]).unwrap();
}
let was_present = f.delete(idx).unwrap();
assert!(
was_present,
"freshly inserted point should be in at least one tree"
);
for (_, sampler, _) in f.trees() {
assert!(!sampler.contains(idx), "idx should be gone from every tree");
}
let score: f64 = f.score(&[0.5, 0.5]).unwrap().into();
assert!(score >= 0.0);
}
#[test]
fn delete_unknown_idx_is_noop() {
let mut f = ForestBuilder::<2>::new()
.num_trees(50)
.sample_size(16)
.seed(3)
.build()
.unwrap();
for i in 0_u32..8 {
let v = f64::from(i) * 0.01;
f.update([v, v]).unwrap();
}
let removed = f.delete(9_999_999).unwrap();
assert!(!removed);
}
#[test]
fn delete_by_value_removes_every_matching_entry() {
let mut f = ForestBuilder::<2>::new()
.num_trees(50)
.sample_size(64)
.seed(5)
.build()
.unwrap();
let target = [42.0_f64, 42.0];
for _ in 0..10 {
f.update(target).unwrap();
}
for i in 0_u32..32 {
let v = f64::from(i) * 0.01;
f.update([v, v]).unwrap();
}
let removed = f.delete_by_value(&target).unwrap();
assert!(removed > 0, "expected to remove at least one target entry");
let second = f.delete_by_value(&target).unwrap();
assert_eq!(second, 0);
}
#[test]
fn thresholded_delete_propagates_to_forest() {
let mut d = ThresholdedForestBuilder::<2>::new()
.num_trees(50)
.sample_size(32)
.min_observations(4)
.seed(7)
.build()
.unwrap();
let (idx, _verdict) = d.process_indexed([0.1, 0.2]).unwrap();
for i in 0_u32..32 {
let v = f64::from(i) * 0.01;
d.process([v, v]).unwrap();
}
let removed = d.delete(idx).unwrap();
assert!(removed);
}
#[test]
fn pool_delete_does_not_auto_create_tenant() {
let mut pool: TenantForestPool<&'static str, 2> = TenantForestPool::new(4, || {
ThresholdedForestBuilder::<2>::new()
.num_trees(50)
.sample_size(16)
.min_observations(4)
.seed(11)
.build()
})
.unwrap();
assert!(!pool.delete(&"unknown", 42).unwrap());
assert!(!pool.contains(&"unknown"));
assert_eq!(pool.delete_by_value(&"unknown", &[0.0, 0.0]).unwrap(), 0);
assert!(!pool.contains(&"unknown"));
}
#[test]
fn pool_delete_by_value_removes_per_tenant_matches() {
let mut pool: TenantForestPool<&'static str, 2> = TenantForestPool::new(4, || {
ThresholdedForestBuilder::<2>::new()
.num_trees(50)
.sample_size(32)
.min_observations(4)
.seed(13)
.build()
})
.unwrap();
let target = [9.0_f64, 9.0];
for _ in 0..8 {
pool.process(&"a", target).unwrap();
}
for _ in 0..8 {
pool.process(&"b", [1.0, 1.0]).unwrap();
}
let removed_a = pool.delete_by_value(&"a", &target).unwrap();
let removed_b = pool.delete_by_value(&"b", &target).unwrap();
assert!(removed_a > 0);
assert_eq!(removed_b, 0, "tenant B never saw the target value");
}