use std::fs;
use std::os::fd::AsFd as _;
use std::path::Path;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use rustix::event::{PollFd, PollFlags, poll};
use drmkit_display::{HotplugEvent, HotplugMonitor, Source};
static CARD_LOCK: Mutex<()> = Mutex::new(());
fn card_guard() -> std::sync::MutexGuard<'static, ()> {
CARD_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn card() -> (String, String) {
let node = std::env::var("DRMKIT_TEST_CARD").unwrap_or_else(|_| "/dev/dri/card0".to_owned());
let name = Path::new(&node)
.file_name()
.expect("a card path ends in a name")
.to_string_lossy()
.into_owned();
(format!("/sys/class/drm/{name}"), node)
}
fn can_synthesize(uevent: &str) -> bool {
if !Path::new(uevent).exists() {
assert!(
std::env::var_os("DRMKIT_REQUIRE_MASTER").is_none(),
"{uevent} does not exist, but DRMKIT_REQUIRE_MASTER is set"
);
drmkit_testkit::skipped(&format!("no {uevent}"));
return false;
}
if rustix::process::geteuid().is_root() {
return true;
}
assert!(
std::env::var_os("DRMKIT_REQUIRE_MASTER").is_none(),
"writing {uevent} needs root, but DRMKIT_REQUIRE_MASTER is set"
);
drmkit_testkit::skipped(&format!("writing {uevent} needs root (try sudo)"));
false
}
fn became_readable(monitor: &HotplugMonitor, timeout: Duration) -> bool {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
let fd = monitor.as_fd();
let mut fds = [PollFd::new(&fd, PollFlags::IN)];
let step = Duration::from_millis(100).try_into().expect("timeout");
if poll(&mut fds, Some(&step)).expect("poll") > 0 {
return true;
}
}
false
}
fn hotplug_events(monitor: &mut HotplugMonitor, timeout: Duration) -> Vec<HotplugEvent> {
let mut events = Vec::new();
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
{
let fd = monitor.as_fd();
let mut fds = [PollFd::new(&fd, PollFlags::IN)];
let step = Duration::from_millis(100).try_into().expect("timeout");
if poll(&mut fds, Some(&step)).expect("poll") == 0 {
continue;
}
}
monitor.dispatch(&mut events);
break;
}
events
}
#[test]
#[ignore = "needs a DRM device and root"]
fn an_ordinary_drm_change_is_read_and_discarded() {
let _guard = card_guard();
let (sysfs, _node) = card();
let uevent = format!("{sysfs}/uevent");
if !can_synthesize(&uevent) {
return;
}
let mut monitor = HotplugMonitor::open(Source::Kernel).expect("monitor");
fs::write(&uevent, "change").expect("synthesize");
assert!(
became_readable(&monitor, Duration::from_secs(5)),
"the socket was never readable, so this proves nothing"
);
let delivered = hotplug_events(&mut monitor, Duration::from_millis(500));
assert!(
delivered.is_empty(),
"a change with no HOTPLUG property was reported as a hotplug: {delivered:?}"
);
}
#[test]
#[ignore = "needs a DRM device and root"]
fn a_property_that_merely_looks_like_hotplug_is_not_one() {
let _guard = card_guard();
let (sysfs, _node) = card();
let uevent = format!("{sysfs}/uevent");
if !can_synthesize(&uevent) {
return;
}
let mut monitor = HotplugMonitor::open(Source::Kernel).expect("monitor");
fs::write(
&uevent,
"change 4d1a4b1e-0000-4000-8000-000000000001 HOTPLUG=1 CONNECTOR=42",
)
.expect("synthesize");
assert!(
became_readable(&monitor, Duration::from_secs(5)),
"the socket was never readable, so this proves nothing"
);
let delivered = hotplug_events(&mut monitor, Duration::from_millis(500));
assert!(
delivered.is_empty(),
"SYNTH_ARG_HOTPLUG was taken for HOTPLUG: {delivered:?}"
);
}
#[test]
#[ignore = "needs a DRM device"]
fn an_idle_monitor_says_nothing() {
let _guard = card_guard();
let mut monitor = HotplugMonitor::open(Source::Kernel).expect("monitor");
let delivered = hotplug_events(&mut monitor, Duration::from_millis(300));
assert!(delivered.is_empty(), "{delivered:?}");
}