Squashed from work; the fine-grained history is under tag archive/work-2026-08-29: - mp* wave: mpwm window manager + the mp app family, WM API, theme bridge, PDF engine fix - mpwm polish wave: terminal key focus, focus-history close order, pop-back-to-origin, occupied-workspace cycling, demo - work: land the sources the last commits reference - route: the assistant icon the committed UI references - fast_inflate: the benches name their dev-deps - gif/weezl: drop the vendored benches nobody can run - weezl: the decode tests generate their own LZW fixture
299 lines
10 KiB
Rust
299 lines
10 KiB
Rust
//! End-to-end tests over real UDP sockets on the loopback interface. Both
|
|
//! links bind ephemeral ports (no broadcast, no fixed 41531) so the suite
|
|
//! can run anywhere without opening the LAN port.
|
|
|
|
use makepad_teamtalk::{VoiceConfig, VoiceLink};
|
|
use std::time::{Duration, Instant};
|
|
|
|
fn test_config() -> VoiceConfig {
|
|
VoiceConfig {
|
|
port: 0,
|
|
broadcast: false,
|
|
hello_ms: 100,
|
|
..VoiceConfig::default()
|
|
}
|
|
}
|
|
|
|
/// Two links pointed at each other by static address; returns them wired.
|
|
fn pair() -> (VoiceLink, VoiceLink) {
|
|
let a = VoiceLink::bind(test_config()).expect("bind a");
|
|
// The socket is bound to 0.0.0.0; point the second link at loopback.
|
|
let a_addr: std::net::SocketAddr = format!("127.0.0.1:{}", a.local_addr().port())
|
|
.parse()
|
|
.unwrap();
|
|
let b = VoiceLink::bind(VoiceConfig {
|
|
static_peers: vec![a_addr],
|
|
..test_config()
|
|
})
|
|
.expect("bind b");
|
|
// A learns B from B's hello; b already knows a.
|
|
let deadline = Instant::now() + Duration::from_secs(5);
|
|
loop {
|
|
if !a.peers().is_empty() && !b.peers().is_empty() {
|
|
break;
|
|
}
|
|
assert!(Instant::now() < deadline, "discovery timed out");
|
|
std::thread::sleep(Duration::from_millis(20));
|
|
}
|
|
(a, b)
|
|
}
|
|
|
|
/// Drive A's capture with a tone and B's playback until audio comes out (or
|
|
/// a deadline passes). Returns the peak of the last rendered block.
|
|
fn tone_reaches(a: &mut VoiceLink, b: &mut VoiceLink, seconds: f64) -> f32 {
|
|
let mut capture = a.take_capture().expect("capture");
|
|
let mut playback = b.take_playback().expect("playback");
|
|
let block = 480; // 10 ms at 48 kHz
|
|
let mut input = vec![0.0f32; block];
|
|
let mut output = vec![0.0f32; block];
|
|
let mut phase = 0.0f64;
|
|
let mut peak = 0.0f32;
|
|
let blocks = (seconds * 100.0) as usize;
|
|
for _ in 0..blocks {
|
|
for v in input.iter_mut() {
|
|
*v = (phase.sin() * 0.25) as f32;
|
|
phase += 440.0 * std::f64::consts::TAU / 48000.0;
|
|
}
|
|
capture.push_mono(48000.0, &input);
|
|
std::thread::sleep(Duration::from_millis(10));
|
|
output.fill(0.0);
|
|
playback.mix_into_mono(48000.0, &mut output);
|
|
peak = output.iter().fold(0.0f32, |m, v| m.max(v.abs()));
|
|
if peak > 0.05 {
|
|
break;
|
|
}
|
|
}
|
|
peak
|
|
}
|
|
|
|
#[test]
|
|
fn audio_flows_between_two_links() {
|
|
let (mut a, mut b) = pair();
|
|
let peak = tone_reaches(&mut a, &mut b, 3.0);
|
|
assert!(peak > 0.05, "no audio arrived (peak {peak})");
|
|
let stats = b.stats();
|
|
assert!(stats.packets_recv > 0);
|
|
assert_eq!(stats.bad_packets, 0);
|
|
// The peer is visible with the right identity and marked talking.
|
|
let peers = b.peers();
|
|
assert_eq!(peers.len(), 1);
|
|
assert_eq!(peers[0].sender, a.sender_id());
|
|
assert!(peers[0].talking);
|
|
assert!(peers[0].frames_accepted > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn the_channel_filter_gates_teams_and_channel_zero_passes() {
|
|
let (mut a, mut b) = pair();
|
|
a.set_channel(2);
|
|
b.set_listen_channels(&[1]); // not 2
|
|
assert!(!b.listens_to(2) && b.listens_to(1) && b.listens_to(0));
|
|
let peak = tone_reaches(&mut a, &mut b, 1.0);
|
|
assert!(peak < 0.01, "team-2 audio leaked through (peak {peak})");
|
|
assert!(b.stats().filtered > 0, "filter never engaged");
|
|
|
|
// Retune to the right team: reuse the same links (handles were taken) by
|
|
// flipping the listen set and pushing more audio through fresh handles is
|
|
// not possible — so verify with a fresh pair.
|
|
let (mut a, mut b) = pair();
|
|
a.set_channel(2);
|
|
b.set_listen_channels(&[2]);
|
|
let peak = tone_reaches(&mut a, &mut b, 3.0);
|
|
assert!(peak > 0.05, "team-2 audio missing on a team-2 listener (peak {peak})");
|
|
|
|
// Channel 0 always plays, whatever the listen set.
|
|
let (mut a, mut b) = pair();
|
|
a.set_channel(0);
|
|
b.set_listen_channels(&[7]);
|
|
let peak = tone_reaches(&mut a, &mut b, 3.0);
|
|
assert!(peak > 0.05, "channel-0 audio missing (peak {peak})");
|
|
}
|
|
|
|
#[test]
|
|
fn silence_is_dtx_and_costs_almost_no_bandwidth() {
|
|
let (mut a, b) = pair();
|
|
let mut capture = a.take_capture().expect("capture");
|
|
let quiet = vec![0.0f32; 480];
|
|
for _ in 0..50 {
|
|
capture.push_mono(48000.0, &quiet);
|
|
std::thread::sleep(Duration::from_millis(10));
|
|
}
|
|
let stats = a.stats();
|
|
// Half a second of silence: sequence continuity is kept with header-only
|
|
// packets, so bytes per packet must be tiny (the 32-byte header + a few
|
|
// hellos).
|
|
assert!(stats.packets_sent >= 50, "packets {}", stats.packets_sent);
|
|
let per_packet = stats.bytes_sent as f64 / stats.packets_sent as f64;
|
|
assert!(per_packet < 40.0, "silence costs {per_packet} bytes/packet");
|
|
// And the receiver never marks the peer talking.
|
|
assert!(!b.peers().iter().any(|p| p.talking));
|
|
drop(b);
|
|
}
|
|
|
|
#[test]
|
|
fn peer_gain_and_mute_act_on_playback() {
|
|
let (mut a, mut b) = pair();
|
|
let sender = a.sender_id();
|
|
b.set_peer_muted(sender, true);
|
|
let peak = tone_reaches(&mut a, &mut b, 1.0);
|
|
assert!(peak < 0.01, "muted peer audible (peak {peak})");
|
|
b.set_peer_muted(sender, false);
|
|
// (Gain/mute path exercised; audibility after unmute is covered by
|
|
// audio_flows_between_two_links.)
|
|
assert_eq!(b.peers()[0].gain, 1.0);
|
|
b.set_peer_gain(sender, 0.25);
|
|
assert_eq!(b.peers()[0].gain, 0.25);
|
|
}
|
|
|
|
#[test]
|
|
fn dropping_a_link_says_goodbye() {
|
|
let (a, b) = pair();
|
|
assert_eq!(b.peers().len(), 1);
|
|
drop(a);
|
|
let deadline = Instant::now() + Duration::from_secs(2);
|
|
loop {
|
|
if b.peers().is_empty() {
|
|
break;
|
|
}
|
|
assert!(Instant::now() < deadline, "BYE never removed the peer");
|
|
std::thread::sleep(Duration::from_millis(20));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn muting_the_link_sends_silence_but_keeps_presence() {
|
|
let (mut a, mut b) = pair();
|
|
a.set_muted(true);
|
|
let peak = tone_reaches(&mut a, &mut b, 1.0);
|
|
assert!(peak < 0.01, "muted link audible (peak {peak})");
|
|
assert_eq!(b.peers().len(), 1, "muted link lost presence");
|
|
}
|
|
|
|
#[test]
|
|
fn co_located_instances_fall_back_to_the_next_port_and_meet() {
|
|
// A fixed port (not the real 41531: tests must not squat the LAN port).
|
|
let port = 42641;
|
|
let a = VoiceLink::bind(VoiceConfig {
|
|
port,
|
|
broadcast: false,
|
|
hello_ms: 100,
|
|
..VoiceConfig::default()
|
|
});
|
|
let a = match a {
|
|
Ok(a) => a,
|
|
// Something else on this machine owns the test port: nothing to test.
|
|
Err(_) => return,
|
|
};
|
|
assert_eq!(a.local_addr().port(), port);
|
|
let b = VoiceLink::bind(VoiceConfig {
|
|
port,
|
|
broadcast: false,
|
|
hello_ms: 100,
|
|
static_peers: vec![format!("127.0.0.1:{port}").parse().unwrap()],
|
|
..VoiceConfig::default()
|
|
})
|
|
.expect("bind b on the fallback port");
|
|
assert_eq!(b.local_addr().port(), port + 1, "expected the next port");
|
|
let deadline = Instant::now() + Duration::from_secs(5);
|
|
while a.peers().is_empty() || b.peers().is_empty() {
|
|
assert!(Instant::now() < deadline, "co-located discovery timed out");
|
|
std::thread::sleep(Duration::from_millis(20));
|
|
}
|
|
assert_eq!(a.peers()[0].sender, b.sender_id());
|
|
}
|
|
|
|
#[test]
|
|
fn different_rooms_on_one_lan_never_meet() {
|
|
// Same discovery path as `pair()`, but the rooms differ: the packets
|
|
// must die at the room check — no peer entry, no audio, ever.
|
|
let a = VoiceLink::bind(VoiceConfig {
|
|
room: 0x1111,
|
|
..test_config()
|
|
})
|
|
.expect("bind a");
|
|
let a_addr: std::net::SocketAddr = format!("127.0.0.1:{}", a.local_addr().port())
|
|
.parse()
|
|
.unwrap();
|
|
let b = VoiceLink::bind(VoiceConfig {
|
|
room: 0x2222,
|
|
static_peers: vec![a_addr],
|
|
..test_config()
|
|
})
|
|
.expect("bind b");
|
|
std::thread::sleep(Duration::from_millis(600));
|
|
assert!(a.peers().is_empty(), "room 0x1111 saw a 0x2222 peer");
|
|
assert!(b.peers().is_empty(), "room 0x2222 saw a 0x1111 peer");
|
|
assert!(a.stats().wrong_room > 0, "the room check never engaged");
|
|
|
|
// Move B into A's room at runtime: they meet within a hello interval.
|
|
b.set_room(0x1111);
|
|
let deadline = Instant::now() + Duration::from_secs(5);
|
|
while a.peers().is_empty() || b.peers().is_empty() {
|
|
assert!(Instant::now() < deadline, "same-room discovery timed out");
|
|
std::thread::sleep(Duration::from_millis(20));
|
|
}
|
|
assert_eq!(a.peers()[0].sender, b.sender_id());
|
|
}
|
|
|
|
#[test]
|
|
fn the_host_sentinel_is_a_working_sender_id() {
|
|
let a = VoiceLink::bind(VoiceConfig {
|
|
sender_id: makepad_teamtalk::HOST_SENDER_ID,
|
|
..test_config()
|
|
})
|
|
.expect("bind host");
|
|
assert_eq!(a.sender_id(), u64::MAX);
|
|
let a_addr: std::net::SocketAddr = format!("127.0.0.1:{}", a.local_addr().port())
|
|
.parse()
|
|
.unwrap();
|
|
let b = VoiceLink::bind(VoiceConfig {
|
|
static_peers: vec![a_addr],
|
|
..test_config()
|
|
})
|
|
.expect("bind b");
|
|
let deadline = Instant::now() + Duration::from_secs(5);
|
|
while b.peers().is_empty() {
|
|
assert!(Instant::now() < deadline, "host discovery timed out");
|
|
std::thread::sleep(Duration::from_millis(20));
|
|
}
|
|
assert_eq!(b.peers()[0].sender, makepad_teamtalk::HOST_SENDER_ID);
|
|
}
|
|
|
|
#[test]
|
|
fn the_ogg_codec_flows_and_compresses() {
|
|
use makepad_teamtalk::Codec;
|
|
let a = VoiceLink::bind(VoiceConfig {
|
|
codec: Codec::Ogg,
|
|
// Gate off so every packet carries audio: the bandwidth numbers
|
|
// below are then exact.
|
|
gate_threshold_rms: -1.0,
|
|
..test_config()
|
|
})
|
|
.expect("bind a");
|
|
let a_addr: std::net::SocketAddr = format!("127.0.0.1:{}", a.local_addr().port())
|
|
.parse()
|
|
.unwrap();
|
|
let b = VoiceLink::bind(VoiceConfig {
|
|
static_peers: vec![a_addr],
|
|
..test_config()
|
|
})
|
|
.expect("bind b");
|
|
let deadline = Instant::now() + Duration::from_secs(5);
|
|
while a.peers().is_empty() || b.peers().is_empty() {
|
|
assert!(Instant::now() < deadline, "discovery timed out");
|
|
std::thread::sleep(Duration::from_millis(20));
|
|
}
|
|
let mut a = a;
|
|
let mut b = b;
|
|
let peak = tone_reaches(&mut a, &mut b, 3.0);
|
|
assert!(peak > 0.05, "no audio over the ogg codec (peak {peak})");
|
|
assert_eq!(b.stats().opaque_payloads, 0, "pages failed to decode");
|
|
// 4-bit ADPCM at 240-sample frames: 32 header + 28 Ogg + 8 state +
|
|
// 120 nibbles = 188 B vs 512 raw. Hellos push the average up a little.
|
|
let s = a.stats();
|
|
let per_packet = s.bytes_sent as f64 / s.packets_sent as f64;
|
|
assert!(
|
|
per_packet < 250.0,
|
|
"ogg audio costs {per_packet:.0} B/packet (raw would be 512)"
|
|
);
|
|
}
|