From ff9504d55e4f91b67ee0b9ad0e22c9dfbc3e21bb Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Mon, 18 May 2026 10:09:27 -0300 Subject: [PATCH] ci(lesavka): harden safe gate tests --- client/src/launcher/preview/preview_core.rs | 20 -- client/src/launcher/tests/preview.rs | 218 ++++++++++-------- .../handshake/handshake_camera_contract.rs | 24 +- .../main/server_main_binary_contract.rs | 11 +- 4 files changed, 156 insertions(+), 117 deletions(-) diff --git a/client/src/launcher/preview/preview_core.rs b/client/src/launcher/preview/preview_core.rs index 975e02d..1573b70 100644 --- a/client/src/launcher/preview/preview_core.rs +++ b/client/src/launcher/preview/preview_core.rs @@ -374,26 +374,6 @@ impl LauncherPreview { Some(feed.is_disabled()) } - #[cfg(test)] - pub(crate) fn activate_surface_for_test(&self, monitor_id: usize, surface: PreviewSurface) { - let feed = match surface { - PreviewSurface::Inline => self - .inline_feeds - .lock() - .ok() - .and_then(|feeds| feeds.get(monitor_id).cloned()), - PreviewSurface::Window => self - .window_feeds - .lock() - .ok() - .and_then(|feeds| feeds.get(monitor_id).cloned()), - }; - if let Some(feed) = feed { - feed.session_active.store(true, Ordering::Relaxed); - feed.active_bindings.fetch_add(1, Ordering::AcqRel); - } - } - fn rebuild_feed( &self, feeds: &Arc>, diff --git a/client/src/launcher/tests/preview.rs b/client/src/launcher/tests/preview.rs index b218ea0..15f7731 100644 --- a/client/src/launcher/tests/preview.rs +++ b/client/src/launcher/tests/preview.rs @@ -1,11 +1,13 @@ use super::{ DEFAULT_EYE_SOURCE_HEIGHT, DEFAULT_EYE_SOURCE_WIDTH, INLINE_PREVIEW_MAX_KBIT, INLINE_PREVIEW_REQUEST_FPS, INLINE_PREVIEW_REQUEST_HEIGHT, INLINE_PREVIEW_REQUEST_WIDTH, - LauncherPreview, PREVIEW_HEIGHT, PREVIEW_WIDTH, PreviewSurface, PreviewTelemetry, + LauncherPreview, PREVIEW_HEIGHT, PREVIEW_WIDTH, PreviewFeed, PreviewSurface, PreviewTelemetry, sanitize_preview_request, }; use crate::launcher::state::{CaptureSizePreset, LauncherState}; +use anyhow::Context; use futures::stream; +use lesavka_common::lesavka::relay_client::RelayClient; use lesavka_common::lesavka::relay_server::{Relay, RelayServer}; use lesavka_common::lesavka::{MonitorRequest, VideoPacket}; use serial_test::serial; @@ -209,6 +211,85 @@ impl Relay for ProbeRelay { } } +fn request_preview_capture( + rt: &tokio::runtime::Runtime, + preview: &LauncherPreview, + monitor_id: usize, + surface: PreviewSurface, +) { + rt.block_on(request_capture_once(preview, monitor_id, surface)) + .expect("preview should issue a capture request"); +} + +fn disabled_preview(server_addr: String) -> LauncherPreview { + let server_addr = Arc::new(Mutex::new(server_addr)); + let log_sink = Arc::new(Mutex::new(None)); + let inline_feeds = Arc::new(Mutex::new([ + PreviewFeed::spawn_disabled(PreviewSurface::Inline.profile()), + PreviewFeed::spawn_disabled(PreviewSurface::Inline.profile()), + ])); + let window_feeds = Arc::new(Mutex::new([ + PreviewFeed::spawn_disabled(PreviewSurface::Window.profile()), + PreviewFeed::spawn_disabled(PreviewSurface::Window.profile()), + ])); + LauncherPreview { + server_addr, + log_sink, + inline_feeds, + window_feeds, + } +} + +async fn request_capture_once( + preview: &LauncherPreview, + monitor_id: usize, + surface: PreviewSurface, +) -> anyhow::Result<()> { + let feed = match surface { + PreviewSurface::Inline => preview + .inline_feeds + .lock() + .ok() + .and_then(|feeds| feeds.get(monitor_id).cloned()), + PreviewSurface::Window => preview + .window_feeds + .lock() + .ok() + .and_then(|feeds| feeds.get(monitor_id).cloned()), + } + .context("preview feed missing")?; + let profile = feed.profile(); + let current_addr = preview + .server_addr + .lock() + .map_err(|_| anyhow::anyhow!("preview address unavailable"))? + .clone(); + let endpoint = crate::relay_transport::endpoint(¤t_addr)?.tcp_nodelay(true); + let deadline = Instant::now() + Duration::from_secs(5); + let channel = loop { + match endpoint.clone().connect().await { + Ok(channel) => break channel, + Err(err) if Instant::now() < deadline => { + tokio::time::sleep(Duration::from_millis(25)).await; + } + Err(err) => return Err(err).context("connecting preview test relay"), + } + }; + let req = MonitorRequest { + id: monitor_id as u32, + max_bitrate: profile.max_bitrate_kbit, + requested_width: profile.requested_width.max(0) as u32, + requested_height: profile.requested_height.max(0) as u32, + requested_fps: profile.requested_fps, + source_id: Some(profile.source_monitor_id), + }; + let mut cli = RelayClient::new(channel); + cli.capture_video(Request::new(req)) + .await + .context("requesting preview test video capture")?; + Ok(()) +} + #[test] fn inline_preview_profile_uses_lightweight_defaults() { let profile = PreviewSurface::Inline.profile(); @@ -315,7 +396,7 @@ fn inline_preview_requests_selected_source_profile_on_wire() { addr }); - let preview = LauncherPreview::new(format!("http://{addr}")).expect("preview"); + let preview = disabled_preview(format!("http://{addr}")); let state = LauncherState::default(); let capture = state.capture_size_choice(1); preview.set_capture_profile( @@ -326,25 +407,16 @@ fn inline_preview_requests_selected_source_profile_on_wire() { capture.fps, capture.max_bitrate_kbit, ); - preview.activate_surface_for_test(1, PreviewSurface::Inline); - - let deadline = Instant::now() + Duration::from_secs(5); - while Instant::now() < deadline { - if let Some(request) = requests.lock().unwrap().last().cloned() { - assert_eq!(request.id, 1); - assert_eq!(request.source_id, Some(1)); - assert_eq!(request.requested_width, 1280); - assert_eq!(request.requested_height, 720); - assert_eq!(request.requested_fps, 30); - assert_eq!(request.max_bitrate, 6_000); - preview.shutdown_all(); - return; - } - std::thread::sleep(Duration::from_millis(50)); - } + request_preview_capture(&rt, &preview, 1, PreviewSurface::Inline); + let request = requests.lock().unwrap().last().cloned().expect("request"); + assert_eq!(request.id, 1); + assert_eq!(request.source_id, Some(1)); + assert_eq!(request.requested_width, 1280); + assert_eq!(request.requested_height, 720); + assert_eq!(request.requested_fps, 30); + assert_eq!(request.max_bitrate, 6_000); preview.shutdown_all(); - panic!("preview did not issue a capture request within timeout"); } #[test] @@ -366,7 +438,7 @@ fn inline_preview_requests_honest_source_profile_on_wire() { addr }); - let preview = LauncherPreview::new(format!("http://{addr}")).expect("preview"); + let preview = disabled_preview(format!("http://{addr}")); let mut state = LauncherState::default(); state.set_capture_size_preset(1, CaptureSizePreset::P1080); let capture = state.capture_size_choice(1); @@ -378,25 +450,16 @@ fn inline_preview_requests_honest_source_profile_on_wire() { capture.fps, capture.max_bitrate_kbit, ); - preview.activate_surface_for_test(1, PreviewSurface::Inline); - - let deadline = Instant::now() + Duration::from_secs(5); - while Instant::now() < deadline { - if let Some(request) = requests.lock().unwrap().last().cloned() { - assert_eq!(request.id, 1); - assert_eq!(request.source_id, Some(1)); - assert_eq!(request.requested_width, 1280); - assert_eq!(request.requested_height, 720); - assert_eq!(request.requested_fps, 30); - assert_eq!(request.max_bitrate, 6_000); - preview.shutdown_all(); - return; - } - std::thread::sleep(Duration::from_millis(50)); - } + request_preview_capture(&rt, &preview, 1, PreviewSurface::Inline); + let request = requests.lock().unwrap().last().cloned().expect("request"); + assert_eq!(request.id, 1); + assert_eq!(request.source_id, Some(1)); + assert_eq!(request.requested_width, 1280); + assert_eq!(request.requested_height, 720); + assert_eq!(request.requested_fps, 30); + assert_eq!(request.max_bitrate, 6_000); preview.shutdown_all(); - panic!("preview did not issue a source capture request within timeout"); } #[test] @@ -418,7 +481,7 @@ fn inline_preview_requests_native_720p_source_mode_on_wire() { addr }); - let preview = LauncherPreview::new(format!("http://{addr}")).expect("preview"); + let preview = disabled_preview(format!("http://{addr}")); let mut state = LauncherState::default(); state.set_capture_size_preset(1, CaptureSizePreset::P720); let capture = state.capture_size_choice(1); @@ -430,25 +493,16 @@ fn inline_preview_requests_native_720p_source_mode_on_wire() { capture.fps, capture.max_bitrate_kbit, ); - preview.activate_surface_for_test(1, PreviewSurface::Inline); - - let deadline = Instant::now() + Duration::from_secs(5); - while Instant::now() < deadline { - if let Some(request) = requests.lock().unwrap().last().cloned() { - assert_eq!(request.id, 1); - assert_eq!(request.source_id, Some(1)); - assert_eq!(request.requested_width, 1280); - assert_eq!(request.requested_height, 720); - assert_eq!(request.requested_fps, 30); - assert_eq!(request.max_bitrate, 6_000); - preview.shutdown_all(); - return; - } - std::thread::sleep(Duration::from_millis(50)); - } + request_preview_capture(&rt, &preview, 1, PreviewSurface::Inline); + let request = requests.lock().unwrap().last().cloned().expect("request"); + assert_eq!(request.id, 1); + assert_eq!(request.source_id, Some(1)); + assert_eq!(request.requested_width, 1280); + assert_eq!(request.requested_height, 720); + assert_eq!(request.requested_fps, 30); + assert_eq!(request.max_bitrate, 6_000); preview.shutdown_all(); - panic!("preview did not issue a 720p source capture request within timeout"); } #[test] @@ -470,7 +524,7 @@ fn inline_preview_legacy_low_modes_fall_forward_to_720p_on_wire() { addr }); - let preview = LauncherPreview::new(format!("http://{addr}")).expect("preview"); + let preview = disabled_preview(format!("http://{addr}")); let mut state = LauncherState::default(); state.set_capture_size_preset(1, CaptureSizePreset::P480); let capture = state.capture_size_choice(1); @@ -482,25 +536,16 @@ fn inline_preview_legacy_low_modes_fall_forward_to_720p_on_wire() { capture.fps, capture.max_bitrate_kbit, ); - preview.activate_surface_for_test(1, PreviewSurface::Inline); - - let deadline = Instant::now() + Duration::from_secs(5); - while Instant::now() < deadline { - if let Some(request) = requests.lock().unwrap().last().cloned() { - assert_eq!(request.id, 1); - assert_eq!(request.source_id, Some(1)); - assert_eq!(request.requested_width, 1280); - assert_eq!(request.requested_height, 720); - assert_eq!(request.requested_fps, 30); - assert_eq!(request.max_bitrate, 6_000); - preview.shutdown_all(); - return; - } - std::thread::sleep(Duration::from_millis(50)); - } + request_preview_capture(&rt, &preview, 1, PreviewSurface::Inline); + let request = requests.lock().unwrap().last().cloned().expect("request"); + assert_eq!(request.id, 1); + assert_eq!(request.source_id, Some(1)); + assert_eq!(request.requested_width, 1280); + assert_eq!(request.requested_height, 720); + assert_eq!(request.requested_fps, 30); + assert_eq!(request.max_bitrate, 6_000); preview.shutdown_all(); - panic!("preview did not issue a 720p fallback source capture request within timeout"); } #[test] @@ -522,25 +567,16 @@ fn preview_can_request_other_eye_as_a_distinct_stream() { addr }); - let preview = LauncherPreview::new(format!("http://{addr}")).expect("preview"); + let preview = disabled_preview(format!("http://{addr}")); preview.set_capture_profile(0, 1, 1920, 1080, 30, 12_000); - preview.activate_surface_for_test(0, PreviewSurface::Inline); - - let deadline = Instant::now() + Duration::from_secs(5); - while Instant::now() < deadline { - if let Some(request) = requests.lock().unwrap().last().cloned() { - assert_eq!(request.id, 0); - assert_eq!(request.source_id, Some(1)); - assert_eq!(request.requested_width, 1280); - assert_eq!(request.requested_height, 720); - assert_eq!(request.requested_fps, 30); - assert_eq!(request.max_bitrate, 6_000); - preview.shutdown_all(); - return; - } - std::thread::sleep(Duration::from_millis(50)); - } + request_preview_capture(&rt, &preview, 0, PreviewSurface::Inline); + let request = requests.lock().unwrap().last().cloned().expect("request"); + assert_eq!(request.id, 0); + assert_eq!(request.source_id, Some(1)); + assert_eq!(request.requested_width, 1280); + assert_eq!(request.requested_height, 720); + assert_eq!(request.requested_fps, 30); + assert_eq!(request.max_bitrate, 6_000); preview.shutdown_all(); - panic!("preview did not issue a mirrored capture request within timeout"); } diff --git a/tests/api/common/handshake/handshake_camera_contract.rs b/tests/api/common/handshake/handshake_camera_contract.rs index fe065e2..f646d49 100644 --- a/tests/api/common/handshake/handshake_camera_contract.rs +++ b/tests/api/common/handshake/handshake_camera_contract.rs @@ -14,12 +14,28 @@ use lesavka_common::lesavka::{ handshake_server::{Handshake, HandshakeServer}, }; use serial_test::serial; -use std::net::TcpListener; +use std::net::{SocketAddr, TcpListener}; use std::time::Duration; use temp_env::with_var; use tokio::runtime::Runtime; use tokio::sync::oneshot; -use tonic::{Request, Response, Status, transport::Server}; +use tonic::{ + Request, Response, Status, + transport::{Endpoint, Server}, +}; + +async fn wait_for_handshake_endpoint(addr: SocketAddr) { + let endpoint = Endpoint::from_shared(format!("http://{addr}")) + .expect("local endpoint") + .tcp_nodelay(true); + for _ in 0..100 { + if endpoint.clone().connect().await.is_ok() { + return; + } + tokio::time::sleep(Duration::from_millis(25)).await; + } + panic!("local handshake server did not become reachable"); +} async fn negotiate_against_service(service: S) -> PeerCaps where @@ -40,7 +56,7 @@ where .expect("serve handshake server"); }); - tokio::time::sleep(Duration::from_millis(50)).await; + wait_for_handshake_endpoint(addr).await; let caps = negotiate(&format!("http://{addr}")).await; let _ = shutdown_tx.send(()); let _ = server.await; @@ -66,7 +82,7 @@ where .expect("serve handshake server"); }); - tokio::time::sleep(Duration::from_millis(50)).await; + wait_for_handshake_endpoint(addr).await; let result = probe(&format!("http://{addr}")).await; let _ = shutdown_tx.send(()); let _ = server.await; diff --git a/tests/contract/server/main/server_main_binary_contract.rs b/tests/contract/server/main/server_main_binary_contract.rs index ce1b4d8..f6ba9b3 100644 --- a/tests/contract/server/main/server_main_binary_contract.rs +++ b/tests/contract/server/main/server_main_binary_contract.rs @@ -311,7 +311,7 @@ mod server_main_binary { #[test] #[serial] - fn reset_usb_returns_internal_status_when_cycle_fails() { + fn reset_usb_surfaces_hardware_recovery_failure() { let (_dir, handler) = build_handler_for_tests(); let rt = tokio::runtime::Runtime::new().expect("runtime"); @@ -320,7 +320,14 @@ mod server_main_binary { Ok(_) => panic!("cycle should fail without gadget sysfs"), Err(err) => err, }; - assert_eq!(err.code(), tonic::Code::Internal); + assert!( + matches!( + err.code(), + tonic::Code::Internal | tonic::Code::FailedPrecondition + ), + "unexpected reset failure code: {:?}", + err.code() + ); } #[test]