lesavka/testing/tests/client_app_include_contract.rs

399 lines
12 KiB
Rust

//! Include-based coverage for client app startup reactor behavior.
//!
//! Scope: compile the client app reactor with deterministic local stubs for
//! capture/render dependencies, then exercise `new` + `run`.
//! Targets: `client/src/app.rs`, `client/src/app/downlink_media.rs`.
//! Why: app orchestration branches should stay stable in CI without physical
//! devices.
mod handshake {
#[allow(dead_code)]
#[derive(Default, Clone, Debug)]
pub struct PeerCaps {
pub camera: bool,
pub microphone: bool,
pub server_version: Option<String>,
}
pub async fn negotiate(_uri: &str) -> PeerCaps {
PeerCaps {
camera: std::env::var("LESAVKA_TEST_CAP_CAMERA").is_ok(),
microphone: std::env::var("LESAVKA_TEST_CAP_MIC").is_ok(),
server_version: None,
}
}
}
#[allow(warnings)]
mod live_capture_clock {
include!("support/live_capture_clock_shim.rs");
}
#[path = "../../client/src/uplink_fresh_queue.rs"]
#[allow(warnings)]
mod uplink_fresh_queue;
#[path = "../../client/src/uplink_telemetry.rs"]
#[allow(warnings)]
mod uplink_telemetry;
mod app_support {
use super::handshake::PeerCaps;
use std::time::Duration;
#[derive(Clone, Copy, Debug)]
pub enum CameraCodec {
H264,
}
#[derive(Clone, Copy, Debug)]
pub struct CameraConfig {
pub codec: CameraCodec,
pub width: u32,
pub height: u32,
pub fps: u32,
}
pub fn resolve_server_addr(_args: &[String], env_addr: Option<&str>) -> String {
env_addr.unwrap_or("http://127.0.0.1:9").to_string()
}
pub fn camera_config_from_caps(caps: &PeerCaps) -> Option<CameraConfig> {
if !caps.camera {
return None;
}
Some(CameraConfig {
codec: CameraCodec::H264,
width: 1280,
height: 720,
fps: 30,
})
}
pub fn sanitize_video_queue(queue: Option<usize>) -> usize {
queue.unwrap_or(64).max(1)
}
pub fn next_delay(delay: Duration) -> Duration {
std::cmp::min(delay.saturating_mul(2), Duration::from_secs(8))
}
}
mod input {
pub mod camera {
use crate::app_support::CameraConfig;
use lesavka_common::lesavka::VideoPacket;
pub struct CameraCapture;
impl CameraCapture {
pub fn new(_source: Option<&str>, _cfg: Option<CameraConfig>) -> anyhow::Result<Self> {
Ok(Self)
}
pub fn pull(&self) -> Option<VideoPacket> {
None
}
}
}
pub mod microphone {
use lesavka_common::lesavka::AudioPacket;
pub struct MicrophoneCapture;
impl MicrophoneCapture {
pub fn new() -> anyhow::Result<Self> {
Ok(Self)
}
pub fn pull(&self) -> Option<AudioPacket> {
None
}
}
}
pub mod inputs {
use lesavka_common::lesavka::{KeyboardReport, MouseReport};
use std::sync::{Arc, atomic::AtomicBool};
use tokio::sync::{broadcast::Sender, mpsc::UnboundedSender};
pub struct InputAggregator {
_kbd_tx: Sender<KeyboardReport>,
_mou_tx: Sender<MouseReport>,
_dev_mode: bool,
_paste_tx: Option<UnboundedSender<String>>,
remote_capture_enabled: Arc<AtomicBool>,
}
impl InputAggregator {
pub fn new(
dev_mode: bool,
kbd_tx: Sender<KeyboardReport>,
mou_tx: Sender<MouseReport>,
paste_tx: Option<UnboundedSender<String>>,
) -> Self {
Self {
_kbd_tx: kbd_tx,
_mou_tx: mou_tx,
_dev_mode: dev_mode,
_paste_tx: paste_tx,
remote_capture_enabled: Arc::new(AtomicBool::new(true)),
}
}
pub fn new_with_capture_mode(
dev_mode: bool,
kbd_tx: Sender<KeyboardReport>,
mou_tx: Sender<MouseReport>,
paste_tx: Option<UnboundedSender<String>>,
capture_remote_boot: bool,
) -> Self {
let aggregator = Self::new(dev_mode, kbd_tx, mou_tx, paste_tx);
aggregator
.remote_capture_enabled
.store(capture_remote_boot, std::sync::atomic::Ordering::Relaxed);
aggregator
}
pub fn init(&mut self) -> anyhow::Result<()> {
Ok(())
}
pub async fn run(&mut self) -> anyhow::Result<()> {
std::future::pending::<()>().await;
#[allow(unreachable_code)]
Ok(())
}
pub fn remote_capture_enabled_handle(&self) -> Arc<AtomicBool> {
Arc::clone(&self.remote_capture_enabled)
}
}
}
}
mod output {
pub mod audio {
use lesavka_common::lesavka::AudioPacket;
pub struct AudioOut;
impl AudioOut {
pub fn new() -> anyhow::Result<Self> {
Ok(Self)
}
pub fn push(&self, _pkt: AudioPacket) {}
}
}
pub mod video {
use lesavka_common::lesavka::VideoPacket;
pub struct MonitorWindow;
pub struct UnifiedMonitorWindow;
impl MonitorWindow {
pub fn new(_id: u32) -> anyhow::Result<Self> {
Ok(Self)
}
pub fn push_packet(&self, _pkt: VideoPacket) {}
}
impl UnifiedMonitorWindow {
pub fn new() -> anyhow::Result<Self> {
Ok(Self)
}
pub fn push_packet(&self, _pkt: VideoPacket) {}
}
}
}
mod paste {
use anyhow::bail;
use lesavka_common::lesavka::PasteRequest;
pub fn build_paste_request(text: &str) -> anyhow::Result<PasteRequest> {
if text == "bad" {
bail!("synthetic paste build failure");
}
Ok(PasteRequest {
nonce: vec![],
data: text.as_bytes().to_vec(),
encrypted: true,
})
}
}
#[path = "../../client/src/app.rs"]
#[allow(warnings)]
mod app_include_contract;
mod tests {
use super::app_include_contract::{
LesavkaClientApp, keyboard_stream_report, mouse_stream_report,
};
use lesavka_common::lesavka::{KeyboardReport, MouseReport};
use serial_test::serial;
use temp_env::with_var;
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
const DOWNLINK_MEDIA_SRC: &str = include_str!("../../client/src/app/downlink_media.rs");
#[test]
#[serial]
fn run_headless_reaches_pending_reactor_branch() {
with_var("LESAVKA_HEADLESS", Some("1"), || {
with_var("LESAVKA_SERVER_ADDR", Some("http://127.0.0.1:9"), || {
with_var("LESAVKA_TEST_CAP_CAMERA", None::<&str>, || {
with_var("LESAVKA_TEST_CAP_MIC", None::<&str>, || {
let rt = tokio::runtime::Runtime::new().expect("runtime");
rt.block_on(async {
let mut app = LesavkaClientApp::new().expect("new app");
let result = tokio::time::timeout(
std::time::Duration::from_millis(80),
app.run(),
)
.await;
assert!(result.is_err(), "headless run should stay pending");
});
});
});
});
});
}
#[test]
#[serial]
fn run_non_headless_starts_stream_tasks_with_stubbed_caps() {
with_var("LESAVKA_HEADLESS", None::<&str>, || {
with_var("LESAVKA_SERVER_ADDR", Some("http://127.0.0.1:9"), || {
with_var("LESAVKA_TEST_CAP_CAMERA", Some("1"), || {
with_var("LESAVKA_TEST_CAP_MIC", Some("1"), || {
let rt = tokio::runtime::Runtime::new().expect("runtime");
rt.block_on(async {
let mut app = LesavkaClientApp::new().expect("new app");
let result = tokio::time::timeout(
std::time::Duration::from_millis(120),
app.run(),
)
.await;
assert!(
result.is_err(),
"run should stay in the central reactor loop"
);
});
});
});
});
});
}
#[test]
fn keyboard_stream_report_turns_lag_into_a_clean_reset() {
let mut remote_capture_was_enabled = true;
let pkt = keyboard_stream_report(
Err(BroadcastStreamRecvError::Lagged(3)),
true,
&mut remote_capture_was_enabled,
)
.expect("lagged keyboard item should produce reset");
assert_eq!(pkt.data, vec![0; 8]);
let passthrough = keyboard_stream_report(
Ok(KeyboardReport {
data: vec![1, 2, 3],
}),
true,
&mut remote_capture_was_enabled,
)
.expect("ok keyboard item should pass through");
assert_eq!(passthrough.data, vec![1, 2, 3]);
}
#[test]
fn mouse_stream_report_turns_lag_into_a_neutral_packet() {
let mut remote_capture_was_enabled = true;
let pkt = mouse_stream_report(
Err(BroadcastStreamRecvError::Lagged(5)),
true,
&mut remote_capture_was_enabled,
)
.expect("lagged mouse item should produce neutral packet");
assert_eq!(pkt.data, vec![0; 4]);
let passthrough = mouse_stream_report(
Ok(MouseReport {
data: vec![9, 8, 7, 6],
}),
true,
&mut remote_capture_was_enabled,
)
.expect("ok mouse item should pass through");
assert_eq!(passthrough.data, vec![9, 8, 7, 6]);
}
#[test]
fn keyboard_stream_report_blocks_stale_packets_after_local_handoff() {
let mut remote_capture_was_enabled = true;
let reset = keyboard_stream_report(
Ok(KeyboardReport {
data: vec![4, 0, 5, 0, 0, 0, 0, 0],
}),
false,
&mut remote_capture_was_enabled,
)
.expect("switching local should emit one reset");
assert_eq!(reset.data, vec![0; 8]);
let dropped = keyboard_stream_report(
Ok(KeyboardReport {
data: vec![1, 2, 3],
}),
false,
&mut remote_capture_was_enabled,
);
assert!(
dropped.is_none(),
"stale keyboard packets should be dropped locally"
);
}
#[test]
fn mouse_stream_report_blocks_stale_packets_after_local_handoff() {
let mut remote_capture_was_enabled = true;
let reset = mouse_stream_report(
Ok(MouseReport {
data: vec![9, 8, 7, 6],
}),
false,
&mut remote_capture_was_enabled,
)
.expect("switching local should emit one neutral mouse packet");
assert_eq!(reset.data, vec![0; 4]);
let dropped = mouse_stream_report(
Ok(MouseReport {
data: vec![1, 1, 1, 1],
}),
false,
&mut remote_capture_was_enabled,
);
assert!(
dropped.is_none(),
"stale mouse packets should be dropped locally"
);
}
#[test]
fn audio_loop_backoff_contract_protects_server_from_reconnect_storms() {
assert!(DOWNLINK_MEDIA_SRC.contains("let mut delay = Duration::from_secs(1);"));
assert!(DOWNLINK_MEDIA_SRC.contains("tokio::time::sleep(delay).await;"));
assert!(DOWNLINK_MEDIA_SRC.contains("delay = app_support::next_delay(delay);"));
assert!(DOWNLINK_MEDIA_SRC.contains("consecutive_source_failures = 0;"));
}
}