use super::*;
use std::os::fd::{AsRawFd as _, OwnedFd};
static CARD_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn card_guard() -> std::sync::MutexGuard<'static, ()> {
CARD_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn test_card() -> String {
std::env::var("DRMKIT_TEST_CARD").unwrap_or_else(|_| "/dev/dri/card0".to_owned())
}
fn check_master_dependent(outcome: Result<()>, what: &str) {
let require = std::env::var_os("DRMKIT_REQUIRE_MASTER").is_some();
match outcome {
Ok(()) => {}
Err(CoreError::NotMaster) if !require => {
println!("note: {what} skipped -- another client holds DRM master");
}
Err(CoreError::NotMaster) => {
panic!("{what}: not DRM master, but DRMKIT_REQUIRE_MASTER is set");
}
Err(e) => panic!("{what} failed: {e}"),
}
}
fn open_dev_null() -> OwnedFd {
std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/null")
.expect("/dev/null")
.into()
}
#[test]
fn open_invalid_path_returns_error() {
let result = Device::open("/dev/dri/nonexistent_card_999");
assert!(result.is_err());
}
#[test]
fn open_empty_path_returns_error() {
assert!(Device::open("").is_err());
}
#[test]
fn open_non_drm_node_is_rejected() {
let result = Device::open("/dev/null");
assert!(
matches!(result, Err(CoreError::NotADrmDevice { .. })),
"/dev/null is a character device but not a DRM one"
);
}
#[test]
fn borrowed_device_does_not_close_the_descriptor() {
let owned = open_dev_null();
let raw = owned.as_raw_fd();
{
let device = unsafe { Device::from_borrowed_fd(raw) };
assert_eq!(device.raw_fd(), raw);
assert!(!device.owns_fd());
}
assert!(
rustix::fs::fstat(&owned).is_ok(),
"a borrowed device must not close the caller's descriptor"
);
}
#[test]
fn moving_a_borrowed_device_keeps_it_borrowed() {
let owned = open_dev_null();
let raw = owned.as_raw_fd();
{
let source = unsafe { Device::from_borrowed_fd(raw) };
let moved = source;
assert_eq!(moved.raw_fd(), raw);
assert!(!moved.owns_fd());
}
assert!(rustix::fs::fstat(&owned).is_ok());
}
#[test]
fn owned_device_closes_on_drop() {
let open_and_drop = |times: usize| {
for _ in 0..times {
let device = Device::from_owned_fd(open_dev_null());
assert!(device.owns_fd());
drop(device);
}
};
let count = || {
std::fs::read_dir("/proc/self/fd")
.expect("/proc/self/fd")
.count()
};
open_and_drop(16);
let after_few = count();
open_and_drop(240);
let after_many = count();
let growth = after_many.saturating_sub(after_few);
assert!(
growth < 120,
"240 further owning devices grew the fd table by {growth}; \
a device that failed to close would grow it by 240"
);
}
#[test]
fn borrowed_device_leaves_the_descriptor_to_the_caller() {
let mut kept = Vec::new();
for _ in 0..64 {
let owned = open_dev_null();
let raw = owned.as_raw_fd();
let device = unsafe { Device::from_borrowed_fd(raw) };
assert_eq!(device.raw_fd(), raw);
drop(device);
assert!(rustix::fs::fstat(&owned).is_ok());
kept.push(owned);
}
assert_eq!(kept.len(), 64);
}
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn open_card_and_enable_atomic_vkms() {
let _guard = card_guard();
let device = Device::open(test_card()).expect("open test card");
device
.enable_universal_planes()
.expect("universal planes must be available");
device.enable_atomic().expect("atomic must be available");
assert!(device.owns_fd());
}
#[test]
fn property_id_on_empty_store_is_an_error() {
let store = PropertyStore::new();
assert!(matches!(
store.property_id(42, "type"),
Err(CoreError::NoSuchObject { object_id: 42 })
));
}
#[test]
fn property_value_on_empty_store_is_an_error() {
let store = PropertyStore::new();
assert!(store.property_value(42, "type").is_err());
assert!(store.is_immutable(42, "type").is_err());
}
#[test]
fn properties_on_empty_store_is_none() {
let store = PropertyStore::new();
assert!(store.properties(42).is_none());
assert!(store.is_empty());
}
#[test]
fn clear_empties_the_store() {
let mut store = PropertyStore::new();
store.clear();
assert!(store.properties(42).is_none());
assert_eq!(store.len(), 0);
}
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn cache_unknown_object_is_an_error_vkms() {
let _guard = card_guard();
let device = Device::open(test_card()).expect("open test card");
let mut store = PropertyStore::new();
assert!(
store
.cache_properties(&device, 0xdead_beef, ObjectType::Connector)
.is_err(),
"an object id the kernel does not know must fail"
);
assert!(store.is_empty(), "a failed cache must not leave an entry");
}
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn cache_real_plane_properties_vkms() {
use drm::control::Device as _;
let device = Device::open(test_card()).expect("open test card");
device.enable_universal_planes().expect("universal planes");
device.enable_atomic().expect("atomic");
let planes = device.plane_handles().expect("plane handles");
let plane = *planes.first().expect("vkms exposes at least one plane");
let plane_id: u32 = plane.into();
let mut store = PropertyStore::new();
store
.cache_properties(&device, plane_id, ObjectType::Plane)
.expect("cache plane properties");
let props = store.properties(plane_id).expect("cached");
assert!(!props.is_empty());
let type_id = store.property_id(plane_id, "type").expect("type property");
assert_ne!(type_id, 0, "property ids are non-zero by construction");
assert!(
store.is_immutable(plane_id, "type").expect("type"),
"plane type is immutable, so a request builder must skip it"
);
assert!(store.property_id(plane_id, "NOT_A_REAL_PROPERTY").is_err());
store.forget(plane_id);
assert!(store.properties(plane_id).is_none());
}
#[test]
fn construct_is_infallible_and_empty() {
let request = AtomicRequest::new();
assert!(request.is_empty());
assert_eq!(request.len(), 0);
assert!(request.writes().is_empty());
}
#[test]
fn add_property_queues_without_validating_against_the_kernel() {
const SENTINEL_OBJECT: u32 = 0xdead_beef;
const SENTINEL_PROP: u32 = 0xcafe_f00d;
let mut request = AtomicRequest::new();
request
.add_property(SENTINEL_OBJECT, SENTINEL_PROP, 0)
.expect("non-zero ids queue");
assert_eq!(request.len(), 1);
assert_eq!(
request.writes()[0],
PropertyWrite {
object_id: SENTINEL_OBJECT,
property_id: SENTINEL_PROP,
value: 0,
}
);
}
#[test]
fn add_property_rejects_zero_ids() {
let mut request = AtomicRequest::new();
assert!(request.add_property(0, 1, 0).is_err(), "zero object id");
assert!(request.add_property(1, 0, 0).is_err(), "zero property id");
assert!(request.add_property(0, 0, 0).is_err());
assert!(request.is_empty(), "a rejected write must not be queued");
}
#[test]
fn writes_preserve_order_and_duplicates() {
let mut request = AtomicRequest::new();
request.add_property(1, 10, 100).unwrap();
request.add_property(2, 20, 200).unwrap();
request.add_property(1, 10, 999).unwrap();
let writes = request.writes();
assert_eq!(writes.len(), 3);
assert_eq!(writes[0].value, 100);
assert_eq!(writes[1].object_id, 2);
assert_eq!(writes[2].value, 999, "the later write must survive");
}
#[test]
fn clear_empties_the_queue() {
let mut request = AtomicRequest::with_capacity(8);
request.add_property(1, 2, 3).unwrap();
assert_eq!(request.len(), 1);
request.clear();
assert!(request.is_empty());
assert!(request.writes().is_empty());
}
#[test]
fn move_transfers_queued_writes() {
let mut source = AtomicRequest::new();
source.add_property(1, 2, 3).unwrap();
source.add_property(4, 5, 6).unwrap();
let moved = source;
assert_eq!(moved.len(), 2);
assert_eq!(moved.writes()[1].object_id, 4);
}
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn test_commit_with_empty_request_vkms() {
let _guard = card_guard();
let device = Device::open(test_card()).expect("open test card");
device.enable_atomic().expect("atomic");
let request = AtomicRequest::new();
check_master_dependent(
request.test(&device, AtomicCommitFlags::empty()),
"empty request must validate",
);
}
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn test_rejects_unknown_objects_vkms() {
let _guard = card_guard();
let device = Device::open(test_card()).expect("open test card");
device.enable_atomic().expect("atomic");
let mut request = AtomicRequest::new();
request.add_property(0xdead_beef, 0xcafe_f00d, 0).unwrap();
match request.test(&device, AtomicCommitFlags::empty()) {
Err(CoreError::NotMaster) => {
println!("note: skipped -- another client holds DRM master");
assert!(
std::env::var_os("DRMKIT_REQUIRE_MASTER").is_none(),
"not DRM master, but DRMKIT_REQUIRE_MASTER is set"
);
}
Err(_) => {}
Ok(()) => panic!("the kernel must reject ids that name nothing"),
}
}
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn test_masks_page_flip_event_vkms() {
let _guard = card_guard();
let device = Device::open(test_card()).expect("open test card");
device.enable_atomic().expect("atomic");
let request = AtomicRequest::new();
check_master_dependent(
request.test(&device, AtomicCommitFlags::PAGE_FLIP_EVENT),
"PAGE_FLIP_EVENT must be masked out rather than rejected",
);
}
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn signed_ranges_are_not_reported_as_ranges_vkms() {
use drm::control::Device as _;
let _guard = card_guard();
let device = Device::open(test_card()).expect("open card");
device.enable_universal_planes().expect("universal planes");
device.enable_atomic().expect("atomic");
let planes = device.plane_handles().expect("planes");
let plane: u32 = (*planes.first().expect("at least one plane")).into();
let mut store = PropertyStore::new();
store
.cache_properties(&device, plane, ObjectType::Plane)
.expect("plane properties");
let src_w = store.range(plane, "SRC_W").expect("SRC_W exists");
let (lo, hi) = src_w.expect("SRC_W is an unsigned range");
assert!(
lo <= hi,
"an unsigned range must not be inverted: [{lo}, {hi}]"
);
assert_eq!(
store.range(plane, "CRTC_X").expect("CRTC_X exists"),
None,
"CRTC_X is a signed range and must not be reported as an unsigned one"
);
}
#[test]
fn a_request_dump_names_every_write() {
let mut request = AtomicRequest::new();
request.add_property(55, 10, 74).expect("write");
request.add_property(55, 11, 66).expect("write");
let names = |id: u32| match id {
10 => Some("FB_ID".to_owned()),
_ => None,
};
let lines = request.dump_lines("test rejected", names);
assert_eq!(
lines[0],
"[atomic] test rejected: 2 properties in this request"
);
assert!(
lines[1].contains("obj=55") && lines[1].contains("FB_ID") && lines[1].ends_with("= 74")
);
assert!(lines[2].contains('?') && lines[2].ends_with("= 66"));
assert_eq!(
AtomicRequest::new().dump_lines("empty", names)[0],
"[atomic] empty: 0 properties in this request"
);
}
#[test]
fn which_modifier_a_framebuffer_declares() {
use crate::device::declared_modifier;
const INVALID: u64 = (1 << 56) - 1;
const TILED: u64 = 0x0600_0000_0000_0001;
let never = || -> bool { panic!("only LINEAR depends on the capability") };
assert_eq!(declared_modifier(INVALID, never), None);
assert_eq!(declared_modifier(TILED, never), Some(TILED));
assert_eq!(declared_modifier(0, || true), Some(0));
assert_eq!(declared_modifier(0, || false), None);
}