use super::*;
use crate::DetectorConfig;
#[cfg(test)]
use std::eprintln;
#[test]
fn n1_equals_1_detector_functions() {
let config = DetectorConfig {
n1: 1,
n2: 1,
n3: 1,
..DetectorConfig::default()
};
let mut d: ActiveSpeakerDetector<u64> = ActiveSpeakerDetector::with_config(config);
d.add_peer(1, 0);
d.add_peer(2, 0);
for i in 0..100 {
let t: u64 = i * 20;
d.record_level(1, 5, t);
d.record_level(2, 127, t);
}
let _ = d.tick(2000);
}
#[test]
fn n1_equals_255_detector_functions() {
let config = DetectorConfig {
n1: 255,
..DetectorConfig::default()
};
let mut d: ActiveSpeakerDetector<u64> = ActiveSpeakerDetector::with_config(config);
d.add_peer(1, 0);
d.add_peer(2, 0);
for i in 0..100 {
let t: u64 = i * 20;
d.record_level(1, 5, t);
d.record_level(2, 127, t);
}
let change = d.tick(2000);
if let Some(c) = change {
assert_eq!(c.peer_id, 1, "loudest must win even with extreme n1");
}
}
#[test]
fn binomial_coefficient_r_greater_than_n_returns_wrong_value() {
use crate::numerics::binomial_coefficient;
let got = binomial_coefficient(5, 10);
assert_eq!(
got, 0,
"C(5, 10) must be 0 mathematically — got {got}. \
This corrupts activity scores when v_l exceeds n_r."
);
}
#[test]
fn compute_activity_score_handles_v_l_gt_n_r() {
use crate::numerics::compute_activity_score;
let result = std::panic::catch_unwind(|| compute_activity_score(10, 5, 0.5, 24.0));
assert!(
result.is_ok(),
"compute_activity_score panicked when v_l > n_r (arithmetic underflow)"
);
}
#[test]
fn compute_activity_score_all_zero() {
use crate::numerics::compute_activity_score;
let s = compute_activity_score(0, 0, 0.5, 0.78);
assert!(s.is_finite(), "score must be finite, got {s}");
assert!(s >= 1.0e-10, "score must be >= MIN_ACTIVITY_SCORE, got {s}");
}
#[test]
fn compute_bigs_empty_bigs_is_internal_only() {
use crate::numerics::compute_bigs;
let littles = [0u8, 0, 0];
let mut bigs = [0u8; 1];
let _ = compute_bigs(&littles, &mut bigs, 7);
}
fn feed(d: &mut ActiveSpeakerDetector, p: u64, lvl: u8, from_ms: u64, ms: u64) {
let mut t = from_ms;
let end = from_ms + ms;
while t < end {
d.record_level(p, lvl, t);
t += 20;
}
}
#[test]
fn score_monotonicity_louder_peer_always_ranks_higher() {
let mut d = ActiveSpeakerDetector::new();
d.add_peer(1, 0);
d.add_peer(2, 0);
let mut t: u64 = 0;
let mut inversions = 0;
for tick_i in 0..10 {
for _ in 0..15 {
d.record_level(1, 5, t); d.record_level(2, 80, t); t += 20;
}
d.tick(t);
let top = d.current_top_k(2);
if top.len() == 2 && top[0] != 1 {
inversions += 1;
eprintln!("tick {tick_i}: unexpected ordering {top:?}");
}
}
assert_eq!(
inversions, 0,
"consistently louder peer 1 was ranked below peer 2 on {inversions} ticks"
);
}
#[test]
fn dominance_stable_across_quiet_ticks() {
let mut d = ActiveSpeakerDetector::new();
d.add_peer(1, 0);
d.add_peer(2, 0);
feed(&mut d, 1, 5, 0, 2000);
feed(&mut d, 2, 127, 0, 2000);
let change = d.tick(2050);
assert_eq!(change.map(|c| c.peer_id), Some(1));
let mut t: u64 = 2050;
for i in 0..5 {
t += 300;
let out = d.tick(t);
assert!(
out.is_none(),
"tick {i} triggered spurious speaker change: {out:?}"
);
assert_eq!(d.current_dominant(), Some(&1), "dominant must remain 1");
}
}
#[test]
fn remove_nonexistent_peer_is_noop() {
let mut d = ActiveSpeakerDetector::new();
d.add_peer(1, 0);
d.add_peer(2, 0);
d.remove_peer(&999);
let scores = d.peer_scores();
assert_eq!(scores.len(), 2);
}
#[test]
fn record_level_auto_registers_peer() {
let mut d = ActiveSpeakerDetector::new();
d.add_peer(1, 0);
feed(&mut d, 1, 127, 0, 2000); feed(&mut d, 99, 5, 0, 2000); let change = d.tick(2050);
assert_eq!(
change.map(|c| c.peer_id),
Some(99),
"auto-registered peer 99 should win"
);
let ids: Vec<u64> = d
.peer_scores()
.into_iter()
.map(|(id, _, _, _)| id)
.collect();
assert!(ids.contains(&99));
}
#[test]
fn tick_with_earlier_time_does_not_panic() {
let mut d = ActiveSpeakerDetector::new();
let t0: u64 = 10_000; d.add_peer(1, t0);
d.add_peer(2, t0);
d.tick(t0);
let earlier = t0 - 100;
let _ = d.tick(earlier);
}
#[test]
fn record_level_with_earlier_time_does_not_panic() {
let mut d = ActiveSpeakerDetector::new();
let t0: u64 = 10_000; d.add_peer(1, t0);
d.record_level(1, 5, t0 + 100);
d.record_level(1, 5, t0);
let _ = d.tick(t0 + 200);
}
#[test]
fn rapid_ticks_do_not_flap() {
let mut d = ActiveSpeakerDetector::new();
d.add_peer(1, 0);
d.add_peer(2, 0);
feed(&mut d, 1, 5, 0, 2000);
feed(&mut d, 2, 127, 0, 2000);
let initial = d.tick(2050);
assert_eq!(initial.map(|c| c.peer_id), Some(1));
let mut t: u64 = 2050;
let mut flaps = 0;
for _ in 0..100 {
t += 10;
if let Some(c) = d.tick(t) {
flaps += 1;
eprintln!("unexpected speaker change: {:?}", c);
}
}
assert_eq!(flaps, 0, "rapid ticks caused {flaps} spurious flaps");
assert_eq!(d.current_dominant(), Some(&1));
}
#[test]
fn all_peers_silent_bootstrap() {
let mut d = ActiveSpeakerDetector::new();
for id in 1..=3u64 {
d.add_peer(id, 0);
}
for id in 1..=3u64 {
feed(&mut d, id, 127, 0, 2000);
}
let change = d.tick(2050);
if let Some(c) = change {
assert_eq!(
Some(&c.peer_id),
d.current_dominant(),
"current_dominant must match the elected peer"
);
}
}
#[test]
fn all_peers_equal_volume() {
let mut d = ActiveSpeakerDetector::new();
for id in 1..=4u64 {
d.add_peer(id, 0);
}
for id in 1..=4u64 {
feed(&mut d, id, 10, 0, 2000);
}
let change = d.tick(2050);
assert!(
change.is_some(),
"bootstrap election with equal-volume peers must produce a winner"
);
let first = change.unwrap().peer_id;
let next = d.tick(2350);
assert!(
next.is_none(),
"dominant must remain stable with equal input, got {next:?}"
);
assert_eq!(d.current_dominant(), Some(&first));
}
#[test]
fn single_then_louder_second_peer_wins() {
let mut d = ActiveSpeakerDetector::new();
d.add_peer(1, 0);
feed(&mut d, 1, 30, 0, 2000);
let c = d.tick(2050);
assert_eq!(c.map(|c| c.peer_id), Some(1));
let t_decay: u64 = 2050;
feed(&mut d, 1, 127, t_decay, 3000);
let mut t = t_decay;
for _ in 0..10 {
t += 300;
d.tick(t);
}
d.add_peer(2, t);
let t1 = t;
let mut t_feed = t1;
let phase1_end = t1 + 200;
while t_feed < phase1_end {
d.record_level(2, 80, t_feed);
d.record_level(1, 127, t_feed);
t_feed += 20;
}
let phase2_end = t_feed + 8000;
while t_feed < phase2_end {
d.record_level(2, 5, t_feed);
d.record_level(1, 127, t_feed);
t_feed += 20;
}
let mut took_over = false;
let mut t2 = t1;
for _tick_i in 0..30 {
t2 += 300;
if let Some(ch) = d.tick(t2) {
if ch.peer_id == 2 {
took_over = true;
break;
}
}
}
assert!(
took_over,
"much louder challenger (peer 2, level 0) should beat silent incumbent (peer 1). \
scores = {:?}",
d.peer_scores()
);
}
#[test]
fn hundred_peer_room_no_panic() {
let mut d = ActiveSpeakerDetector::new();
for id in 0..100u64 {
d.add_peer(id, 0);
}
let mut t: u64 = 0;
for step in 0..120u64 {
for id in 0..100u64 {
let lvl = ((id * 7 + step * 13) % 128) as u8;
d.record_level(id, lvl, t);
}
t += 20;
}
let _ = d.tick(t + 20);
if let Some(dom) = d.current_dominant() {
assert!(*dom < 100, "dominant {} is not a valid peer id", dom);
}
}
#[test]
fn extreme_config_all_ones() {
let config = DetectorConfig {
n1: 1,
n2: 1,
n3: 1,
..DetectorConfig::default()
};
let mut d: ActiveSpeakerDetector<u64> = ActiveSpeakerDetector::with_config(config);
d.add_peer(1, 0);
let c = d.tick(300);
assert_eq!(c.map(|c| c.peer_id), Some(1));
}
#[test]
fn extreme_config_small_n2_with_speech_must_not_panic() {
let config = DetectorConfig {
n2: 1,
..DetectorConfig::default()
};
let mut d: ActiveSpeakerDetector<u64> = ActiveSpeakerDetector::with_config(config);
d.add_peer(1, 0);
d.add_peer(2, 0);
let mut t: u64 = 0;
for i in 0..300 {
let lvl = if i % 6 == 0 { 80 } else { 5 };
d.record_level(1, lvl, t);
d.record_level(2, 127, t);
t += 20;
}
let _ = d.tick(t);
}
#[test]
fn extreme_config_small_n3_must_not_panic() {
let config = DetectorConfig {
n3: 1,
..DetectorConfig::default()
};
let mut d: ActiveSpeakerDetector<u64> = ActiveSpeakerDetector::with_config(config);
d.add_peer(1, 0);
d.add_peer(2, 0);
let mut t: u64 = 0;
for i in 0..1500 {
let lvl = if i % 6 == 0 { 80 } else { 5 };
d.record_level(1, lvl, t);
d.record_level(2, 127, t);
t += 20;
}
let _ = d.tick(t);
}
#[test]
fn extreme_config_n1_zero_does_not_panic() {
let config = DetectorConfig {
n1: 0,
..DetectorConfig::default()
};
let mut d: ActiveSpeakerDetector<u64> = ActiveSpeakerDetector::with_config(config);
d.add_peer(1, 0);
d.add_peer(2, 0);
for i in 0..100u64 {
d.record_level(1, 5, i * 20);
d.record_level(2, 127, i * 20);
}
let _ = d.tick(2000);
}
#[test]
fn current_top_k_zero_returns_empty() {
let mut d = ActiveSpeakerDetector::new();
d.add_peer(1, 0);
d.add_peer(2, 0);
let top = d.current_top_k(0);
assert!(top.is_empty());
}
#[test]
fn peer_scores_before_any_tick() {
let mut d = ActiveSpeakerDetector::new();
d.add_peer(1, 0);
d.add_peer(2, 0);
let scores = d.peer_scores();
assert_eq!(scores.len(), 2);
for (_, imm, med, lng) in scores {
assert!((imm - 1.0e-10).abs() < 1e-20, "imm={imm}");
assert!((med - 1.0e-10).abs() < 1e-20, "med={med}");
assert!((lng - 1.0e-10).abs() < 1e-20, "lng={lng}");
}
}
#[test]
fn tick_after_removing_all_peers() {
let mut d = ActiveSpeakerDetector::new();
d.add_peer(1, 0);
d.add_peer(2, 0);
feed(&mut d, 1, 5, 0, 1000);
d.tick(1050);
d.remove_peer(&1);
d.remove_peer(&2);
let c = d.tick(1350);
assert_eq!(c, None);
assert_eq!(d.current_dominant(), None);
}
#[test]
fn dominance_clears_on_remove_then_readd() {
let mut d = ActiveSpeakerDetector::new();
d.add_peer(1, 0);
d.add_peer(2, 0);
feed(&mut d, 1, 5, 0, 2000);
feed(&mut d, 2, 127, 0, 2000);
let c = d.tick(2050);
assert_eq!(c.map(|c| c.peer_id), Some(1));
assert_eq!(d.current_dominant(), Some(&1));
d.remove_peer(&1);
assert_eq!(d.current_dominant(), None, "dominance must clear on remove");
let t1: u64 = 2100;
d.add_peer(1, t1);
assert_eq!(
d.current_dominant(),
None,
"re-add should NOT restore lost dominance"
);
}
#[test]
fn c2_margin_always_finite_and_nonnegative() {
let mut d = ActiveSpeakerDetector::new();
d.add_peer(1, 0);
d.add_peer(2, 0);
feed(&mut d, 1, 5, 0, 2000);
feed(&mut d, 2, 127, 0, 2000);
let c = d.tick(2050).unwrap();
assert!(
c.c2_margin.is_finite(),
"c2_margin not finite: {}",
c.c2_margin
);
assert!(c.c2_margin >= 0.0, "c2_margin negative: {}", c.c2_margin);
assert_eq!(c.c2_margin, 0.0);
}
#[test]
fn stress_10_peers_1000_ticks() {
let mut d = ActiveSpeakerDetector::new();
for id in 0..10u64 {
d.add_peer(id, 0);
}
let mut state: u64 = 0xDEAD_BEEF;
let mut next_u8 = || {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(state >> 56) as u8
};
let mut tick_t: u64 = 2000;
for tick_i in 0..1000 {
for _ in 0..10 {
for id in 0..10u64 {
let lvl = next_u8() & 0x7F; d.record_level(id, lvl, tick_t);
}
tick_t += 20;
}
let _change = d.tick(tick_t);
tick_t += 300;
if let Some(dom) = d.current_dominant() {
assert!(*dom < 10, "tick {tick_i}: invalid dominant id {}", *dom);
}
for (id, imm, med, lng) in d.peer_scores() {
assert!(
imm.is_finite() && med.is_finite() && lng.is_finite(),
"tick {tick_i} peer {id}: non-finite score ({imm},{med},{lng})"
);
assert!(imm >= 1.0e-10, "imm score below floor: {imm}");
assert!(med >= 1.0e-10, "med score below floor: {med}");
assert!(lng >= 1.0e-10, "lng score below floor: {lng}");
}
}
}
#[test]
fn constant_loud_signal_produces_zero_activity_score() {
let mut d = ActiveSpeakerDetector::new();
d.add_peer(1, 0);
feed(&mut d, 1, 0, 0, 2000); d.tick(2050);
d.tick(2350); let scores = d.peer_scores();
let (_, imm, med, lng) = scores[0];
assert_eq!(
imm, 1.0e-10,
"constant-loud peer has immediate score at floor (min_level latch). got {imm}"
);
assert_eq!(
med, 1.0e-10,
"constant-loud peer has medium score at floor. got {med}"
);
assert_eq!(
lng, 1.0e-10,
"constant-loud peer has long score at floor. got {lng}"
);
}
#[test]
fn current_dominant_always_valid_after_removes() {
let mut d = ActiveSpeakerDetector::new();
for id in 1..=5u64 {
d.add_peer(id, 0);
}
for id in 1..=5u64 {
let lvl = if id == 3 { 5 } else { 127 };
feed(&mut d, id, lvl, 0, 2000);
}
let c = d.tick(2050);
assert_eq!(c.map(|c| c.peer_id), Some(3));
d.remove_peer(&1);
assert_eq!(d.current_dominant(), Some(&3));
d.remove_peer(&3);
assert_eq!(d.current_dominant(), None);
}
#[test]
fn max_volume_produces_finite_scores() {
let mut d = ActiveSpeakerDetector::new();
d.add_peer(1, 0);
d.add_peer(2, 0);
feed(&mut d, 1, 0, 0, 3000); feed(&mut d, 2, 127, 0, 3000); let _ = d.tick(3050);
for (id, imm, med, lng) in d.peer_scores() {
assert!(
imm.is_finite() && med.is_finite() && lng.is_finite(),
"peer {id} non-finite score ({imm},{med},{lng})"
);
}
}
#[test]
fn record_level_above_127_is_clamped() {
let mut d = ActiveSpeakerDetector::new();
d.add_peer(1, 0);
d.add_peer(2, 0);
feed(&mut d, 1, 5, 0, 2000);
let mut t: u64 = 0;
for _ in 0..100 {
d.record_level(2, 255, t);
t += 20;
}
let c = d.tick(2050);
assert_eq!(
c.map(|c| c.peer_id),
Some(1),
"clamped-silent peer 2 should not win over loud peer 1"
);
}
#[test]
fn top_k_1_matches_current_dominant() {
let mut d = ActiveSpeakerDetector::new();
for id in 1..=4u64 {
d.add_peer(id, 0);
feed(&mut d, id, 20, 0, 2000);
}
let c = d.tick(2050);
let dom = c.map(|c| c.peer_id).expect("some peer must win");
let top = d.current_top_k(1);
assert_eq!(top.len(), 1);
assert_eq!(
top[0], dom,
"current_top_k(1) ({}) must match current_dominant ({})",
top[0], dom
);
}