Files
feresaria/compositor/src/startup_tests.rs
T
Samuel Onoja 73f21e20ce general improvement and bug fixes, cursor scaling, DMA-BUF routing and nested backend recovery
ignore performance-baseline.md

more improvement and code quality fixes
2026-10-04 18:33:05 +01:00

254 lines
10 KiB
Rust

use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::os::unix::process::ExitStatusExt;
use std::process::{Command, Stdio};
use std::sync::Arc;
use std::time::{Duration, Instant};
use calloop::EventLoop;
use calloop::signals::{Signal, Signals};
use smithay::input::SeatHandler;
use smithay::input::pointer::{CursorImageStatus, MotionEvent};
use smithay::reexports::wayland_server::protocol::{wl_compositor::WlCompositor, wl_surface::WlSurface};
use smithay::reexports::wayland_server::{Display, Resource};
use smithay::utils::SERIAL_COUNTER;
use crate::{Ferese, RedrawRequest, after_dispatch, after_dispatch_with_redraw};
// Each state needs its own sockets and environment. A subprocess also bounds
// the cursor deadlock test without changing environment variables across tests.
pub(crate) fn private_runtime(test: &str) -> bool {
const CHILD: &str = "FERESE_STARTUP_TEST_CHILD";
if std::env::var_os(CHILD).is_some_and(|value| value == test) {
return true;
}
let directory = tempfile::tempdir().unwrap();
let log_path = directory.path().join("test.log");
let log = std::fs::File::create(&log_path).unwrap();
let mut child = Command::new(std::env::current_exe().unwrap())
.args(["--exact", test, "--include-ignored", "--nocapture"])
.env(CHILD, test)
.env("XDG_RUNTIME_DIR", directory.path())
.env("XDG_CONFIG_HOME", directory.path())
.env("XDG_STATE_HOME", directory.path())
.env("XDG_CACHE_HOME", directory.path())
.env_remove("FERESE_SOCKET")
.stdout(Stdio::from(log.try_clone().unwrap()))
.stderr(Stdio::from(log))
.spawn()
.unwrap();
let deadline = Instant::now() + Duration::from_secs(20);
let status = loop {
if let Some(status) = child.try_wait().unwrap() {
break status;
}
if Instant::now() >= deadline {
child.kill().unwrap();
child.wait().unwrap();
panic!("{test} did not complete; possible startup deadlock");
}
std::thread::sleep(Duration::from_millis(10));
};
assert!(status.success(), "{}", std::fs::read_to_string(log_path).unwrap());
false
}
pub(crate) fn state(events: &mut EventLoop<'static, Ferese>) -> Ferese {
let mut config = crate::config::Config::default().runtime_config().unwrap();
config.wallpaper.path = None;
Ferese::new(events, Display::new().unwrap(), config).unwrap()
}
#[test]
fn launched_clients_unblock_termination_signals_and_keep_private_sockets() {
if !private_runtime("startup_tests::launched_clients_unblock_termination_signals_and_keep_private_sockets") {
return;
}
let mut events = EventLoop::try_new().unwrap();
let mut state = state(&mut events);
let signals = Signals::new(&[Signal::SIGINT, Signal::SIGTERM, Signal::SIGUSR1]).unwrap();
let mut private = crate::private_client::ClientCapabilities::EFFECTS;
private.insert(crate::private_client::ClientCapabilities::SHELL_CONTROL);
for capabilities in [crate::private_client::ClientCapabilities::default(), private] {
let mut child = crate::process::spawn_client(&mut state, ["/bin/sleep", "30"], capabilities).unwrap();
let status = std::fs::read_to_string(format!("/proc/{}/status", child.id())).unwrap();
let blocked = status.lines().find(|line| line.starts_with("SigBlk:")).unwrap();
let blocked = u64::from_str_radix(blocked.split_whitespace().nth(1).unwrap(), 16).unwrap();
assert_eq!(blocked & ((1 << (libc::SIGINT - 1)) | (1 << (libc::SIGTERM - 1))), 0);
assert_ne!(blocked & (1 << (libc::SIGUSR1 - 1)), 0, "unrelated mask was lost");
if !capabilities.is_empty() {
let environment = std::fs::read(format!("/proc/{}/environ", child.id())).unwrap();
for name in ["WAYLAND_SOCKET=", "FERESE_SHELL_CONTROL_SOCKET="] {
let variable = environment
.split(|byte| *byte == 0)
.filter_map(|value| std::str::from_utf8(value).ok())
.find_map(|value| value.strip_prefix(name))
.unwrap();
let fd = std::fs::read_link(format!("/proc/{}/fd/{variable}", child.id())).unwrap();
assert!(fd.to_string_lossy().starts_with("socket:["));
}
}
crate::process::terminate_child(&mut child);
assert_eq!(
child.wait().unwrap().signal(),
Some(libc::SIGTERM),
"child needed SIGKILL"
);
}
// Child setup must not unblock the compositor's own signalfd mask.
let mut mask = unsafe { std::mem::zeroed() };
assert_eq!(
unsafe { libc::pthread_sigmask(libc::SIG_BLOCK, std::ptr::null(), &mut mask) },
0
);
assert_eq!(unsafe { libc::sigismember(&mask, libc::SIGINT) }, 1);
assert_eq!(unsafe { libc::sigismember(&mask, libc::SIGTERM) }, 1);
drop(signals);
}
#[test]
fn explicit_client_exit_is_reaped_without_stopping_the_compositor() {
if !private_runtime("startup_tests::explicit_client_exit_is_reaped_without_stopping_the_compositor") {
return;
}
let mut events = EventLoop::try_new().unwrap();
let mut state = state(&mut events);
let child = crate::process::spawn_client(
&mut state,
["/bin/sh", "-c", "exit 7"],
crate::private_client::ClientCapabilities::default(),
)
.unwrap();
let pid = child.id();
let watched = crate::process::watch_client_exit(&events.handle(), child);
let deadline = Instant::now() + Duration::from_secs(2);
while watched.borrow().is_some() {
assert!(Instant::now() < deadline, "client was not reaped");
events.dispatch(Duration::from_millis(10), &mut state).unwrap();
after_dispatch(&mut state);
}
let mut status = 0;
assert_eq!(unsafe { libc::waitpid(pid as i32, &mut status, libc::WNOHANG) }, -1);
assert_eq!(std::io::Error::last_os_error().raw_os_error(), Some(libc::ECHILD));
// Exit watching removes itself; subsequent event-loop dispatch remains usable.
events.dispatch(Duration::ZERO, &mut state).unwrap();
}
#[test]
fn check_config_rejects_an_otherwise_valid_oversized_file() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("config.kdl");
let valid = format!("//{}\n", "x".repeat(60 * 1024 - 3));
std::fs::write(&path, &valid).unwrap();
crate::check_config(&path).unwrap();
assert!(crate::theme::prepare(&(valid.clone() + "\n"), directory.path()).is_ok());
std::fs::write(&path, valid + "\n").unwrap();
let error = crate::check_config(&path).unwrap_err();
assert!(error.contains("exceeds 60 KiB"), "{error}");
assert_eq!(crate::theme::read_source(&path).unwrap_err(), error);
}
#[test]
fn deferred_cursor_redraw_runs_after_input_lock_is_released() {
if !private_runtime("startup_tests::deferred_cursor_redraw_runs_after_input_lock_is_released") {
return;
}
let mut events = EventLoop::try_new().unwrap();
let mut state = state(&mut events);
let (server, mut client_wire) = UnixStream::pair().unwrap();
let client = state
.display_handle
.insert_client(server, Arc::new(crate::state::ClientState::default()))
.unwrap();
let compositor = client
.create_resource::<WlCompositor, (), Ferese>(&state.display_handle, 6, ())
.unwrap();
// wl_compositor.create_surface(new_id=2).
for word in [compositor.id().protocol_id(), 12u32 << 16, 2] {
client_wire.write_all(&word.to_ne_bytes()).unwrap();
}
events.dispatch(Duration::from_millis(10), &mut state).unwrap();
let surface = client
.object_from_protocol_id::<WlSurface>(&state.display_handle, 2)
.unwrap();
let pointer = state.seat.get_pointer().unwrap();
let seat = state.seat.clone();
state.cursor_image(&seat, CursorImageStatus::Hidden);
after_dispatch_with_redraw(&mut state, |_, _| {});
events.handle().insert_idle(move |state| {
for _ in 0..2 {
let event = MotionEvent {
location: (10.0, 20.0).into(),
serial: SERIAL_COUNTER.next_serial(),
time: 0,
};
pointer.motion(state, Some((surface.clone(), (0.0, 0.0).into())), &event);
// Smithay invokes Ferese::cursor_image while holding its real pointer lock.
pointer.motion(state, None, &event);
assert!(state.cursor_redraw_pending);
}
});
let signal = events.get_signal();
let mut redraws = 0;
events
.run(Some(Duration::ZERO), &mut state, |state| {
after_dispatch_with_redraw(state, |state, request| {
assert!(matches!(request, RedrawRequest::Cursor));
assert_eq!(
state.seat.get_pointer().unwrap().current_location(),
(10.0, 20.0).into()
);
redraws += 1;
});
assert!(!state.cursor_redraw_pending);
signal.stop();
})
.unwrap();
assert_eq!(redraws, 1);
after_dispatch_with_redraw(&mut state, |_, _| panic!("redraw was not consumed"));
}
#[test]
fn wayland_roundtrip_completes_without_a_rendered_frame() {
if !private_runtime("startup_tests::wayland_roundtrip_completes_without_a_rendered_frame") {
return;
}
let mut events = EventLoop::try_new().unwrap();
let mut state = state(&mut events);
let (server, mut client) = UnixStream::pair().unwrap();
state
.display_handle
.insert_client(server, Arc::new(crate::state::ClientState::default()))
.unwrap();
// wl_display.sync(new_id=2), using native-endian Wayland wire format.
for word in [1u32, 12u32 << 16, 2u32] {
client.write_all(&word.to_ne_bytes()).unwrap();
}
events.dispatch(Duration::from_millis(10), &mut state).unwrap();
client.set_read_timeout(Some(Duration::from_millis(20))).unwrap();
let mut reply = [0u8; 12];
assert!(
client.read_exact(&mut reply).is_err(),
"dispatch flushed replies before after_dispatch"
);
after_dispatch(&mut state);
client.set_read_timeout(Some(Duration::from_secs(1))).unwrap();
client.read_exact(&mut reply).unwrap();
assert_eq!(u32::from_ne_bytes(reply[0..4].try_into().unwrap()), 2);
assert_eq!(u32::from_ne_bytes(reply[4..8].try_into().unwrap()) & 0xffff, 0);
}