lesavka/testing/tests/client_app_include_contract.rs

244 lines
6.9 KiB
Rust
Raw Normal View History

//! Include-based coverage for client app startup reactor behavior.
//!
//! Scope: compile `client/src/app.rs` as a module with deterministic local
//! stubs for capture/render dependencies, then exercise `new` + `run`.
//! Targets: `client/src/app.rs`.
//! Why: app orchestration branches should stay stable in CI without physical
//! devices.
mod handshake {
#[derive(Default, Clone, Debug)]
pub struct PeerCaps {
pub camera: bool,
pub microphone: bool,
}
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(),
}
}
}
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 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>>,
}
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,
}
}
pub fn init(&mut self) -> anyhow::Result<()> {
Ok(())
}
pub async fn run(&mut self) -> anyhow::Result<()> {
std::future::pending::<()>().await;
#[allow(unreachable_code)]
Ok(())
}
}
}
}
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;
impl MonitorWindow {
pub fn new(_id: u32) -> 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;
use serial_test::serial;
use temp_env::with_var;
#[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"
);
});
});
});
});
});
}
}