xwayland-satellite/tests/integration.rs
Shawn Wallace beb7c3ebe0 Offset output positions to always have positive coordinates
Honestly, this is something that should probably be fixed in Xwayland itself,
but they don't seem interested in fixing it:
https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/395#note_555613

Fixes #15
2025-03-16 16:00:10 -04:00

1466 lines
46 KiB
Rust

use rustix::event::{poll, PollFd, PollFlags};
use rustix::process::{Pid, Signal, WaitOptions};
use std::io::Write;
use std::mem::ManuallyDrop;
use std::os::fd::{AsRawFd, BorrowedFd};
use std::os::unix::net::UnixStream;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex, Once,
};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use wayland_protocols::xdg::shell::server::xdg_toplevel;
use wayland_server::Resource;
use xcb::{x, Xid};
use xwayland_satellite as xwls;
use xwayland_satellite::xstate::WmSizeHintsFlags;
#[derive(Default)]
struct TestDataInner {
server_created: AtomicBool,
server_connected: AtomicBool,
display: Mutex<Option<String>>,
server: Mutex<Option<UnixStream>>,
pid: Mutex<Option<u32>>,
quit_rx: Mutex<Option<UnixStream>>,
}
#[derive(Default, Clone)]
struct TestData(Arc<TestDataInner>);
impl TestData {
fn new(server: UnixStream, quit_rx: UnixStream) -> Self {
Self(Arc::new(TestDataInner {
server: Mutex::new(server.into()),
quit_rx: Mutex::new(Some(quit_rx)),
..Default::default()
}))
}
}
impl std::ops::Deref for TestData {
type Target = Arc<TestDataInner>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl xwls::RunData for TestData {
fn created_server(&self) {
self.server_created.store(true, Ordering::Relaxed);
}
fn connected_server(&self) {
self.server_connected.store(true, Ordering::Relaxed);
}
fn quit_rx(&self) -> Option<UnixStream> {
self.quit_rx.lock().unwrap().take()
}
fn xwayland_ready(&self, display: String, pid: u32) {
*self.display.lock().unwrap() = Some(display);
*self.pid.lock().unwrap() = Some(pid);
}
fn display(&self) -> Option<&str> {
None
}
fn server(&self) -> Option<UnixStream> {
let mut server = self.server.lock().unwrap();
assert!(server.is_some());
server.take()
}
}
struct Fixture {
testwl: testwl::Server,
thread: ManuallyDrop<JoinHandle<Option<()>>>,
pollfd: PollFd<'static>,
display: String,
pid: Pid,
quit_tx: UnixStream,
}
impl Drop for Fixture {
fn drop(&mut self) {
let thread = unsafe { ManuallyDrop::take(&mut self.thread) };
// Sending anything to the quit receiver to stop the main loop. Then we guarantee a main
// thread does not use file descriptors which outlive the Fixture's BorrowedFd
let return_ptr = Box::into_raw(Box::new(0_usize)) as usize;
self.quit_tx.write_all(&return_ptr.to_ne_bytes()).unwrap();
thread.join().expect("Main thread panicked");
rustix::process::kill_process(self.pid, Signal::Term).unwrap();
rustix::process::waitpid(Some(self.pid), WaitOptions::NOHANG).unwrap();
}
}
impl Fixture {
fn new_preset(pre_connect: impl FnOnce(&mut testwl::Server)) -> Self {
static INIT: Once = Once::new();
INIT.call_once(|| {
env_logger::builder()
.is_test(true)
.filter_level(log::LevelFilter::Debug)
.parse_default_env()
.init();
});
let (quit_tx, quit_rx) = UnixStream::pair().unwrap();
let (a, b) = UnixStream::pair().unwrap();
let mut testwl = testwl::Server::new(false);
pre_connect(&mut testwl);
testwl.connect(a);
let our_data = TestData::new(b, quit_rx);
let data = our_data.clone();
let thread = std::thread::spawn(move || xwls::main(data));
// wait for connection
let fd = unsafe { BorrowedFd::borrow_raw(testwl.poll_fd().as_raw_fd()) };
let pollfd = PollFd::from_borrowed_fd(fd, PollFlags::IN);
assert!(poll(&mut [pollfd.clone()], 100).unwrap() > 0);
testwl.dispatch();
let try_bool_timeout = |b: &AtomicBool| {
let timeout = Duration::from_secs(1);
let mut res = b.load(Ordering::Relaxed);
let start = Instant::now();
while !res && start.elapsed() < timeout {
res = b.load(Ordering::Relaxed);
}
res
};
assert!(
try_bool_timeout(&our_data.server_created),
"creating server"
);
assert!(
try_bool_timeout(&our_data.server_connected),
"connecting to server"
);
let mut f = [pollfd.clone()];
let start = std::time::Instant::now();
// Give Xwayland time to do its thing
let mut ready = our_data.display.lock().unwrap().is_some();
while !ready && start.elapsed() < Duration::from_millis(2000) {
let n = poll(&mut f, 100).unwrap();
if n > 0 {
testwl.dispatch();
}
ready = our_data.display.lock().unwrap().is_some();
}
assert!(ready, "connecting to xwayland failed");
let display = our_data.display.lock().unwrap().take().unwrap();
let pid = our_data.pid.lock().unwrap().take().unwrap();
Self {
testwl,
thread: ManuallyDrop::new(thread),
pollfd,
display,
pid: Pid::from_raw(pid as _).expect("Xwayland PID was invalid?"),
quit_tx,
}
}
fn new() -> Self {
Self::new_preset(|_| {})
}
#[track_caller]
fn wait_and_dispatch(&mut self) {
let mut pollfd = [self.pollfd.clone()];
assert!(
poll(&mut pollfd, 50).unwrap() > 0,
"Did not receive any events"
);
self.pollfd.clear_revents();
self.testwl.dispatch();
while poll(&mut pollfd, 50).unwrap() > 0 {
self.testwl.dispatch();
self.pollfd.clear_revents();
}
}
fn configure_and_verify_new_toplevel(
&mut self,
connection: &mut Connection,
window: x::Window,
surface: testwl::SurfaceId,
) {
let data = self.testwl.get_surface_data(surface).unwrap();
assert!(
matches!(data.role, Some(testwl::SurfaceRole::Toplevel(_))),
"surface role was wrong: {:?}",
data.role
);
self.testwl
.configure_toplevel(surface, 100, 100, vec![xdg_toplevel::State::Activated]);
self.testwl.focus_toplevel(surface);
self.wait_and_dispatch();
let geometry = connection.get_reply(&x::GetGeometry {
drawable: x::Drawable::Window(window),
});
assert_eq!(geometry.x(), 0);
assert_eq!(geometry.y(), 0);
assert_eq!(geometry.width(), 100);
assert_eq!(geometry.height(), 100);
}
#[track_caller]
fn map_as_toplevel(
&mut self,
connection: &mut Connection,
window: x::Window,
) -> testwl::SurfaceId {
connection.map_window(window);
self.wait_and_dispatch();
let surface = self
.testwl
.last_created_surface_id()
.expect("No surface created");
self.configure_and_verify_new_toplevel(connection, window, surface);
surface
}
#[track_caller]
fn map_as_popup(
&mut self,
connection: &mut Connection,
window: x::Window,
x: i16,
y: i16,
width: u16,
height: u16,
) -> testwl::SurfaceId {
connection.map_window(window);
self.wait_and_dispatch();
let surface = self
.testwl
.last_created_surface_id()
.expect("No surface created");
let data = self.testwl.get_surface_data(surface).unwrap();
assert!(
matches!(data.role, Some(testwl::SurfaceRole::Popup(_))),
"surface role was wrong: {:?}",
data.role
);
self.testwl.configure_popup(surface);
self.wait_and_dispatch();
let geometry = connection.get_reply(&x::GetGeometry {
drawable: x::Drawable::Window(window),
});
assert_eq!(geometry.x(), x);
assert_eq!(geometry.y(), y);
assert_eq!(geometry.width(), width);
assert_eq!(geometry.height(), height);
surface
}
/// Triggers a Wayland side toplevel Close event and processes the corresponding
/// X11 side WM_DELETE_WINDOW client message
fn wm_delete_window(
&mut self,
connection: &mut Connection,
window: x::Window,
surface: testwl::SurfaceId,
) {
connection.set_property(
window,
x::ATOM_ATOM,
connection.atoms.wm_protocols,
&[connection.atoms.wm_delete_window],
);
self.testwl.close_toplevel(surface);
let event = connection.await_event();
let xcb::Event::X(x::Event::ClientMessage(event)) = event else {
panic!("Expected ClientMessage event, got {event:?}");
};
assert_eq!(event.window(), window);
assert_eq!(event.format(), 32);
assert_eq!(event.r#type(), connection.atoms.wm_protocols);
match event.data() {
x::ClientMessageData::Data32(d) => {
assert_eq!(d[0], connection.atoms.wm_delete_window.resource_id())
}
other => panic!("wrong data type: {other:?}"),
}
}
fn create_output(&mut self, x: i32, y: i32) -> wayland_server::protocol::wl_output::WlOutput {
self.testwl.new_output(x, y);
self.wait_and_dispatch();
self.testwl.last_created_output()
}
}
xcb::atoms_struct! {
struct Atoms {
wm_protocols => b"WM_PROTOCOLS",
net_active_window => b"_NET_ACTIVE_WINDOW",
wm_delete_window => b"WM_DELETE_WINDOW",
clipboard => b"CLIPBOARD",
targets => b"TARGETS",
multiple => b"MULTIPLE",
wm_check => b"_NET_SUPPORTING_WM_CHECK",
mime1 => b"text/plain" only_if_exists = false,
mime2 => b"blah/blah" only_if_exists = false,
incr => b"INCR",
}
}
struct Connection {
inner: xcb::Connection,
pollfd: PollFd<'static>,
atoms: Atoms,
root: x::Window,
visual: u32,
wm_window: x::Window,
}
impl std::ops::Deref for Connection {
type Target = xcb::Connection;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl Connection {
fn new(display: &str) -> Self {
let (inner, _) =
xcb::Connection::connect_with_extensions(Some(display), &[xcb::Extension::XFixes], &[])
.unwrap();
// xfixes init
let reply = inner
.wait_for_reply(inner.send_request(&xcb::xfixes::QueryVersion {
client_major_version: 1,
client_minor_version: 0,
}))
.unwrap();
assert_eq!(reply.major_version(), 1);
let fd = unsafe { BorrowedFd::borrow_raw(inner.as_raw_fd()) };
let pollfd = PollFd::from_borrowed_fd(fd, PollFlags::IN);
let atoms = Atoms::intern_all(&inner).unwrap();
let screen = inner.get_setup().roots().next().unwrap();
let root = screen.root();
let visual = screen.root_visual();
let wm_window: x::Window = inner
.wait_for_reply(inner.send_request(&x::GetProperty {
delete: false,
window: root,
property: atoms.wm_check,
r#type: x::ATOM_WINDOW,
long_offset: 0,
long_length: 1,
}))
.expect("Couldn't get WM window")
.value()[0];
Self {
inner,
pollfd,
atoms,
root,
visual,
wm_window,
}
}
fn new_window(
&self,
parent: x::Window,
x: i16,
y: i16,
width: u16,
height: u16,
override_redirect: bool,
) -> x::Window {
let wid = self.inner.generate_id();
let req = x::CreateWindow {
depth: 0,
wid,
parent,
x,
y,
width,
height,
border_width: 0,
class: x::WindowClass::InputOutput,
visual: self.visual,
value_list: &[x::Cw::OverrideRedirect(override_redirect)],
};
self.inner
.send_and_check_request(&req)
.expect("creating window failed");
wid
}
#[track_caller]
fn map_window(&self, window: x::Window) {
self.send_and_check_request(&x::MapWindow { window })
.unwrap();
}
#[track_caller]
fn destroy_window(&self, window: x::Window) {
self.send_and_check_request(&x::DestroyWindow { window })
.unwrap();
}
#[track_caller]
fn set_property<P: x::PropEl>(
&self,
window: x::Window,
r#type: x::Atom,
property: x::Atom,
data: &[P],
) {
self.send_and_check_request(&x::ChangeProperty {
mode: x::PropMode::Replace,
window,
r#type,
property,
data,
})
.unwrap();
}
#[track_caller]
#[must_use]
fn await_event(&mut self) -> xcb::Event {
if let Some(event) = self.poll_for_event().expect("Failed to poll for event") {
return event;
}
assert!(
poll(&mut [self.pollfd.clone()], 100).expect("poll failed") > 0,
"Did not get any X11 events"
);
self.pollfd.clear_revents();
self.poll_for_event()
.expect("Failed to poll for event after pollfd")
.unwrap()
}
#[track_caller]
fn get_reply<R: xcb::Request>(
&self,
req: &R,
) -> <R::Cookie as xcb::CookieWithReplyChecked>::Reply
where
R::Cookie: xcb::CookieWithReplyChecked,
{
self.wait_for_reply(self.send_request(req)).unwrap()
}
#[track_caller]
fn set_selection_owner(&self, window: x::Window) {
self.send_and_check_request(&x::SetSelectionOwner {
owner: window,
selection: self.atoms.clipboard,
time: x::CURRENT_TIME,
})
.unwrap();
let owner = self.get_reply(&x::GetSelectionOwner {
selection: self.atoms.clipboard,
});
assert_eq!(window, owner.owner(), "Unexpected selection owner");
}
#[track_caller]
fn await_selection_request(&mut self) -> x::SelectionRequestEvent {
match self.await_event() {
xcb::Event::X(x::Event::SelectionRequest(r)) => r,
other => panic!("Didn't get selection request event, instead got {other:?}"),
}
}
#[track_caller]
fn await_selection_notify(&mut self) -> x::SelectionNotifyEvent {
match self.await_event() {
xcb::Event::X(x::Event::SelectionNotify(r)) => r,
other => panic!("Didn't get selection notify event, instead got {other:?}"),
}
}
#[track_caller]
fn send_selection_notify(&self, request: &x::SelectionRequestEvent) {
self.send_and_check_request(&x::SendEvent {
propagate: false,
destination: x::SendEventDest::Window(request.requestor()),
event_mask: x::EventMask::empty(),
event: &x::SelectionNotifyEvent::new(
request.time(),
request.requestor(),
request.selection(),
request.target(),
request.property(),
),
})
.unwrap();
}
#[track_caller]
fn verify_clipboard_owner(&self, window: x::Window) {
let owner = self.get_reply(&x::GetSelectionOwner {
selection: self.atoms.clipboard,
});
assert_eq!(owner.owner(), window, "Clipboard owner does not match");
}
#[track_caller]
fn await_selection_owner_change(&mut self) -> xcb::xfixes::SelectionNotifyEvent {
match self.await_event() {
xcb::Event::XFixes(xcb::xfixes::Event::SelectionNotify(e)) => e,
other => panic!("Expected XFixes SelectionNotify, got {other:?}"),
}
}
#[track_caller]
fn get_selection_owner_change_events(&self, enable: bool, window: x::Window) {
let event_mask = if enable {
xcb::xfixes::SelectionEventMask::SET_SELECTION_OWNER
} else {
xcb::xfixes::SelectionEventMask::empty()
};
self.send_and_check_request(&xcb::xfixes::SelectSelectionInput {
window,
selection: self.atoms.clipboard,
event_mask,
})
.unwrap();
}
}
#[test]
fn toplevel_flow() {
let mut f = Fixture::new();
let mut connection = Connection::new(&f.display);
let window = connection.new_window(connection.root, 0, 0, 200, 200, false);
// Pre-map properties
connection.set_property(
window,
x::ATOM_STRING,
x::ATOM_WM_NAME,
c"window".to_bytes(),
);
connection.set_property(
window,
x::ATOM_STRING,
x::ATOM_WM_CLASS,
&[
c"instance".to_bytes_with_nul(),
c"class".to_bytes_with_nul(),
]
.concat(),
);
let flags = (WmSizeHintsFlags::ProgramMaxSize | WmSizeHintsFlags::ProgramMinSize).bits();
connection.set_property(
window,
x::ATOM_WM_SIZE_HINTS,
x::ATOM_WM_NORMAL_HINTS,
&[flags, 0, 0, 0, 0, 50, 100, 300, 400],
);
connection.set_property(
window,
x::ATOM_STRING,
x::ATOM_WM_NAME,
c"window".to_bytes(),
);
connection.map_window(window);
f.wait_and_dispatch();
let surface = f
.testwl
.last_created_surface_id()
.expect("No surface created!");
f.configure_and_verify_new_toplevel(&mut connection, window, surface);
let data = f.testwl.get_surface_data(surface).unwrap();
assert_eq!(data.toplevel().title, Some("window".into()));
assert_eq!(data.toplevel().app_id, Some("class".into()));
assert_eq!(
data.toplevel().min_size,
Some(testwl::Vec2 { x: 50, y: 100 })
);
assert_eq!(
data.toplevel().max_size,
Some(testwl::Vec2 { x: 300, y: 400 })
);
// Post map properties
connection.set_property(
window,
x::ATOM_STRING,
x::ATOM_WM_NAME,
c"bindow".to_bytes(),
);
connection.set_property(
window,
x::ATOM_STRING,
x::ATOM_WM_CLASS,
c"boink".to_bytes(),
);
connection.set_property(
window,
x::ATOM_WM_SIZE_HINTS,
x::ATOM_WM_NORMAL_HINTS,
&[flags, 1, 2, 3, 4, 25, 50, 150, 200],
);
f.wait_and_dispatch();
let data = f.testwl.get_surface_data(surface).unwrap();
let toplevel = data.toplevel().toplevel.clone();
assert_eq!(data.toplevel().title, Some("bindow".into()));
assert_eq!(data.toplevel().app_id, Some("boink".into()));
assert_eq!(
data.toplevel().min_size,
Some(testwl::Vec2 { x: 25, y: 50 })
);
assert_eq!(
data.toplevel().max_size,
Some(testwl::Vec2 { x: 150, y: 200 })
);
f.wm_delete_window(&mut connection, window, surface);
// Simulate killing client
drop(connection);
f.wait_and_dispatch();
assert!(!toplevel.is_alive());
}
#[test]
fn reparent() {
let mut f = Fixture::new();
let mut connection = Connection::new(&f.display);
let parent = connection.new_window(connection.root, 0, 0, 1, 1, false);
let child = connection.new_window(parent, 0, 0, 20, 20, false);
connection
.send_and_check_request(&x::ReparentWindow {
window: child,
parent: connection.root,
x: 0,
y: 0,
})
.unwrap();
connection.map_window(child);
f.wait_and_dispatch();
let surface = f
.testwl
.last_created_surface_id()
.expect("No surface created!");
f.configure_and_verify_new_toplevel(&mut connection, child, surface);
}
#[test]
fn window_properties_after_reparent() {
let mut f = Fixture::new();
let mut connection = Connection::new(&f.display);
let child = connection.new_window(connection.root, 0, 0, 1, 1, true);
connection.map_window(child);
f.wait_and_dispatch();
let child_surface = f
.testwl
.last_created_surface_id()
.expect("No surface created!");
f.configure_and_verify_new_toplevel(&mut connection, child, child_surface);
let other = connection.new_window(connection.root, 0, 0, 100, 100, false);
connection.map_window(other);
f.wait_and_dispatch();
let other_surface = f
.testwl
.last_created_surface_id()
.expect("No surface created!");
f.configure_and_verify_new_toplevel(&mut connection, other, other_surface);
connection.send_request(&x::UnmapWindow { window: child });
let parent = connection.new_window(connection.root, 0, 0, 20, 20, false);
connection.send_request(&x::ReparentWindow {
window: child,
parent,
x: 0,
y: 0,
});
// The server should get the notifications for these properties and shouldn't crash
connection.set_property(
child,
x::ATOM_WM_SIZE_HINTS,
x::ATOM_WM_NORMAL_HINTS,
&[16u32, 0, 0, 0, 0, 200, 400],
);
connection.set_property(child, x::ATOM_STRING, x::ATOM_WM_NAME, b"title\0");
connection.set_property(child, x::ATOM_STRING, x::ATOM_WM_CLASS, c"class".to_bytes());
f.wait_and_dispatch();
}
#[test]
fn input_focus() {
let mut f = Fixture::new();
let mut connection = Connection::new(&f.display);
let conn = std::cell::RefCell::new(&mut connection);
let check_focus = |win: x::Window| {
let connection = conn.borrow();
let focus = connection.get_reply(&x::GetInputFocus {}).focus();
assert_eq!(win, focus);
let reply = connection.get_reply(&x::GetProperty {
delete: false,
window: connection.root,
property: connection.atoms.net_active_window,
r#type: x::ATOM_WINDOW,
long_offset: 0,
long_length: 1,
});
assert_eq!(&[win], reply.value::<x::Window>());
};
let mut create_win = || {
let mut connection = conn.borrow_mut();
let win = connection.new_window(connection.root, 0, 0, 20, 20, false);
connection.map_window(win);
f.wait_and_dispatch();
let surface = f
.testwl
.last_created_surface_id()
.expect("No surface created!");
f.configure_and_verify_new_toplevel(&mut connection, win, surface);
(win, surface)
};
let (win1, surface1) = create_win();
check_focus(win1);
let (win2, surface2) = create_win();
check_focus(win2);
f.testwl.focus_toplevel(surface1);
// Seems the event doesn't get caught by wait_and_dispatch...
std::thread::sleep(std::time::Duration::from_millis(10));
check_focus(win1);
f.testwl.unfocus_toplevel();
std::thread::sleep(std::time::Duration::from_millis(10));
check_focus(x::WINDOW_NONE);
f.testwl.focus_toplevel(surface2);
std::thread::sleep(std::time::Duration::from_millis(10));
check_focus(win2);
conn.borrow().destroy_window(win2);
f.wait_and_dispatch();
check_focus(x::WINDOW_NONE);
f.wm_delete_window(&mut connection, win1, surface1);
}
#[test]
fn quick_delete() {
let mut f = Fixture::new();
let connection = Connection::new(&f.display);
let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
connection.map_window(window);
f.wait_and_dispatch();
let surf = f
.testwl
.last_created_surface_id()
.expect("No surface created");
connection.set_property(
window,
x::ATOM_STRING,
x::ATOM_WM_NAME,
c"bindow".to_bytes(),
);
connection.set_property(
window,
x::ATOM_STRING,
x::ATOM_WM_CLASS,
&[c"f".to_bytes_with_nul(), c"ssalc".to_bytes_with_nul()].concat(),
);
let flags = (WmSizeHintsFlags::ProgramMaxSize | WmSizeHintsFlags::ProgramMinSize).bits();
connection.set_property(
window,
x::ATOM_WM_SIZE_HINTS,
x::ATOM_WM_NORMAL_HINTS,
&[flags, 1, 2, 3, 4, 25, 50, 150, 200],
);
connection
.send_and_check_request(&x::ConfigureWindow {
window,
value_list: &[x::ConfigWindow::X(10), x::ConfigWindow::Y(40)],
})
.unwrap();
f.testwl
.configure_toplevel(surf, 100, 100, vec![xdg_toplevel::State::Activated]);
connection.destroy_window(window);
f.wait_and_dispatch();
assert_eq!(f.testwl.get_surface_data(surf), None);
}
#[test]
fn copy_from_x11() {
let mut f = Fixture::new();
let mut connection = Connection::new(&f.display);
let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
f.map_as_toplevel(&mut connection, window);
connection.set_selection_owner(window);
let request = connection.await_selection_request();
assert_eq!(request.target(), connection.atoms.targets);
connection.set_property(
request.requestor(),
x::ATOM_ATOM,
request.property(),
&[connection.atoms.mime1, connection.atoms.mime2],
);
connection.send_selection_notify(&request);
f.wait_and_dispatch();
struct MimeData {
mime: x::Atom,
data: testwl::PasteData,
}
let mimes_truth = [
MimeData {
mime: connection.atoms.mime1,
data: testwl::PasteData {
mime_type: "text/plain".to_string(),
data: b"hello world".to_vec(),
},
},
MimeData {
mime: connection.atoms.mime2,
data: testwl::PasteData {
mime_type: "blah/blah".to_string(),
data: vec![1, 2, 3, 4],
},
},
];
let advertised_mimes = f.testwl.data_source_mimes();
assert_eq!(
advertised_mimes.len(),
mimes_truth.len(),
"Wrong number of advertised mimes: {advertised_mimes:?}"
);
for MimeData { data, .. } in &mimes_truth {
assert!(
advertised_mimes.contains(&data.mime_type),
"Missing mime type {}",
data.mime_type
);
}
let data = f.testwl.paste_data(|mime, _| {
let request = connection.await_selection_request();
let data = mimes_truth
.iter()
.find(|data| data.data.mime_type == mime)
.unwrap_or_else(|| panic!("Asked for unknown mime: {mime}"));
connection.set_property(
request.requestor(),
data.mime,
request.property(),
&data.data.data,
);
connection.send_selection_notify(&request);
true
});
let mut found_mimes = Vec::new();
for testwl::PasteData { mime_type, data } in data {
match &mime_type {
x if x == "text/plain" => {
assert_eq!(&data, b"hello world");
}
x if x == "blah/blah" => {
assert_eq!(&data, &[1, 2, 3, 4]);
}
other => panic!("unexpected mime type: {other} ({data:?})"),
}
found_mimes.push(mime_type);
}
assert!(
found_mimes.contains(&"text/plain".to_string()),
"Didn't get mime data for text/plain"
);
assert!(
found_mimes.contains(&"blah/blah".to_string()),
"Didn't get mime data for blah/blah"
);
}
#[test]
fn copy_from_wayland() {
let mut f = Fixture::new();
let mut connection = Connection::new(&f.display);
let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
connection.get_selection_owner_change_events(true, window);
f.map_as_toplevel(&mut connection, window);
let offer = vec![
testwl::PasteData {
mime_type: "text/plain".into(),
data: b"boingloings".to_vec(),
},
testwl::PasteData {
mime_type: "yah/hah".into(),
data: vec![1, 2, 3, 2, 1],
},
];
f.testwl.create_data_offer(offer.clone());
connection.await_selection_owner_change();
connection.verify_clipboard_owner(connection.wm_window);
connection.get_selection_owner_change_events(false, window);
let dest1_atom = connection
.get_reply(&x::InternAtom {
name: b"dest1",
only_if_exists: false,
})
.atom();
connection
.send_and_check_request(&x::ConvertSelection {
requestor: window,
selection: connection.atoms.clipboard,
target: connection.atoms.targets,
property: dest1_atom,
time: x::CURRENT_TIME,
})
.unwrap();
let request = connection.await_selection_notify();
assert_eq!(request.requestor(), window);
assert_eq!(request.selection(), connection.atoms.clipboard);
assert_eq!(request.target(), connection.atoms.targets);
assert_eq!(request.property(), dest1_atom);
let reply = connection.get_reply(&x::GetProperty {
delete: true,
window,
property: dest1_atom,
r#type: x::ATOM_ATOM,
long_offset: 0,
long_length: 10,
});
let targets: &[x::Atom] = reply.value();
assert_eq!(targets.len(), 2);
for testwl::PasteData { mime_type, data } in offer {
let atom = connection
.get_reply(&x::InternAtom {
only_if_exists: true,
name: mime_type.as_bytes(),
})
.atom();
assert_ne!(atom, x::ATOM_NONE);
assert!(targets.contains(&atom));
std::thread::sleep(std::time::Duration::from_millis(50));
connection
.send_and_check_request(&x::ConvertSelection {
requestor: window,
selection: connection.atoms.clipboard,
target: atom,
property: dest1_atom,
time: x::CURRENT_TIME,
})
.unwrap();
f.wait_and_dispatch();
let request = connection.await_selection_notify();
assert_eq!(request.requestor(), window);
assert_eq!(request.selection(), connection.atoms.clipboard);
assert_eq!(request.target(), atom);
assert_eq!(request.property(), dest1_atom);
let val: Vec<u8> = connection
.get_reply(&x::GetProperty {
delete: true,
window,
property: dest1_atom,
r#type: x::ATOM_ANY,
long_offset: 0,
long_length: 10,
})
.value()
.to_vec();
assert_eq!(val, data);
}
}
// TODO: this test doesn't actually match real behavior for some reason...
#[test]
fn different_output_position() {
let mut f = Fixture::new();
//f.testwl.enable_xdg_output_manager();
let mut connection = Connection::new(&f.display);
let window = connection.new_window(connection.root, 0, 0, 200, 200, false);
connection.map_window(window);
f.wait_and_dispatch();
let surface = f
.testwl
.last_created_surface_id()
.expect("No surface created!");
f.configure_and_verify_new_toplevel(&mut connection, window, surface);
let output = f.create_output(0, 0);
f.testwl.move_surface_to_output(surface, &output);
f.testwl.move_pointer_to(surface, 10.0, 10.0);
f.wait_and_dispatch();
let reply = connection.get_reply(&x::QueryPointer { window });
assert!(reply.same_screen());
assert_eq!(reply.win_x(), 10);
assert_eq!(reply.win_y(), 10);
let output = f.create_output(100, 0);
//f.testwl.move_xdg_output(&output, 100, 0);
f.testwl.move_surface_to_output(surface, &output);
f.testwl.move_pointer_to(surface, 150.0, 12.0);
f.wait_and_dispatch();
let reply = connection.get_reply(&x::QueryPointer { window });
println!("reply: {reply:?}");
assert!(reply.same_screen());
assert_eq!(reply.win_x(), 150);
assert_eq!(reply.win_y(), 12);
}
#[test]
fn bad_clipboard_data() {
let mut f = Fixture::new();
let mut connection = Connection::new(&f.display);
let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
f.map_as_toplevel(&mut connection, window);
connection.set_selection_owner(window);
let request = connection.await_selection_request();
assert_eq!(request.target(), connection.atoms.targets);
connection.set_property(
request.requestor(),
x::ATOM_ATOM,
request.property(),
&[connection.atoms.mime2],
);
connection.send_selection_notify(&request);
f.wait_and_dispatch();
let mut data = f.testwl.paste_data(|_, _| {
let request = connection.await_selection_request();
assert_eq!(request.target(), connection.atoms.mime2);
// Don't actually set any data as requested - just report success
connection.send_selection_notify(&request);
true
});
connection.verify_clipboard_owner(window);
assert_eq!(data.len(), 1, "Unexpected data: {data:?}");
let data = data.pop().unwrap();
assert_eq!(data.mime_type, "blah/blah");
assert!(data.data.is_empty(), "Unexpected data: {:?}", data.data);
}
// issue #42
#[test]
fn funny_window_title() {
let mut f = Fixture::new();
let mut connection = Connection::new(&f.display);
let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
connection.set_property(window, x::ATOM_STRING, x::ATOM_WM_NAME, b"title\0\0\0\0");
connection.map_window(window);
f.wait_and_dispatch();
let surface = f
.testwl
.last_created_surface_id()
.expect("No surface created!");
f.configure_and_verify_new_toplevel(&mut connection, window, surface);
let data = f.testwl.get_surface_data(surface).unwrap();
assert_eq!(data.toplevel().title, Some("title".into()));
connection.set_property(
window,
x::ATOM_STRING,
x::ATOM_WM_NAME,
b"title\0irrelevantdata\0",
);
f.wait_and_dispatch();
let data = f.testwl.get_surface_data(surface).unwrap();
assert_eq!(data.toplevel().title, Some("title".into()));
}
#[test]
fn close_window() {
let mut f = Fixture::new();
let mut connection = Connection::new(&f.display);
let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
connection.map_window(window);
f.wait_and_dispatch();
let surface = f
.testwl
.last_created_surface_id()
.expect("No surface created");
f.configure_and_verify_new_toplevel(&mut connection, window, surface);
f.wm_delete_window(&mut connection, window, surface);
connection
.send_and_check_request(&x::DeleteProperty {
window,
property: connection.atoms.wm_protocols,
})
.unwrap();
f.testwl.close_toplevel(surface);
f.wait_and_dispatch();
// Connection should no longer work (KillClient)
assert!(connection.poll_for_event().is_err());
}
// TODO: figure out if the sleeps in this test can be dealt with...
#[test]
fn primary_output() {
let mut f = Fixture::new_preset(|testwl| {
testwl.new_output(0, 0); // WL-1
testwl.new_output(500, 500); // WL-2
});
let mut conn = Connection::new(&f.display);
let reply = conn.get_reply(&xcb::randr::GetScreenResources { window: conn.root });
let config_timestamp = reply.config_timestamp();
let mut it = reply.outputs().iter().copied().map(|output| {
let reply = conn.get_reply(&xcb::randr::GetOutputInfo {
output,
config_timestamp,
});
let name = std::str::from_utf8(reply.name()).unwrap();
(
f.testwl
.get_output(name)
.unwrap_or_else(|| panic!("Couldn't find output {name}")),
output,
)
});
let (wl_output1, output1) = it.next().expect("Couldn't find first output");
let (wl_output2, output2) = it.next().expect("Couldn't find second output");
assert_eq!(it.collect::<Vec<_>>(), vec![]);
let window1 = conn.new_window(conn.root, 0, 0, 20, 20, false);
let surface1 = f.map_as_toplevel(&mut conn, window1);
f.testwl.move_surface_to_output(surface1, &wl_output1);
let window2 = conn.new_window(conn.root, 0, 0, 20, 20, false);
std::thread::sleep(std::time::Duration::from_millis(10));
let surface2 = f.map_as_toplevel(&mut conn, window2);
f.testwl.move_surface_to_output(surface2, &wl_output2);
assert_ne!(surface1, surface2);
f.wait_and_dispatch();
f.testwl.focus_toplevel(surface1);
std::thread::sleep(std::time::Duration::from_millis(10));
let reply = conn.get_reply(&xcb::randr::GetOutputPrimary { window: conn.root });
assert_eq!(reply.output(), output1);
f.testwl.focus_toplevel(surface2);
std::thread::sleep(std::time::Duration::from_millis(10));
let reply = conn.get_reply(&xcb::randr::GetOutputPrimary { window: conn.root });
assert_eq!(reply.output(), output2);
let wl_output3 = f.create_output(24, 46);
f.testwl.move_surface_to_output(surface2, &wl_output3);
std::thread::sleep(std::time::Duration::from_millis(10));
let reply = conn.get_reply(&xcb::randr::GetScreenResources { window: conn.root });
assert_eq!(reply.outputs().len(), 3);
let output3 = reply
.outputs()
.iter()
.copied()
.find(|o| ![output1, output2].contains(o))
.unwrap();
let reply = conn.get_reply(&xcb::randr::GetOutputPrimary { window: conn.root });
assert_eq!(reply.output(), output3);
}
#[test]
fn incr_copy_from_x11() {
let mut f = Fixture::new();
let mut connection = Connection::new(&f.display);
let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
f.map_as_toplevel(&mut connection, window);
connection.set_selection_owner(window);
let request = connection.await_selection_request();
assert_eq!(request.target(), connection.atoms.targets);
connection.set_property(
request.requestor(),
x::ATOM_ATOM,
request.property(),
&[connection.atoms.targets, connection.atoms.mime1],
);
connection.send_selection_notify(&request);
f.wait_and_dispatch();
let mut destination_property = x::Atom::none();
let mut begin_incr = Some(|connection: &mut Connection| {
let request = connection.await_selection_request();
assert_eq!(request.target(), connection.atoms.mime1);
connection
.send_and_check_request(&x::ChangeWindowAttributes {
window: request.requestor(),
value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)],
})
.unwrap();
connection.set_property(
request.requestor(),
connection.atoms.incr,
request.property(),
&[3000u32],
);
connection.send_selection_notify(&request);
// skip NewValue
let notify = match connection.poll_for_event().unwrap() {
Some(xcb::Event::X(x::Event::PropertyNotify(p))) => p,
other => panic!("Didn't get property notify event, instead got {other:?}"),
};
assert_eq!(notify.atom(), request.property());
assert_eq!(notify.state(), x::Property::NewValue);
request.property()
});
let data: Vec<u8> = std::iter::successors(Some(1u8), |n| Some(n.wrapping_add(1)))
.take(3000)
.collect();
let mut it = data.chunks(500).enumerate();
let mut paste_data = f.testwl.paste_data(|_, testwl| {
if let Some(begin) = begin_incr.take() {
destination_property = begin(&mut connection);
testwl.dispatch();
return false;
}
assert_ne!(destination_property, x::Atom::none());
let notify = match connection.await_event() {
xcb::Event::X(x::Event::PropertyNotify(p)) => p,
other => panic!("Didn't get property notify event, instead got {other:?}"),
};
match it.next() {
Some((idx, chunk)) => {
assert_eq!(notify.atom(), destination_property, "chunk {idx}");
assert_eq!(notify.state(), x::Property::Delete, "chunk {idx}");
connection.set_property(
notify.window(),
connection.atoms.mime1,
destination_property,
chunk,
);
testwl.dispatch();
// skip NewValue
let notify = match connection.poll_for_event().unwrap() {
Some(xcb::Event::X(x::Event::PropertyNotify(p))) => p,
other => panic!("Didn't get property notify event, instead got {other:?}"),
};
assert_eq!(notify.atom(), destination_property, "chunk {idx}");
assert_eq!(notify.state(), x::Property::NewValue, "chunk {idx}");
false
}
None => {
// INCR completed!
assert_eq!(notify.atom(), destination_property);
assert_eq!(notify.state(), x::Property::Delete);
connection.set_property::<u8>(
notify.window(),
connection.atoms.mime1,
destination_property,
&[],
);
true
}
}
});
assert_eq!(f.testwl.data_source_mimes(), vec!["text/plain"]);
assert_eq!(paste_data.len(), 1);
let paste_data = paste_data.swap_remove(0);
assert_eq!(paste_data.mime_type, "text/plain");
assert_eq!(&paste_data.data, &data);
}
#[test]
fn wayland_then_x11_clipboard_owner() {
let mut f = Fixture::new();
let mut connection = Connection::new(&f.display);
let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
connection.get_selection_owner_change_events(true, window);
f.map_as_toplevel(&mut connection, window);
let offer = vec![
testwl::PasteData {
mime_type: "text/plain".into(),
data: b"boingloings".to_vec(),
},
testwl::PasteData {
mime_type: "yah/hah".into(),
data: vec![1, 2, 3, 2, 1],
},
];
f.testwl.create_data_offer(offer.clone());
connection.await_selection_owner_change();
connection.verify_clipboard_owner(connection.wm_window);
connection.get_selection_owner_change_events(false, window);
connection.set_selection_owner(window);
f.testwl.dispatch();
connection.verify_clipboard_owner(window);
let request = connection.await_selection_request();
assert_eq!(request.selection(), connection.atoms.clipboard);
assert_eq!(request.target(), connection.atoms.targets);
}
#[test]
fn fake_selection_targets() {
let mut f = Fixture::new();
let mut connection = Connection::new(&f.display);
let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
connection.get_selection_owner_change_events(true, window);
let data = b"boingloings";
f.map_as_toplevel(&mut connection, window);
let offer = vec![testwl::PasteData {
mime_type: "text/plain;charset=utf-8".into(),
data: data.to_vec(),
}];
f.testwl.create_data_offer(offer.clone());
connection.await_selection_owner_change();
connection.verify_clipboard_owner(connection.wm_window);
connection.get_selection_owner_change_events(false, window);
let utf8_string = connection
.get_reply(&x::InternAtom {
only_if_exists: false,
name: b"UTF8_STRING",
})
.atom();
connection
.send_and_check_request(&x::ConvertSelection {
requestor: window,
selection: connection.atoms.clipboard,
target: utf8_string,
property: utf8_string,
time: x::CURRENT_TIME,
})
.unwrap();
f.wait_and_dispatch();
let notify = connection.await_selection_notify();
assert_eq!(notify.property(), utf8_string, "ConvertSelection failed");
let reply = connection.get_reply(&x::GetProperty {
delete: false,
window,
property: utf8_string,
r#type: utf8_string,
long_offset: 0,
long_length: data.len() as u32,
});
let paste_data: &[u8] = reply.value();
assert_eq!(
std::str::from_utf8(paste_data).unwrap(),
std::str::from_utf8(data).unwrap()
);
}
#[test]
fn popup_done() {
let mut f = Fixture::new();
let mut conn = Connection::new(&f.display);
let toplevel = conn.new_window(conn.root, 0, 0, 20, 20, false);
f.map_as_toplevel(&mut conn, toplevel);
let popup = conn.new_window(conn.root, 0, 0, 20, 20, true);
let surface = f.map_as_popup(&mut conn, popup, 0, 0, 20, 20);
f.testwl.popup_done(surface);
f.wait_and_dispatch();
let reply = conn
.wait_for_reply(conn.send_request(&x::GetWindowAttributes { window: popup }))
.expect("Couldn't get window attributes");
assert_eq!(reply.map_state(), x::MapState::Unmapped);
}
#[test]
fn negative_output_coordinates() {
let mut f = Fixture::new();
let output = f.create_output(-500, -500);
let mut connection = Connection::new(&f.display);
let window = connection.new_window(connection.root, 0, 0, 200, 200, false);
let surface = f.map_as_toplevel(&mut connection, window);
f.testwl.move_surface_to_output(surface, &output);
f.testwl.move_pointer_to(surface, 30.0, 40.0);
f.wait_and_dispatch();
let tree = connection.get_reply(&x::QueryTree { window });
let geo = connection.get_reply(&x::GetGeometry {
drawable: x::Drawable::Window(window),
});
let reply = connection.get_reply(&x::TranslateCoordinates {
src_window: tree.parent(),
dst_window: connection.root,
src_x: geo.x(),
src_y: geo.y(),
});
assert!(reply.same_screen());
assert_eq!(reply.dst_x(), 0);
assert_eq!(reply.dst_y(), 0);
let ptr_reply = connection.get_reply(&x::QueryPointer {
window: connection.root,
});
assert!(ptr_reply.same_screen());
assert_eq!(ptr_reply.child(), window);
assert_eq!(ptr_reply.win_x(), 30);
assert_eq!(ptr_reply.win_y(), 40);
}