2026-04-13 02:52:32 -03:00
|
|
|
//! Extra integration coverage for server main HID startup branches.
|
|
|
|
|
//!
|
|
|
|
|
//! Scope: include `server/src/main.rs` and exercise successful handler startup
|
|
|
|
|
//! with synthetic HID endpoints.
|
|
|
|
|
//! Targets: `server/src/main.rs`.
|
|
|
|
|
//! Why: the main contract file is near the 500 LOC cap, so additional branch
|
|
|
|
|
//! coverage lives here.
|
|
|
|
|
|
|
|
|
|
#[allow(warnings)]
|
|
|
|
|
mod server_main_binary_extra {
|
|
|
|
|
include!(env!("LESAVKA_SERVER_MAIN_SRC"));
|
|
|
|
|
|
2026-04-14 23:35:29 -03:00
|
|
|
use futures_util::stream;
|
2026-04-14 23:03:18 -03:00
|
|
|
use lesavka_common::lesavka::relay_client::RelayClient;
|
2026-04-13 02:52:32 -03:00
|
|
|
use serial_test::serial;
|
2026-04-16 12:58:05 -03:00
|
|
|
use std::path::Path;
|
2026-04-13 02:52:32 -03:00
|
|
|
use temp_env::with_var;
|
|
|
|
|
use tempfile::tempdir;
|
|
|
|
|
|
2026-04-14 23:03:18 -03:00
|
|
|
async fn connect_with_retry(addr: std::net::SocketAddr) -> tonic::transport::Channel {
|
|
|
|
|
let endpoint = tonic::transport::Endpoint::from_shared(format!("http://{addr}"))
|
|
|
|
|
.expect("endpoint")
|
|
|
|
|
.tcp_nodelay(true);
|
|
|
|
|
for _ in 0..40 {
|
|
|
|
|
if let Ok(channel) = endpoint.clone().connect().await {
|
|
|
|
|
return channel;
|
|
|
|
|
}
|
|
|
|
|
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
|
|
|
|
|
}
|
|
|
|
|
panic!("failed to connect to local tonic server");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 23:35:29 -03:00
|
|
|
fn write_file(path: &Path, content: &str) {
|
|
|
|
|
if let Some(parent) = path.parent() {
|
|
|
|
|
std::fs::create_dir_all(parent).expect("create parent");
|
|
|
|
|
}
|
|
|
|
|
std::fs::write(path, content).expect("write file");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn with_fake_gadget_roots(sys_root: &Path, cfg_root: &Path, f: impl FnOnce()) {
|
|
|
|
|
let sys_root = sys_root.to_string_lossy().to_string();
|
|
|
|
|
let cfg_root = cfg_root.to_string_lossy().to_string();
|
|
|
|
|
with_var("LESAVKA_GADGET_SYSFS_ROOT", Some(sys_root), || {
|
|
|
|
|
with_var("LESAVKA_GADGET_CONFIGFS_ROOT", Some(cfg_root), f);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_fake_gadget_tree(base: &Path, ctrl: &str, gadget_name: &str, state: &str) {
|
|
|
|
|
write_file(
|
|
|
|
|
&base.join(format!("sys/class/udc/{ctrl}/state")),
|
|
|
|
|
&format!("{state}\n"),
|
|
|
|
|
);
|
|
|
|
|
write_file(
|
|
|
|
|
&base.join(format!("cfg/{gadget_name}/UDC")),
|
|
|
|
|
&format!("{ctrl}\n"),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 23:03:18 -03:00
|
|
|
fn build_handler_for_tests_with_modes(
|
|
|
|
|
kb_writable: bool,
|
|
|
|
|
ms_writable: bool,
|
|
|
|
|
) -> (tempfile::TempDir, Handler) {
|
|
|
|
|
let dir = tempdir().expect("tempdir");
|
|
|
|
|
let kb_path = dir.path().join("hidg0.bin");
|
|
|
|
|
let ms_path = dir.path().join("hidg1.bin");
|
|
|
|
|
std::fs::write(&kb_path, []).expect("create kb file");
|
|
|
|
|
std::fs::write(&ms_path, []).expect("create ms file");
|
|
|
|
|
|
|
|
|
|
let kb_std = std::fs::OpenOptions::new()
|
|
|
|
|
.read(true)
|
|
|
|
|
.write(kb_writable)
|
|
|
|
|
.create(kb_writable)
|
|
|
|
|
.truncate(kb_writable)
|
|
|
|
|
.open(&kb_path)
|
|
|
|
|
.expect("open kb");
|
|
|
|
|
let ms_std = std::fs::OpenOptions::new()
|
|
|
|
|
.read(true)
|
|
|
|
|
.write(ms_writable)
|
|
|
|
|
.create(ms_writable)
|
|
|
|
|
.truncate(ms_writable)
|
|
|
|
|
.open(&ms_path)
|
|
|
|
|
.expect("open ms");
|
|
|
|
|
let kb = tokio::fs::File::from_std(kb_std);
|
|
|
|
|
let ms = tokio::fs::File::from_std(ms_std);
|
|
|
|
|
|
|
|
|
|
(
|
|
|
|
|
dir,
|
|
|
|
|
Handler {
|
2026-04-21 13:08:20 -03:00
|
|
|
kb: std::sync::Arc::new(tokio::sync::Mutex::new(Some(kb))),
|
|
|
|
|
ms: std::sync::Arc::new(tokio::sync::Mutex::new(Some(ms))),
|
2026-04-14 23:03:18 -03:00
|
|
|
gadget: UsbGadget::new("lesavka"),
|
|
|
|
|
did_cycle: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
|
|
|
|
camera_rt: std::sync::Arc::new(CameraRuntime::new()),
|
|
|
|
|
capture_power: CapturePowerManager::new(),
|
2026-04-19 04:24:27 -03:00
|
|
|
eye_hubs: std::sync::Arc::new(tokio::sync::Mutex::new(
|
|
|
|
|
std::collections::HashMap::new(),
|
|
|
|
|
)),
|
2026-04-14 23:03:18 -03:00
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn build_handler_for_tests() -> (tempfile::TempDir, Handler) {
|
|
|
|
|
build_handler_for_tests_with_modes(true, true)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 02:52:32 -03:00
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn handler_new_and_reopen_hid_succeed_with_override_paths() {
|
|
|
|
|
let dir = tempdir().expect("tempdir");
|
|
|
|
|
std::fs::write(dir.path().join("hidg0"), "").expect("create hidg0");
|
|
|
|
|
std::fs::write(dir.path().join("hidg1"), "").expect("create hidg1");
|
|
|
|
|
let hid_dir = dir.path().to_string_lossy().to_string();
|
|
|
|
|
|
|
|
|
|
with_var("LESAVKA_HID_DIR", Some(hid_dir), || {
|
|
|
|
|
with_var("LESAVKA_ALLOW_GADGET_CYCLE", None::<&str>, || {
|
|
|
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
|
|
|
rt.block_on(async {
|
|
|
|
|
let handler = Handler::new(UsbGadget::new("lesavka"))
|
|
|
|
|
.await
|
|
|
|
|
.expect("handler startup");
|
|
|
|
|
handler.reopen_hid().await.expect("reopen hid");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn handler_new_with_cycle_enabled_can_still_open_override_paths() {
|
|
|
|
|
let dir = tempdir().expect("tempdir");
|
|
|
|
|
std::fs::write(dir.path().join("hidg0"), "").expect("create hidg0");
|
|
|
|
|
std::fs::write(dir.path().join("hidg1"), "").expect("create hidg1");
|
|
|
|
|
let hid_dir = dir.path().to_string_lossy().to_string();
|
|
|
|
|
|
|
|
|
|
with_var("LESAVKA_HID_DIR", Some(hid_dir), || {
|
|
|
|
|
with_var("LESAVKA_ALLOW_GADGET_CYCLE", Some("1"), || {
|
|
|
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
|
|
|
rt.block_on(async {
|
|
|
|
|
let _handler = Handler::new(UsbGadget::new("lesavka"))
|
|
|
|
|
.await
|
|
|
|
|
.expect("handler startup with cycle enabled");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-04-14 23:03:18 -03:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn stream_keyboard_writes_reports_to_hid_file() {
|
|
|
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
|
|
|
rt.block_on(async {
|
|
|
|
|
let (dir, handler) = build_handler_for_tests();
|
|
|
|
|
let kb_path = dir.path().join("hidg0.bin");
|
|
|
|
|
|
|
|
|
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
|
|
|
|
|
let addr = listener.local_addr().expect("addr");
|
|
|
|
|
drop(listener);
|
|
|
|
|
|
|
|
|
|
let server = tokio::spawn(async move {
|
|
|
|
|
let _ = tonic::transport::Server::builder()
|
|
|
|
|
.add_service(RelayServer::new(handler))
|
|
|
|
|
.serve(addr)
|
|
|
|
|
.await;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let channel = connect_with_retry(addr).await;
|
|
|
|
|
let mut cli = RelayClient::new(channel);
|
|
|
|
|
let (tx, rx) = tokio::sync::mpsc::channel(4);
|
|
|
|
|
tx.send(KeyboardReport {
|
|
|
|
|
data: vec![1, 2, 3, 4, 5, 6, 7, 8],
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("send keyboard packet");
|
|
|
|
|
drop(tx);
|
|
|
|
|
|
|
|
|
|
let outbound = tokio_stream::wrappers::ReceiverStream::new(rx);
|
|
|
|
|
let mut resp = cli
|
|
|
|
|
.stream_keyboard(tonic::Request::new(outbound))
|
|
|
|
|
.await
|
|
|
|
|
.expect("stream keyboard");
|
|
|
|
|
let echoed = resp
|
|
|
|
|
.get_mut()
|
|
|
|
|
.message()
|
|
|
|
|
.await
|
|
|
|
|
.expect("grpc result")
|
|
|
|
|
.expect("echo packet");
|
|
|
|
|
assert_eq!(echoed.data, vec![1, 2, 3, 4, 5, 6, 7, 8]);
|
|
|
|
|
|
|
|
|
|
tokio::time::sleep(std::time::Duration::from_millis(30)).await;
|
|
|
|
|
let written = std::fs::read(&kb_path).expect("read hidg0 file");
|
|
|
|
|
assert!(
|
|
|
|
|
!written.is_empty(),
|
|
|
|
|
"keyboard stream should write HID bytes to target file"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
server.abort();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn stream_mouse_writes_reports_to_hid_file() {
|
|
|
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
|
|
|
rt.block_on(async {
|
|
|
|
|
let (dir, handler) = build_handler_for_tests();
|
|
|
|
|
let ms_path = dir.path().join("hidg1.bin");
|
|
|
|
|
|
|
|
|
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
|
|
|
|
|
let addr = listener.local_addr().expect("addr");
|
|
|
|
|
drop(listener);
|
|
|
|
|
|
|
|
|
|
let server = tokio::spawn(async move {
|
|
|
|
|
let _ = tonic::transport::Server::builder()
|
|
|
|
|
.add_service(RelayServer::new(handler))
|
|
|
|
|
.serve(addr)
|
|
|
|
|
.await;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let channel = connect_with_retry(addr).await;
|
|
|
|
|
let mut cli = RelayClient::new(channel);
|
|
|
|
|
let (tx, rx) = tokio::sync::mpsc::channel(4);
|
|
|
|
|
tx.send(MouseReport {
|
|
|
|
|
data: vec![8, 7, 6, 5, 4, 3, 2, 1],
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("send mouse packet");
|
|
|
|
|
drop(tx);
|
|
|
|
|
|
|
|
|
|
let outbound = tokio_stream::wrappers::ReceiverStream::new(rx);
|
|
|
|
|
let mut resp = cli
|
|
|
|
|
.stream_mouse(tonic::Request::new(outbound))
|
|
|
|
|
.await
|
|
|
|
|
.expect("stream mouse");
|
|
|
|
|
let echoed = resp
|
|
|
|
|
.get_mut()
|
|
|
|
|
.message()
|
|
|
|
|
.await
|
|
|
|
|
.expect("grpc result")
|
|
|
|
|
.expect("echo packet");
|
|
|
|
|
assert_eq!(echoed.data, vec![8, 7, 6, 5, 4, 3, 2, 1]);
|
|
|
|
|
|
|
|
|
|
tokio::time::sleep(std::time::Duration::from_millis(30)).await;
|
|
|
|
|
let written = std::fs::read(&ms_path).expect("read hidg1 file");
|
|
|
|
|
assert!(
|
|
|
|
|
!written.is_empty(),
|
|
|
|
|
"mouse stream should write HID bytes to target file"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
server.abort();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn stream_keyboard_recovers_when_hid_write_fails() {
|
|
|
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
|
|
|
rt.block_on(async {
|
|
|
|
|
let (_dir, handler) = build_handler_for_tests_with_modes(false, true);
|
|
|
|
|
|
|
|
|
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
|
|
|
|
|
let addr = listener.local_addr().expect("addr");
|
|
|
|
|
drop(listener);
|
|
|
|
|
|
|
|
|
|
let server = tokio::spawn(async move {
|
|
|
|
|
let _ = tonic::transport::Server::builder()
|
|
|
|
|
.add_service(RelayServer::new(handler))
|
|
|
|
|
.serve(addr)
|
|
|
|
|
.await;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let channel = connect_with_retry(addr).await;
|
|
|
|
|
let mut cli = RelayClient::new(channel);
|
|
|
|
|
let (tx, rx) = tokio::sync::mpsc::channel(4);
|
|
|
|
|
tx.send(KeyboardReport {
|
|
|
|
|
data: vec![11, 12, 13, 14, 15, 16, 17, 18],
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("send keyboard packet");
|
|
|
|
|
drop(tx);
|
|
|
|
|
|
|
|
|
|
let outbound = tokio_stream::wrappers::ReceiverStream::new(rx);
|
|
|
|
|
let mut resp = cli
|
|
|
|
|
.stream_keyboard(tonic::Request::new(outbound))
|
|
|
|
|
.await
|
|
|
|
|
.expect("stream keyboard");
|
|
|
|
|
let echoed = resp
|
|
|
|
|
.get_mut()
|
|
|
|
|
.message()
|
|
|
|
|
.await
|
|
|
|
|
.expect("grpc result")
|
|
|
|
|
.expect("echo packet");
|
|
|
|
|
assert_eq!(echoed.data, vec![11, 12, 13, 14, 15, 16, 17, 18]);
|
|
|
|
|
|
|
|
|
|
server.abort();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn stream_mouse_recovers_when_hid_write_fails() {
|
|
|
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
|
|
|
rt.block_on(async {
|
|
|
|
|
let (_dir, handler) = build_handler_for_tests_with_modes(true, false);
|
|
|
|
|
|
|
|
|
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
|
|
|
|
|
let addr = listener.local_addr().expect("addr");
|
|
|
|
|
drop(listener);
|
|
|
|
|
|
|
|
|
|
let server = tokio::spawn(async move {
|
|
|
|
|
let _ = tonic::transport::Server::builder()
|
|
|
|
|
.add_service(RelayServer::new(handler))
|
|
|
|
|
.serve(addr)
|
|
|
|
|
.await;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let channel = connect_with_retry(addr).await;
|
|
|
|
|
let mut cli = RelayClient::new(channel);
|
|
|
|
|
let (tx, rx) = tokio::sync::mpsc::channel(4);
|
|
|
|
|
tx.send(MouseReport {
|
|
|
|
|
data: vec![21, 22, 23, 24, 25, 26, 27, 28],
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("send mouse packet");
|
|
|
|
|
drop(tx);
|
|
|
|
|
|
|
|
|
|
let outbound = tokio_stream::wrappers::ReceiverStream::new(rx);
|
|
|
|
|
let mut resp = cli
|
|
|
|
|
.stream_mouse(tonic::Request::new(outbound))
|
|
|
|
|
.await
|
|
|
|
|
.expect("stream mouse");
|
|
|
|
|
let echoed = resp
|
|
|
|
|
.get_mut()
|
|
|
|
|
.message()
|
|
|
|
|
.await
|
|
|
|
|
.expect("grpc result")
|
|
|
|
|
.expect("echo packet");
|
|
|
|
|
assert_eq!(echoed.data, vec![21, 22, 23, 24, 25, 26, 27, 28]);
|
|
|
|
|
|
|
|
|
|
server.abort();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn stream_microphone_returns_internal_error_without_uac_device() {
|
|
|
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
|
|
|
rt.block_on(async {
|
|
|
|
|
let (_dir, handler) = build_handler_for_tests();
|
|
|
|
|
|
|
|
|
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
|
|
|
|
|
let addr = listener.local_addr().expect("addr");
|
|
|
|
|
drop(listener);
|
|
|
|
|
|
|
|
|
|
let server = tokio::spawn(async move {
|
|
|
|
|
let _ = tonic::transport::Server::builder()
|
|
|
|
|
.add_service(RelayServer::new(handler))
|
|
|
|
|
.serve(addr)
|
|
|
|
|
.await;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let channel = connect_with_retry(addr).await;
|
|
|
|
|
let mut cli = RelayClient::new(channel);
|
|
|
|
|
let (_tx, rx) = tokio::sync::mpsc::channel::<AudioPacket>(4);
|
|
|
|
|
let outbound = tokio_stream::wrappers::ReceiverStream::new(rx);
|
|
|
|
|
let err = cli
|
|
|
|
|
.stream_microphone(tonic::Request::new(outbound))
|
|
|
|
|
.await
|
|
|
|
|
.expect_err("missing UAC sink should fail stream setup");
|
|
|
|
|
assert_eq!(err.code(), tonic::Code::Internal);
|
|
|
|
|
|
|
|
|
|
server.abort();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn stream_camera_reports_error_or_terminates_cleanly_without_camera_hardware() {
|
|
|
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
|
|
|
rt.block_on(async {
|
|
|
|
|
let (_dir, handler) = build_handler_for_tests();
|
|
|
|
|
|
|
|
|
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
|
|
|
|
|
let addr = listener.local_addr().expect("addr");
|
|
|
|
|
drop(listener);
|
|
|
|
|
|
|
|
|
|
let server = tokio::spawn(async move {
|
|
|
|
|
let _ = tonic::transport::Server::builder()
|
|
|
|
|
.add_service(RelayServer::new(handler))
|
|
|
|
|
.serve(addr)
|
|
|
|
|
.await;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let channel = connect_with_retry(addr).await;
|
|
|
|
|
let mut cli = RelayClient::new(channel);
|
|
|
|
|
let (tx, rx) = tokio::sync::mpsc::channel(4);
|
|
|
|
|
tx.send(VideoPacket {
|
|
|
|
|
id: 2,
|
|
|
|
|
pts: 1,
|
|
|
|
|
data: vec![0, 1, 2, 3],
|
2026-04-16 21:18:34 -03:00
|
|
|
..Default::default()
|
2026-04-14 23:03:18 -03:00
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("send camera packet");
|
|
|
|
|
drop(tx);
|
|
|
|
|
|
|
|
|
|
let outbound = tokio_stream::wrappers::ReceiverStream::new(rx);
|
|
|
|
|
let result = cli.stream_camera(tonic::Request::new(outbound)).await;
|
|
|
|
|
match result {
|
|
|
|
|
Ok(mut stream) => {
|
|
|
|
|
let _ = stream.get_mut().message().await;
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
assert!(
|
|
|
|
|
matches!(
|
|
|
|
|
err.code(),
|
|
|
|
|
tonic::Code::Internal | tonic::Code::Unavailable | tonic::Code::Unknown
|
|
|
|
|
),
|
|
|
|
|
"unexpected camera stream error code: {}",
|
|
|
|
|
err.code()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
server.abort();
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-04-14 23:35:29 -03:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn reset_usb_succeeds_with_fake_cycle_and_override_hid_paths() {
|
|
|
|
|
let dir = tempdir().expect("tempdir");
|
|
|
|
|
let hid_dir = dir.path().join("hid");
|
|
|
|
|
std::fs::create_dir_all(&hid_dir).expect("create hid dir");
|
|
|
|
|
std::fs::write(hid_dir.join("hidg0"), "").expect("create hidg0");
|
|
|
|
|
std::fs::write(hid_dir.join("hidg1"), "").expect("create hidg1");
|
|
|
|
|
build_fake_gadget_tree(dir.path(), "fake-ctrl.usb", "lesavka", "configured");
|
|
|
|
|
|
|
|
|
|
with_fake_gadget_roots(&dir.path().join("sys"), &dir.path().join("cfg"), || {
|
|
|
|
|
with_var(
|
|
|
|
|
"LESAVKA_HID_DIR",
|
|
|
|
|
Some(hid_dir.to_string_lossy().to_string()),
|
|
|
|
|
|| {
|
|
|
|
|
let kb = tokio::fs::File::from_std(
|
|
|
|
|
std::fs::OpenOptions::new()
|
|
|
|
|
.read(true)
|
|
|
|
|
.write(true)
|
|
|
|
|
.open(hid_dir.join("hidg0"))
|
|
|
|
|
.expect("open hidg0"),
|
|
|
|
|
);
|
|
|
|
|
let ms = tokio::fs::File::from_std(
|
|
|
|
|
std::fs::OpenOptions::new()
|
|
|
|
|
.read(true)
|
|
|
|
|
.write(true)
|
|
|
|
|
.open(hid_dir.join("hidg1"))
|
|
|
|
|
.expect("open hidg1"),
|
|
|
|
|
);
|
|
|
|
|
let handler = Handler {
|
2026-04-21 13:08:20 -03:00
|
|
|
kb: std::sync::Arc::new(tokio::sync::Mutex::new(Some(kb))),
|
|
|
|
|
ms: std::sync::Arc::new(tokio::sync::Mutex::new(Some(ms))),
|
2026-04-14 23:35:29 -03:00
|
|
|
gadget: UsbGadget::new("lesavka"),
|
|
|
|
|
did_cycle: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
|
|
|
|
camera_rt: std::sync::Arc::new(CameraRuntime::new()),
|
|
|
|
|
capture_power: CapturePowerManager::new(),
|
2026-04-19 04:24:27 -03:00
|
|
|
eye_hubs: std::sync::Arc::new(tokio::sync::Mutex::new(
|
|
|
|
|
std::collections::HashMap::new(),
|
|
|
|
|
)),
|
2026-04-14 23:35:29 -03:00
|
|
|
};
|
|
|
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
|
|
|
let reply = rt
|
|
|
|
|
.block_on(async { handler.reset_usb(tonic::Request::new(Empty {})).await })
|
|
|
|
|
.expect("reset usb should succeed on fake gadget tree")
|
|
|
|
|
.into_inner();
|
|
|
|
|
assert!(reply.ok);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-04-19 04:24:27 -03:00
|
|
|
fn shared_eye_hub_forwards_inner_packets() {
|
2026-04-14 23:35:29 -03:00
|
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
|
|
|
rt.block_on(async {
|
|
|
|
|
let lease = CapturePowerManager::new().acquire().await;
|
|
|
|
|
let packet = VideoPacket {
|
|
|
|
|
id: 2,
|
|
|
|
|
pts: 42,
|
|
|
|
|
data: vec![9, 8, 7],
|
2026-04-16 21:18:34 -03:00
|
|
|
..Default::default()
|
2026-04-14 23:35:29 -03:00
|
|
|
};
|
2026-04-19 04:24:27 -03:00
|
|
|
let hub = EyeHub::spawn(stream::iter(vec![Ok(packet.clone())]), lease);
|
|
|
|
|
hub.subscribers
|
|
|
|
|
.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
|
|
|
|
|
let mut rx = hub.tx.subscribe();
|
|
|
|
|
let observed = rx.recv().await.expect("hub packet");
|
2026-04-14 23:35:29 -03:00
|
|
|
assert_eq!(observed.id, packet.id);
|
|
|
|
|
assert_eq!(observed.pts, packet.pts);
|
|
|
|
|
assert_eq!(observed.data, packet.data);
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-04-19 14:14:14 -03:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn conflicting_eye_hubs_for_the_same_source_are_pruned_before_reopen() {
|
|
|
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
|
|
|
rt.block_on(async {
|
|
|
|
|
let requested_key = EyeHubKey {
|
|
|
|
|
source_id: 1,
|
|
|
|
|
requested_width: 1280,
|
|
|
|
|
requested_height: 720,
|
|
|
|
|
requested_fps: 60,
|
|
|
|
|
};
|
|
|
|
|
let stale_same_source_key = EyeHubKey {
|
|
|
|
|
source_id: 1,
|
|
|
|
|
requested_width: 1920,
|
|
|
|
|
requested_height: 1080,
|
|
|
|
|
requested_fps: 60,
|
|
|
|
|
};
|
|
|
|
|
let keep_other_source_key = EyeHubKey {
|
|
|
|
|
source_id: 0,
|
|
|
|
|
requested_width: 1920,
|
|
|
|
|
requested_height: 1080,
|
|
|
|
|
requested_fps: 60,
|
|
|
|
|
};
|
|
|
|
|
let stale_same_source = EyeHub::spawn(
|
|
|
|
|
stream::pending::<Result<VideoPacket, tonic::Status>>(),
|
|
|
|
|
CapturePowerManager::new().acquire().await,
|
|
|
|
|
);
|
|
|
|
|
let stopped_other_source = EyeHub::spawn(
|
|
|
|
|
stream::pending::<Result<VideoPacket, tonic::Status>>(),
|
|
|
|
|
CapturePowerManager::new().acquire().await,
|
|
|
|
|
);
|
|
|
|
|
stopped_other_source.shutdown();
|
|
|
|
|
let keep_other_source = EyeHub::spawn(
|
|
|
|
|
stream::pending::<Result<VideoPacket, tonic::Status>>(),
|
|
|
|
|
CapturePowerManager::new().acquire().await,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let mut hubs = std::collections::HashMap::new();
|
|
|
|
|
hubs.insert(stale_same_source_key, stale_same_source.clone());
|
|
|
|
|
hubs.insert(
|
|
|
|
|
EyeHubKey {
|
|
|
|
|
source_id: 0,
|
|
|
|
|
requested_width: 1280,
|
|
|
|
|
requested_height: 720,
|
|
|
|
|
requested_fps: 60,
|
|
|
|
|
},
|
|
|
|
|
stopped_other_source,
|
|
|
|
|
);
|
|
|
|
|
hubs.insert(keep_other_source_key, keep_other_source.clone());
|
|
|
|
|
|
|
|
|
|
let removed = take_conflicting_eye_hubs(&mut hubs, requested_key);
|
|
|
|
|
|
|
|
|
|
assert_eq!(removed.len(), 2);
|
|
|
|
|
assert!(!hubs.contains_key(&stale_same_source_key));
|
|
|
|
|
assert!(hubs.contains_key(&keep_other_source_key));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn eye_hub_shutdown_marks_the_hub_as_not_running() {
|
|
|
|
|
let rt = tokio::runtime::Runtime::new().expect("runtime");
|
|
|
|
|
rt.block_on(async {
|
|
|
|
|
let hub = EyeHub::spawn(
|
|
|
|
|
stream::pending::<Result<VideoPacket, tonic::Status>>(),
|
|
|
|
|
CapturePowerManager::new().acquire().await,
|
|
|
|
|
);
|
|
|
|
|
assert!(hub.running.load(std::sync::atomic::Ordering::Relaxed));
|
|
|
|
|
hub.shutdown();
|
|
|
|
|
assert!(!hub.running.load(std::sync::atomic::Ordering::Relaxed));
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-04-13 02:52:32 -03:00
|
|
|
}
|