330 lines
11 KiB
Rust
330 lines
11 KiB
Rust
|
|
use super::*;
|
||
|
|
use crate::launcher::{
|
||
|
|
devices::DeviceCatalog, preview::PreviewBinding, state::LauncherState,
|
||
|
|
ui_components::build_launcher_view,
|
||
|
|
};
|
||
|
|
use serial_test::serial;
|
||
|
|
use std::{cell::RefCell, rc::Rc};
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn local_test_detail_mentions_idle_and_running_modes() {
|
||
|
|
assert!(local_test_detail(false, false, false, false).contains("idle"));
|
||
|
|
let running = local_test_detail(true, true, false, false);
|
||
|
|
assert!(running.contains("camera preview"));
|
||
|
|
assert!(running.contains("mic monitor"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn gpio_power_label_tracks_detected_devices() {
|
||
|
|
let mut power = CapturePowerStatus::default();
|
||
|
|
assert_eq!(gpio_power_label(&power), "Unavailable");
|
||
|
|
|
||
|
|
power.available = true;
|
||
|
|
assert_eq!(gpio_power_label(&power), "Power Off");
|
||
|
|
|
||
|
|
power.enabled = true;
|
||
|
|
assert_eq!(gpio_power_label(&power), "No Eyes");
|
||
|
|
|
||
|
|
power.detected_devices = 1;
|
||
|
|
assert_eq!(gpio_power_label(&power), "1 Eye");
|
||
|
|
|
||
|
|
power.detected_devices = 2;
|
||
|
|
assert_eq!(gpio_power_label(&power), "2 Eyes");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn server_chip_state_tracks_connection_not_just_reachability() {
|
||
|
|
let mut state = LauncherState::new();
|
||
|
|
assert_eq!(server_light_state(&state, false), StatusLightState::Idle);
|
||
|
|
assert_eq!(server_version_label(&state), "-");
|
||
|
|
|
||
|
|
state.set_server_available(true);
|
||
|
|
state.set_server_version(Some("0.12.3".to_string()));
|
||
|
|
assert_eq!(server_light_state(&state, false), StatusLightState::Caution);
|
||
|
|
assert_eq!(server_version_label(&state), "v0.12.3");
|
||
|
|
|
||
|
|
assert_eq!(server_light_state(&state, true), StatusLightState::Live);
|
||
|
|
|
||
|
|
state.set_server_version(Some("v0.12.4".to_string()));
|
||
|
|
assert_eq!(server_version_label(&state), "v0.12.4");
|
||
|
|
|
||
|
|
state.set_server_version(Some(" ".to_string()));
|
||
|
|
assert_eq!(server_version_label(&state), "-");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn capture_power_detail_mentions_detected_eyes_when_powered() {
|
||
|
|
let power = CapturePowerStatus {
|
||
|
|
available: true,
|
||
|
|
enabled: true,
|
||
|
|
detail: "active/running".to_string(),
|
||
|
|
detected_devices: 1,
|
||
|
|
..Default::default()
|
||
|
|
};
|
||
|
|
|
||
|
|
assert!(capture_power_detail(&power).contains("1 eye detected"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn compact_device_name_prefers_basename_when_available() {
|
||
|
|
assert_eq!(compact_device_name("/dev/video0"), "video0");
|
||
|
|
assert_eq!(compact_device_name("alsa_input.usb"), "alsa_input.usb");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn strip_ansi_sequences_removes_terminal_codes() {
|
||
|
|
let raw = "\u{1b}[32mINFO\u{1b}[0m hello";
|
||
|
|
assert_eq!(strip_ansi_sequences(raw), "INFO hello");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn classify_log_tags_assigns_prefix_and_severity_colors() {
|
||
|
|
let tags = classify_log_tags("[relay] WARN pipeline failed");
|
||
|
|
assert!(tags.contains(&"log-relay"));
|
||
|
|
assert!(tags.contains(&"log-error") || tags.contains(&"log-warn"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
#[doc = "Verifies the default console filter hides relay INFO noise."]
|
||
|
|
fn session_log_filter_hides_noisy_info_by_default_but_keeps_errors() {
|
||
|
|
assert!(!should_show_session_log_line(
|
||
|
|
"[relay] 2026-04-22T23:20:17Z INFO ThreadId(01) audio packet received packet=3000",
|
||
|
|
ConsoleLogLevel::Warn
|
||
|
|
));
|
||
|
|
assert!(!should_show_session_log_line(
|
||
|
|
"[relay] 2026-04-22T23:20:17Z INFO ThreadId(04) decoded audio level rms=-32",
|
||
|
|
ConsoleLogLevel::Warn
|
||
|
|
));
|
||
|
|
assert!(should_show_session_log_line(
|
||
|
|
"[relay] 2026-04-22T23:20:17Z WARN pipeline is recovering",
|
||
|
|
ConsoleLogLevel::Warn
|
||
|
|
));
|
||
|
|
assert!(should_show_session_log_line(
|
||
|
|
"[relay] 2026-04-22T23:20:17Z INFO ❌ connect failed",
|
||
|
|
ConsoleLogLevel::Error
|
||
|
|
));
|
||
|
|
assert!(should_show_session_log_line(
|
||
|
|
"[launcher] Relay connected with inputs routed to remote.",
|
||
|
|
ConsoleLogLevel::Error
|
||
|
|
));
|
||
|
|
assert!(should_show_session_log_line(
|
||
|
|
"[relay] 2026-04-22T23:20:17Z INFO audio packet received",
|
||
|
|
ConsoleLogLevel::Info
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn write_audio_gain_request_formats_live_control_file() {
|
||
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
||
|
|
let path = dir.path().join("gain.control");
|
||
|
|
write_audio_gain_request(&path, 425).expect("write gain");
|
||
|
|
let raw = std::fs::read_to_string(path).expect("read gain");
|
||
|
|
assert!(raw.starts_with("4.250 "), "{raw}");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn write_mic_gain_request_formats_live_control_file() {
|
||
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
||
|
|
let path = dir.path().join("mic-gain.control");
|
||
|
|
write_mic_gain_request(&path, 325).expect("write gain");
|
||
|
|
let raw = std::fs::read_to_string(path).expect("read gain");
|
||
|
|
assert!(raw.starts_with("3.250 "), "{raw}");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[gtk::test]
|
||
|
|
#[serial]
|
||
|
|
fn dock_all_displays_to_preview_closes_popouts_and_resets_surfaces() {
|
||
|
|
if gtk::gdk::Display::default().is_none() {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
let app = gtk::Application::builder()
|
||
|
|
.application_id("dev.lesavka.test-dock")
|
||
|
|
.build();
|
||
|
|
let _ = app.register(None::<>k::gio::Cancellable>);
|
||
|
|
|
||
|
|
let state = Rc::new(RefCell::new(LauncherState::new()));
|
||
|
|
state
|
||
|
|
.borrow_mut()
|
||
|
|
.set_display_surface(0, DisplaySurface::Window);
|
||
|
|
state
|
||
|
|
.borrow_mut()
|
||
|
|
.set_display_surface(1, DisplaySurface::Window);
|
||
|
|
let state_snapshot = state.borrow().clone();
|
||
|
|
let view = build_launcher_view(
|
||
|
|
&app,
|
||
|
|
"http://127.0.0.1:50051",
|
||
|
|
&DeviceCatalog::default(),
|
||
|
|
&state_snapshot,
|
||
|
|
);
|
||
|
|
let child_proc = Rc::new(RefCell::new(None::<RelayChild>));
|
||
|
|
|
||
|
|
let left_binding = PreviewBinding::test_stub();
|
||
|
|
let right_binding = PreviewBinding::test_stub();
|
||
|
|
{
|
||
|
|
let mut popouts = view.popouts.borrow_mut();
|
||
|
|
popouts[0] = Some(PopoutWindowHandle {
|
||
|
|
window: gtk::ApplicationWindow::builder()
|
||
|
|
.application(&app)
|
||
|
|
.title("Left")
|
||
|
|
.build(),
|
||
|
|
frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false),
|
||
|
|
picture: gtk::Picture::new(),
|
||
|
|
status_label: gtk::Label::new(None),
|
||
|
|
binding: left_binding,
|
||
|
|
});
|
||
|
|
popouts[1] = Some(PopoutWindowHandle {
|
||
|
|
window: gtk::ApplicationWindow::builder()
|
||
|
|
.application(&app)
|
||
|
|
.title("Right")
|
||
|
|
.build(),
|
||
|
|
frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false),
|
||
|
|
picture: gtk::Picture::new(),
|
||
|
|
status_label: gtk::Label::new(None),
|
||
|
|
binding: right_binding,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
dock_all_displays_to_preview(&state, &child_proc, &view.popouts, &view.widgets);
|
||
|
|
|
||
|
|
assert!(view.popouts.borrow().iter().all(|handle| handle.is_none()));
|
||
|
|
assert_eq!(state.borrow().display_surface(0), DisplaySurface::Preview);
|
||
|
|
assert_eq!(state.borrow().display_surface(1), DisplaySurface::Preview);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[gtk::test]
|
||
|
|
#[serial]
|
||
|
|
fn dock_all_displays_to_preview_handles_reentrant_close_callbacks() {
|
||
|
|
if gtk::gdk::Display::default().is_none() {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
let app = gtk::Application::builder()
|
||
|
|
.application_id("dev.lesavka.test-reentrant-dock")
|
||
|
|
.build();
|
||
|
|
let _ = app.register(None::<>k::gio::Cancellable>);
|
||
|
|
|
||
|
|
let state = Rc::new(RefCell::new(LauncherState::new()));
|
||
|
|
state
|
||
|
|
.borrow_mut()
|
||
|
|
.set_display_surface(0, DisplaySurface::Window);
|
||
|
|
let state_snapshot = state.borrow().clone();
|
||
|
|
let view = build_launcher_view(
|
||
|
|
&app,
|
||
|
|
"http://127.0.0.1:50051",
|
||
|
|
&DeviceCatalog::default(),
|
||
|
|
&state_snapshot,
|
||
|
|
);
|
||
|
|
let child_proc = Rc::new(RefCell::new(None::<RelayChild>));
|
||
|
|
|
||
|
|
let popouts = Rc::clone(&view.popouts);
|
||
|
|
let window = gtk::ApplicationWindow::builder()
|
||
|
|
.application(&app)
|
||
|
|
.title("Reentrant")
|
||
|
|
.build();
|
||
|
|
{
|
||
|
|
let popouts = Rc::clone(&popouts);
|
||
|
|
window.connect_close_request(move |_| {
|
||
|
|
let _ = popouts.borrow_mut()[0].take();
|
||
|
|
glib::Propagation::Proceed
|
||
|
|
});
|
||
|
|
}
|
||
|
|
{
|
||
|
|
let mut slot = popouts.borrow_mut();
|
||
|
|
slot[0] = Some(PopoutWindowHandle {
|
||
|
|
window,
|
||
|
|
frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false),
|
||
|
|
picture: gtk::Picture::new(),
|
||
|
|
status_label: gtk::Label::new(None),
|
||
|
|
binding: PreviewBinding::test_stub(),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
dock_all_displays_to_preview(&state, &child_proc, &popouts, &view.widgets);
|
||
|
|
|
||
|
|
assert!(popouts.borrow().iter().all(|handle| handle.is_none()));
|
||
|
|
assert_eq!(state.borrow().display_surface(0), DisplaySurface::Preview);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[gtk::test]
|
||
|
|
#[serial]
|
||
|
|
fn shutdown_launcher_runtime_closes_preview_bindings_and_popouts() {
|
||
|
|
if gtk::gdk::Display::default().is_none() {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
let app = gtk::Application::builder()
|
||
|
|
.application_id("dev.lesavka.test-shutdown")
|
||
|
|
.build();
|
||
|
|
let _ = app.register(None::<>k::gio::Cancellable>);
|
||
|
|
|
||
|
|
let state = Rc::new(RefCell::new(LauncherState::new()));
|
||
|
|
let state_snapshot = state.borrow().clone();
|
||
|
|
let view = build_launcher_view(
|
||
|
|
&app,
|
||
|
|
"http://127.0.0.1:50051",
|
||
|
|
&DeviceCatalog::default(),
|
||
|
|
&state_snapshot,
|
||
|
|
);
|
||
|
|
let child_proc = Rc::new(RefCell::new(None::<RelayChild>));
|
||
|
|
let tests = Rc::new(RefCell::new(DeviceTestController::new()));
|
||
|
|
|
||
|
|
let left_binding = PreviewBinding::test_stub();
|
||
|
|
let right_binding = PreviewBinding::test_stub();
|
||
|
|
*view.widgets.display_panes[0].preview_binding.borrow_mut() = Some(left_binding.clone());
|
||
|
|
*view.widgets.display_panes[1].preview_binding.borrow_mut() = Some(right_binding.clone());
|
||
|
|
|
||
|
|
{
|
||
|
|
let mut popouts = view.popouts.borrow_mut();
|
||
|
|
popouts[0] = Some(PopoutWindowHandle {
|
||
|
|
window: gtk::ApplicationWindow::builder()
|
||
|
|
.application(&app)
|
||
|
|
.title("Left")
|
||
|
|
.build(),
|
||
|
|
frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false),
|
||
|
|
picture: gtk::Picture::new(),
|
||
|
|
status_label: gtk::Label::new(None),
|
||
|
|
binding: PreviewBinding::test_stub(),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
*view.diagnostics_popout.borrow_mut() = Some(
|
||
|
|
gtk::ApplicationWindow::builder()
|
||
|
|
.application(&app)
|
||
|
|
.title("Diagnostics")
|
||
|
|
.build(),
|
||
|
|
);
|
||
|
|
*view.log_popout.borrow_mut() = Some(
|
||
|
|
gtk::ApplicationWindow::builder()
|
||
|
|
.application(&app)
|
||
|
|
.title("Log")
|
||
|
|
.build(),
|
||
|
|
);
|
||
|
|
|
||
|
|
shutdown_launcher_runtime(
|
||
|
|
&child_proc,
|
||
|
|
&tests,
|
||
|
|
None,
|
||
|
|
&view.widgets,
|
||
|
|
&view.popouts,
|
||
|
|
&view.diagnostics_popout,
|
||
|
|
&view.log_popout,
|
||
|
|
);
|
||
|
|
|
||
|
|
assert!(view.popouts.borrow().iter().all(|handle| handle.is_none()));
|
||
|
|
assert!(
|
||
|
|
view.widgets.display_panes[0]
|
||
|
|
.preview_binding
|
||
|
|
.borrow()
|
||
|
|
.is_none()
|
||
|
|
);
|
||
|
|
assert!(
|
||
|
|
view.widgets.display_panes[1]
|
||
|
|
.preview_binding
|
||
|
|
.borrow()
|
||
|
|
.is_none()
|
||
|
|
);
|
||
|
|
assert!(view.diagnostics_popout.borrow().is_none());
|
||
|
|
assert!(view.log_popout.borrow().is_none());
|
||
|
|
}
|