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())
}
#[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<Mutex<[PreviewFeed; 2]>>,

View File

@ -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(&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]
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");
}

View File

@ -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<S>(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;

View File

@ -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]