diff --git a/.gitignore b/.gitignore index c5c4672b..aa39b719 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ !.gitignore !/.cargo /target +/testruns diff --git a/Cargo.toml b/Cargo.toml index dcff4f90..5d14ca93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,3 +54,4 @@ opt-level = 3 [features] rc_tracking = [] +it = [] diff --git a/src/acceptor.rs b/src/acceptor.rs index 7cb619a8..02401f6a 100644 --- a/src/acceptor.rs +++ b/src/acceptor.rs @@ -7,7 +7,7 @@ use { }, std::rc::Rc, thiserror::Error, - uapi::{c, format_ustr, Errno, OwnedFd, Ustring}, + uapi::{c, format_ustr, Errno, OwnedFd, Ustr, Ustring}, }; #[derive(Debug, Error)] @@ -48,7 +48,7 @@ pub struct Acceptor { struct AllocatedSocket { // wayland-x - name: Ustring, + name: String, // /run/user/1000/wayland-x path: Ustring, insecure: Rc, @@ -56,6 +56,8 @@ struct AllocatedSocket { lock_path: Ustring, _lock_fd: OwnedFd, // /run/user/1000/wayland-x.jay + #[cfg_attr(not(feature = "it"), allow(dead_code))] + secure_path: Ustring, secure: Rc, } @@ -74,8 +76,8 @@ fn bind_socket( ) -> Result { let mut addr: c::sockaddr_un = uapi::pod_zeroed(); addr.sun_family = c::AF_UNIX as _; - let name = format_ustr!("wayland-{}", id); - let path = format_ustr!("{}/{}", xrd, name.display()); + let name = format!("wayland-{}", id); + let path = format_ustr!("{}/{}", xrd, name); let jay_path = format_ustr!("{}.jay", path.display()); let lock_path = format_ustr!("{}.lock", path.display()); if jay_path.len() + 1 > addr.sun_path.len() { @@ -110,6 +112,7 @@ fn bind_socket( insecure: insecure.clone(), lock_path, _lock_fd: lock_fd, + secure_path: jay_path, secure: secure.clone(), }) } @@ -145,7 +148,7 @@ fn allocate_socket() -> Result { } impl Acceptor { - pub fn install(state: &Rc) -> Result, AcceptorError> { + pub fn install(state: &Rc) -> Result, AcceptorError> { let socket = allocate_socket()?; log::info!("bound to socket {}", socket.path.display()); for fd in [&socket.secure, &socket.insecure] { @@ -155,7 +158,6 @@ impl Acceptor { } let id1 = state.el.id(); let id2 = state.el.id(); - let name = socket.name.to_owned(); let acc = Rc::new(Acceptor { ids: [id1, id2], socket, @@ -169,10 +171,18 @@ impl Acceptor { )?; state .el - .insert(id2, Some(acc.socket.secure.raw()), c::EPOLLIN, acc)?; - let name = Rc::new(name.display().to_string()); - state.socket_path.set(name.clone()); - Ok(name) + .insert(id2, Some(acc.socket.secure.raw()), c::EPOLLIN, acc.clone())?; + state.acceptor.set(Some(acc.clone())); + Ok(acc) + } + + pub fn socket_name(&self) -> &str { + &self.socket.name + } + + #[cfg_attr(not(feature = "it"), allow(dead_code))] + pub fn secure_path(&self) -> &Ustr { + self.socket.secure_path.as_ustr() } } diff --git a/src/backend.rs b/src/backend.rs index c21de5e5..0c877e0f 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -6,6 +6,7 @@ use { video::drm::ConnectorType, }, std::{ + any::Any, error::Error, fmt::{Debug, Display, Formatter}, rc::Rc, @@ -17,6 +18,7 @@ linear_ids!(InputDeviceIds, InputDeviceId); pub trait Backend { fn run(self: Rc) -> SpawnedFuture>>; + fn into_any(self: Rc) -> Rc; fn switch_to(&self, vtnr: u32) { let _ = vtnr; @@ -30,7 +32,7 @@ pub trait Backend { false } - fn is_freestanding(&self) -> bool { + fn import_environment(&self) -> bool { false } @@ -57,6 +59,7 @@ pub struct MonitorInfo { pub height_mm: i32, } +#[derive(Copy, Clone, Debug)] pub struct ConnectorKernelId { pub ty: ConnectorType, pub idx: u32, @@ -84,6 +87,8 @@ pub enum ConnectorEvent { ModeChanged(Mode), } +pub type TransformMatrix = [[f64; 2]; 2]; + pub trait InputDevice { fn id(&self) -> InputDeviceId; fn removed(&self) -> bool; @@ -94,11 +99,11 @@ pub trait InputDevice { fn set_left_handed(&self, left_handed: bool); fn set_accel_profile(&self, profile: InputDeviceAccelProfile); fn set_accel_speed(&self, speed: f64); - fn set_transform_matrix(&self, matrix: [[f64; 2]; 2]); + fn set_transform_matrix(&self, matrix: TransformMatrix); fn name(&self) -> Rc; } -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)] pub enum InputDeviceCapability { Keyboard, Pointer, diff --git a/src/backends/dummy.rs b/src/backends/dummy.rs index c8b372c4..4473c060 100644 --- a/src/backends/dummy.rs +++ b/src/backends/dummy.rs @@ -4,7 +4,7 @@ use { backend::{Backend, Connector, ConnectorEvent, ConnectorId, ConnectorKernelId}, video::drm::ConnectorType, }, - std::{error::Error, rc::Rc}, + std::{any::Any, error::Error, rc::Rc}, }; pub struct DummyBackend; @@ -13,6 +13,10 @@ impl Backend for DummyBackend { fn run(self: Rc) -> SpawnedFuture>> { unreachable!(); } + + fn into_any(self: Rc) -> Rc { + self + } } pub struct DummyOutput { diff --git a/src/backends/metal.rs b/src/backends/metal.rs index 448b31fa..33012c6f 100644 --- a/src/backends/metal.rs +++ b/src/backends/metal.rs @@ -7,7 +7,7 @@ use { async_engine::{AsyncError, AsyncFd, SpawnedFuture}, backend::{ Backend, BackendEvent, InputDevice, InputDeviceAccelProfile, InputDeviceCapability, - InputDeviceId, InputEvent, KeyState, + InputDeviceId, InputEvent, KeyState, TransformMatrix, }, backends::metal::video::{MetalDrmDevice, PendingDrmDevice}, dbus::{DbusError, SignalHandler}, @@ -40,6 +40,7 @@ use { }, }, std::{ + any::Any, cell::{Cell, RefCell}, error::Error, ffi::{CStr, CString}, @@ -148,6 +149,10 @@ impl Backend for MetalBackend { }) } + fn into_any(self: Rc) -> Rc { + self + } + fn switch_to(&self, vtnr: u32) { self.session.switch_to(vtnr, move |res| { if let Err(e) = res { @@ -188,7 +193,7 @@ impl Backend for MetalBackend { true } - fn is_freestanding(&self) -> bool { + fn import_environment(&self) -> bool { true } @@ -288,7 +293,7 @@ struct MetalInputDevice { left_handed: Cell>, accel_profile: Cell>, accel_speed: Cell>, - transform_matrix: Cell>, + transform_matrix: Cell>, } impl Drop for MetalInputDevice { @@ -419,7 +424,7 @@ impl InputDevice for MetalInputDevice { } } - fn set_transform_matrix(&self, matrix: [[f64; 2]; 2]) { + fn set_transform_matrix(&self, matrix: TransformMatrix) { self.transform_matrix.set(Some(matrix)); } diff --git a/src/backends/x.rs b/src/backends/x.rs index 3a965f45..f7655d5c 100644 --- a/src/backends/x.rs +++ b/src/backends/x.rs @@ -4,7 +4,7 @@ use { backend::{ AxisSource, Backend, BackendEvent, Connector, ConnectorEvent, ConnectorId, ConnectorKernelId, InputDevice, InputDeviceAccelProfile, InputDeviceCapability, - InputDeviceId, InputEvent, KeyState, Mode, MonitorInfo, ScrollAxis, + InputDeviceId, InputEvent, KeyState, Mode, MonitorInfo, ScrollAxis, TransformMatrix, }, fixed::Fixed, format::XRGB8888, @@ -48,6 +48,7 @@ use { }, }, std::{ + any::Any, borrow::Cow, cell::{Cell, RefCell}, collections::VecDeque, @@ -242,6 +243,10 @@ impl Backend for XBackend { Ok(()) }) } + + fn into_any(self: Rc) -> Rc { + self + } } pub struct XBackend { @@ -1054,7 +1059,7 @@ impl InputDevice for XSeatKeyboard { let _ = speed; } - fn set_transform_matrix(&self, matrix: [[f64; 2]; 2]) { + fn set_transform_matrix(&self, matrix: TransformMatrix) { let _ = matrix; } @@ -1103,7 +1108,7 @@ impl InputDevice for XSeatMouse { let _ = speed; } - fn set_transform_matrix(&self, matrix: [[f64; 2]; 2]) { + fn set_transform_matrix(&self, matrix: TransformMatrix) { let _ = matrix; } diff --git a/src/cli.rs b/src/cli.rs index 130b8358..b75582ab 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -44,6 +44,8 @@ pub enum Cmd { Screenshot(ScreenshotArgs), /// Inspect/modify the idle (screensaver) settings. Idle(IdleArgs), + #[cfg(feature = "it")] + RunTests, } #[derive(Args, Debug)] @@ -101,7 +103,7 @@ pub struct ScreenshotArgs { pub filename: Option, } -#[derive(Args, Debug)] +#[derive(Args, Debug, Default)] pub struct RunArgs { /// The backends to try. /// @@ -184,5 +186,7 @@ pub fn main() { Cmd::SetLogLevel(a) => set_log_level::main(cli.global, a), Cmd::Screenshot(a) => screenshot::main(cli.global, a), Cmd::Idle(a) => idle::main(cli.global, a), + #[cfg(feature = "it")] + Cmd::RunTests => crate::it::run_tests(), } } diff --git a/src/client.rs b/src/client.rs index 1584efc8..a91e35cc 100644 --- a/src/client.rs +++ b/src/client.rs @@ -42,6 +42,11 @@ impl ClientId { pub fn raw(self) -> u64 { self.0 } + + #[cfg_attr(not(feature = "it"), allow(dead_code))] + pub fn from_raw(val: u64) -> Self { + Self(val) + } } impl Display for ClientId { diff --git a/src/compositor.rs b/src/compositor.rs index aabfbdb9..6a7116aa 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "it")] +use crate::it::test_backend::TestBackend; use { crate::{ acceptor::{Acceptor, AcceptorError}, @@ -36,7 +38,7 @@ use { }, ahash::AHashSet, forker::ForkerProxy, - std::{cell::Cell, ops::Deref, rc::Rc, sync::Arc, time::Duration}, + std::{cell::Cell, future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration}, thiserror::Error, uapi::c, }; @@ -44,12 +46,9 @@ use { pub const MAX_EXTENTS: i32 = (1 << 22) - 1; pub fn start_compositor(global: GlobalArgs, args: RunArgs) { - let forker = match ForkerProxy::create() { - Ok(f) => Rc::new(f), - Err(e) => fatal!("Could not create a forker process: {}", ErrorFmt(e)), - }; + let forker = create_forker(); let logger = Logger::install_compositor(global.log_level.into()); - if let Err(e) = start_compositor2(forker, logger.clone(), args) { + if let Err(e) = start_compositor2(forker, Some(logger.clone()), args, None) { let e = ErrorFmt(e); log::error!("A fatal error occurred: {}", e); eprintln!("A fatal error occurred: {}", e); @@ -59,8 +58,21 @@ pub fn start_compositor(global: GlobalArgs, args: RunArgs) { log::info!("Exit"); } +#[cfg(feature = "it")] +pub fn start_compositor_for_test(future: TestFuture) -> Result<(), CompositorError> { + let forker = create_forker(); + start_compositor2(forker, None, RunArgs::default(), Some(future)) +} + +fn create_forker() -> Rc { + match ForkerProxy::create() { + Ok(f) => Rc::new(f), + Err(e) => fatal!("Could not create a forker process: {}", ErrorFmt(e)), + } +} + #[derive(Debug, Error)] -enum MainError { +pub enum CompositorError { #[error("The client acceptor caused an error")] AcceptorError(#[from] AcceptorError), #[error("The event loop caused an error")] @@ -86,11 +98,14 @@ const STATIC_VARS: &[(&str, &str)] = &[ ("_JAVA_AWT_WM_NONREPARENTING", "1"), ]; +pub type TestFuture = Box) -> Box>>; + fn start_compositor2( forker: Rc, - logger: Arc, + logger: Option>, run_args: RunArgs, -) -> Result<(), MainError> { + test_future: Option, +) -> Result<(), CompositorError> { log::info!("pid = {}", uapi::getpid()); init_fd_limit(); leaks::init(); @@ -155,19 +170,22 @@ fn start_compositor2( handler: Default::default(), queue: Default::default(), }, - socket_path: Default::default(), + acceptor: Default::default(), serial: Default::default(), idle_inhibitor_ids: Default::default(), run_toplevel, }); create_dummy_output(&state); - let socket_path = Acceptor::install(&state)?; + let acceptor = Acceptor::install(&state)?; forker.install(&state); - forker.setenv(WAYLAND_DISPLAY.as_bytes(), socket_path.as_bytes()); + forker.setenv( + WAYLAND_DISPLAY.as_bytes(), + acceptor.socket_name().as_bytes(), + ); for (key, val) in STATIC_VARS { forker.setenv(key.as_bytes(), val.as_bytes()); } - let _compositor = engine.spawn(start_compositor3(state.clone())); + let _compositor = engine.spawn(start_compositor3(state.clone(), test_future)); el.run()?; state.xwayland.handler.borrow_mut().take(); state.clients.clear(); @@ -178,8 +196,8 @@ fn start_compositor2( Ok(()) } -async fn start_compositor3(state: Rc) { - let backend = match create_backend(&state).await { +async fn start_compositor3(state: Rc, test_future: Option) { + let backend = match create_backend(&state, test_future).await { Some(b) => b, _ => { log::error!("Could not create a backend"); @@ -190,8 +208,10 @@ async fn start_compositor3(state: Rc) { state.backend.set(backend.clone()); state.globals.add_singletons(&backend); - if backend.is_freestanding() { - import_environment(&state, WAYLAND_DISPLAY, &state.socket_path.get()); + if backend.import_environment() { + if let Some(acc) = state.acceptor.get() { + import_environment(&state, WAYLAND_DISPLAY, acc.socket_name()); + } for (key, val) in STATIC_VARS { import_environment(&state, key, val); } @@ -228,7 +248,17 @@ fn start_global_event_handlers( res } -async fn create_backend(state: &Rc) -> Option> { +async fn create_backend( + state: &Rc, + #[allow(unused_variables)] test_future: Option, +) -> Option> { + #[cfg(feature = "it")] + if let Some(tf) = test_future { + return Some(Rc::new(TestBackend { + state: state.clone(), + test_future: tf, + })); + } let mut backends = &state.run_args.backends[..]; if backends.is_empty() { backends = &[CliBackend::X11, CliBackend::Metal]; diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 3640a5b5..90085583 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -13,6 +13,7 @@ use { }, wire::{jay_compositor::*, JayCompositorId}, }, + bstr::ByteSlice, log::Level, std::{ops::Deref, rc::Rc}, thiserror::Error, @@ -80,7 +81,11 @@ impl JayCompositor { let log_file = Rc::new(JayLogFile::new(req.id, &self.client)); track!(self.client, log_file); self.client.add_client_obj(&log_file)?; - log_file.send_path(self.client.state.logger.path()); + let path = match &self.client.state.logger { + Some(logger) => logger.path(), + _ => "".as_bytes().as_bstr(), + }; + log_file.send_path(path); Ok(()) } @@ -106,7 +111,9 @@ impl JayCompositor { TRACE => Level::Trace, _ => return Err(JayCompositorError::UnknownLogLevel(req.level)), }; - self.client.state.logger.set_level(level); + if let Some(logger) = &self.client.state.logger { + logger.set_level(level); + } Ok(()) } @@ -152,6 +159,15 @@ impl JayCompositor { self.client.add_client_obj(&idle)?; Ok(()) } + + fn get_client_id(&self, parser: MsgParser<'_, '_>) -> Result<(), JayCompositorError> { + let _req: GetClientId = self.client.parse(self, parser)?; + self.client.event(ClientId { + self_id: self.id, + client_id: self.client.id.raw(), + }); + Ok(()) + } } object_base! { @@ -163,11 +179,12 @@ object_base! { SET_LOG_LEVEL => set_log_level, TAKE_SCREENSHOT => take_screenshot, GET_IDLE => get_idle, + GET_CLIENT_ID => get_client_id, } impl Object for JayCompositor { fn num_requests(&self) -> u32 { - GET_IDLE + 1 + GET_CLIENT_ID + 1 } } diff --git a/src/it.rs b/src/it.rs new file mode 100644 index 00000000..d055a3e3 --- /dev/null +++ b/src/it.rs @@ -0,0 +1,113 @@ +#![cfg(feature = "it")] + +use { + crate::it::{test_backend::TestBackend, testrun::TestRun, tests::TestCase}, + ahash::AHashMap, + isnt::std_1::collections::IsntHashMapExt, + log::Level, + std::{ + cell::{Cell, RefCell}, + future::pending, + pin::Pin, + rc::Rc, + time::SystemTime, + }, + uapi::c, +}; + +#[macro_use] +mod test_error; +#[macro_use] +mod test_object; +pub mod test_backend; +mod test_client; +mod test_ifs; +mod test_logger; +mod test_mem; +mod test_transport; +mod testrun; +mod tests; + +pub fn run_tests() { + test_logger::install(); + test_logger::set_level(Level::Trace); + let it_run = ItRun { + path: format!( + "{}/testruns/{}", + env!("CARGO_MANIFEST_DIR"), + humantime::format_rfc3339_millis(SystemTime::now()) + ), + failed: Default::default(), + }; + for test in tests::tests() { + run_test(&it_run, test); + } + let failed = it_run.failed.borrow_mut(); + if failed.is_not_empty() { + let mut failed: Vec<_> = failed.iter().collect(); + failed.sort_by_key(|f| f.0); + log::error!("The following tests failed:"); + for (name, errors) in failed { + log::error!(" {}:", name); + for error in errors { + log::error!(" {}", error); + } + } + fatal!("Some tests failed"); + } +} + +struct ItRun { + path: String, + failed: RefCell>>, +} + +fn run_test(it_run: &ItRun, test: &'static dyn TestCase) { + let dir = format!("{}/{}", it_run.path, test.name()); + std::fs::create_dir_all(&dir).unwrap(); + let log_path = format!("{}/log", dir); + let log_file = Rc::new(uapi::open(log_path.as_str(), c::O_WRONLY | c::O_CREAT, 0o644).unwrap()); + test_logger::set_file(log_file); + let errors = Rc::new(Cell::new(Vec::new())); + let errors2 = errors.clone(); + let res = crate::compositor::start_compositor_for_test(Box::new(move |state| { + let state = state.clone(); + let server_addr = { + let mut addr: c::sockaddr_un = uapi::pod_zeroed(); + addr.sun_family = c::AF_UNIX as _; + let acceptor = state.acceptor.get().unwrap(); + let path = acceptor.secure_path(); + let sun_path = uapi::as_bytes_mut(&mut addr.sun_path[..]); + sun_path[..path.len()].copy_from_slice(path.as_bytes()); + sun_path[path.len()] = 0; + addr + }; + let backend: Rc = state.backend.get().into_any().downcast().unwrap(); + let testrun = Rc::new(TestRun { + state: state.clone(), + backend, + errors: Default::default(), + server_addr, + }); + let errors = errors2.clone(); + Box::new(async move { + let future: Pin<_> = test.run(testrun.clone()).into(); + if let Err(e) = future.await { + testrun.errors.push(e.to_string()); + } + errors.set(testrun.errors.take()); + state.el.stop(); + pending().await + }) + })); + let errors = errors.take(); + if errors.len() > 0 { + log::error!("The following errors occurred:"); + for e in &errors { + log::error!(" {}", e); + } + it_run.failed.borrow_mut().insert(test.name(), errors); + } + test_logger::unset_file(); + let _ = res; +} diff --git a/src/it/test_backend.rs b/src/it/test_backend.rs new file mode 100644 index 00000000..2b0fe542 --- /dev/null +++ b/src/it/test_backend.rs @@ -0,0 +1,136 @@ +use { + crate::{ + async_engine::SpawnedFuture, + backend::{ + Backend, Connector, ConnectorEvent, ConnectorId, ConnectorKernelId, InputDevice, + InputDeviceAccelProfile, InputDeviceCapability, InputDeviceId, InputEvent, + TransformMatrix, + }, + compositor::TestFuture, + state::State, + utils::{clonecell::CloneCell, copyhashmap::CopyHashMap, syncqueue::SyncQueue}, + }, + std::{any::Any, cell::Cell, error::Error, pin::Pin, rc::Rc}, +}; + +pub struct TestBackend { + pub state: Rc, + pub test_future: TestFuture, +} + +impl Backend for TestBackend { + fn run(self: Rc) -> SpawnedFuture>> { + let future = (self.test_future)(&self.state); + self.state.eng.spawn(async move { + let future: Pin<_> = future.into(); + future.await; + Ok(()) + }) + } + + fn into_any(self: Rc) -> Rc { + self + } + + fn switch_to(&self, vtnr: u32) { + let _ = vtnr; + } + + fn set_idle(&self, _idle: bool) {} + + fn supports_idle(&self) -> bool { + true + } + + fn supports_presentation_feedback(&self) -> bool { + true + } +} + +pub struct TestConnector { + pub id: ConnectorId, + pub kernel_id: ConnectorKernelId, + pub events: SyncQueue, + pub on_change: CloneCell>>, +} + +impl Connector for TestConnector { + fn id(&self) -> ConnectorId { + self.id + } + + fn kernel_id(&self) -> ConnectorKernelId { + self.kernel_id + } + + fn event(&self) -> Option { + self.events.pop() + } + + fn on_change(&self, cb: Rc) { + self.on_change.set(Some(cb)); + } + + fn damage(&self) { + todo!() + } +} + +pub struct TestInputDevice { + pub id: InputDeviceId, + pub remove: Cell, + pub events: SyncQueue, + pub on_change: CloneCell>>, + pub capabilities: CopyHashMap, + pub transform_matrix: Cell, + pub name: Rc, + pub accel_speed: Cell, + pub accel_profile: Cell, + pub left_handed: Cell, +} + +impl InputDevice for TestInputDevice { + fn id(&self) -> InputDeviceId { + self.id + } + + fn removed(&self) -> bool { + self.remove.get() + } + + fn event(&self) -> Option { + self.events.pop() + } + + fn on_change(&self, cb: Rc) { + self.on_change.set(Some(cb)); + } + + fn grab(&self, _grab: bool) { + // nothing + } + + fn has_capability(&self, cap: InputDeviceCapability) -> bool { + self.capabilities.contains(&cap) + } + + fn set_left_handed(&self, left_handed: bool) { + self.left_handed.set(left_handed); + } + + fn set_accel_profile(&self, profile: InputDeviceAccelProfile) { + self.accel_profile.set(profile); + } + + fn set_accel_speed(&self, speed: f64) { + self.accel_speed.set(speed) + } + + fn set_transform_matrix(&self, matrix: TransformMatrix) { + self.transform_matrix.set(matrix); + } + + fn name(&self) -> Rc { + self.name.clone() + } +} diff --git a/src/it/test_client.rs b/src/it/test_client.rs new file mode 100644 index 00000000..5e42b1e9 --- /dev/null +++ b/src/it/test_client.rs @@ -0,0 +1,34 @@ +use { + crate::{ + client::Client, + it::{ + test_ifs::{ + test_compositor::TestCompositor, test_jay_compositor::TestJayCompositor, + test_registry::TestRegistry, test_shm::TestShm, + }, + test_transport::TestTransport, + testrun::TestRun, + }, + }, + std::rc::Rc, +}; + +pub struct TestClient { + pub run: Rc, + pub server: Rc, + pub transport: Rc, + pub registry: Rc, + pub jc: Rc, + pub comp: Rc, + pub shm: Rc, +} + +impl TestClient { + pub fn error(&self, msg: &str) { + self.transport.error(msg) + } + + pub async fn sync(self: &Rc) { + self.transport.sync().await + } +} diff --git a/src/it/test_error.rs b/src/it/test_error.rs new file mode 100644 index 00000000..d73d37fc --- /dev/null +++ b/src/it/test_error.rs @@ -0,0 +1,136 @@ +use { + crate::utils::errorfmt::ErrorFmt, + std::{ + error::Error, + fmt::{Debug, Display, Formatter}, + }, +}; + +pub struct TestError { + error: Box, + source: Option>, +} + +impl TestError { + pub fn new(d: D) -> Self { + Self { + error: Box::new(DisplayError { msg: d }), + source: None, + } + } +} + +struct DisplayError { + msg: T, +} + +impl Debug for DisplayError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.msg, f) + } +} + +impl Display for DisplayError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.msg, f) + } +} + +impl Error for DisplayError {} + +impl Debug for TestError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TestError") + .field("error", &self.error) + .field("source", &self.source) + .finish() + } +} + +impl Display for TestError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut e_prev = self; + let mut e_opt = Some(self); + let mut first = true; + while let Some(e) = e_opt { + if first { + write!(f, "{}", e.error)?; + first = false; + } else { + write!(f, ": {}", e.error)?; + } + e_prev = e; + e_opt = e.source.as_deref(); + } + if let Some(e) = e_prev.error.source() { + write!(f, ": ")?; + ErrorFmt(e).fmt(f)?; + } + Ok(()) + } +} + +impl From for TestError { + fn from(error: T) -> Self { + Self { + error: Box::new(error), + source: None, + } + } +} + +pub trait TestErrorExt { + type Context; + + fn with_context(self, f: F) -> Self::Context + where + T: Display + 'static, + F: FnOnce() -> T; +} + +impl TestErrorExt for Result +where + E: StdError, +{ + type Context = Result; + + fn with_context(self, f: F) -> Self::Context + where + D: Display + 'static, + F: FnOnce() -> D, + { + match self { + Ok(v) => Ok(v), + Err(e) => Err(e.with_context(f())), + } + } +} + +pub trait StdError: 'static { + fn with_context(self, d: D) -> TestError; +} + +impl StdError for E { + fn with_context(self, d: D) -> TestError { + TestError { + error: Box::new(DisplayError { msg: d }), + source: Some(Box::new(self.into())), + } + } +} + +impl StdError for TestError { + fn with_context(self, d: D) -> TestError { + TestError { + error: Box::new(DisplayError { msg: d }), + source: Some(Box::new(self)), + } + } +} + +macro_rules! bail { + ($($tt:tt)*) => {{ + let msg = format!($($tt)*); + return Err(crate::it::test_error::TestError::new(msg)); + }} +} diff --git a/src/it/test_ifs.rs b/src/it/test_ifs.rs new file mode 100644 index 00000000..089ed624 --- /dev/null +++ b/src/it/test_ifs.rs @@ -0,0 +1,8 @@ +pub mod test_callback; +pub mod test_compositor; +pub mod test_display; +pub mod test_jay_compositor; +pub mod test_registry; +pub mod test_shm; +pub mod test_shm_buffer; +pub mod test_shm_pool; diff --git a/src/it/test_ifs/test_callback.rs b/src/it/test_ifs/test_callback.rs new file mode 100644 index 00000000..1a1f88c2 --- /dev/null +++ b/src/it/test_ifs/test_callback.rs @@ -0,0 +1,45 @@ +use { + crate::{ + it::{ + test_error::TestError, test_object::TestObject, test_transport::TestTransport, + testrun::ParseFull, + }, + utils::buffd::MsgParser, + wire::{wl_callback::*, WlCallbackId}, + }, + std::{cell::Cell, rc::Rc}, +}; + +pub struct TestCallback { + pub id: WlCallbackId, + pub transport: Rc, + pub handler: Cell>>, + pub done: Cell, +} + +impl TestCallback { + fn handle_done(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let _ev = Done::parse_full(parser)?; + self.dispatch(); + Ok(()) + } + + fn dispatch(&self) { + self.done.set(true); + if let Some(handler) = self.handler.take() { + handler(); + } + } +} + +test_object! { + TestCallback, WlCallback; + + DONE => handle_done, +} + +impl TestObject for TestCallback { + fn on_remove(&self, _transport: &TestTransport) { + self.dispatch(); + } +} diff --git a/src/it/test_ifs/test_compositor.rs b/src/it/test_ifs/test_compositor.rs new file mode 100644 index 00000000..c6a57971 --- /dev/null +++ b/src/it/test_ifs/test_compositor.rs @@ -0,0 +1,18 @@ +use { + crate::{ + it::{test_object::TestObject, test_transport::TestTransport}, + wire::WlCompositorId, + }, + std::rc::Rc, +}; + +pub struct TestCompositor { + pub id: WlCompositorId, + pub transport: Rc, +} + +test_object! { + TestCompositor, WlCompositor; +} + +impl TestObject for TestCompositor {} diff --git a/src/it/test_ifs/test_display.rs b/src/it/test_ifs/test_display.rs new file mode 100644 index 00000000..11c4e510 --- /dev/null +++ b/src/it/test_ifs/test_display.rs @@ -0,0 +1,55 @@ +use { + crate::{ + it::{ + test_error::TestError, test_object::TestObject, test_transport::TestTransport, + testrun::ParseFull, + }, + object::ObjectId, + utils::buffd::MsgParser, + wire::{wl_display::*, WlDisplayId}, + }, + std::rc::Rc, +}; + +pub struct TestDisplay { + pub transport: Rc, + pub id: WlDisplayId, +} + +impl TestDisplay { + fn handle_error(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let ev = Error::parse_full(parser)?; + let msg = format!("Compositor sent an error: {}", ev.message); + self.transport.error(&msg); + self.transport.kill(); + Ok(()) + } + + fn handle_delete_id(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let ev = DeleteId::parse_full(parser)?; + match self.transport.objects.remove(&ObjectId::from_raw(ev.id)) { + None => { + let msg = format!( + "Compositor sent delete_id for object {} which does not exist", + ev.id + ); + self.transport.error(&msg); + self.transport.kill(); + } + Some(obj) => { + obj.on_remove(&self.transport); + self.transport.obj_ids.borrow_mut().release(ev.id); + } + } + Ok(()) + } +} + +test_object! { + TestDisplay, WlDisplay; + + ERROR => handle_error, + DELETE_ID => handle_delete_id, +} + +impl TestObject for TestDisplay {} diff --git a/src/it/test_ifs/test_jay_compositor.rs b/src/it/test_ifs/test_jay_compositor.rs new file mode 100644 index 00000000..0e28c2ce --- /dev/null +++ b/src/it/test_ifs/test_jay_compositor.rs @@ -0,0 +1,48 @@ +use { + crate::{ + client::ClientId, + it::{ + test_error::TestError, test_object::TestObject, test_transport::TestTransport, + testrun::ParseFull, + }, + utils::buffd::MsgParser, + wire::{ + jay_compositor::{self, *}, + JayCompositorId, + }, + }, + std::{cell::Cell, rc::Rc}, +}; + +pub struct TestJayCompositor { + pub id: JayCompositorId, + pub transport: Rc, + pub client_id: Cell>, +} + +impl TestJayCompositor { + pub async fn get_client_id(&self) -> Result { + if self.client_id.get().is_none() { + self.transport.send(GetClientId { self_id: self.id }); + } + self.transport.sync().await; + match self.client_id.get() { + Some(c) => Ok(c), + _ => bail!("Compositor did not send a client id"), + } + } + + fn handle_client_id(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let ev = jay_compositor::ClientId::parse_full(parser)?; + self.client_id.set(Some(ClientId::from_raw(ev.client_id))); + Ok(()) + } +} + +test_object! { + TestJayCompositor, JayCompositor; + + CLIENT_ID => handle_client_id, +} + +impl TestObject for TestJayCompositor {} diff --git a/src/it/test_ifs/test_registry.rs b/src/it/test_ifs/test_registry.rs new file mode 100644 index 00000000..d31fb5f9 --- /dev/null +++ b/src/it/test_ifs/test_registry.rs @@ -0,0 +1,185 @@ +use { + crate::{ + it::{ + test_error::TestError, + test_ifs::{ + test_compositor::TestCompositor, test_jay_compositor::TestJayCompositor, + test_shm::TestShm, + }, + test_object::TestObject, + test_transport::TestTransport, + testrun::ParseFull, + }, + utils::{buffd::MsgParser, clonecell::CloneCell, copyhashmap::CopyHashMap}, + wire::{wl_registry::*, WlRegistryId}, + }, + std::{cell::Cell, rc::Rc}, +}; + +pub struct TestGlobal { + pub name: u32, + pub interface: String, + pub version: u32, +} + +pub struct TestRegistrySingletons { + pub jay_compositor: u32, + pub wl_compositor: u32, + pub wl_shm: u32, +} + +pub struct TestRegistry { + pub id: WlRegistryId, + pub transport: Rc, + pub globals: CopyHashMap>, + pub singletons: CloneCell>>, + pub jay_compositor: CloneCell>>, + pub compositor: CloneCell>>, + pub shm: CloneCell>>, +} + +macro_rules! singleton { + ($field:expr) => { + if let Some(s) = $field.get() { + return Ok(s); + } + }; +} + +impl TestRegistry { + pub async fn get_singletons(&self) -> Result, TestError> { + singleton!(self.singletons); + self.transport.sync().await; + singleton!(self.singletons); + let mut jay_compositor = 0; + let mut wl_compositor = 0; + let mut wl_shm = 0; + for global in self.globals.lock().values() { + match global.interface.as_str() { + "jay_compositor" => jay_compositor = global.name, + "wl_compositor" => wl_compositor = global.name, + "wl_shm" => wl_shm = global.name, + _ => {} + } + } + macro_rules! singleton { + ($($name:ident,)*) => { + TestRegistrySingletons { + $( + $name: { + if $name == 0 { + bail!("Compositor did not send {} singleton", stringify!($name)); + } + $name + }, + )* + } + } + } + let singletons = Rc::new(singleton! { + jay_compositor, + wl_compositor, + wl_shm, + }); + self.singletons.set(Some(singletons.clone())); + Ok(singletons) + } + + pub async fn get_jay_compositor(&self) -> Result, TestError> { + singleton!(self.jay_compositor); + let singletons = self.get_singletons().await?; + singleton!(self.jay_compositor); + let jc = Rc::new(TestJayCompositor { + id: self.transport.id(), + transport: self.transport.clone(), + client_id: Default::default(), + }); + self.bind(&jc, singletons.jay_compositor, 1)?; + self.jay_compositor.set(Some(jc.clone())); + Ok(jc) + } + + pub async fn get_compositor(&self) -> Result, TestError> { + singleton!(self.compositor); + let singletons = self.get_singletons().await?; + singleton!(self.compositor); + let jc = Rc::new(TestCompositor { + id: self.transport.id(), + transport: self.transport.clone(), + }); + self.bind(&jc, singletons.wl_compositor, 4)?; + self.compositor.set(Some(jc.clone())); + Ok(jc) + } + + pub async fn get_shm(&self) -> Result, TestError> { + singleton!(self.shm); + let singletons = self.get_singletons().await?; + singleton!(self.shm); + let jc = Rc::new(TestShm { + id: self.transport.id(), + transport: self.transport.clone(), + formats: Default::default(), + formats_awaited: Cell::new(false), + }); + self.bind(&jc, singletons.wl_shm, 1)?; + self.shm.set(Some(jc.clone())); + Ok(jc) + } + + pub fn bind( + &self, + obj: &Rc, + name: u32, + version: u32, + ) -> Result<(), TestError> { + self.transport.send(Bind { + self_id: self.id, + name, + interface: obj.interface().name(), + version, + id: obj.id().into(), + }); + self.transport.add_obj(obj.clone())?; + Ok(()) + } + + fn handle_global(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let ev = Global::parse_full(parser)?; + let prev = self.globals.set( + ev.name, + Rc::new(TestGlobal { + name: ev.name, + interface: ev.interface.to_string(), + version: ev.version, + }), + ); + if prev.is_some() { + self.transport.error(&format!( + "Compositor sent global {} multiple times", + ev.name + )); + } + Ok(()) + } + + fn handle_global_remove(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let ev = GlobalRemove::parse_full(parser)?; + if self.globals.remove(&ev.name).is_none() { + self.transport.error(&format!( + "Compositor sent global_remove for {} which does not exist", + ev.name + )); + } + Ok(()) + } +} + +test_object! { + TestRegistry, WlRegistry; + + GLOBAL => handle_global, + GLOBAL_REMOVE => handle_global_remove, +} + +impl TestObject for TestRegistry {} diff --git a/src/it/test_ifs/test_shm.rs b/src/it/test_ifs/test_shm.rs new file mode 100644 index 00000000..8c4f5f77 --- /dev/null +++ b/src/it/test_ifs/test_shm.rs @@ -0,0 +1,59 @@ +use { + crate::{ + it::{ + test_error::TestError, test_ifs::test_shm_pool::TestShmPool, test_mem::TestMem, + test_object::TestObject, test_transport::TestTransport, testrun::ParseFull, + }, + utils::{buffd::MsgParser, clonecell::CloneCell, copyhashmap::CopyHashMap}, + wire::{wl_shm::*, WlShmId}, + }, + std::{cell::Cell, rc::Rc}, +}; + +pub struct TestShm { + pub id: WlShmId, + pub transport: Rc, + pub formats: CopyHashMap, + pub formats_awaited: Cell, +} + +impl TestShm { + pub async fn formats(&self) -> &CopyHashMap { + if !self.formats_awaited.replace(true) { + self.transport.sync().await; + } + &self.formats + } + + pub fn create_pool(&self, size: usize) -> Result, TestError> { + let mem = TestMem::new(size)?; + let pool = Rc::new(TestShmPool { + id: self.transport.id(), + transport: self.transport.clone(), + mem: CloneCell::new(mem.clone()), + destroyed: Cell::new(false), + }); + self.transport.send(CreatePool { + self_id: self.id, + id: pool.id, + fd: mem.fd.clone(), + size: size as _, + }); + self.transport.add_obj(pool.clone())?; + Ok(pool) + } + + fn handle_format(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let ev = Format::parse_full(parser)?; + self.formats.set(ev.format, ()); + Ok(()) + } +} + +test_object! { + TestShm, WlShm; + + FORMAT => handle_format, +} + +impl TestObject for TestShm {} diff --git a/src/it/test_ifs/test_shm_buffer.rs b/src/it/test_ifs/test_shm_buffer.rs new file mode 100644 index 00000000..54510df1 --- /dev/null +++ b/src/it/test_ifs/test_shm_buffer.rs @@ -0,0 +1,61 @@ +use { + crate::{ + it::{ + test_error::TestError, test_mem::TestMem, test_object::TestObject, + test_transport::TestTransport, testrun::ParseFull, + }, + utils::buffd::MsgParser, + wire::{wl_buffer::*, WlBufferId}, + }, + std::{ + cell::Cell, + ops::{Deref, Range}, + rc::Rc, + }, +}; + +pub struct TestShmBuffer { + pub id: WlBufferId, + pub transport: Rc, + pub range: Range, + pub mem: Rc, + pub released: Cell, + pub destroyed: Cell, +} + +impl TestShmBuffer { + pub fn destroy(&self) { + if self.destroyed.replace(true) { + return; + } + self.transport.send(Destroy { self_id: self.id }); + } + + fn handle_release(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let _ev = Release::parse_full(parser)?; + self.released.set(true); + Ok(()) + } +} + +impl Deref for TestShmBuffer { + type Target = [Cell]; + + fn deref(&self) -> &Self::Target { + &self.mem[self.range.clone()] + } +} + +impl Drop for TestShmBuffer { + fn drop(&mut self) { + self.destroy(); + } +} + +test_object! { + TestShmBuffer, WlBuffer; + + RELEASE => handle_release, +} + +impl TestObject for TestShmBuffer {} diff --git a/src/it/test_ifs/test_shm_pool.rs b/src/it/test_ifs/test_shm_pool.rs new file mode 100644 index 00000000..a58dbb81 --- /dev/null +++ b/src/it/test_ifs/test_shm_pool.rs @@ -0,0 +1,86 @@ +use { + crate::{ + format::Format, + it::{ + test_error::TestError, test_ifs::test_shm_buffer::TestShmBuffer, test_mem::TestMem, + test_object::TestObject, test_transport::TestTransport, + }, + utils::clonecell::CloneCell, + wire::{wl_shm_pool::*, WlShmPoolId}, + }, + std::{cell::Cell, rc::Rc}, +}; + +pub struct TestShmPool { + pub id: WlShmPoolId, + pub transport: Rc, + pub mem: CloneCell>, + pub destroyed: Cell, +} + +impl TestShmPool { + pub fn create_buffer( + &self, + offset: i32, + width: i32, + height: i32, + stride: i32, + format: &Format, + ) -> Result, TestError> { + let size = (height * stride) as usize; + let start = offset as usize; + let end = start + size; + let mem = self.mem.get(); + if end > mem.len() { + bail!("Out-of-bounds buffer"); + } + let buffer = Rc::new(TestShmBuffer { + id: self.transport.id(), + transport: self.transport.clone(), + range: start..end, + mem, + released: Cell::new(true), + destroyed: Cell::new(false), + }); + self.transport.add_obj(buffer.clone())?; + self.transport.send(CreateBuffer { + self_id: self.id, + id: buffer.id, + offset, + width, + height, + stride, + format: format.wl_id.unwrap_or(format.drm), + }); + Ok(buffer) + } + + pub fn resize(&self, size: usize) -> Result<(), TestError> { + let mem = self.mem.get().grow(size)?; + self.mem.set(mem); + self.transport.send(Resize { + self_id: self.id, + size: size as _, + }); + Ok(()) + } + + pub fn destroy(&self) { + if self.destroyed.replace(true) { + return; + } + self.transport.send(Destroy { self_id: self.id }); + } +} + +impl Drop for TestShmPool { + fn drop(&mut self) { + self.destroy() + } +} + +test_object! { + TestShmPool, WlShmPool; +} + +impl TestObject for TestShmPool {} diff --git a/src/it/test_logger.rs b/src/it/test_logger.rs new file mode 100644 index 00000000..669ed8c3 --- /dev/null +++ b/src/it/test_logger.rs @@ -0,0 +1,73 @@ +use { + crate::utils::clonecell::CloneCell, + log::{Level, LevelFilter, Log, Metadata, Record}, + std::{cell::Cell, fmt::Write as FmtWrite, io::Write, rc::Rc, time::SystemTime}, + uapi::{Fd, OwnedFd}, +}; + +#[thread_local] +static LEVEL: Cell = Cell::new(Level::Info); + +#[thread_local] +static FILE: CloneCell>> = CloneCell::new(None); + +pub fn install() { + log::set_logger(&Logger).unwrap(); + log::set_max_level(LevelFilter::Info); +} + +pub fn set_level(level: Level) { + LEVEL.set(level); + log::set_max_level(level.to_level_filter()); +} + +pub fn set_file(file: Rc) { + FILE.set(Some(file)); +} + +pub fn unset_file() { + FILE.set(None); +} + +struct Logger; + +impl Log for Logger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= LEVEL.get() + } + + fn log(&self, record: &Record) { + if record.level() > LEVEL.get() { + return; + } + let mut buf = String::new(); + let now = SystemTime::now(); + let _ = if let Some(mp) = record.module_path() { + writeln!( + buf, + "[{} {:5} {}] {}", + humantime::format_rfc3339_millis(now), + record.level(), + mp, + record.args(), + ) + } else { + writeln!( + buf, + "[{} {:5}] {}", + humantime::format_rfc3339_millis(now), + record.level(), + record.args(), + ) + }; + let mut fd = match FILE.get() { + Some(f) => f.borrow(), + _ => Fd::new(2), + }; + let _ = fd.write_all(buf.as_bytes()); + } + + fn flush(&self) { + // nothing + } +} diff --git a/src/it/test_mem.rs b/src/it/test_mem.rs new file mode 100644 index 00000000..e7e608d2 --- /dev/null +++ b/src/it/test_mem.rs @@ -0,0 +1,71 @@ +use { + crate::{ + it::test_error::TestError, + utils::{oserror::OsError, ptr_ext::PtrExt}, + }, + std::{cell::Cell, ops::Deref, ptr, rc::Rc}, + uapi::{c, OwnedFd}, +}; + +pub struct TestMem { + pub fd: Rc, + slice: *const [Cell], +} + +impl TestMem { + pub fn new(size: usize) -> Result, TestError> { + let fd = uapi::memfd_create("test_pool", c::MFD_CLOEXEC | c::MFD_ALLOW_SEALING)?; + uapi::fcntl_add_seals(fd.raw(), c::F_SEAL_SHRINK)?; + uapi::ftruncate(fd.raw(), size as _)?; + let slice = map(fd.raw(), size)?; + Ok(Rc::new(Self { + fd: Rc::new(fd), + slice, + })) + } + + pub fn grow(&self, size: usize) -> Result, TestError> { + let cur_len = uapi::fstat(self.fd.raw())?; + if size > cur_len.st_size as _ { + uapi::ftruncate(self.fd.raw(), size as _)?; + } + let slice = map(self.fd.raw(), size)?; + Ok(Rc::new(Self { + fd: self.fd.clone(), + slice, + })) + } +} + +impl Deref for TestMem { + type Target = [Cell]; + + fn deref(&self) -> &Self::Target { + unsafe { &*self.slice } + } +} + +fn map(fd: c::c_int, size: usize) -> Result<*const [Cell], TestError> { + unsafe { + let res = c::mmap( + ptr::null_mut(), + size as _, + c::PROT_READ | c::PROT_WRITE, + c::MAP_SHARED, + fd, + 0, + ); + if res == c::MAP_FAILED { + bail!("Could not map memory: {}", OsError::default()); + } + Ok(std::slice::from_raw_parts(res as _, size)) + } +} + +impl Drop for TestMem { + fn drop(&mut self) { + unsafe { + c::munmap(self.slice.deref().as_ptr() as _, self.slice.deref().len()); + } + } +} diff --git a/src/it/test_object.rs b/src/it/test_object.rs new file mode 100644 index 00000000..95c5038a --- /dev/null +++ b/src/it/test_object.rs @@ -0,0 +1,54 @@ +use { + crate::{ + it::{test_error::TestError, test_transport::TestTransport}, + object::{Interface, ObjectId}, + utils::buffd::MsgParser, + }, + std::rc::Rc, +}; + +macro_rules! test_object { + ($oname:ident, $ifname:ident; $($code:ident => $f:ident,)*) => { + impl crate::it::test_object::TestObjectBase for $oname { + fn id(&self) -> crate::object::ObjectId { + self.id.into() + } + + #[allow(unused_variables, unreachable_code)] + fn handle_request( + self: std::rc::Rc, + request: u32, + parser: crate::utils::buffd::MsgParser<'_, '_>, + ) -> Result<(), crate::it::test_error::TestError> { + use crate::it::test_error::TestErrorExt; + let res: Result<(), crate::it::test_error::TestError> = match request { + $( + $code => $oname::$f(&self, parser).with_context(|| format!("While handling a `{}` event", stringify!($f))), + )* + _ => Err(crate::it::test_error::TestError::new("Unknown event {}")), + }; + res.with_context(|| format!("In object {} of type `{}`", self.id(), self.interface().name())) + } + + fn interface(&self) -> crate::object::Interface { + crate::wire::$ifname + } + } + }; +} + +pub trait TestObjectBase: 'static { + fn id(&self) -> ObjectId; + fn handle_request( + self: Rc, + request: u32, + parser: MsgParser<'_, '_>, + ) -> Result<(), TestError>; + fn interface(&self) -> Interface; +} + +pub trait TestObject: TestObjectBase { + fn on_remove(&self, transport: &TestTransport) { + let _ = transport; + } +} diff --git a/src/it/test_transport.rs b/src/it/test_transport.rs new file mode 100644 index 00000000..d842096e --- /dev/null +++ b/src/it/test_transport.rs @@ -0,0 +1,254 @@ +use { + crate::{ + async_engine::{AsyncFd, SpawnedFuture}, + client::{ClientId, EventFormatter}, + it::{ + test_error::{StdError, TestError}, + test_ifs::{test_callback::TestCallback, test_registry::TestRegistry}, + test_object::TestObject, + testrun::TestRun, + }, + object::{ObjectId, WL_DISPLAY_ID}, + utils::{ + asyncevent::AsyncEvent, + bitfield::Bitfield, + buffd::{BufFdIn, BufFdOut, MsgFormatter, MsgParser, OutBuffer, OutBufferSwapchain}, + copyhashmap::CopyHashMap, + stack::Stack, + vec_ext::VecExt, + }, + wire::wl_display, + }, + std::{ + cell::{Cell, RefCell}, + collections::VecDeque, + future::Future, + mem, + rc::Rc, + task::Poll, + }, +}; + +pub struct TestTransport { + pub run: Rc, + pub fd: AsyncFd, + pub client_id: Cell, + pub bufs: Stack>, + pub swapchain: Rc>, + pub flush_request: AsyncEvent, + pub incoming: Cell>>, + pub outgoing: Cell>>, + pub objects: CopyHashMap>, + pub obj_ids: RefCell, + pub killed: Cell, +} + +impl TestTransport { + pub fn get_registry(self: &Rc) -> Rc { + let reg = Rc::new(TestRegistry { + id: self.id(), + transport: self.clone(), + globals: Default::default(), + singletons: Default::default(), + jay_compositor: Default::default(), + compositor: Default::default(), + shm: Default::default(), + }); + self.send(wl_display::GetRegistry { + self_id: WL_DISPLAY_ID, + registry: reg.id, + }); + let _ = self.add_obj(reg.clone()); + reg + } + + pub fn add_obj(&self, obj: Rc) -> Result<(), TestError> { + if self.killed.get() { + bail!("Transport has already been killed"); + } + let id = obj.id(); + if self.objects.set(id, obj).is_some() { + bail!("There already is an object with id {}", id); + } + Ok(()) + } + + pub fn kill(&self) { + self.outgoing.take(); + self.incoming.take(); + for (_, object) in self.objects.lock().drain() { + object.on_remove(self); + } + } + + pub fn sync(self: &Rc) -> impl Future { + let cb = Rc::new(TestCallback { + id: self.id(), + transport: self.clone(), + handler: Cell::new(None), + done: Cell::new(self.killed.get()), + }); + self.send(wl_display::Sync { + self_id: WL_DISPLAY_ID, + callback: cb.id, + }); + let _ = self.add_obj(cb.clone()); + futures_util::future::poll_fn(move |ctx| { + if cb.done.get() { + Poll::Ready(()) + } else { + let waker = ctx.waker().clone(); + cb.handler.set(Some(Box::new(move || waker.wake()))); + Poll::Pending + } + }) + } + + pub fn id>(&self) -> T { + ObjectId::from_raw(self.obj_ids.borrow_mut().acquire()).into() + } + + pub fn error(&self, msg: &str) { + let msg = format!("In client {}: {}", self.client_id.get(), msg); + self.run.errors.push(msg); + } + + pub fn init(self: &Rc) { + self.incoming.set(Some( + self.run.state.eng.spawn( + Incoming { + tc: self.clone(), + buf: BufFdIn::new(self.fd.clone()), + } + .run(), + ), + )); + self.outgoing.set(Some( + self.run.state.eng.spawn( + Outgoing { + tc: self.clone(), + buf: BufFdOut::new(self.fd.clone()), + buffers: Default::default(), + } + .run(), + ), + )); + } + + pub fn send(&self, msg: M) { + if self.killed.get() { + return; + } + let mut fds = vec![]; + let mut swapchain = self.swapchain.borrow_mut(); + let mut fmt = MsgFormatter::new(&mut swapchain.cur, &mut fds); + msg.format(&mut fmt); + fmt.write_len(); + if swapchain.cur.is_full() { + swapchain.commit(); + } + self.flush_request.trigger(); + } +} + +struct Outgoing { + tc: Rc, + buf: BufFdOut, + buffers: VecDeque, +} + +impl Outgoing { + async fn run(mut self: Self) { + loop { + self.tc.flush_request.triggered().await; + if let Err(e) = self.flush().await { + let msg = format!( + "Could not process an outgoing message for client {}: {}", + self.tc.client_id.get(), + e + ); + log::error!("{}", msg); + self.tc.run.errors.push(msg); + break; + } + } + } + + async fn flush(&mut self) -> Result<(), TestError> { + { + let mut swapchain = self.tc.swapchain.borrow_mut(); + swapchain.commit(); + mem::swap(&mut swapchain.pending, &mut self.buffers); + } + while let Some(mut cur) = self.buffers.pop_front() { + if let Err(e) = self.buf.flush_no_timeout(&mut cur).await { + return Err(e.with_context("Could not write to wayland socket")); + } + self.tc.swapchain.borrow_mut().free.push(cur); + } + Ok(()) + } +} + +struct Incoming { + tc: Rc, + buf: BufFdIn, +} + +impl Incoming { + async fn run(mut self: Self) { + loop { + if let Err(e) = self.handle_msg().await { + let msg = format!( + "Could not process an incoming message for client {}: {}", + self.tc.client_id.get(), + e + ); + log::error!("{}", msg); + self.tc.run.errors.push(msg); + break; + } + } + self.tc.kill(); + } + + async fn handle_msg(&mut self) -> Result<(), TestError> { + let mut hdr = [0u32, 0]; + if let Err(e) = self.buf.read_full(&mut hdr[..]).await { + return Err(e.with_context("Could not read from wayland socket")); + } + let obj_id = ObjectId::from_raw(hdr[0]); + let len = (hdr[1] >> 16) as usize; + let request = hdr[1] & 0xffff; + if len < 8 { + bail!("Message size is < 8"); + } + if len % 4 != 0 { + bail!("Message size is not a multiple of 4"); + } + let len = len / 4 - 2; + let mut data_buf = self.tc.bufs.pop().unwrap_or_default(); + data_buf.clear(); + data_buf.reserve(len); + let unused = data_buf.split_at_spare_mut_ext().1; + if let Err(e) = self.buf.read_full(&mut unused[..len]).await { + return Err(e.with_context("Could not read from wayland socket")); + } + unsafe { + data_buf.set_len(len); + } + let object = match self.tc.objects.get(&obj_id) { + Some(obj) => obj, + _ => bail!( + "Compositor sent a message for object {} which does not exist", + obj_id + ), + }; + let parser = MsgParser::new(&mut self.buf, &data_buf); + object.handle_request(request, parser)?; + if data_buf.capacity() > 0 { + self.tc.bufs.push(data_buf); + } + Ok(()) + } +} diff --git a/src/it/testrun.rs b/src/it/testrun.rs new file mode 100644 index 00000000..b1b5bd94 --- /dev/null +++ b/src/it/testrun.rs @@ -0,0 +1,100 @@ +use { + crate::{ + client::{ClientId, RequestParser}, + it::{ + test_backend::TestBackend, + test_client::TestClient, + test_error::{TestError, TestErrorExt}, + test_ifs::test_display::TestDisplay, + test_transport::TestTransport, + }, + object::WL_DISPLAY_ID, + state::State, + utils::{bitfield::Bitfield, buffd::MsgParser, oserror::OsErrorExt, stack::Stack}, + }, + std::{ + cell::{Cell, RefCell}, + rc::Rc, + }, + uapi::c, +}; + +pub struct TestRun { + pub state: Rc, + pub backend: Rc, + pub errors: Stack, + pub server_addr: c::sockaddr_un, +} + +impl TestRun { + pub async fn create_client(self: &Rc) -> Result, TestError> { + self.create_client2() + .await + .with_context(|| "Could not create a client") + } + + async fn create_client2(self: &Rc) -> Result, TestError> { + let socket = uapi::socket( + c::AF_UNIX, + c::SOCK_STREAM | c::SOCK_CLOEXEC | c::SOCK_NONBLOCK, + 0, + ) + .to_os_error() + .with_context(|| "Could not create a unix socket")?; + let socket = Rc::new(socket); + uapi::connect(socket.raw(), &self.server_addr) + .to_os_error() + .with_context(|| "Could not connect to the compositor")?; + let fd = self + .state + .eng + .fd(&socket) + .with_context(|| "Could not create an async fd")?; + let mut obj_ids = Bitfield::default(); + obj_ids.take(0); + obj_ids.take(1); + let transport = Rc::new(TestTransport { + run: self.clone(), + fd, + client_id: Cell::new(ClientId::from_raw(0)), + bufs: Default::default(), + swapchain: Default::default(), + flush_request: Default::default(), + incoming: Default::default(), + outgoing: Default::default(), + objects: Default::default(), + obj_ids: RefCell::new(obj_ids), + killed: Cell::new(false), + }); + transport.add_obj(Rc::new(TestDisplay { + transport: transport.clone(), + id: WL_DISPLAY_ID, + }))?; + transport.init(); + let registry = transport.get_registry(); + let jc = registry.get_jay_compositor().await?; + let client_id = jc.get_client_id().await?; + let client = self.state.clients.get(client_id)?; + Ok(Rc::new(TestClient { + run: self.clone(), + server: client, + transport, + jc, + comp: registry.get_compositor().await?, + shm: registry.get_shm().await?, + registry, + })) + } +} + +pub trait ParseFull<'a>: Sized { + fn parse_full(parser: MsgParser<'_, 'a>) -> Result; +} + +impl<'a, T: RequestParser<'a>> ParseFull<'a> for T { + fn parse_full(mut parser: MsgParser<'_, 'a>) -> Result { + let res = T::parse(&mut parser)?; + parser.eof()?; + Ok(res) + } +} diff --git a/src/it/tests.rs b/src/it/tests.rs new file mode 100644 index 00000000..016b0384 --- /dev/null +++ b/src/it/tests.rs @@ -0,0 +1,47 @@ +use { + crate::it::{test_error::TestError, testrun::TestRun}, + std::{future::Future, rc::Rc}, +}; + +macro_rules! testcase { + () => { + pub struct Test; + + impl crate::it::tests::TestCase for Test { + fn name(&self) -> &'static str { + module_path!().strip_prefix("jay::it::tests::").unwrap() + } + + fn run( + &self, + testrun: std::rc::Rc, + ) -> Box>> { + Box::new(test(testrun)) + } + } + }; +} + +macro_rules! tassert { + ($cond:expr) => { + if !$cond { + bail!( + "Assert `{}` failed ({}:{})", + stringify!($cond), + file!(), + line!() + ); + } + }; +} + +mod t0001_shm_formats; + +pub trait TestCase { + fn name(&self) -> &'static str; + fn run(&self, testrun: Rc) -> Box>>; +} + +pub fn tests() -> Vec<&'static dyn TestCase> { + vec![&t0001_shm_formats::Test] +} diff --git a/src/it/tests/t0001_shm_formats.rs b/src/it/tests/t0001_shm_formats.rs new file mode 100644 index 00000000..26117aa6 --- /dev/null +++ b/src/it/tests/t0001_shm_formats.rs @@ -0,0 +1,18 @@ +use { + crate::{ + format::{ARGB8888, XRGB8888}, + it::{test_error::TestError, testrun::TestRun}, + }, + std::rc::Rc, +}; + +testcase!(); + +/// Test that wl_shm supports the required formats +async fn test(run: Rc) -> Result<(), TestError> { + let client = run.create_client().await?; + let formats = client.shm.formats().await; + tassert!(formats.contains(&XRGB8888.wl_id.unwrap())); + tassert!(formats.contains(&ARGB8888.wl_id.unwrap())); + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 002967de..1d95c2cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,6 +52,7 @@ mod forker; mod format; mod globals; mod ifs; +mod it; mod libinput; mod logger; mod logind; diff --git a/src/state.rs b/src/state.rs index 6e91596a..8bea1092 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,5 +1,6 @@ use { crate::{ + acceptor::Acceptor, async_engine::{AsyncEngine, SpawnedFuture}, backend::{ Backend, BackendEvent, Connector, ConnectorId, ConnectorIds, InputDevice, @@ -83,14 +84,14 @@ pub struct State { pub pending_float_titles: AsyncQueue>, pub dbus: Dbus, pub fdcloser: Arc, - pub logger: Arc, + pub logger: Option>, pub connectors: CopyHashMap>, pub outputs: CopyHashMap>, pub status: CloneCell>, pub idle: IdleState, pub run_args: RunArgs, pub xwayland: XWaylandState, - pub socket_path: CloneCell>, + pub acceptor: CloneCell>>, pub serial: NumCell>, pub run_toplevel: Rc, } diff --git a/src/utils/bitfield.rs b/src/utils/bitfield.rs index 930c38f0..cdb58aa2 100644 --- a/src/utils/bitfield.rs +++ b/src/utils/bitfield.rs @@ -14,7 +14,7 @@ impl Bitfield { let idx = val as usize / SEG_SIZE; let pos = val as usize % SEG_SIZE; while self.vals.len() <= idx { - self.vals.push(0); + self.vals.push(!0); } self.vals[idx] &= !(1 << pos); } diff --git a/src/utils/clonecell.rs b/src/utils/clonecell.rs index 03d12037..90160ff9 100644 --- a/src/utils/clonecell.rs +++ b/src/utils/clonecell.rs @@ -12,7 +12,7 @@ use { }, }; -pub struct CloneCell { +pub struct CloneCell { data: UnsafeCell, } @@ -26,19 +26,22 @@ impl Clone for CloneCell { impl Debug for CloneCell { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - unsafe { self.data.get().deref().fmt(f) } + self.get().fmt(f) } } -impl CloneCell { - pub fn new(t: T) -> Self { +impl CloneCell { + pub const fn new(t: T) -> Self { Self { data: UnsafeCell::new(t), } } #[inline(always)] - pub fn get(&self) -> T { + pub fn get(&self) -> T + where + T: UnsafeCellCloneSafe, + { unsafe { self.data.get().deref().clone() } } @@ -52,7 +55,7 @@ impl CloneCell { where T: Default, { - unsafe { mem::take(self.data.get().deref_mut()) } + self.set(T::default()) } } diff --git a/src/utils/copyhashmap.rs b/src/utils/copyhashmap.rs index 0ec070ea..0f8810cb 100644 --- a/src/utils/copyhashmap.rs +++ b/src/utils/copyhashmap.rs @@ -37,10 +37,8 @@ impl CopyHashMap { Self::default() } - pub fn set(&self, k: K, v: V) { - unsafe { - self.map.get().deref_mut().insert(k, v); - } + pub fn set(&self, k: K, v: V) -> Option { + unsafe { self.map.get().deref_mut().insert(k, v) } } pub fn get(&self, k: &Q) -> Option diff --git a/src/utils/oserror.rs b/src/utils/oserror.rs index 148c02bd..a0ea8f5f 100644 --- a/src/utils/oserror.rs +++ b/src/utils/oserror.rs @@ -204,3 +204,17 @@ impl Display for OsError { write!(f, "{} (os error {})", msg, self.0) } } + +pub trait OsErrorExt { + type Container; + + fn to_os_error(self) -> Self::Container; +} + +impl OsErrorExt for Result { + type Container = Result; + + fn to_os_error(self) -> Self::Container { + self.map_err(|e| e.into()) + } +} diff --git a/src/xwayland.rs b/src/xwayland.rs index af43cd32..6f4c8c26 100644 --- a/src/xwayland.rs +++ b/src/xwayland.rs @@ -100,7 +100,7 @@ pub async fn manage(state: Rc) { forker.setenv(DISPLAY.as_bytes(), display.as_bytes()); log::info!("Allocated display :{} for Xwayland", xsocket.id); log::info!("Waiting for connection attempt"); - if state.backend.get().is_freestanding() { + if state.backend.get().import_environment() { import_environment(&state, DISPLAY, &display); } let res = XWaylandError::tria(async { diff --git a/wire/jay_compositor.txt b/wire/jay_compositor.txt index 90095c41..d5924dca 100644 --- a/wire/jay_compositor.txt +++ b/wire/jay_compositor.txt @@ -22,3 +22,13 @@ msg take_screenshot = 4 { msg get_idle = 5 { id: id(jay_idle), } + +msg get_client_id = 6 { + +} + +# events + +msg client_id = 0 { + client_id: pod(u64), +}