138 lines
5.0 KiB
Rust
138 lines
5.0 KiB
Rust
// Unit coverage for client upstream bundled media queue behavior.
|
|
//
|
|
// Scope: exercise the freshness queue with complete `UpstreamMediaBundle`
|
|
// values rather than split audio/video packets.
|
|
// Targets: `client/src/uplink_fresh_queue.rs`.
|
|
// Why: the upstream sync win depends on dropping stale media as complete A/V
|
|
// bundles so the server never receives an orphaned tone or flash.
|
|
|
|
#[path = "../../../../client/src/uplink_fresh_queue.rs"]
|
|
#[allow(warnings)]
|
|
mod uplink_fresh_queue;
|
|
|
|
use lesavka_common::lesavka::{AudioEncoding, AudioPacket, UpstreamMediaBundle, VideoPacket};
|
|
use std::time::Duration;
|
|
use uplink_fresh_queue::{FreshPacketQueue, FreshQueueConfig, FreshQueuePolicy};
|
|
|
|
fn bundle(seq: u64, capture_pts_us: u64) -> UpstreamMediaBundle {
|
|
UpstreamMediaBundle {
|
|
session_id: 7,
|
|
seq,
|
|
capture_start_us: capture_pts_us.saturating_sub(20_000),
|
|
capture_end_us: capture_pts_us.saturating_add(20_000),
|
|
video: Some(VideoPacket {
|
|
seq,
|
|
pts: capture_pts_us,
|
|
data: vec![0, 0, 0, 1, 0x26, 0x01, seq as u8],
|
|
client_capture_pts_us: capture_pts_us,
|
|
client_send_pts_us: capture_pts_us.saturating_add(5_000),
|
|
client_queue_age_ms: 5,
|
|
..Default::default()
|
|
}),
|
|
audio: vec![
|
|
AudioPacket {
|
|
seq: seq.saturating_mul(2),
|
|
pts: capture_pts_us.saturating_sub(20_000),
|
|
data: vec![0x11; 1_920],
|
|
client_capture_pts_us: capture_pts_us.saturating_sub(20_000),
|
|
client_send_pts_us: capture_pts_us.saturating_add(5_000),
|
|
client_queue_age_ms: 25,
|
|
..Default::default()
|
|
},
|
|
AudioPacket {
|
|
seq: seq.saturating_mul(2).saturating_add(1),
|
|
pts: capture_pts_us.saturating_add(20_000),
|
|
data: vec![0x22; 1_920],
|
|
client_capture_pts_us: capture_pts_us.saturating_add(20_000),
|
|
client_send_pts_us: capture_pts_us.saturating_add(25_000),
|
|
client_queue_age_ms: 5,
|
|
..Default::default()
|
|
},
|
|
],
|
|
audio_sample_rate: 48_000,
|
|
audio_channels: 2,
|
|
audio_encoding: AudioEncoding::PcmS16le as i32,
|
|
video_width: 1280,
|
|
video_height: 720,
|
|
video_fps: 30,
|
|
}
|
|
}
|
|
|
|
fn assert_complete_bundle(bundle: &UpstreamMediaBundle, expected_seq: u64) {
|
|
assert_eq!(bundle.seq, expected_seq);
|
|
assert!(
|
|
bundle.video.is_some(),
|
|
"freshness queue must not split video out of its bundle"
|
|
);
|
|
assert_eq!(
|
|
bundle.audio.len(),
|
|
2,
|
|
"freshness queue must not split audio out of its bundle"
|
|
);
|
|
let video = bundle.video.as_ref().expect("video packet");
|
|
assert!(
|
|
bundle.capture_start_us <= video.client_capture_pts_us
|
|
&& bundle.capture_end_us >= video.client_capture_pts_us
|
|
);
|
|
assert!(bundle.audio.iter().all(|audio| {
|
|
bundle.capture_start_us <= audio.client_capture_pts_us
|
|
&& bundle.capture_end_us >= audio.client_capture_pts_us
|
|
}));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn latest_only_policy_drops_superseded_bundles_as_complete_av_units() {
|
|
let queue = FreshPacketQueue::new(FreshQueueConfig {
|
|
capacity: 8,
|
|
max_age: Duration::from_secs(1),
|
|
policy: FreshQueuePolicy::LatestOnly,
|
|
});
|
|
|
|
for seq in 1..=3 {
|
|
let _ = queue.push(
|
|
bundle(seq, 1_000_000 + seq * 33_333),
|
|
Duration::from_millis(10),
|
|
);
|
|
}
|
|
|
|
let popped = queue.pop_fresh().await;
|
|
assert_eq!(popped.dropped_stale, 2);
|
|
assert_eq!(popped.queue_depth, 0);
|
|
assert_complete_bundle(&popped.packet.expect("newest bundle"), 3);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn stale_bundle_drop_never_delivers_audio_or_video_halves() {
|
|
let queue = FreshPacketQueue::new(FreshQueueConfig {
|
|
capacity: 4,
|
|
max_age: Duration::from_millis(200),
|
|
policy: FreshQueuePolicy::DrainOldest,
|
|
});
|
|
|
|
let _ = queue.push(bundle(1, 1_000_000), Duration::from_millis(250));
|
|
let _ = queue.push(bundle(2, 1_033_333), Duration::from_millis(25));
|
|
|
|
let popped = queue.pop_fresh().await;
|
|
assert_eq!(popped.dropped_stale, 1);
|
|
assert_complete_bundle(&popped.packet.expect("fresh bundle"), 2);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn capacity_pressure_drops_oldest_bundles_instead_of_backlogging_media() {
|
|
let queue = FreshPacketQueue::new(FreshQueueConfig {
|
|
capacity: 2,
|
|
max_age: Duration::from_secs(1),
|
|
policy: FreshQueuePolicy::DrainOldest,
|
|
});
|
|
|
|
let first = queue.push(bundle(1, 1_000_000), Duration::from_millis(5));
|
|
let second = queue.push(bundle(2, 1_033_333), Duration::from_millis(5));
|
|
let third = queue.push(bundle(3, 1_066_666), Duration::from_millis(5));
|
|
|
|
assert_eq!(first.dropped_queue_full, 0);
|
|
assert_eq!(second.queue_depth, 2);
|
|
assert_eq!(third.dropped_queue_full, 1);
|
|
assert_complete_bundle(&queue.pop_fresh().await.packet.expect("second bundle"), 2);
|
|
assert_complete_bundle(&queue.pop_fresh().await.packet.expect("third bundle"), 3);
|
|
}
|