Skip to content
File

Blob: firmware/vendor/str0m/tests/network-conditions.rs

rust396 lines
1//! Tests for realistic network simulation conditions.
2 
3use std::net::Ipv4Addr;
4use std::time::Duration;
5 
6use netem::{NetemConfig, Probability, RandomLoss};
7use str0m::format::Codec;
8use str0m::media::{Direction, MediaKind};
9use str0m::{Event, RtcError};
10 
11mod common;
12use common::{Peer, TestRtc, init_crypto_default, init_log, negotiate, progress};
13 
14/// Test connection with high latency (500ms+).
15#[test]
16fn network_high_latency_500ms() -> Result<(), RtcError> {
17 init_log();
18 init_crypto_default();
19 
20 let mut l = TestRtc::new(Peer::Left);
21 let mut r = TestRtc::new(Peer::Right);
22 
23 l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
24 r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());
25 
26 // Apply high latency to both directions
27 l.set_netem(NetemConfig::new().latency(Duration::from_millis(500)));
28 r.set_netem(NetemConfig::new().latency(Duration::from_millis(500)));
29 
30 let mid = negotiate(&mut l, &mut r, |change| {
31 change.add_media(MediaKind::Audio, Direction::SendRecv, None, None, None)
32 });
33 
34 // Connection might take longer with high latency
35 loop {
36 if l.is_connected() && r.is_connected() {
37 break;
38 }
39 if l.duration() > Duration::from_secs(30) {
40 panic!("Failed to connect with high latency");
41 }
42 progress(&mut l, &mut r)?;
43 }
44 
45 let max = l.last.max(r.last);
46 l.last = max;
47 r.last = max;
48 
49 // Send and receive audio
50 let params = l.params_opus();
51 assert_eq!(params.spec().codec, Codec::Opus);
52 let pt = params.pt();
53 let data = vec![1_u8; 80];
54 
55 let mut received_count = 0;
56 let send_until = l.duration() + Duration::from_secs(5);
57 
58 loop {
59 if l.duration() >= send_until {
60 break;
61 }
62 
63 let wallclock = l.start + l.duration();
64 let time = l.duration().into();
65 l.writer(mid)
66 .unwrap()
67 .write(pt, wallclock, time, data.clone())?;
68 progress(&mut l, &mut r)?;
69 
70 received_count = r
71 .events
72 .iter()
73 .filter(|(_, e)| matches!(e, Event::MediaData(_)))
74 .count();
75 }
76 
77 // Should still receive data despite high latency
78 assert!(
79 received_count > 10,
80 "Should receive data with high latency, got {}",
81 received_count
82 );
83 
84 Ok(())
85}
86 
87/// Test connection with packet loss.
88#[test]
89fn network_packet_loss() -> Result<(), RtcError> {
90 init_log();
91 init_crypto_default();
92 
93 let mut l = TestRtc::new(Peer::Left);
94 let mut r = TestRtc::new(Peer::Right);
95 
96 l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
97 r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());
98 
99 let mid = negotiate(&mut l, &mut r, |change| {
100 change.add_media(MediaKind::Audio, Direction::SendRecv, None, None, None)
101 });
102 
103 loop {
104 if l.is_connected() && r.is_connected() {
105 break;
106 }
107 if l.duration() > Duration::from_secs(10) {
108 panic!("Failed to connect");
109 }
110 progress(&mut l, &mut r)?;
111 }
112 
113 let max = l.last.max(r.last);
114 l.last = max;
115 r.last = max;
116 
117 // Apply 10% packet loss to receiver
118 r.set_netem(
119 NetemConfig::new()
120 .loss(RandomLoss::new(Probability::new(0.1)))
121 .seed(42),
122 );
123 
124 let params = l.params_opus();
125 let pt = params.pt();
126 let data = vec![1_u8; 80];
127 
128 let send_until = l.duration() + Duration::from_secs(5);
129 
130 loop {
131 if l.duration() >= send_until {
132 break;
133 }
134 
135 let wallclock = l.start + l.duration();
136 let time = l.duration().into();
137 l.writer(mid)
138 .unwrap()
139 .write(pt, wallclock, time, data.clone())?;
140 progress(&mut l, &mut r)?;
141 }
142 
143 let received_count = r
144 .events
145 .iter()
146 .filter(|(_, e)| matches!(e, Event::MediaData(_)))
147 .count();
148 
149 // Should still receive most data (90% with 10% loss)
150 assert!(
151 received_count > 50,
152 "Should receive most data with 10% loss, got {}",
153 received_count
154 );
155 
156 Ok(())
157}
158 
159/// Test recovery after network outage.
160#[test]
161fn network_recovery_from_outage() -> Result<(), RtcError> {
162 init_log();
163 init_crypto_default();
164 
165 let mut l = TestRtc::new(Peer::Left);
166 let mut r = TestRtc::new(Peer::Right);
167 
168 l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
169 r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());
170 
171 let mid = negotiate(&mut l, &mut r, |change| {
172 change.add_media(MediaKind::Audio, Direction::SendRecv, None, None, None)
173 });
174 
175 loop {
176 if l.is_connected() && r.is_connected() {
177 break;
178 }
179 progress(&mut l, &mut r)?;
180 }
181 
182 let max = l.last.max(r.last);
183 l.last = max;
184 r.last = max;
185 
186 let params = l.params_opus();
187 let pt = params.pt();
188 let data = vec![1_u8; 80];
189 
190 // Phase 1: Normal operation
191 let phase1_until = l.duration() + Duration::from_millis(500);
192 loop {
193 if l.duration() >= phase1_until {
194 break;
195 }
196 let wallclock = l.start + l.duration();
197 let time = l.duration().into();
198 l.writer(mid)
199 .unwrap()
200 .write(pt, wallclock, time, data.clone())?;
201 progress(&mut l, &mut r)?;
202 }
203 
204 // Phase 2: Simulated outage (100% loss)
205 r.set_netem(NetemConfig::new().loss(RandomLoss::new(Probability::ONE)));
206 let phase2_until = l.duration() + Duration::from_millis(500);
207 loop {
208 if l.duration() >= phase2_until {
209 break;
210 }
211 let wallclock = l.start + l.duration();
212 let time = l.duration().into();
213 l.writer(mid)
214 .unwrap()
215 .write(pt, wallclock, time, data.clone())?;
216 progress(&mut l, &mut r)?;
217 }
218 
219 // Phase 3: Recovery (restore network)
220 r.set_netem(NetemConfig::new());
221 let phase3_until = l.duration() + Duration::from_secs(2);
222 loop {
223 if l.duration() >= phase3_until {
224 break;
225 }
226 let wallclock = l.start + l.duration();
227 let time = l.duration().into();
228 l.writer(mid)
229 .unwrap()
230 .write(pt, wallclock, time, data.clone())?;
231 progress(&mut l, &mut r)?;
232 }
233 
234 // Should have received data in phases 1 and 3
235 let received_count = r
236 .events
237 .iter()
238 .filter(|(_, e)| matches!(e, Event::MediaData(_)))
239 .count();
240 
241 assert!(
242 received_count > 20,
243 "Should recover and receive data after outage, got {}",
244 received_count
245 );
246 
247 Ok(())
248}
249 
250/// Test asymmetric latency (different in each direction).
251#[test]
252fn network_asymmetric_latency() -> Result<(), RtcError> {
253 init_log();
254 init_crypto_default();
255 
256 let mut l = TestRtc::new(Peer::Left);
257 let mut r = TestRtc::new(Peer::Right);
258 
259 l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
260 r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());
261 
262 // Different latency in each direction
263 l.set_netem(NetemConfig::new().latency(Duration::from_millis(50))); // L -> R: 50ms
264 r.set_netem(NetemConfig::new().latency(Duration::from_millis(200))); // R -> L: 200ms
265 
266 let mid = negotiate(&mut l, &mut r, |change| {
267 change.add_media(MediaKind::Audio, Direction::SendRecv, None, None, None)
268 });
269 
270 loop {
271 if l.is_connected() && r.is_connected() {
272 break;
273 }
274 if l.duration() > Duration::from_secs(15) {
275 panic!("Failed to connect with asymmetric latency");
276 }
277 progress(&mut l, &mut r)?;
278 }
279 
280 let max = l.last.max(r.last);
281 l.last = max;
282 r.last = max;
283 
284 let params = l.params_opus();
285 let pt = params.pt();
286 let data = vec![1_u8; 80];
287 
288 let send_until = l.duration() + Duration::from_secs(3);
289 
290 loop {
291 if l.duration() >= send_until {
292 break;
293 }
294 
295 let wallclock = l.start + l.duration();
296 let time = l.duration().into();
297 l.writer(mid)
298 .unwrap()
299 .write(pt, wallclock, time, data.clone())?;
300 progress(&mut l, &mut r)?;
301 }
302 
303 let received_count = r
304 .events
305 .iter()
306 .filter(|(_, e)| matches!(e, Event::MediaData(_)))
307 .count();
308 
309 assert!(
310 received_count > 10,
311 "Should work with asymmetric latency, got {}",
312 received_count
313 );
314 
315 Ok(())
316}
317 
318/// Test moderate latency with some loss.
319#[test]
320fn network_latency_with_loss() -> Result<(), RtcError> {
321 init_log();
322 init_crypto_default();
323 
324 let mut l = TestRtc::new(Peer::Left);
325 let mut r = TestRtc::new(Peer::Right);
326 
327 l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
328 r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());
329 
330 // Moderate latency with some loss
331 l.set_netem(
332 NetemConfig::new()
333 .latency(Duration::from_millis(100))
334 .loss(RandomLoss::new(Probability::new(0.02)))
335 .seed(42),
336 );
337 r.set_netem(
338 NetemConfig::new()
339 .latency(Duration::from_millis(100))
340 .loss(RandomLoss::new(Probability::new(0.02)))
341 .seed(43),
342 );
343 
344 let mid = negotiate(&mut l, &mut r, |change| {
345 change.add_media(MediaKind::Audio, Direction::SendRecv, None, None, None)
346 });
347 
348 loop {
349 if l.is_connected() && r.is_connected() {
350 break;
351 }
352 if l.duration() > Duration::from_secs(15) {
353 panic!("Failed to connect");
354 }
355 progress(&mut l, &mut r)?;
356 }
357 
358 let max = l.last.max(r.last);
359 l.last = max;
360 r.last = max;
361 
362 let params = l.params_opus();
363 let pt = params.pt();
364 let data = vec![1_u8; 80];
365 
366 let send_until = l.duration() + Duration::from_secs(5);
367 
368 loop {
369 if l.duration() >= send_until {
370 break;
371 }
372 
373 let wallclock = l.start + l.duration();
374 let time = l.duration().into();
375 l.writer(mid)
376 .unwrap()
377 .write(pt, wallclock, time, data.clone())?;
378 progress(&mut l, &mut r)?;
379 }
380 
381 let received_count = r
382 .events
383 .iter()
384 .filter(|(_, e)| matches!(e, Event::MediaData(_)))
385 .count();
386 
387 // Should receive most data (98%+ with 2% loss)
388 assert!(
389 received_count > 80,
390 "Should receive most data with latency and loss, got {}",
391 received_count
392 );
393 
394 Ok(())
395}