makepad/libs/teamtalk/tests/loopback.rs
Admin 4ddba62d4b engine libs: csg document, stitch simd, sim providers, teamtalk LAN voice, gltf writer, render, route and maps, bench hygiene
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
2026-08-29 09:26:27 +02:00

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)"
);
}