fix(input): expose usb recovery state
This commit is contained in:
parent
2cce2c165c
commit
d60124ce94
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.11.41"
|
version = "0.11.42"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -102,6 +102,7 @@ impl InputAggregator {
|
|||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
let clipboard_control_path =
|
let clipboard_control_path =
|
||||||
launcher_routing_path_from_env("LESAVKA_LAUNCHER_CLIPBOARD_CONTROL");
|
launcher_routing_path_from_env("LESAVKA_LAUNCHER_CLIPBOARD_CONTROL");
|
||||||
|
let remote_failsafe_timeout = remote_failsafe_timeout_from_env();
|
||||||
Self {
|
Self {
|
||||||
kbd_tx,
|
kbd_tx,
|
||||||
mou_tx,
|
mou_tx,
|
||||||
@ -126,8 +127,9 @@ impl InputAggregator {
|
|||||||
last_quick_toggle_at: None,
|
last_quick_toggle_at: None,
|
||||||
pending_release_started_at: None,
|
pending_release_started_at: None,
|
||||||
pending_release_timeout: pending_release_timeout_from_env(),
|
pending_release_timeout: pending_release_timeout_from_env(),
|
||||||
remote_failsafe_started_at: capture_remote_boot.then(Instant::now),
|
remote_failsafe_started_at: (capture_remote_boot && !remote_failsafe_timeout.is_zero())
|
||||||
remote_failsafe_timeout: remote_failsafe_timeout_from_env(),
|
.then(Instant::now),
|
||||||
|
remote_failsafe_timeout,
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
last_routing_request_raw: routing_control_path
|
last_routing_request_raw: routing_control_path
|
||||||
.as_deref()
|
.as_deref()
|
||||||
@ -1024,6 +1026,12 @@ fn pending_release_timeout_from_env() -> Duration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn remote_failsafe_timeout_from_env() -> Duration {
|
fn remote_failsafe_timeout_from_env() -> Duration {
|
||||||
|
if let Some(secs) = std::env::var("LESAVKA_INPUT_REMOTE_FAILSAFE_SECS")
|
||||||
|
.ok()
|
||||||
|
.and_then(|raw| raw.parse::<u64>().ok())
|
||||||
|
{
|
||||||
|
return Duration::from_secs(secs);
|
||||||
|
}
|
||||||
let millis = std::env::var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS")
|
let millis = std::env::var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|raw| raw.parse::<u64>().ok())
|
.and_then(|raw| raw.parse::<u64>().ok())
|
||||||
|
|||||||
@ -27,6 +27,8 @@ pub const LAUNCHER_FOCUS_SIGNAL_ENV: &str = "LESAVKA_LAUNCHER_FOCUS_SIGNAL";
|
|||||||
pub const DEFAULT_LAUNCHER_FOCUS_SIGNAL_PATH: &str = "/tmp/lesavka-launcher-focus.signal";
|
pub const DEFAULT_LAUNCHER_FOCUS_SIGNAL_PATH: &str = "/tmp/lesavka-launcher-focus.signal";
|
||||||
pub const LAUNCHER_CLIPBOARD_CONTROL_ENV: &str = "LESAVKA_LAUNCHER_CLIPBOARD_CONTROL";
|
pub const LAUNCHER_CLIPBOARD_CONTROL_ENV: &str = "LESAVKA_LAUNCHER_CLIPBOARD_CONTROL";
|
||||||
pub const DEFAULT_LAUNCHER_CLIPBOARD_CONTROL_PATH: &str = "/tmp/lesavka-launcher-clipboard.control";
|
pub const DEFAULT_LAUNCHER_CLIPBOARD_CONTROL_PATH: &str = "/tmp/lesavka-launcher-clipboard.control";
|
||||||
|
pub const REMOTE_INPUT_FAILSAFE_SECONDS_ENV: &str = "LESAVKA_INPUT_REMOTE_FAILSAFE_SECS";
|
||||||
|
pub const DEFAULT_REMOTE_INPUT_FAILSAFE_SECONDS: &str = "0";
|
||||||
|
|
||||||
pub fn maybe_run_launcher(args: &[String]) -> Result<bool> {
|
pub fn maybe_run_launcher(args: &[String]) -> Result<bool> {
|
||||||
if should_run_launcher(args) {
|
if should_run_launcher(args) {
|
||||||
@ -53,6 +55,13 @@ pub fn runtime_env_vars(state: &LauncherState) -> BTreeMap<String, String> {
|
|||||||
state.view_mode.as_env().to_string(),
|
state.view_mode.as_env().to_string(),
|
||||||
);
|
);
|
||||||
envs.insert("LESAVKA_CLIPBOARD_DELAY_MS".to_string(), "18".to_string());
|
envs.insert("LESAVKA_CLIPBOARD_DELAY_MS".to_string(), "18".to_string());
|
||||||
|
envs.insert(
|
||||||
|
REMOTE_INPUT_FAILSAFE_SECONDS_ENV.to_string(),
|
||||||
|
std::env::var(REMOTE_INPUT_FAILSAFE_SECONDS_ENV)
|
||||||
|
.ok()
|
||||||
|
.filter(|value| value.trim().parse::<u64>().is_ok())
|
||||||
|
.unwrap_or_else(|| DEFAULT_REMOTE_INPUT_FAILSAFE_SECONDS.to_string()),
|
||||||
|
);
|
||||||
if matches!(state.view_mode, ViewMode::Unified) {
|
if matches!(state.view_mode, ViewMode::Unified) {
|
||||||
envs.insert("LESAVKA_DISABLE_VIDEO_RENDER".to_string(), "1".to_string());
|
envs.insert("LESAVKA_DISABLE_VIDEO_RENDER".to_string(), "1".to_string());
|
||||||
}
|
}
|
||||||
@ -165,6 +174,10 @@ mod tests {
|
|||||||
envs.get("LESAVKA_CLIPBOARD_DELAY_MS"),
|
envs.get("LESAVKA_CLIPBOARD_DELAY_MS"),
|
||||||
Some(&"18".to_string())
|
Some(&"18".to_string())
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
envs.get(REMOTE_INPUT_FAILSAFE_SECONDS_ENV),
|
||||||
|
Some(&DEFAULT_REMOTE_INPUT_FAILSAFE_SECONDS.to_string())
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
envs.get("LESAVKA_DISABLE_VIDEO_RENDER"),
|
envs.get("LESAVKA_DISABLE_VIDEO_RENDER"),
|
||||||
Some(&"1".to_string())
|
Some(&"1".to_string())
|
||||||
@ -216,6 +229,30 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_env_vars_passes_through_remote_failsafe_launch_option() {
|
||||||
|
temp_env::with_var(REMOTE_INPUT_FAILSAFE_SECONDS_ENV, Some("60"), || {
|
||||||
|
let state = LauncherState::new();
|
||||||
|
let envs = runtime_env_vars(&state);
|
||||||
|
assert_eq!(
|
||||||
|
envs.get(REMOTE_INPUT_FAILSAFE_SECONDS_ENV),
|
||||||
|
Some(&"60".to_string())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_env_vars_keeps_remote_failsafe_disabled_for_invalid_launch_option() {
|
||||||
|
temp_env::with_var(REMOTE_INPUT_FAILSAFE_SECONDS_ENV, Some("later"), || {
|
||||||
|
let state = LauncherState::new();
|
||||||
|
let envs = runtime_env_vars(&state);
|
||||||
|
assert_eq!(
|
||||||
|
envs.get(REMOTE_INPUT_FAILSAFE_SECONDS_ENV),
|
||||||
|
Some(&DEFAULT_REMOTE_INPUT_FAILSAFE_SECONDS.to_string())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_env_vars_do_not_disable_breakout_video_windows() {
|
fn runtime_env_vars_do_not_disable_breakout_video_windows() {
|
||||||
let mut state = LauncherState::new();
|
let mut state = LauncherState::new();
|
||||||
|
|||||||
@ -110,13 +110,13 @@ pub struct LauncherView {
|
|||||||
pub const LESAVKA_ICON_NAME: &str = "dev.lesavka.launcher";
|
pub const LESAVKA_ICON_NAME: &str = "dev.lesavka.launcher";
|
||||||
const LESAVKA_ICON_SEARCH_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/icons");
|
const LESAVKA_ICON_SEARCH_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/icons");
|
||||||
const LAUNCHER_DEFAULT_WIDTH: i32 = 1360;
|
const LAUNCHER_DEFAULT_WIDTH: i32 = 1360;
|
||||||
const LAUNCHER_DEFAULT_HEIGHT: i32 = 780;
|
const LAUNCHER_DEFAULT_HEIGHT: i32 = 820;
|
||||||
const OPERATIONS_RAIL_WIDTH: i32 = 288;
|
const OPERATIONS_RAIL_WIDTH: i32 = 288;
|
||||||
const CAMERA_PREVIEW_VIEWPORT_HEIGHT: i32 = 158;
|
const CAMERA_PREVIEW_VIEWPORT_HEIGHT: i32 = 158;
|
||||||
const CAMERA_PREVIEW_VIEWPORT_WIDTH: i32 = 280;
|
const CAMERA_PREVIEW_VIEWPORT_WIDTH: i32 = 280;
|
||||||
const EYE_PREVIEW_MIN_HEIGHT: i32 = 226;
|
const EYE_PREVIEW_MIN_HEIGHT: i32 = 258;
|
||||||
const EYE_PREVIEW_MIN_WIDTH: i32 = 402;
|
const EYE_PREVIEW_MIN_WIDTH: i32 = 460;
|
||||||
const SIDE_LOG_HEIGHT: i32 = 104;
|
const SIDE_LOG_HEIGHT: i32 = 124;
|
||||||
|
|
||||||
pub fn build_launcher_view(
|
pub fn build_launcher_view(
|
||||||
app: >k::Application,
|
app: >k::Application,
|
||||||
@ -188,8 +188,8 @@ pub fn build_launcher_view(
|
|||||||
let operations = gtk::Box::new(gtk::Orientation::Vertical, 8);
|
let operations = gtk::Box::new(gtk::Orientation::Vertical, 8);
|
||||||
operations.set_size_request(OPERATIONS_RAIL_WIDTH, -1);
|
operations.set_size_request(OPERATIONS_RAIL_WIDTH, -1);
|
||||||
operations.set_hexpand(false);
|
operations.set_hexpand(false);
|
||||||
operations.set_vexpand(false);
|
operations.set_vexpand(true);
|
||||||
operations.set_valign(gtk::Align::Start);
|
operations.set_valign(gtk::Align::Fill);
|
||||||
content.append(&operations);
|
content.append(&operations);
|
||||||
|
|
||||||
let display_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
let display_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.11.41"
|
version = "0.11.42"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,8 @@ INPUT_TESTS=(
|
|||||||
--test client_keyboard_include_extra_contract
|
--test client_keyboard_include_extra_contract
|
||||||
--test client_mouse_include_contract
|
--test client_mouse_include_contract
|
||||||
--test client_mouse_include_extra_contract
|
--test client_mouse_include_extra_contract
|
||||||
|
--test server_gadget_include_contract
|
||||||
|
--test server_main_binary_extra_contract
|
||||||
--test server_runtime_smoke_contract
|
--test server_runtime_smoke_contract
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
|||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
|
||||||
VIDEO_TESTS=(
|
VIDEO_TESTS=(
|
||||||
|
--test client_launcher_layout_contract
|
||||||
--test video_downstream_feed_contract
|
--test video_downstream_feed_contract
|
||||||
--test server_video_include_contract
|
--test server_video_include_contract
|
||||||
--test video_support_contract
|
--test video_support_contract
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.11.41"
|
version = "0.11.42"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
@ -38,6 +38,19 @@ impl UsbGadget {
|
|||||||
Ok(std::fs::read_to_string(p)?.trim().to_owned())
|
Ok(std::fs::read_to_string(p)?.trim().to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn current_controller_state() -> anyhow::Result<(String, String)> {
|
||||||
|
let ctrl = Self::find_controller()?;
|
||||||
|
let state = Self::state(&ctrl)?;
|
||||||
|
Ok((ctrl, state))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn host_attached_state(state: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
state,
|
||||||
|
"configured" | "addressed" | "default" | "suspended" | "unknown"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/*---- helpers ----*/
|
/*---- helpers ----*/
|
||||||
|
|
||||||
/// Find the first controller in /sys/class/udc (e.g. `1000480000.usb`)
|
/// Find the first controller in /sys/class/udc (e.g. `1000480000.usb`)
|
||||||
|
|||||||
@ -345,6 +345,30 @@ impl Handler {
|
|||||||
error!("💥 restart UVC helper failed: {e:#}");
|
error!("💥 restart UVC helper failed: {e:#}");
|
||||||
return Err(Status::internal(e.to_string()));
|
return Err(Status::internal(e.to_string()));
|
||||||
}
|
}
|
||||||
|
match UsbGadget::current_controller_state() {
|
||||||
|
Ok((ctrl, state)) if UsbGadget::host_attached_state(&state) => {
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
info!(
|
||||||
|
"✅ USB host enumerated gadget after recovery ctrl={ctrl} state={state}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok((ctrl, state)) => {
|
||||||
|
let message = format!(
|
||||||
|
"USB gadget recovery ran, but UDC {ctrl} is still {state}; the controlled host has not enumerated the relay HID/audio/video gadget"
|
||||||
|
);
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
warn!("⚠️ {message}");
|
||||||
|
return Err(Status::failed_precondition(message));
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let message = format!(
|
||||||
|
"USB gadget recovery ran, but the relay cannot read UDC state: {err:#}"
|
||||||
|
);
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
warn!("⚠️ {message}");
|
||||||
|
return Err(Status::failed_precondition(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(Response::new(ResetUsbReply { ok: true }))
|
Ok(Response::new(ResetUsbReply { ok: true }))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@ -463,23 +463,48 @@ mod inputs_contract {
|
|||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn remote_failsafe_timeout_env_is_opt_in_and_allows_disable() {
|
fn remote_failsafe_timeout_env_is_opt_in_and_allows_disable() {
|
||||||
with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", None::<&str>, || {
|
with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_SECS", None::<&str>, || {
|
||||||
assert_eq!(remote_failsafe_timeout_from_env(), Duration::from_millis(0));
|
with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", None::<&str>, || {
|
||||||
|
assert_eq!(remote_failsafe_timeout_from_env(), Duration::from_millis(0));
|
||||||
|
});
|
||||||
|
with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", Some("0"), || {
|
||||||
|
assert_eq!(remote_failsafe_timeout_from_env(), Duration::from_millis(0));
|
||||||
|
});
|
||||||
|
with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", Some("60000"), || {
|
||||||
|
assert_eq!(
|
||||||
|
remote_failsafe_timeout_from_env(),
|
||||||
|
Duration::from_millis(60_000)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", Some("1500"), || {
|
||||||
|
assert_eq!(
|
||||||
|
remote_failsafe_timeout_from_env(),
|
||||||
|
Duration::from_millis(1_500)
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", Some("0"), || {
|
with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_SECS", Some("0"), || {
|
||||||
assert_eq!(remote_failsafe_timeout_from_env(), Duration::from_millis(0));
|
with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", Some("60000"), || {
|
||||||
|
assert_eq!(remote_failsafe_timeout_from_env(), Duration::ZERO);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", Some("60000"), || {
|
with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_SECS", Some("60"), || {
|
||||||
assert_eq!(
|
assert_eq!(remote_failsafe_timeout_from_env(), Duration::from_secs(60));
|
||||||
remote_failsafe_timeout_from_env(),
|
|
||||||
Duration::from_millis(60_000)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", Some("1500"), || {
|
}
|
||||||
assert_eq!(
|
|
||||||
remote_failsafe_timeout_from_env(),
|
#[test]
|
||||||
Duration::from_millis(1_500)
|
#[serial]
|
||||||
);
|
fn boot_remote_capture_only_arms_failsafe_when_launch_option_is_nonzero() {
|
||||||
|
with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_SECS", Some("0"), || {
|
||||||
|
let agg = new_aggregator();
|
||||||
|
assert_eq!(agg.remote_failsafe_timeout, Duration::ZERO);
|
||||||
|
assert!(agg.remote_failsafe_started_at.is_none());
|
||||||
|
});
|
||||||
|
with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_SECS", Some("60"), || {
|
||||||
|
let agg = new_aggregator();
|
||||||
|
assert_eq!(agg.remote_failsafe_timeout, Duration::from_secs(60));
|
||||||
|
assert!(agg.remote_failsafe_started_at.is_some());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
84
testing/tests/client_launcher_layout_contract.rs
Normal file
84
testing/tests/client_launcher_layout_contract.rs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
//! Contract tests for the launcher layout proportions.
|
||||||
|
//!
|
||||||
|
//! Scope: statically guard the GTK layout constants and sizing glue used by
|
||||||
|
//! the launcher shell.
|
||||||
|
//! Targets: `client/src/launcher/ui_components.rs`.
|
||||||
|
//! Why: the launcher is an operational control surface; accidental spacing
|
||||||
|
//! regressions can hide diagnostics or make eye/device previews unusable.
|
||||||
|
|
||||||
|
const UI_SRC: &str = include_str!("../../client/src/launcher/ui_components.rs");
|
||||||
|
|
||||||
|
fn const_i32(name: &str) -> i32 {
|
||||||
|
let needle = format!("const {name}: i32 = ");
|
||||||
|
let line = UI_SRC
|
||||||
|
.lines()
|
||||||
|
.find(|line| line.trim_start().starts_with(&needle))
|
||||||
|
.unwrap_or_else(|| panic!("missing {name} constant"));
|
||||||
|
line.trim_start()
|
||||||
|
.trim_start_matches(&needle)
|
||||||
|
.trim_end_matches(';')
|
||||||
|
.parse()
|
||||||
|
.unwrap_or_else(|err| panic!("invalid {name} constant: {err}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn launcher_default_size_stays_inside_1080p() {
|
||||||
|
assert_eq!(const_i32("LAUNCHER_DEFAULT_WIDTH"), 1360);
|
||||||
|
assert_eq!(const_i32("LAUNCHER_DEFAULT_HEIGHT"), 820);
|
||||||
|
assert!(const_i32("LAUNCHER_DEFAULT_WIDTH") <= 1920);
|
||||||
|
assert!(const_i32("LAUNCHER_DEFAULT_HEIGHT") <= 1080);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn eye_panes_keep_the_locked_larger_preview_footprint() {
|
||||||
|
assert_eq!(const_i32("EYE_PREVIEW_MIN_WIDTH"), 460);
|
||||||
|
assert_eq!(const_i32("EYE_PREVIEW_MIN_HEIGHT"), 258);
|
||||||
|
assert!(
|
||||||
|
UI_SRC.contains("caption_label.set_halign(gtk::Align::End)")
|
||||||
|
|| UI_SRC.contains("capture_label.set_halign(gtk::Align::End)")
|
||||||
|
);
|
||||||
|
assert!(UI_SRC.contains("capture_label.set_ellipsize(pango::EllipsizeMode::Start);"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn device_staging_and_testing_bottoms_stay_locked_together() {
|
||||||
|
assert!(UI_SRC.contains("staging_row.set_homogeneous(true);"));
|
||||||
|
assert!(UI_SRC.contains("devices_panel.set_valign(gtk::Align::Fill);"));
|
||||||
|
assert!(UI_SRC.contains("preview_panel.set_valign(gtk::Align::Fill);"));
|
||||||
|
assert!(UI_SRC.contains(
|
||||||
|
"let device_body_height_group = gtk::SizeGroup::new(gtk::SizeGroupMode::Vertical);"
|
||||||
|
));
|
||||||
|
assert!(UI_SRC.contains("device_body_height_group.add_widget(&devices_body);"));
|
||||||
|
assert!(UI_SRC.contains("device_body_height_group.add_widget(&testing_row);"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn device_testing_keeps_webcam_and_mic_playback_as_equal_bottom_columns() {
|
||||||
|
assert_eq!(const_i32("CAMERA_PREVIEW_VIEWPORT_WIDTH"), 280);
|
||||||
|
assert_eq!(const_i32("CAMERA_PREVIEW_VIEWPORT_HEIGHT"), 158);
|
||||||
|
assert!(UI_SRC.contains("webcam_group.set_valign(gtk::Align::Fill);"));
|
||||||
|
assert!(UI_SRC.contains("playback_group.set_valign(gtk::Align::Fill);"));
|
||||||
|
assert!(UI_SRC.contains("playback_body.set_valign(gtk::Align::Fill);"));
|
||||||
|
assert!(UI_SRC.contains("audio_check_meter.set_vexpand(true);"));
|
||||||
|
assert!(UI_SRC.contains("playback_body.append(&audio_check_meter);"));
|
||||||
|
assert!(UI_SRC.contains("playback_body.append(µphone_replay_button);"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn operations_column_fills_height_and_splits_extra_space_between_logs() {
|
||||||
|
assert_eq!(const_i32("SIDE_LOG_HEIGHT"), 124);
|
||||||
|
assert!(UI_SRC.contains("operations.set_vexpand(true);"));
|
||||||
|
assert!(UI_SRC.contains("operations.set_valign(gtk::Align::Fill);"));
|
||||||
|
assert_eq!(
|
||||||
|
UI_SRC
|
||||||
|
.matches(".min_content_height(SIDE_LOG_HEIGHT)")
|
||||||
|
.count(),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
UI_SRC
|
||||||
|
.matches(".max_content_height(SIDE_LOG_HEIGHT)")
|
||||||
|
.count(),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -64,6 +64,29 @@ mod gadget_include_contract {
|
|||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn host_attached_state_matches_udc_states_that_can_accept_hid() {
|
||||||
|
for state in ["configured", "addressed", "default", "suspended", "unknown"] {
|
||||||
|
assert!(UsbGadget::host_attached_state(state), "{state}");
|
||||||
|
}
|
||||||
|
assert!(!UsbGadget::host_attached_state("not attached"));
|
||||||
|
assert!(!UsbGadget::host_attached_state("broken"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn current_controller_state_reads_fake_udc_tree() {
|
||||||
|
let dir = tempdir().expect("tempdir");
|
||||||
|
let ctrl = "fake-ctrl.usb";
|
||||||
|
build_fake_tree(dir.path(), ctrl, "lesavka-test", "configured");
|
||||||
|
|
||||||
|
with_fake_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
|
||||||
|
let (found_ctrl, state) = UsbGadget::current_controller_state().expect("current state");
|
||||||
|
assert_eq!(found_ctrl, ctrl);
|
||||||
|
assert_eq!(state, "configured");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wait_state_any_times_out_for_missing_controller() {
|
fn wait_state_any_times_out_for_missing_controller() {
|
||||||
let result = UsbGadget::wait_state_any("definitely-missing-udc", 0);
|
let result = UsbGadget::wait_state_any("definitely-missing-udc", 0);
|
||||||
|
|||||||
@ -441,7 +441,7 @@ mod server_main_binary_extra {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn reset_usb_succeeds_with_fake_cycle_and_override_hid_paths() {
|
fn reset_usb_reports_host_not_attached_after_fake_cycle() {
|
||||||
let dir = tempdir().expect("tempdir");
|
let dir = tempdir().expect("tempdir");
|
||||||
let hid_dir = dir.path().join("hid");
|
let hid_dir = dir.path().join("hid");
|
||||||
std::fs::create_dir_all(&hid_dir).expect("create hid dir");
|
std::fs::create_dir_all(&hid_dir).expect("create hid dir");
|
||||||
@ -480,11 +480,15 @@ mod server_main_binary_extra {
|
|||||||
)),
|
)),
|
||||||
};
|
};
|
||||||
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
||||||
let reply = rt
|
let err = rt
|
||||||
.block_on(async { handler.reset_usb(tonic::Request::new(Empty {})).await })
|
.block_on(async { handler.reset_usb(tonic::Request::new(Empty {})).await })
|
||||||
.expect("reset usb should succeed on fake gadget tree")
|
.expect_err("reset usb should report a host that never enumerates");
|
||||||
.into_inner();
|
assert_eq!(err.code(), tonic::Code::FailedPrecondition);
|
||||||
assert!(reply.ok);
|
assert!(
|
||||||
|
err.message().contains("still not attached"),
|
||||||
|
"unexpected reset error: {}",
|
||||||
|
err.message()
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user