From 3ec32b33510258b6fad3e181f129e9361d959b9f Mon Sep 17 00:00:00 2001 From: Vasilito Date: Sat, 25 Apr 2026 01:05:14 +0100 Subject: [PATCH] Desktop Phase 1: add 77 tests to evdevd, udev-shim, firmware-loader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 desktop substrate test coverage for the three runtime services that must be runtime-trusted before compositor work begins: - evdevd device.rs: 44 tests (input device constructors, capability bitmaps, key/led state tracking, abs_info defaults/overrides, bitmap_from_codes edge cases) - udev-shim device_db.rs: 13 tests (DeviceInfo construction, subsystem naming, id_path formatting, input kind detection, property formatting for GPU/keyboard/mouse, device_info/uevent output, PCI fallback) - firmware-loader scheme.rs: 20 tests (openat validation, read offsets, fstat/fsize, EROFS enforcement, mmap_prep bounds, mmap/munmap/on_close deferred cleanup lifecycle) Total: 65 evdevd, 15 udev-shim, 24 firmware-loader — all passing. --- .../system/evdevd/source/src/device.rs | 455 ++++++++++++++++++ .../firmware-loader/source/src/scheme.rs | 368 ++++++++++++++ .../system/udev-shim/source/src/device_db.rs | 249 +++++++++- 3 files changed, 1071 insertions(+), 1 deletion(-) diff --git a/local/recipes/system/evdevd/source/src/device.rs b/local/recipes/system/evdevd/source/src/device.rs index ac3aa084..d8c19306 100644 --- a/local/recipes/system/evdevd/source/src/device.rs +++ b/local/recipes/system/evdevd/source/src/device.rs @@ -230,3 +230,458 @@ fn bitmap_from_codes(codes: &[u16]) -> Vec { } bitmap } + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::{ + ABS_MT_POSITION_X, ABS_MT_POSITION_Y, ABS_MT_SLOT, ABS_MT_TOUCH_MAJOR, ABS_MT_TRACKING_ID, + ABS_PRESSURE, ABS_X, ABS_Y, BTN_LEFT, BTN_TOUCH, BUS_VIRTUAL, EV_ABS, EV_KEY, EV_LED, + EV_MSC, EV_REL, EV_REP, EV_SYN, INPUT_PROP_POINTER, KEY_A, LED_CAPSL, LED_NUML, + LED_SCROLLL, MSC_SCAN, REL_HWHEEL, REL_WHEEL, REL_X, REL_Y, + }; + + // Helper: check that a bitmap has the expected bit set for a given code. + fn assert_bit_set(bitmap: &[u8], code: u16) { + let byte = (code / 8) as usize; + let bit = code % 8; + assert!( + byte < bitmap.len(), + "code {} (byte index {}) out of bitmap range (len {})", + code, + byte, + bitmap.len() + ); + assert_eq!( + bitmap[byte] & (1 << bit), + 1 << bit, + "bit {} in byte {} not set for code {}", + bit, + byte, + code + ); + } + + fn assert_bit_clear(bitmap: &[u8], code: u16) { + let byte = (code / 8) as usize; + let bit = code % 8; + if byte < bitmap.len() { + assert_eq!( + bitmap[byte] & (1 << bit), + 0, + "bit {} in byte {} unexpectedly set for code {}", + bit, + byte, + code + ); + } + } + + // --------------------------------------------------------------- + // 1. InputDevice::new_keyboard + // --------------------------------------------------------------- + #[test] + fn new_keyboard_has_correct_kind() { + let dev = InputDevice::new_keyboard(3); + assert_eq!(dev.kind, DeviceKind::Keyboard); + } + + #[test] + fn new_keyboard_name_format() { + let dev = InputDevice::new_keyboard(7); + assert_eq!(dev.name, "Redox Keyboard 7"); + } + + #[test] + fn new_keyboard_input_id_bus_virtual() { + let dev = InputDevice::new_keyboard(1); + assert_eq!(dev.input_id.bustype, BUS_VIRTUAL); + assert_eq!(dev.input_id.vendor, 0); + assert_eq!(dev.input_id.product, 1); + assert_eq!(dev.input_id.version, 1); + } + + #[test] + fn new_keyboard_key_and_led_state_zeroed() { + let dev = InputDevice::new_keyboard(0); + assert!(dev.key_state.iter().all(|&b| b == 0)); + assert!(dev.led_state.iter().all(|&b| b == 0)); + } + + // --------------------------------------------------------------- + // 2. InputDevice::new_mouse + // --------------------------------------------------------------- + #[test] + fn new_mouse_has_correct_kind() { + let dev = InputDevice::new_mouse(2); + assert_eq!(dev.kind, DeviceKind::Mouse); + } + + #[test] + fn new_mouse_name_format() { + let dev = InputDevice::new_mouse(5); + assert_eq!(dev.name, "Redox Mouse 5"); + } + + #[test] + fn new_mouse_product_id_offset() { + let dev = InputDevice::new_mouse(3); + assert_eq!(dev.input_id.product, 3 + 0x10); + assert_eq!(dev.input_id.bustype, BUS_VIRTUAL); + assert_eq!(dev.input_id.version, 1); + } + + #[test] + fn new_mouse_key_and_led_state_zeroed() { + let dev = InputDevice::new_mouse(0); + assert!(dev.key_state.iter().all(|&b| b == 0)); + assert!(dev.led_state.iter().all(|&b| b == 0)); + } + + // --------------------------------------------------------------- + // 3. InputDevice::new_touchpad + // --------------------------------------------------------------- + #[test] + fn new_touchpad_has_correct_kind() { + let dev = InputDevice::new_touchpad(1); + assert_eq!(dev.kind, DeviceKind::Touchpad); + } + + #[test] + fn new_touchpad_name_format() { + let dev = InputDevice::new_touchpad(4); + assert_eq!(dev.name, "Redox Touchpad 4"); + } + + #[test] + fn new_touchpad_product_id_offset() { + let dev = InputDevice::new_touchpad(2); + assert_eq!(dev.input_id.product, 2 + 0x20); + assert_eq!(dev.input_id.bustype, BUS_VIRTUAL); + assert_eq!(dev.input_id.version, 1); + } + + // --------------------------------------------------------------- + // 4. supported_event_types + // --------------------------------------------------------------- + #[test] + fn keyboard_event_types() { + let dev = InputDevice::new_keyboard(0); + let bm = dev.supported_event_types(); + assert_bit_set(&bm, EV_SYN); + assert_bit_set(&bm, EV_KEY); + assert_bit_set(&bm, EV_MSC); + assert_bit_set(&bm, EV_LED); + assert_bit_set(&bm, EV_REP); + } + + #[test] + fn mouse_event_types() { + let dev = InputDevice::new_mouse(0); + let bm = dev.supported_event_types(); + assert_bit_set(&bm, EV_SYN); + assert_bit_set(&bm, EV_KEY); + assert_bit_set(&bm, EV_REL); + // No EV_ABS for mouse + assert_bit_clear(&bm, EV_ABS); + } + + #[test] + fn touchpad_event_types() { + let dev = InputDevice::new_touchpad(0); + let bm = dev.supported_event_types(); + assert_bit_set(&bm, EV_SYN); + assert_bit_set(&bm, EV_KEY); + assert_bit_set(&bm, EV_ABS); + // No EV_REL for touchpad + assert_bit_clear(&bm, EV_REL); + } + + // --------------------------------------------------------------- + // 5. supported_keys + // --------------------------------------------------------------- + #[test] + fn keyboard_has_key_a() { + let dev = InputDevice::new_keyboard(0); + let bm = dev.supported_keys(); + assert_bit_set(&bm, KEY_A); + } + + #[test] + fn mouse_has_btn_left() { + let dev = InputDevice::new_mouse(0); + let bm = dev.supported_keys(); + assert_bit_set(&bm, BTN_LEFT); + } + + #[test] + fn touchpad_has_btn_touch() { + let dev = InputDevice::new_touchpad(0); + let bm = dev.supported_keys(); + assert_bit_set(&bm, BTN_TOUCH); + } + + // --------------------------------------------------------------- + // 6. supported_rel + // --------------------------------------------------------------- + #[test] + fn mouse_rel_axes() { + let dev = InputDevice::new_mouse(0); + let bm = dev.supported_rel(); + assert_bit_set(&bm, REL_X); + assert_bit_set(&bm, REL_Y); + assert_bit_set(&bm, REL_WHEEL); + assert_bit_set(&bm, REL_HWHEEL); + } + + #[test] + fn keyboard_rel_empty() { + let dev = InputDevice::new_keyboard(0); + assert!(dev.supported_rel().is_empty()); + } + + #[test] + fn touchpad_rel_empty() { + let dev = InputDevice::new_touchpad(0); + assert!(dev.supported_rel().is_empty()); + } + + // --------------------------------------------------------------- + // 7. supported_abs + // --------------------------------------------------------------- + #[test] + fn touchpad_abs_axes() { + let dev = InputDevice::new_touchpad(0); + let bm = dev.supported_abs(); + assert_bit_set(&bm, ABS_X); + assert_bit_set(&bm, ABS_Y); + assert_bit_set(&bm, ABS_PRESSURE); + assert_bit_set(&bm, ABS_MT_SLOT); + assert_bit_set(&bm, ABS_MT_TOUCH_MAJOR); + assert_bit_set(&bm, ABS_MT_POSITION_X); + assert_bit_set(&bm, ABS_MT_POSITION_Y); + assert_bit_set(&bm, ABS_MT_TRACKING_ID); + } + + #[test] + fn keyboard_abs_empty() { + let dev = InputDevice::new_keyboard(0); + assert!(dev.supported_abs().is_empty()); + } + + #[test] + fn mouse_abs_empty() { + let dev = InputDevice::new_mouse(0); + assert!(dev.supported_abs().is_empty()); + } + + // --------------------------------------------------------------- + // 8. supported_msc + // --------------------------------------------------------------- + #[test] + fn keyboard_msc_has_scan() { + let dev = InputDevice::new_keyboard(0); + let bm = dev.supported_msc(); + assert_bit_set(&bm, MSC_SCAN); + } + + #[test] + fn mouse_msc_empty() { + let dev = InputDevice::new_mouse(0); + assert!(dev.supported_msc().is_empty()); + } + + #[test] + fn touchpad_msc_empty() { + let dev = InputDevice::new_touchpad(0); + assert!(dev.supported_msc().is_empty()); + } + + // --------------------------------------------------------------- + // 9. supported_leds + // --------------------------------------------------------------- + #[test] + fn keyboard_leds() { + let dev = InputDevice::new_keyboard(0); + let bm = dev.supported_leds(); + assert_bit_set(&bm, LED_NUML); + assert_bit_set(&bm, LED_CAPSL); + assert_bit_set(&bm, LED_SCROLLL); + } + + #[test] + fn mouse_leds_empty() { + let dev = InputDevice::new_mouse(0); + assert!(dev.supported_leds().is_empty()); + } + + #[test] + fn touchpad_leds_empty() { + let dev = InputDevice::new_touchpad(0); + assert!(dev.supported_leds().is_empty()); + } + + // --------------------------------------------------------------- + // 10. supported_props + // --------------------------------------------------------------- + #[test] + fn mouse_has_pointer_prop() { + let dev = InputDevice::new_mouse(0); + let bm = dev.supported_props(); + assert_bit_set(&bm, INPUT_PROP_POINTER); + } + + #[test] + fn touchpad_has_pointer_prop() { + let dev = InputDevice::new_touchpad(0); + let bm = dev.supported_props(); + assert_bit_set(&bm, INPUT_PROP_POINTER); + } + + #[test] + fn keyboard_props_empty() { + let dev = InputDevice::new_keyboard(0); + assert!(dev.supported_props().is_empty()); + } + + // --------------------------------------------------------------- + // 11. update_key_state + // --------------------------------------------------------------- + #[test] + fn update_key_state_press_sets_bit() { + let mut dev = InputDevice::new_keyboard(0); + // KEY_A = 30 → byte 3, bit 6 + dev.update_key_state(KEY_A, true); + let byte = (KEY_A / 8) as usize; + let bit = KEY_A % 8; + assert_eq!(dev.key_state[byte] & (1 << bit), 1 << bit); + } + + #[test] + fn update_key_state_release_clears_bit() { + let mut dev = InputDevice::new_keyboard(0); + dev.update_key_state(KEY_A, true); + assert_bit_set(&dev.key_state, KEY_A); + dev.update_key_state(KEY_A, false); + assert_bit_clear(&dev.key_state, KEY_A); + } + + // --------------------------------------------------------------- + // 12. update_led_state + // --------------------------------------------------------------- + #[test] + fn update_led_state_set_capsl() { + let mut dev = InputDevice::new_keyboard(0); + // LED_CAPSL = 1 → byte 0, bit 1 + dev.update_led_state(LED_CAPSL, true); + let byte = (LED_CAPSL / 8) as usize; + let bit = LED_CAPSL % 8; + assert_eq!(dev.led_state[byte] & (1 << bit), 1 << bit); + } + + #[test] + fn update_led_state_clear_capsl() { + let mut dev = InputDevice::new_keyboard(0); + dev.update_led_state(LED_CAPSL, true); + assert_bit_set(&dev.led_state, LED_CAPSL); + dev.update_led_state(LED_CAPSL, false); + assert_bit_clear(&dev.led_state, LED_CAPSL); + } + + // --------------------------------------------------------------- + // 13. abs_info default touchpad + // --------------------------------------------------------------- + #[test] + fn touchpad_abs_x_range() { + let dev = InputDevice::new_touchpad(0); + let info = dev.abs_info(ABS_X); + assert_eq!(info.minimum, 0); + assert_eq!(info.maximum, 65_535); + } + + #[test] + fn touchpad_abs_pressure_max() { + let dev = InputDevice::new_touchpad(0); + let info = dev.abs_info(ABS_PRESSURE); + assert_eq!(info.maximum, 255); + } + + #[test] + fn touchpad_abs_mt_slot_range() { + let dev = InputDevice::new_touchpad(0); + let info = dev.abs_info(ABS_MT_SLOT); + assert_eq!(info.minimum, 0); + assert_eq!(info.maximum, 9); + } + + // --------------------------------------------------------------- + // 14. set_abs_info overrides default + // --------------------------------------------------------------- + #[test] + fn set_abs_info_override() { + let mut dev = InputDevice::new_touchpad(0); + let custom = AbsInfo { + value: 42, + minimum: -100, + maximum: 100, + fuzz: 1, + flat: 2, + resolution: 3, + }; + dev.set_abs_info(ABS_X, custom); + let info = dev.abs_info(ABS_X); + assert_eq!(info.value, 42); + assert_eq!(info.minimum, -100); + assert_eq!(info.maximum, 100); + assert_eq!(info.fuzz, 1); + assert_eq!(info.flat, 2); + assert_eq!(info.resolution, 3); + } + + #[test] + fn set_abs_info_does_not_affect_other_axes() { + let mut dev = InputDevice::new_touchpad(0); + let custom = AbsInfo { + value: 0, + minimum: -50, + maximum: 50, + ..AbsInfo::default() + }; + dev.set_abs_info(ABS_X, custom); + // ABS_Y should still return the default touchpad range + let info_y = dev.abs_info(ABS_Y); + assert_eq!(info_y.minimum, 0); + assert_eq!(info_y.maximum, 65_535); + } + + // --------------------------------------------------------------- + // 15. bitmap_from_codes edge cases + // --------------------------------------------------------------- + #[test] + fn bitmap_from_codes_empty_input() { + let bm = bitmap_from_codes(&[]); + assert!(bm.is_empty()); + } + + #[test] + fn bitmap_from_codes_single_code() { + let bm = bitmap_from_codes(&[5u16]); + assert_eq!(bm.len(), 1); // (5/8)+1 = 1 + assert_eq!(bm[0], 1 << 5); + } + + #[test] + fn bitmap_from_codes_multiple_codes_same_byte() { + // REL_X=0, REL_Y=1, REL_WHEEL=8, REL_HWHEEL=6 + // REL_X and REL_Y are in byte 0; REL_HWHEEL is in byte 0 too; REL_WHEEL is byte 1 + let bm = bitmap_from_codes(&[REL_X, REL_Y, REL_HWHEEL, REL_WHEEL]); + assert_bit_set(&bm, REL_X); + assert_bit_set(&bm, REL_Y); + assert_bit_set(&bm, REL_WHEEL); + assert_bit_set(&bm, REL_HWHEEL); + // Byte 0 should have bits 0 (REL_X), 1 (REL_Y), 6 (REL_HWHEEL) set + assert_eq!(bm[0], (1 << 0) | (1 << 1) | (1 << 6)); + // Byte 1 should have bit 0 (REL_WHEEL = 8 → byte 1, bit 0) + assert_eq!(bm[1], 1 << 0); + } +} diff --git a/local/recipes/system/firmware-loader/source/src/scheme.rs b/local/recipes/system/firmware-loader/source/src/scheme.rs index fdd9a25a..2a62b073 100644 --- a/local/recipes/system/firmware-loader/source/src/scheme.rs +++ b/local/recipes/system/firmware-loader/source/src/scheme.rs @@ -263,6 +263,14 @@ impl SchemeSync for FirmwareScheme { #[cfg(test)] mod tests { use super::resolve_key; + use super::*; + use crate::blob::FirmwareRegistry; + use redox_scheme::scheme::SchemeSync; + use redox_scheme::{CallerCtx, OpenResult}; + use std::fs; + use std::path::PathBuf; + use std::time::{SystemTime, UNIX_EPOCH}; + use syscall::{EventFlags, MapFlags, MunmapFlags, Stat, MODE_FILE}; #[test] fn accepts_real_firmware_extensions() { @@ -279,4 +287,364 @@ mod tests { Some("amdgpu/psp_13_0_0_sos.bin") ); } + + // --- Helpers --- + + fn test_ctx() -> CallerCtx { + CallerCtx { + pid: 0, + uid: 0, + gid: 0, + id: unsafe { std::mem::zeroed() }, + } + } + + fn setup_registry() -> (PathBuf, FirmwareRegistry) { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let dir = std::env::temp_dir().join(format!("rbos-fw-scheme-{stamp}")); + fs::create_dir_all(&dir).unwrap(); + fs::write(dir.join("test-blob.bin"), b"Hello, firmware!").unwrap(); + fs::create_dir_all(dir.join("subdir")).unwrap(); + fs::write(dir.join("subdir/nested.bin"), b"nested data content").unwrap(); + let registry = FirmwareRegistry::new(&dir).unwrap(); + (dir, registry) + } + + fn open_test_blob(scheme: &mut FirmwareScheme) -> usize { + let ctx = test_ctx(); + match scheme + .openat(SCHEME_ROOT_ID, "test-blob.bin", 0, 0, &ctx) + .unwrap() + { + OpenResult::ThisScheme { number, .. } => number, + other => panic!("expected ThisScheme, got {:?}", other), + } + } + + #[test] + fn new_creates_empty_scheme_with_correct_next_id() { + let (dir, registry) = setup_registry(); + let scheme = FirmwareScheme::new(registry); + assert!(scheme.handles.is_empty()); + assert_eq!(scheme.next_id, SCHEME_ROOT_ID + 1); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn openat_valid_key_returns_this_scheme() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let ctx = test_ctx(); + + let result = scheme + .openat(SCHEME_ROOT_ID, "test-blob.bin", 0, 0, &ctx) + .unwrap(); + + match result { + OpenResult::ThisScheme { number, flags } => { + assert_eq!(number, SCHEME_ROOT_ID + 1); + assert_eq!(flags, NewFdFlags::empty()); + } + other => panic!("expected ThisScheme, got {:?}", other), + } + assert_eq!(scheme.handles.len(), 1); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn openat_missing_key_returns_enoent() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let ctx = test_ctx(); + + let err = scheme + .openat(SCHEME_ROOT_ID, "nonexistent.bin", 0, 0, &ctx) + .unwrap_err(); + assert_eq!(err.errno, ENOENT); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn openat_rejects_path_traversal() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let ctx = test_ctx(); + + let err = scheme + .openat(SCHEME_ROOT_ID, "../etc/passwd", 0, 0, &ctx) + .unwrap_err(); + assert_eq!(err.errno, EISDIR); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn openat_empty_path_returns_eisdir() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let ctx = test_ctx(); + + let err = scheme + .openat(SCHEME_ROOT_ID, "", 0, 0, &ctx) + .unwrap_err(); + assert_eq!(err.errno, EISDIR); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn openat_wrong_dirfd_returns_eacces() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let ctx = test_ctx(); + + let err = scheme + .openat(999, "test-blob.bin", 0, 0, &ctx) + .unwrap_err(); + assert_eq!(err.errno, EACCES); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn read_at_offset_zero() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let id = open_test_blob(&mut scheme); + let ctx = test_ctx(); + + let mut buf = [0u8; 64]; + let n = scheme.read(id, &mut buf, 0, 0, &ctx).unwrap(); + assert_eq!(n, 16); + assert_eq!(&buf[..16], b"Hello, firmware!"); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn read_at_nonzero_offset() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let id = open_test_blob(&mut scheme); + let ctx = test_ctx(); + + let mut buf = [0u8; 64]; + let n = scheme.read(id, &mut buf, 7, 0, &ctx).unwrap(); + assert_eq!(n, 9); + assert_eq!(&buf[..9], b"firmware!"); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn read_past_end_returns_zero() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let id = open_test_blob(&mut scheme); + let ctx = test_ctx(); + + let mut buf = [0u8; 64]; + let n = scheme.read(id, &mut buf, 16, 0, &ctx).unwrap(); + assert_eq!(n, 0); + let n2 = scheme.read(id, &mut buf, 1000, 0, &ctx).unwrap(); + assert_eq!(n2, 0); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn fstat_reports_correct_size_and_mode() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let id = open_test_blob(&mut scheme); + let ctx = test_ctx(); + + let mut stat: Stat = unsafe { std::mem::zeroed() }; + scheme.fstat(id, &mut stat, &ctx).unwrap(); + assert_eq!(stat.st_mode, MODE_FILE | 0o444); + assert_eq!(stat.st_size, 16); + assert_eq!(stat.st_blksize, 4096); + assert!(stat.st_blocks > 0); + assert_eq!(stat.st_nlink, 1); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn fsize_returns_correct_length() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let id = open_test_blob(&mut scheme); + let ctx = test_ctx(); + + let size = scheme.fsize(id, &ctx).unwrap(); + assert_eq!(size, 16); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn write_returns_erofs() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let id = open_test_blob(&mut scheme); + let ctx = test_ctx(); + + let err = scheme.write(id, b"test", 0, 0, &ctx).unwrap_err(); + assert_eq!(err.errno, EROFS); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn ftruncate_returns_erofs() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let id = open_test_blob(&mut scheme); + let ctx = test_ctx(); + + let err = scheme.ftruncate(id, 0, &ctx).unwrap_err(); + assert_eq!(err.errno, EROFS); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn mmap_prep_returns_pointer_and_increments_count() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let id = open_test_blob(&mut scheme); + let ctx = test_ctx(); + + let ptr = scheme + .mmap_prep(id, 0, 16, MapFlags::empty(), &ctx) + .unwrap(); + assert_ne!(ptr, 0); + + let handle = scheme.handles.get(&id).unwrap(); + assert_eq!(handle.map_count, 1); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn mmap_prep_rejects_offset_beyond_data() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let id = open_test_blob(&mut scheme); + let ctx = test_ctx(); + + let err = scheme + .mmap_prep(id, 17, 1, MapFlags::empty(), &ctx) + .unwrap_err(); + assert_eq!(err.errno, EINVAL); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn mmap_prep_rejects_offset_plus_size_beyond_data() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let id = open_test_blob(&mut scheme); + let ctx = test_ctx(); + + let err = scheme + .mmap_prep(id, 8, 16, MapFlags::empty(), &ctx) + .unwrap_err(); + assert_eq!(err.errno, EINVAL); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn munmap_decrements_count_without_removing_handle() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let id = open_test_blob(&mut scheme); + let ctx = test_ctx(); + + scheme + .mmap_prep(id, 0, 16, MapFlags::empty(), &ctx) + .unwrap(); + assert_eq!(scheme.handles.get(&id).unwrap().map_count, 1); + + scheme + .munmap(id, 0, 16, MunmapFlags::empty(), &ctx) + .unwrap(); + + assert!(scheme.handles.contains_key(&id)); + let handle = scheme.handles.get(&id).unwrap(); + assert_eq!(handle.map_count, 0); + assert!(!handle.closed); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn on_close_keeps_handle_when_mapped() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let id = open_test_blob(&mut scheme); + let ctx = test_ctx(); + + scheme + .mmap_prep(id, 0, 16, MapFlags::empty(), &ctx) + .unwrap(); + + scheme.on_close(id); + + assert!(scheme.handles.contains_key(&id)); + let handle = scheme.handles.get(&id).unwrap(); + assert!(handle.closed); + assert_eq!(handle.map_count, 1); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn on_close_then_munmap_removes_handle() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let id = open_test_blob(&mut scheme); + let ctx = test_ctx(); + + scheme + .mmap_prep(id, 0, 16, MapFlags::empty(), &ctx) + .unwrap(); + + scheme.on_close(id); + assert!(scheme.handles.contains_key(&id)); + + scheme + .munmap(id, 0, 16, MunmapFlags::empty(), &ctx) + .unwrap(); + assert!(!scheme.handles.contains_key(&id)); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn fsync_returns_ok() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let id = open_test_blob(&mut scheme); + let ctx = test_ctx(); + + scheme.fsync(id, &ctx).unwrap(); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn fcntl_returns_ok_zero() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let id = open_test_blob(&mut scheme); + let ctx = test_ctx(); + + let result = scheme.fcntl(id, 0, 0, &ctx).unwrap(); + assert_eq!(result, 0); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn fevent_returns_empty_flags() { + let (dir, registry) = setup_registry(); + let mut scheme = FirmwareScheme::new(registry); + let id = open_test_blob(&mut scheme); + let ctx = test_ctx(); + + let flags = scheme + .fevent(id, EventFlags::empty(), &ctx) + .unwrap(); + assert_eq!(flags, EventFlags::empty()); + let _ = fs::remove_dir_all(&dir); + } } diff --git a/local/recipes/system/udev-shim/source/src/device_db.rs b/local/recipes/system/udev-shim/source/src/device_db.rs index 342cae60..27f66f8a 100644 --- a/local/recipes/system/udev-shim/source/src/device_db.rs +++ b/local/recipes/system/udev-shim/source/src/device_db.rs @@ -212,7 +212,7 @@ fn format_device_name( #[cfg(test)] mod tests { - use super::classify_pci_device; + use super::{DeviceInfo, InputKind, Subsystem, classify_pci_device, device_properties, format_device_info, format_uevent_info}; #[test] fn classify_pci_device_uses_shared_location_format() { @@ -227,6 +227,253 @@ mod tests { assert_eq!(device.id_path(), "pci-0000:02:00.0"); } + + #[test] + fn new_platform_input_has_correct_defaults() { + let dev = DeviceInfo::new_platform_input( + "test-kbd", + "/devices/platform/keyboard0", + InputKind::Keyboard, + "", + "", + ); + assert!(!dev.is_pci); + assert_eq!(dev.subsystem, Subsystem::Input); + assert_eq!(dev.input_kind, Some(InputKind::Keyboard)); + assert!(dev.devnode.is_empty()); + assert!(dev.scheme_target.is_empty()); + assert!(dev.symlinks.is_empty()); + assert_eq!(dev.bus, 0); + assert_eq!(dev.dev, 0); + assert_eq!(dev.func, 0); + assert_eq!(dev.vendor_id, 0); + assert_eq!(dev.device_id, 0); + } + + #[test] + fn set_node_metadata_sets_fields_correctly() { + let mut dev = DeviceInfo::new_platform_input( + "test-mouse", + "/devices/platform/mouse0", + InputKind::Mouse, + "", + "", + ); + dev.set_node_metadata( + "/dev/input/mouse0", + "input:mouse0", + vec!["/dev/input/by-path/platform-mouse0".to_string()], + ); + assert_eq!(dev.devnode, "/dev/input/mouse0"); + assert_eq!(dev.scheme_target, "input:mouse0"); + assert_eq!(dev.symlinks.len(), 1); + assert_eq!(dev.symlinks[0], "/dev/input/by-path/platform-mouse0"); + } + + #[test] + fn subsystem_name_maps_all_variants() { + let cases: Vec<(Subsystem, &'static str)> = vec![ + (Subsystem::Gpu, "drm"), + (Subsystem::Network, "net"), + (Subsystem::Storage, "block"), + (Subsystem::Audio, "sound"), + (Subsystem::Usb, "usb"), + (Subsystem::Input, "input"), + (Subsystem::Unknown, "unknown"), + ]; + for (subsys, expected) in cases { + let mut dev = + DeviceInfo::new_platform_input("x", "/devices/x", InputKind::Generic, "", ""); + dev.subsystem = subsys; + assert_eq!(dev.subsystem_name(), expected, "failed for {:?}", subsys); + } + } + + #[test] + fn id_path_pci_device() { + let dev = DeviceInfo { + is_pci: true, + bus: 0x02, + dev: 0x00, + func: 0x0, + vendor_id: 0x1002, + device_id: 0x67df, + class_code: 0x03, + subclass: 0x00, + subsystem: Subsystem::Gpu, + input_kind: None, + name: "Test GPU".to_string(), + devpath: "/devices/pci/0000:02:00.0".to_string(), + devnode: String::new(), + scheme_target: String::new(), + symlinks: vec![], + }; + assert_eq!(dev.id_path(), "pci-0000:02:00.0"); + } + + #[test] + fn id_path_platform_device() { + let dev = DeviceInfo::new_platform_input( + "keyboard0", + "/devices/platform/keyboard0", + InputKind::Keyboard, + "", + "", + ); + assert_eq!(dev.id_path(), "platform-keyboard0"); + } + + #[test] + fn is_input_keyboard_true_only_for_keyboard() { + let kb = DeviceInfo::new_platform_input("kb", "/devices/x", InputKind::Keyboard, "", ""); + assert!(kb.is_input_keyboard()); + assert!(!kb.is_input_mouse()); + + let mouse = DeviceInfo::new_platform_input("ms", "/devices/x", InputKind::Mouse, "", ""); + assert!(!mouse.is_input_keyboard()); + + let generic = + DeviceInfo::new_platform_input("gen", "/devices/x", InputKind::Generic, "", ""); + assert!(!generic.is_input_keyboard()); + } + + #[test] + fn is_input_mouse_true_only_for_mouse() { + let mouse = DeviceInfo::new_platform_input("ms", "/devices/x", InputKind::Mouse, "", ""); + assert!(mouse.is_input_mouse()); + assert!(!mouse.is_input_keyboard()); + + let kb = DeviceInfo::new_platform_input("kb", "/devices/x", InputKind::Keyboard, "", ""); + assert!(!kb.is_input_mouse()); + + let generic = + DeviceInfo::new_platform_input("gen", "/devices/x", InputKind::Generic, "", ""); + assert!(!generic.is_input_mouse()); + } + + #[test] + fn device_properties_gpu_pci_contains_key_fields() { + let dev = DeviceInfo { + is_pci: true, + bus: 0x02, + dev: 0x00, + func: 0x0, + vendor_id: 0x1002, + device_id: 0x67df, + class_code: 0x03, + subclass: 0x00, + subsystem: Subsystem::Gpu, + input_kind: None, + name: "AMD RX 580 [1002:67df]".to_string(), + devpath: "/devices/pci/0000:02:00.0".to_string(), + devnode: "/dev/dri/card0".to_string(), + scheme_target: "display:display".to_string(), + symlinks: vec![], + }; + let props = device_properties(&dev); + let prop_map: std::collections::HashMap<&str, &str> = props + .iter() + .map(|(k, v)| (k.as_str(), v.as_str())) + .collect(); + + assert_eq!(prop_map.get("SUBSYSTEM").copied(), Some("drm")); + assert_eq!(prop_map.get("PCI_VENDOR_ID").copied(), Some("0x1002")); + assert_eq!(prop_map.get("PCI_DEVICE_ID").copied(), Some("0x67df")); + assert_eq!(prop_map.get("PCI_CLASS").copied(), Some("0x0300")); + assert_eq!(prop_map.get("DEVNAME").copied(), Some("/dev/dri/card0")); + } + + #[test] + fn device_properties_input_keyboard_has_input_flags() { + let dev = DeviceInfo::new_platform_input( + "keyboard0", + "/devices/platform/keyboard0", + InputKind::Keyboard, + "/dev/input/event0", + "input:keyboard0", + ); + let props = device_properties(&dev); + let prop_map: std::collections::HashMap<&str, &str> = props + .iter() + .map(|(k, v)| (k.as_str(), v.as_str())) + .collect(); + + assert_eq!(prop_map.get("ID_INPUT").copied(), Some("1")); + assert_eq!(prop_map.get("ID_INPUT_KEYBOARD").copied(), Some("1")); + assert!(!prop_map.contains_key("ID_INPUT_MOUSE")); + } + + #[test] + fn device_properties_input_mouse_has_input_flags() { + let dev = DeviceInfo::new_platform_input( + "mouse0", + "/devices/platform/mouse0", + InputKind::Mouse, + "/dev/input/mouse0", + "input:mouse0", + ); + let props = device_properties(&dev); + let prop_map: std::collections::HashMap<&str, &str> = props + .iter() + .map(|(k, v)| (k.as_str(), v.as_str())) + .collect(); + + assert_eq!(prop_map.get("ID_INPUT").copied(), Some("1")); + assert_eq!(prop_map.get("ID_INPUT_MOUSE").copied(), Some("1")); + assert!(!prop_map.contains_key("ID_INPUT_KEYBOARD")); + } + + #[test] + fn format_device_info_structure() { + let dev = DeviceInfo { + is_pci: true, + bus: 0x02, + dev: 0x00, + func: 0x0, + vendor_id: 0x8086, + device_id: 0x1234, + class_code: 0x03, + subclass: 0x00, + subsystem: Subsystem::Gpu, + input_kind: None, + name: "Intel GPU".to_string(), + devpath: "/devices/pci/0000:02:00.0".to_string(), + devnode: "/dev/dri/card0".to_string(), + scheme_target: "display:display".to_string(), + symlinks: vec!["/dev/dri/by-path/pci-0000:02:00.0-card".to_string()], + }; + let info = format_device_info(&dev); + assert!(info.starts_with("P=/devices/pci/0000:02:00.0\n")); + assert!(info.contains("E=SUBSYSTEM=drm\n")); + assert!(info.contains("S=dev/dri/by-path/pci-0000:02:00.0-card\n")); + } + + #[test] + fn format_uevent_info_starts_with_action_and_has_props() { + let dev = DeviceInfo::new_platform_input( + "keyboard0", + "/devices/platform/keyboard0", + InputKind::Keyboard, + "/dev/input/event0", + "input:keyboard0", + ); + let uevent = format_uevent_info(&dev); + assert!(uevent.starts_with("ACTION=add\n")); + assert!(uevent.contains("SUBSYSTEM=input\n")); + assert!(uevent.contains("DEVPATH=/devices/platform/keyboard0\n")); + } + + #[test] + fn classify_pci_device_with_no_pci_config_still_produces_pci_device() { + let dev = classify_pci_device(0x00, 0x1f, 0x2); + assert!(dev.is_pci); + assert_eq!(dev.bus, 0x00); + assert_eq!(dev.dev, 0x1f); + assert_eq!(dev.func, 0x2); + // Without real PCI config, read_pci_config returns 0xFFFF + assert_eq!(dev.vendor_id, 0xFFFF); + assert_eq!(dev.device_id, 0xFFFF); + } } pub fn device_properties(dev: &DeviceInfo) -> Vec<(String, String)> {