ci(lesavka): harden safe gate tests

This commit is contained in:
Brad Stein 2026-05-18 10:09:27 -03:00
parent 52cbf2311f
commit ff9504d55e
4 changed files with 156 additions and 117 deletions

View File

@ -374,26 +374,6 @@ impl LauncherPreview {
Some(feed.is_disabled()) 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( fn rebuild_feed(
&self, &self,
feeds: &Arc<Mutex<[PreviewFeed; 2]>>, feeds: &Arc<Mutex<[PreviewFeed; 2]>>,

View File

@ -1,11 +1,13 @@
use super::{ use super::{
DEFAULT_EYE_SOURCE_HEIGHT, DEFAULT_EYE_SOURCE_WIDTH, INLINE_PREVIEW_MAX_KBIT, DEFAULT_EYE_SOURCE_HEIGHT, DEFAULT_EYE_SOURCE_WIDTH, INLINE_PREVIEW_MAX_KBIT,
INLINE_PREVIEW_REQUEST_FPS, INLINE_PREVIEW_REQUEST_HEIGHT, INLINE_PREVIEW_REQUEST_WIDTH, 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, sanitize_preview_request,
}; };
use crate::launcher::state::{CaptureSizePreset, LauncherState}; use crate::launcher::state::{CaptureSizePreset, LauncherState};
use anyhow::Context;
use futures::stream; use futures::stream;
use lesavka_common::lesavka::relay_client::RelayClient;
use lesavka_common::lesavka::relay_server::{Relay, RelayServer}; use lesavka_common::lesavka::relay_server::{Relay, RelayServer};
use lesavka_common::lesavka::{MonitorRequest, VideoPacket}; use lesavka_common::lesavka::{MonitorRequest, VideoPacket};
use serial_test::serial; 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(&current_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] #[test]
fn inline_preview_profile_uses_lightweight_defaults() { fn inline_preview_profile_uses_lightweight_defaults() {
let profile = PreviewSurface::Inline.profile(); let profile = PreviewSurface::Inline.profile();
@ -315,7 +396,7 @@ fn inline_preview_requests_selected_source_profile_on_wire() {
addr addr
}); });
let preview = LauncherPreview::new(format!("http://{addr}")).expect("preview"); let preview = disabled_preview(format!("http://{addr}"));
let state = LauncherState::default(); let state = LauncherState::default();
let capture = state.capture_size_choice(1); let capture = state.capture_size_choice(1);
preview.set_capture_profile( preview.set_capture_profile(
@ -326,11 +407,9 @@ fn inline_preview_requests_selected_source_profile_on_wire() {
capture.fps, capture.fps,
capture.max_bitrate_kbit, capture.max_bitrate_kbit,
); );
preview.activate_surface_for_test(1, PreviewSurface::Inline); request_preview_capture(&rt, &preview, 1, PreviewSurface::Inline);
let deadline = Instant::now() + Duration::from_secs(5); let request = requests.lock().unwrap().last().cloned().expect("request");
while Instant::now() < deadline {
if let Some(request) = requests.lock().unwrap().last().cloned() {
assert_eq!(request.id, 1); assert_eq!(request.id, 1);
assert_eq!(request.source_id, Some(1)); assert_eq!(request.source_id, Some(1));
assert_eq!(request.requested_width, 1280); assert_eq!(request.requested_width, 1280);
@ -338,13 +417,6 @@ fn inline_preview_requests_selected_source_profile_on_wire() {
assert_eq!(request.requested_fps, 30); assert_eq!(request.requested_fps, 30);
assert_eq!(request.max_bitrate, 6_000); assert_eq!(request.max_bitrate, 6_000);
preview.shutdown_all(); preview.shutdown_all();
return;
}
std::thread::sleep(Duration::from_millis(50));
}
preview.shutdown_all();
panic!("preview did not issue a capture request within timeout");
} }
#[test] #[test]
@ -366,7 +438,7 @@ fn inline_preview_requests_honest_source_profile_on_wire() {
addr addr
}); });
let preview = LauncherPreview::new(format!("http://{addr}")).expect("preview"); let preview = disabled_preview(format!("http://{addr}"));
let mut state = LauncherState::default(); let mut state = LauncherState::default();
state.set_capture_size_preset(1, CaptureSizePreset::P1080); state.set_capture_size_preset(1, CaptureSizePreset::P1080);
let capture = state.capture_size_choice(1); let capture = state.capture_size_choice(1);
@ -378,11 +450,9 @@ fn inline_preview_requests_honest_source_profile_on_wire() {
capture.fps, capture.fps,
capture.max_bitrate_kbit, capture.max_bitrate_kbit,
); );
preview.activate_surface_for_test(1, PreviewSurface::Inline); request_preview_capture(&rt, &preview, 1, PreviewSurface::Inline);
let deadline = Instant::now() + Duration::from_secs(5); let request = requests.lock().unwrap().last().cloned().expect("request");
while Instant::now() < deadline {
if let Some(request) = requests.lock().unwrap().last().cloned() {
assert_eq!(request.id, 1); assert_eq!(request.id, 1);
assert_eq!(request.source_id, Some(1)); assert_eq!(request.source_id, Some(1));
assert_eq!(request.requested_width, 1280); assert_eq!(request.requested_width, 1280);
@ -390,13 +460,6 @@ fn inline_preview_requests_honest_source_profile_on_wire() {
assert_eq!(request.requested_fps, 30); assert_eq!(request.requested_fps, 30);
assert_eq!(request.max_bitrate, 6_000); assert_eq!(request.max_bitrate, 6_000);
preview.shutdown_all(); preview.shutdown_all();
return;
}
std::thread::sleep(Duration::from_millis(50));
}
preview.shutdown_all();
panic!("preview did not issue a source capture request within timeout");
} }
#[test] #[test]
@ -418,7 +481,7 @@ fn inline_preview_requests_native_720p_source_mode_on_wire() {
addr addr
}); });
let preview = LauncherPreview::new(format!("http://{addr}")).expect("preview"); let preview = disabled_preview(format!("http://{addr}"));
let mut state = LauncherState::default(); let mut state = LauncherState::default();
state.set_capture_size_preset(1, CaptureSizePreset::P720); state.set_capture_size_preset(1, CaptureSizePreset::P720);
let capture = state.capture_size_choice(1); let capture = state.capture_size_choice(1);
@ -430,11 +493,9 @@ fn inline_preview_requests_native_720p_source_mode_on_wire() {
capture.fps, capture.fps,
capture.max_bitrate_kbit, capture.max_bitrate_kbit,
); );
preview.activate_surface_for_test(1, PreviewSurface::Inline); request_preview_capture(&rt, &preview, 1, PreviewSurface::Inline);
let deadline = Instant::now() + Duration::from_secs(5); let request = requests.lock().unwrap().last().cloned().expect("request");
while Instant::now() < deadline {
if let Some(request) = requests.lock().unwrap().last().cloned() {
assert_eq!(request.id, 1); assert_eq!(request.id, 1);
assert_eq!(request.source_id, Some(1)); assert_eq!(request.source_id, Some(1));
assert_eq!(request.requested_width, 1280); assert_eq!(request.requested_width, 1280);
@ -442,13 +503,6 @@ fn inline_preview_requests_native_720p_source_mode_on_wire() {
assert_eq!(request.requested_fps, 30); assert_eq!(request.requested_fps, 30);
assert_eq!(request.max_bitrate, 6_000); assert_eq!(request.max_bitrate, 6_000);
preview.shutdown_all(); preview.shutdown_all();
return;
}
std::thread::sleep(Duration::from_millis(50));
}
preview.shutdown_all();
panic!("preview did not issue a 720p source capture request within timeout");
} }
#[test] #[test]
@ -470,7 +524,7 @@ fn inline_preview_legacy_low_modes_fall_forward_to_720p_on_wire() {
addr addr
}); });
let preview = LauncherPreview::new(format!("http://{addr}")).expect("preview"); let preview = disabled_preview(format!("http://{addr}"));
let mut state = LauncherState::default(); let mut state = LauncherState::default();
state.set_capture_size_preset(1, CaptureSizePreset::P480); state.set_capture_size_preset(1, CaptureSizePreset::P480);
let capture = state.capture_size_choice(1); let capture = state.capture_size_choice(1);
@ -482,11 +536,9 @@ fn inline_preview_legacy_low_modes_fall_forward_to_720p_on_wire() {
capture.fps, capture.fps,
capture.max_bitrate_kbit, capture.max_bitrate_kbit,
); );
preview.activate_surface_for_test(1, PreviewSurface::Inline); request_preview_capture(&rt, &preview, 1, PreviewSurface::Inline);
let deadline = Instant::now() + Duration::from_secs(5); let request = requests.lock().unwrap().last().cloned().expect("request");
while Instant::now() < deadline {
if let Some(request) = requests.lock().unwrap().last().cloned() {
assert_eq!(request.id, 1); assert_eq!(request.id, 1);
assert_eq!(request.source_id, Some(1)); assert_eq!(request.source_id, Some(1));
assert_eq!(request.requested_width, 1280); assert_eq!(request.requested_width, 1280);
@ -494,13 +546,6 @@ fn inline_preview_legacy_low_modes_fall_forward_to_720p_on_wire() {
assert_eq!(request.requested_fps, 30); assert_eq!(request.requested_fps, 30);
assert_eq!(request.max_bitrate, 6_000); assert_eq!(request.max_bitrate, 6_000);
preview.shutdown_all(); preview.shutdown_all();
return;
}
std::thread::sleep(Duration::from_millis(50));
}
preview.shutdown_all();
panic!("preview did not issue a 720p fallback source capture request within timeout");
} }
#[test] #[test]
@ -522,13 +567,11 @@ fn preview_can_request_other_eye_as_a_distinct_stream() {
addr 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.set_capture_profile(0, 1, 1920, 1080, 30, 12_000);
preview.activate_surface_for_test(0, PreviewSurface::Inline); request_preview_capture(&rt, &preview, 0, PreviewSurface::Inline);
let deadline = Instant::now() + Duration::from_secs(5); let request = requests.lock().unwrap().last().cloned().expect("request");
while Instant::now() < deadline {
if let Some(request) = requests.lock().unwrap().last().cloned() {
assert_eq!(request.id, 0); assert_eq!(request.id, 0);
assert_eq!(request.source_id, Some(1)); assert_eq!(request.source_id, Some(1));
assert_eq!(request.requested_width, 1280); assert_eq!(request.requested_width, 1280);
@ -536,11 +579,4 @@ fn preview_can_request_other_eye_as_a_distinct_stream() {
assert_eq!(request.requested_fps, 30); assert_eq!(request.requested_fps, 30);
assert_eq!(request.max_bitrate, 6_000); assert_eq!(request.max_bitrate, 6_000);
preview.shutdown_all(); preview.shutdown_all();
return;
}
std::thread::sleep(Duration::from_millis(50));
}
preview.shutdown_all();
panic!("preview did not issue a mirrored capture request within timeout");
} }

View File

@ -14,12 +14,28 @@ use lesavka_common::lesavka::{
handshake_server::{Handshake, HandshakeServer}, handshake_server::{Handshake, HandshakeServer},
}; };
use serial_test::serial; use serial_test::serial;
use std::net::TcpListener; use std::net::{SocketAddr, TcpListener};
use std::time::Duration; use std::time::Duration;
use temp_env::with_var; use temp_env::with_var;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use tokio::sync::oneshot; 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<S>(service: S) -> PeerCaps async fn negotiate_against_service<S>(service: S) -> PeerCaps
where where
@ -40,7 +56,7 @@ where
.expect("serve handshake server"); .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 caps = negotiate(&format!("http://{addr}")).await;
let _ = shutdown_tx.send(()); let _ = shutdown_tx.send(());
let _ = server.await; let _ = server.await;
@ -66,7 +82,7 @@ where
.expect("serve handshake server"); .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 result = probe(&format!("http://{addr}")).await;
let _ = shutdown_tx.send(()); let _ = shutdown_tx.send(());
let _ = server.await; let _ = server.await;

View File

@ -311,7 +311,7 @@ mod server_main_binary {
#[test] #[test]
#[serial] #[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 (_dir, handler) = build_handler_for_tests();
let rt = tokio::runtime::Runtime::new().expect("runtime"); let rt = tokio::runtime::Runtime::new().expect("runtime");
@ -320,7 +320,14 @@ mod server_main_binary {
Ok(_) => panic!("cycle should fail without gadget sysfs"), Ok(_) => panic!("cycle should fail without gadget sysfs"),
Err(err) => err, 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] #[test]