From 0a809464570e6a419662543914dec1dd91059022 Mon Sep 17 00:00:00 2001 From: Vasilito Date: Sat, 25 Apr 2026 01:32:35 +0100 Subject: [PATCH] Desktop Phase 1: add 42 tests to redox-drm scheme/driver and redbear-hwutils --- .../gpu/redox-drm/source/src/driver.rs | 87 +++++ .../gpu/redox-drm/source/src/scheme.rs | 360 ++++++++++++++++++ .../system/redbear-hwutils/source/src/lib.rs | 115 +++++- 3 files changed, 561 insertions(+), 1 deletion(-) diff --git a/local/recipes/gpu/redox-drm/source/src/driver.rs b/local/recipes/gpu/redox-drm/source/src/driver.rs index 9347bcda..dffc5c46 100644 --- a/local/recipes/gpu/redox-drm/source/src/driver.rs +++ b/local/recipes/gpu/redox-drm/source/src/driver.rs @@ -113,3 +113,90 @@ pub trait GpuDriver: Send + Sync { )) } } + +#[cfg(test)] +mod tests { + use std::mem::{discriminant, offset_of, size_of}; + + use super::*; + + #[test] + fn redox_private_cs_submit_size() { + // src_handle(u32) + dst_handle(u32) + src_offset(u64) + dst_offset(u64) + byte_count(u64) + // = 4 + 4 + 8 + 8 + 8 = 32 bytes + assert_eq!(size_of::(), 32); + } + + #[test] + fn redox_private_cs_submit_result_size() { + // seqno(u64) = 8 bytes + assert_eq!(size_of::(), 8); + } + + #[test] + fn redox_private_cs_wait_size() { + // seqno(u64) + timeout_ns(u64) = 16 bytes + assert_eq!(size_of::(), 16); + } + + #[test] + fn redox_private_cs_wait_result_size() { + // completed(bool) + 7 padding + completed_seqno(u64) = 16 bytes + assert_eq!(size_of::(), 16); + } + + #[test] + fn driver_event_vblank_size() { + let event = DriverEvent::Vblank { + crtc_id: 0xDEADBEEF, + count: 0x1234_5678_9ABC_DEF0, + }; + match event { + DriverEvent::Vblank { crtc_id, count } => { + assert_eq!(crtc_id, 0xDEADBEEF); + assert_eq!(count, 0x1234_5678_9ABC_DEF0); + } + DriverEvent::Hotplug { .. } => panic!("expected Vblank, got Hotplug"), + } + let enum_size = size_of::(); + assert!(enum_size > 0, "DriverEvent must be non-zero-sized"); + } + + #[test] + fn driver_event_hotplug_size() { + let event = DriverEvent::Hotplug { + connector_id: 0xCAFEBABE, + }; + match event { + DriverEvent::Hotplug { connector_id } => { + assert_eq!(connector_id, 0xCAFEBABE); + } + DriverEvent::Vblank { .. } => panic!("expected Hotplug, got Vblank"), + } + let vblank = DriverEvent::Vblank { + crtc_id: 0, + count: 0, + }; + let hotplug = DriverEvent::Hotplug { connector_id: 0 }; + assert_ne!( + discriminant(&vblank), + discriminant(&hotplug), + "Vblank and Hotplug must have distinct discriminants" + ); + } + + #[test] + fn redox_private_cs_submit_is_repr_c() { + assert_eq!(offset_of!(RedoxPrivateCsSubmit, src_handle), 0); + assert_eq!(offset_of!(RedoxPrivateCsSubmit, dst_handle), 4); + assert_eq!(offset_of!(RedoxPrivateCsSubmit, src_offset), 8); + assert_eq!(offset_of!(RedoxPrivateCsSubmit, dst_offset), 16); + assert_eq!(offset_of!(RedoxPrivateCsSubmit, byte_count), 24); + } + + #[test] + fn redox_private_cs_wait_is_repr_c() { + assert_eq!(offset_of!(RedoxPrivateCsWait, seqno), 0); + assert_eq!(offset_of!(RedoxPrivateCsWait, timeout_ns), 8); + } +} diff --git a/local/recipes/gpu/redox-drm/source/src/scheme.rs b/local/recipes/gpu/redox-drm/source/src/scheme.rs index 7179841f..582f77c3 100644 --- a/local/recipes/gpu/redox-drm/source/src/scheme.rs +++ b/local/recipes/gpu/redox-drm/source/src/scheme.rs @@ -2168,4 +2168,364 @@ mod tests { assert_eq!(err.errno, EINVAL); } + + #[test] + fn gem_has_other_refs_returns_false_when_only_current_handle_owns_gem() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + + let create = DrmGemCreateWire { + size: 4096, + ..DrmGemCreateWire::default() + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap(); + let created = read_response::(&mut scheme, card); + + let gem_handle = created.handle; + assert!( + !scheme.gem_has_other_refs(card, gem_handle), + "only one handle owns the GEM, so gem_has_other_refs should be false" + ); + } + + #[test] + fn gem_has_other_refs_returns_true_when_another_handle_owns_gem() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card_a = open_card(&mut scheme); + let card_b = open_card(&mut scheme); + + let create = DrmGemCreateWire { + size: 4096, + ..DrmGemCreateWire::default() + }; + write_ioctl(&mut scheme, card_a, DRM_IOCTL_GEM_CREATE, &create).unwrap(); + let created = read_response::(&mut scheme, card_a); + let gem_handle = created.handle; + + let export = DrmPrimeHandleToFdWire { + handle: gem_handle, + flags: 0, + }; + write_ioctl(&mut scheme, card_a, DRM_IOCTL_PRIME_HANDLE_TO_FD, &export).unwrap(); + let exported = read_response::(&mut scheme, card_a); + + let import = DrmPrimeFdToHandleWire { + fd: exported.fd, + _pad: 0, + }; + write_ioctl(&mut scheme, card_b, DRM_IOCTL_PRIME_FD_TO_HANDLE, &import).unwrap(); + let imported = read_response::(&mut scheme, card_b); + + assert!( + scheme.gem_has_other_refs(card_a, imported.handle), + "card_b owns the same GEM, so gem_has_other_refs from card_a should be true" + ); + } + + #[test] + fn gem_is_mapped_returns_false_initially() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + + let create = DrmGemCreateWire { + size: 4096, + ..DrmGemCreateWire::default() + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap(); + let created = read_response::(&mut scheme, card); + + assert!( + !scheme.gem_is_mapped(created.handle), + "freshly created GEM should not be mapped" + ); + } + + #[test] + fn gem_is_mapped_returns_true_after_pin() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + + let create = DrmGemCreateWire { + size: 4096, + ..DrmGemCreateWire::default() + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap(); + let created = read_response::(&mut scheme, card); + let gem_handle = created.handle; + + scheme.pin_mapped_gem(card, gem_handle).unwrap(); + + assert!( + scheme.gem_is_mapped(gem_handle), + "GEM should be mapped after pin_mapped_gem" + ); + } + + #[test] + fn gem_export_refcount_starts_at_zero() { + let scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + + assert_eq!( + scheme.gem_export_refcount(9999), + 0, + "unknown GEM should have refcount 0" + ); + } + + #[test] + fn bump_export_ref_increments_from_zero_to_one() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let gem_handle: GemHandle = 42; + + scheme.bump_export_ref(gem_handle); + + assert_eq!( + scheme.gem_export_refcount(gem_handle), + 1, + "bumping an unknown GEM should set its refcount to 1" + ); + } + + #[test] + fn bump_export_ref_saturates_on_overflow() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let gem_handle: GemHandle = 42; + + scheme.gem_export_refs.insert(gem_handle, usize::MAX); + + scheme.bump_export_ref(gem_handle); + + assert_eq!( + scheme.gem_export_refcount(gem_handle), + usize::MAX, + "saturating add should keep refcount at usize::MAX" + ); + } + + #[test] + fn drop_export_ref_decrements_count() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let gem_handle: GemHandle = 42; + + scheme.bump_export_ref(gem_handle); + scheme.bump_export_ref(gem_handle); + assert_eq!(scheme.gem_export_refcount(gem_handle), 2); + + scheme.drop_export_ref(gem_handle); + + assert_eq!( + scheme.gem_export_refcount(gem_handle), + 1, + "dropping once should decrement from 2 to 1" + ); + } + + #[test] + fn drop_export_ref_removes_entry_when_count_reaches_zero() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let gem_handle: GemHandle = 42; + + scheme.bump_export_ref(gem_handle); + assert_eq!(scheme.gem_export_refcount(gem_handle), 1); + + scheme.drop_export_ref(gem_handle); + + assert_eq!( + scheme.gem_export_refcount(gem_handle), + 0, + "dropping the last ref should remove the entry" + ); + } + + #[test] + fn drop_export_ref_cleans_up_prime_exports() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let gem_handle: GemHandle = 77; + let export_token: u32 = 100; + + scheme.prime_exports.insert(export_token, gem_handle); + scheme.bump_export_ref(gem_handle); + assert_eq!(scheme.gem_export_refcount(gem_handle), 1); + assert!(scheme.prime_exports.contains_key(&export_token)); + + scheme.drop_export_ref(gem_handle); + + assert_eq!(scheme.gem_export_refcount(gem_handle), 0); + assert!( + !scheme.prime_exports.values().any(|&h| h == gem_handle), + "drop_export_ref should clean up prime_exports entries for this GEM" + ); + } + + #[test] + fn gem_can_close_returns_false_when_gem_backs_framebuffer() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + + let create = DrmGemCreateWire { + size: 16384, + ..DrmGemCreateWire::default() + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap(); + let created = read_response::(&mut scheme, card); + let gem_handle = created.handle; + + let addfb = DrmAddFbWire { + width: 64, + height: 64, + pitch: 256, + bpp: 32, + depth: 24, + handle: gem_handle, + fb_id: 0, + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_MODE_ADDFB, &addfb).unwrap(); + let fb_resp = read_response::(&mut scheme, card); + + if let Some(handle) = scheme.handles.get_mut(&card) { + handle.owned_gems.retain(|&h| h != gem_handle); + } + + assert!( + !scheme.gem_can_close(gem_handle), + "GEM backing a framebuffer should not be closeable" + ); + assert!(scheme.fb_registry.contains_key(&fb_resp.fb_id)); + } + + #[test] + fn gem_can_close_returns_false_when_gem_is_mapped() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + + let create = DrmGemCreateWire { + size: 4096, + ..DrmGemCreateWire::default() + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap(); + let created = read_response::(&mut scheme, card); + let gem_handle = created.handle; + + scheme.pin_mapped_gem(card, gem_handle).unwrap(); + + if let Some(handle) = scheme.handles.get_mut(&card) { + handle.owned_gems.retain(|&h| h != gem_handle); + } + + assert!( + !scheme.gem_can_close(gem_handle), + "mapped GEM should not be closeable" + ); + } + + #[test] + fn gem_can_close_returns_true_when_unreferenced() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + + let create = DrmGemCreateWire { + size: 4096, + ..DrmGemCreateWire::default() + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap(); + let created = read_response::(&mut scheme, card); + let gem_handle = created.handle; + + if let Some(handle) = scheme.handles.get_mut(&card) { + handle.owned_gems.retain(|&h| h != gem_handle); + } + + assert!( + scheme.gem_can_close(gem_handle), + "unreferenced, unmapped GEM with no FB or export refs should be closeable" + ); + } + + #[test] + fn allocate_handle_returns_sequential_ids() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + + let id_a = scheme.allocate_handle(NodeKind::Card); + let id_b = scheme.allocate_handle(NodeKind::Card); + + assert!( + id_b > id_a, + "second allocated handle ID ({id_b}) should be greater than first ({id_a})" + ); + } + + #[test] + fn is_fb_active_returns_false_for_unknown_fb() { + let scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + + assert!( + !scheme.is_fb_active(12345), + "unknown fb_id should not be active" + ); + } + + #[test] + fn is_fb_active_returns_true_for_active_crtc_fb() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + + let create = DrmGemCreateWire { + size: 640 * 480 * 4, + ..DrmGemCreateWire::default() + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap(); + let created = read_response::(&mut scheme, card); + let gem_handle = created.handle; + + let addfb = DrmAddFbWire { + width: 640, + height: 480, + pitch: 2560, + bpp: 32, + depth: 24, + handle: gem_handle, + fb_id: 0, + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_MODE_ADDFB, &addfb).unwrap(); + let fb_resp = read_response::(&mut scheme, card); + let fb_id = fb_resp.fb_id; + + let mode = DrmModeWire { + clock: 25200, + hdisplay: 640, + hsync_start: 656, + hsync_end: 752, + htotal: 800, + vdisplay: 480, + vsync_start: 490, + vsync_end: 492, + vtotal: 525, + vrefresh: 60, + ..DrmModeWire::default() + }; + let setcrtc = DrmSetCrtcWire { + crtc_id: 0, + fb_handle: fb_id, + connector_count: 0, + connectors: [0; 8], + mode, + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_MODE_SETCRTC, &setcrtc).unwrap(); + + assert!( + scheme.is_fb_active(fb_id), + "FB programmed on a CRTC should be active" + ); + } + + #[test] + fn validate_gem_create_size_rejects_zero() { + let scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + + let err = scheme + .validate_gem_create_size(0, "test-zero-size") + .unwrap_err(); + + assert_eq!(err.errno, EINVAL, "zero-sized GEM creation should return EINVAL"); + } } diff --git a/local/recipes/system/redbear-hwutils/source/src/lib.rs b/local/recipes/system/redbear-hwutils/source/src/lib.rs index 745ff68a..ea4bf4f9 100644 --- a/local/recipes/system/redbear-hwutils/source/src/lib.rs +++ b/local/recipes/system/redbear-hwutils/source/src/lib.rs @@ -165,7 +165,120 @@ pub fn lookup_pci_device_name(vendor_id: u16, device_id: u16) -> Option #[cfg(test)] mod tests { - use super::parse_pci_id_database; + use super::{describe_usb_device, parse_args, parse_pci_id_database, parse_pci_location}; + + // --- parse_pci_location tests --- + + #[test] + fn parse_pci_location_valid_input() { + let loc = parse_pci_location("0000--00--1f.2").unwrap(); + assert_eq!(loc.segment, 0x0000); + assert_eq!(loc.bus, 0x00); + assert_eq!(loc.device, 0x1f); + assert_eq!(loc.function, 2); + } + + #[test] + fn parse_pci_location_scheme_path_format() { + let loc = parse_pci_location("0003--01--0a.3").unwrap(); + assert_eq!(loc.scheme_path(), "/scheme/pci/0003--01--0a.3"); + } + + #[test] + fn parse_pci_location_display_format() { + let loc = parse_pci_location("00ff--02--1c.0").unwrap(); + assert_eq!(format!("{loc}"), "00ff:02:1c.0"); + } + + #[test] + fn parse_pci_location_missing_double_dash_returns_none() { + assert!(parse_pci_location("0000.00--1f.2").is_none()); + } + + #[test] + fn parse_pci_location_missing_dot_returns_none() { + assert!(parse_pci_location("0000--00--1f-2").is_none()); + } + + #[test] + fn parse_pci_location_non_hex_segment_returns_none() { + assert!(parse_pci_location("zzzz--00--1f.2").is_none()); + } + + #[test] + fn parse_pci_location_empty_string_returns_none() { + assert!(parse_pci_location("").is_none()); + } + + // --- describe_usb_device tests --- + + #[test] + fn describe_usb_device_both_fields() { + assert_eq!( + describe_usb_device(Some("Logitech"), Some("USB Mouse")), + "Logitech USB Mouse" + ); + } + + #[test] + fn describe_usb_device_manufacturer_only() { + assert_eq!(describe_usb_device(Some("Logitech"), None), "Logitech"); + } + + #[test] + fn describe_usb_device_product_only() { + assert_eq!(describe_usb_device(None, Some("USB Mouse")), "USB Mouse"); + } + + #[test] + fn describe_usb_device_both_none() { + assert_eq!(describe_usb_device(None, None), "USB device"); + } + + #[test] + fn describe_usb_device_empty_manufacturer_filtered() { + assert_eq!(describe_usb_device(Some(""), Some("USB Mouse")), "USB Mouse"); + } + + #[test] + fn describe_usb_device_empty_product_filtered() { + assert_eq!(describe_usb_device(Some("Logitech"), Some("")), "Logitech"); + } + + // --- parse_args tests --- + + #[test] + fn parse_args_empty_extras_returns_ok() { + assert!(parse_args("prog", "usage", vec!["prog".to_string()]).is_ok()); + } + + #[test] + fn parse_args_help_flag_returns_err_empty() { + let result = parse_args("prog", "usage text", vec!["prog".to_string(), "--help".to_string()]); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), ""); + } + + #[test] + fn parse_args_h_flag_returns_err_empty() { + let result = parse_args("prog", "usage text", vec!["prog".to_string(), "-h".to_string()]); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), ""); + } + + #[test] + fn parse_args_unknown_argument_returns_err_with_message() { + let result = parse_args( + "prog", + "usage text", + vec!["prog".to_string(), "bogus".to_string()], + ); + assert!(result.is_err()); + let msg = result.unwrap_err(); + assert!(msg.contains("unsupported arguments"), "expected 'unsupported arguments' in: {msg}"); + } + + // --- original pci_id_database tests --- #[test] fn parses_vendor_and_device_entries_from_pci_ids() {