116 lines
3.9 KiB
Rust
116 lines
3.9 KiB
Rust
#![allow(dead_code)]
|
|
|
|
// Unit coverage for client audio recovery knobs.
|
|
//
|
|
// Scope: include the client recovery parser helpers without starting GTK,
|
|
// GStreamer, or any relay process.
|
|
// Targets: `client/src/app/audio_recovery_config.rs` and
|
|
// `client/src/app/uplink_media/webcam_media_loop.rs`.
|
|
// Why: the real-world Google Meet fix depends on one safe startup UAC epoch
|
|
// retirement and bounded recovery retries, not on manual operator folklore.
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
use lesavka_common::lesavka::{KeyboardReport, MouseReport};
|
|
use serial_test::serial;
|
|
use temp_env::{with_var, with_var_unset, with_vars};
|
|
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
|
|
use tracing::warn;
|
|
|
|
include!("../../../../client/src/app/audio_recovery_config.rs");
|
|
|
|
const WEBCAM_MEDIA_LOOP_SRC: &str = include_str!(concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/client/src/app/uplink_media/webcam_media_loop.rs"
|
|
));
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn remote_audio_usb_recovery_is_opt_in_and_cooldown_bounded() {
|
|
with_var_unset("LESAVKA_AUDIO_AUTO_RECOVER_USB", || {
|
|
assert!(
|
|
!audio_usb_auto_recover_enabled(),
|
|
"USB-level audio recovery must stay opt-in because hard gadget recovery can disturb a live call"
|
|
);
|
|
});
|
|
|
|
with_vars(
|
|
[
|
|
("LESAVKA_AUDIO_AUTO_RECOVER_USB", Some("1")),
|
|
("LESAVKA_AUDIO_AUTO_RECOVER_AFTER", Some("0")),
|
|
("LESAVKA_AUDIO_AUTO_RECOVER_COOLDOWN_MS", Some("2500")),
|
|
],
|
|
|| {
|
|
assert!(audio_usb_auto_recover_enabled());
|
|
assert_eq!(
|
|
audio_usb_recover_after(),
|
|
3,
|
|
"invalid thresholds should fall back to the safe default"
|
|
);
|
|
assert_eq!(audio_usb_recover_cooldown(), Duration::from_millis(2500));
|
|
},
|
|
);
|
|
|
|
with_var(
|
|
"LESAVKA_AUDIO_AUTO_RECOVER_COOLDOWN_MS",
|
|
Some("not-a-number"),
|
|
|| {
|
|
assert_eq!(
|
|
audio_usb_recover_cooldown(),
|
|
Duration::from_secs(60),
|
|
"malformed cooldowns should not create a tight recovery loop"
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn remote_audio_error_classifier_targets_source_epoch_failures_only() {
|
|
for message in [
|
|
"remote speaker capture produced no audio samples after 3000 ms on hw:UAC2Gadget,0",
|
|
"remote speaker capture stalled for 3000 ms on hw:UAC2Gadget,0",
|
|
"remote speaker capture cadence is too low on hw:UAC2Gadget,0",
|
|
] {
|
|
assert!(
|
|
is_recoverable_remote_audio_error(message),
|
|
"expected recoverable remote audio source error: {message}"
|
|
);
|
|
}
|
|
|
|
for message in [
|
|
"TLS handshake failed",
|
|
"recover_uac returned permission denied",
|
|
"video decoder stalled",
|
|
"microphone capture device missing locally",
|
|
] {
|
|
assert!(
|
|
!is_recoverable_remote_audio_error(message),
|
|
"unrelated failures must not trigger USB/audio recovery: {message}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn startup_epoch_auto_heal_is_one_shot_uac_recovery_after_three_seconds() {
|
|
for marker in [
|
|
"const DEFAULT_UPSTREAM_AUTO_HEAL_AFTER_MS: u64 = 3_000;",
|
|
"let mut startup_epoch_heal_delay = upstream_epoch_auto_heal_delay();",
|
|
"startup_epoch_heal_delay.take()",
|
|
"spawn_upstream_epoch_auto_heal(ep.clone(), heal_delay);",
|
|
"client.recover_uac(Request::new(Empty {})).await",
|
|
"automatic upstream A/V epoch heal requested",
|
|
] {
|
|
assert!(
|
|
WEBCAM_MEDIA_LOOP_SRC.contains(marker),
|
|
"startup epoch auto-heal should preserve marker {marker}"
|
|
);
|
|
}
|
|
|
|
for forbidden in ["reset_usb(Request::new(Empty {})).await", "recover_uvc("] {
|
|
assert!(
|
|
!WEBCAM_MEDIA_LOOP_SRC.contains(forbidden),
|
|
"startup epoch auto-heal must not affect USB or video recovery path: {forbidden}"
|
|
);
|
|
}
|
|
}
|