188 lines
5.7 KiB
Rust
188 lines
5.7 KiB
Rust
// Chaos contract for upstream media backpressure behavior.
|
|
//
|
|
// Scope: prove the freshness-first code path still models stalls, queue
|
|
// pressure, and stale media drops as first-class failure modes.
|
|
// Targets: `client/src/uplink_latency_harness.rs`,
|
|
// `client/src/uplink_fresh_queue.rs`, and `client/src/app/uplink_media/*`.
|
|
// Why: real WAN calls fail by forming backlog; chaos coverage keeps the code
|
|
// biased toward bounded stutter over minutes of delayed media.
|
|
|
|
const LATENCY_HARNESS: &str = include_str!(concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/client/src/uplink_latency_harness.rs"
|
|
));
|
|
const FRESH_QUEUE: &str = include_str!(concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/client/src/uplink_fresh_queue.rs"
|
|
));
|
|
const DROP_LOGGING: &str = include_str!(concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/client/src/app/uplink_media/drop_logging.rs"
|
|
));
|
|
|
|
#[path = "../../../../client/src/uplink_fresh_queue.rs"]
|
|
#[allow(warnings)]
|
|
mod uplink_fresh_queue;
|
|
|
|
mod input {
|
|
pub mod camera {
|
|
#[derive(Clone, Copy)]
|
|
pub enum CameraCodec {
|
|
Hevc,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub struct CameraConfig {
|
|
pub codec: CameraCodec,
|
|
}
|
|
}
|
|
}
|
|
|
|
use lesavka_common::lesavka::{AudioEncoding, AudioPacket, UpstreamMediaBundle, VideoPacket};
|
|
use std::time::Duration;
|
|
use uplink_fresh_queue::{FreshPacketQueue, FreshQueueConfig, FreshQueuePolicy};
|
|
|
|
include!("../../../../client/src/app/uplink_media/video_keyframes.rs");
|
|
|
|
fn bundle(seq: u64, capture_pts_us: u64, video_data: Vec<u8>) -> UpstreamMediaBundle {
|
|
UpstreamMediaBundle {
|
|
session_id: 13,
|
|
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: video_data,
|
|
client_capture_pts_us: capture_pts_us,
|
|
..Default::default()
|
|
}),
|
|
audio: vec![AudioPacket {
|
|
seq,
|
|
pts: capture_pts_us,
|
|
client_capture_pts_us: capture_pts_us,
|
|
..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,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn upstream_backpressure_model_prefers_bounded_drops_over_latency_debt() {
|
|
for marker in [
|
|
"freshness_max_age",
|
|
"max_queue_depth",
|
|
"preserve_backlog_camera_policy_accumulates_stale_video_after_a_stall",
|
|
"drop_oldest_policy_keeps_media_live_by_sacrificing_old_packets",
|
|
] {
|
|
assert!(
|
|
LATENCY_HARNESS.contains(marker),
|
|
"latency harness should preserve chaos marker {marker}"
|
|
);
|
|
}
|
|
|
|
for marker in [
|
|
"FreshQueuePolicy::LatestOnly",
|
|
"dropped_queue_full",
|
|
"dropped_stale",
|
|
"pop_fresh_discards_stale_packets_before_returning_live_media",
|
|
] {
|
|
assert!(
|
|
FRESH_QUEUE.contains(marker),
|
|
"fresh queue should preserve chaos marker {marker}"
|
|
);
|
|
}
|
|
|
|
assert!(
|
|
DROP_LOGGING
|
|
.contains("queue is dropping stale/superseded packets to preserve live A/V sync"),
|
|
"operator logs should explain freshness-first drops during chaos conditions"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn jitter_burst_latest_only_policy_outputs_the_newest_complete_bundle() {
|
|
let queue = FreshPacketQueue::new(FreshQueueConfig {
|
|
capacity: 8,
|
|
max_age: Duration::from_millis(150),
|
|
policy: FreshQueuePolicy::LatestOnly,
|
|
});
|
|
|
|
let _ = queue.push(
|
|
bundle(1, 1_000_000, vec![0, 0, 0, 1, 0x26, 0x01]),
|
|
Duration::from_millis(120),
|
|
);
|
|
let _ = queue.push(
|
|
bundle(2, 1_033_333, vec![0, 0, 0, 1, 0x02, 0x01]),
|
|
Duration::from_millis(80),
|
|
);
|
|
let _ = queue.push(
|
|
bundle(3, 1_066_666, vec![0, 0, 0, 1, 0x26, 0x01]),
|
|
Duration::from_millis(20),
|
|
);
|
|
|
|
let popped = queue.pop_fresh().await;
|
|
let packet = popped.packet.expect("fresh bundle after jitter burst");
|
|
|
|
assert_eq!(popped.dropped_stale, 2);
|
|
assert_eq!(packet.seq, 3);
|
|
assert!(packet.video.is_some());
|
|
assert_eq!(packet.audio.len(), 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn reordered_stale_arrival_is_dropped_as_a_whole_bundle() {
|
|
let queue = FreshPacketQueue::new(FreshQueueConfig {
|
|
capacity: 4,
|
|
max_age: Duration::from_millis(150),
|
|
policy: FreshQueuePolicy::DrainOldest,
|
|
});
|
|
|
|
let _ = queue.push(
|
|
bundle(10, 1_300_000, vec![0, 0, 0, 1, 0x26, 0x01]),
|
|
Duration::from_millis(220),
|
|
);
|
|
let _ = queue.push(
|
|
bundle(8, 1_233_334, vec![0, 0, 0, 1, 0x26, 0x01]),
|
|
Duration::from_millis(15),
|
|
);
|
|
|
|
let popped = queue.pop_fresh().await;
|
|
let packet = popped.packet.expect("fresh reordered bundle");
|
|
|
|
assert_eq!(popped.dropped_stale, 1);
|
|
assert_eq!(packet.seq, 8);
|
|
assert!(packet.video.is_some());
|
|
assert_eq!(packet.audio.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_keyframe_after_capture_gap_holds_predictive_video_until_irap() {
|
|
let mut waiting_for_keyframe = false;
|
|
assert!(upstream_camera_uses_hevc(Some(
|
|
input::camera::CameraConfig {
|
|
codec: input::camera::CameraCodec::Hevc,
|
|
}
|
|
)));
|
|
note_hevc_capture_gap(true, &mut waiting_for_keyframe);
|
|
|
|
let delta = bundle(20, 2_000_000, vec![0, 0, 0, 1, 0x02, 0x01]);
|
|
let recovery = bundle(21, 2_033_333, vec![0, 0, 0, 1, 0x26, 0x01]);
|
|
|
|
assert!(waiting_for_keyframe);
|
|
assert!(should_hold_hevc_bundle_for_keyframe_recovery(
|
|
waiting_for_keyframe,
|
|
&delta
|
|
));
|
|
assert!(!should_hold_hevc_bundle_for_keyframe_recovery(
|
|
waiting_for_keyframe,
|
|
&recovery
|
|
));
|
|
assert!(bundle_has_hevc_recovery_keyframe(&recovery));
|
|
}
|