diff --git a/src/it/test_ifs.rs b/src/it/test_ifs.rs index 5a9283d5..c2e2d576 100644 --- a/src/it/test_ifs.rs +++ b/src/it/test_ifs.rs @@ -5,6 +5,10 @@ pub mod test_content_type; pub mod test_content_type_manager; pub mod test_cursor_shape_device; pub mod test_cursor_shape_manager; +pub mod test_data_control_device; +pub mod test_data_control_manager; +pub mod test_data_control_offer; +pub mod test_data_control_source; pub mod test_data_device; pub mod test_data_device_manager; pub mod test_data_offer; diff --git a/src/it/test_ifs/test_data_control_device.rs b/src/it/test_ifs/test_data_control_device.rs new file mode 100644 index 00000000..f15941b2 --- /dev/null +++ b/src/it/test_ifs/test_data_control_device.rs @@ -0,0 +1,111 @@ +use { + crate::{ + it::{ + test_error::{TestError, TestResult}, + test_ifs::{ + test_data_control_offer::TestDataControlOffer, + test_data_control_source::TestDataControlSource, + }, + test_object::TestObject, + test_transport::TestTransport, + test_utils::test_expected_event::TEEH, + testrun::ParseFull, + }, + utils::{buffd::MsgParser, copyhashmap::CopyHashMap}, + wire::{ + zwlr_data_control_device_v1::*, ZwlrDataControlDeviceV1Id, ZwlrDataControlOfferV1Id, + }, + }, + std::{cell::Cell, rc::Rc}, +}; + +pub struct TestDataControlDevice { + pub id: ZwlrDataControlDeviceV1Id, + pub tran: Rc, + pub destroyed: Cell, + pub pending_offer: CopyHashMap>, + pub selection: TEEH>>, + pub primary_selection: TEEH>>, +} + +impl TestDataControlDevice { + #[allow(dead_code)] + pub fn destroy(&self) -> TestResult { + if !self.destroyed.replace(true) { + self.tran.send(Destroy { self_id: self.id })?; + } + Ok(()) + } + + pub fn set_selection(&self, source: &TestDataControlSource) -> TestResult { + self.tran.send(SetSelection { + self_id: self.id, + source: source.id, + })?; + Ok(()) + } + + #[allow(dead_code)] + pub fn set_primary_selection(&self, source: &TestDataControlSource) -> TestResult { + self.tran.send(SetPrimarySelection { + self_id: self.id, + source: source.id, + })?; + Ok(()) + } + + fn handle_data_offer(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let ev = DataOffer::parse_full(parser)?; + let obj = Rc::new(TestDataControlOffer { + id: ev.id, + tran: self.tran.clone(), + destroyed: Cell::new(false), + offers: Default::default(), + }); + self.tran.add_obj(obj.clone())?; + self.pending_offer.set(obj.id, obj); + Ok(()) + } + + fn take_offer( + &self, + id: ZwlrDataControlOfferV1Id, + ) -> TestResult>> { + if id.is_none() { + Ok(None) + } else { + match self.pending_offer.remove(&id) { + Some(o) => Ok(Some(o)), + _ => bail!("Unknown offer {}", id), + } + } + } + + fn handle_selection(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let ev = Selection::parse_full(parser)?; + self.selection.push(self.take_offer(ev.id)?); + Ok(()) + } + + fn handle_primary_selection(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let ev = PrimarySelection::parse_full(parser)?; + self.primary_selection.push(self.take_offer(ev.id)?); + Ok(()) + } + + fn handle_finished(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let _ev = Finished::parse_full(parser)?; + Ok(()) + } +} + +test_object! { + TestDataControlDevice, ZwlrDataControlDeviceV1; + + DATA_OFFER => handle_data_offer, + SELECTION => handle_selection, + FINISHED => handle_finished, + PRIMARY_SELECTION => handle_primary_selection, +} + +impl TestObject for TestDataControlDevice {} diff --git a/src/it/test_ifs/test_data_control_manager.rs b/src/it/test_ifs/test_data_control_manager.rs new file mode 100644 index 00000000..197b1d19 --- /dev/null +++ b/src/it/test_ifs/test_data_control_manager.rs @@ -0,0 +1,84 @@ +use { + crate::{ + it::{ + test_error::TestResult, + test_ifs::{ + test_data_control_device::TestDataControlDevice, + test_data_control_source::TestDataControlSource, test_seat::TestSeat, + }, + test_object::TestObject, + test_transport::TestTransport, + }, + wire::{zwlr_data_control_manager_v1::*, ZwlrDataControlManagerV1Id}, + }, + std::{cell::Cell, rc::Rc}, +}; + +pub struct TestDataControlManager { + pub id: ZwlrDataControlManagerV1Id, + pub tran: Rc, + pub destroyed: Cell, +} + +impl TestDataControlManager { + pub fn new(tran: &Rc) -> Self { + Self { + id: tran.id(), + tran: tran.clone(), + destroyed: Cell::new(false), + } + } + + pub fn create_data_source(&self) -> TestResult> { + let obj = Rc::new(TestDataControlSource { + id: self.tran.id(), + tran: self.tran.clone(), + destroyed: Cell::new(false), + cancelled: Cell::new(false), + sends: Default::default(), + }); + self.tran.add_obj(obj.clone())?; + self.tran.send(CreateDataSource { + self_id: self.id, + id: obj.id, + })?; + Ok(obj) + } + + pub fn get_data_device(&self, seat: &TestSeat) -> TestResult> { + let obj = Rc::new(TestDataControlDevice { + id: self.tran.id(), + tran: self.tran.clone(), + destroyed: Cell::new(false), + pending_offer: Default::default(), + selection: Default::default(), + primary_selection: Default::default(), + }); + self.tran.add_obj(obj.clone())?; + self.tran.send(GetDataDevice { + self_id: self.id, + id: obj.id, + seat: seat.id, + })?; + Ok(obj) + } + + pub fn destroy(&self) -> TestResult { + if !self.destroyed.replace(true) { + self.tran.send(Destroy { self_id: self.id })?; + } + Ok(()) + } +} + +impl Drop for TestDataControlManager { + fn drop(&mut self) { + let _ = self.destroy(); + } +} + +test_object! { + TestDataControlManager, ZwlrDataControlManagerV1; +} + +impl TestObject for TestDataControlManager {} diff --git a/src/it/test_ifs/test_data_control_offer.rs b/src/it/test_ifs/test_data_control_offer.rs new file mode 100644 index 00000000..dfb6d4fd --- /dev/null +++ b/src/it/test_ifs/test_data_control_offer.rs @@ -0,0 +1,64 @@ +use { + crate::{ + it::{ + test_error::{TestError, TestResult}, + test_object::TestObject, + test_transport::TestTransport, + testrun::ParseFull, + }, + utils::buffd::MsgParser, + wire::{zwlr_data_control_offer_v1::*, ZwlrDataControlOfferV1Id}, + }, + ahash::AHashSet, + std::{ + cell::{Cell, RefCell}, + rc::Rc, + }, + uapi::{c, OwnedFd}, +}; + +pub struct TestDataControlOffer { + pub id: ZwlrDataControlOfferV1Id, + pub tran: Rc, + pub destroyed: Cell, + pub offers: RefCell>, +} + +impl TestDataControlOffer { + pub fn destroy(&self) -> TestResult { + if !self.destroyed.replace(true) { + self.tran.send(Destroy { self_id: self.id })?; + } + Ok(()) + } + + pub fn receive(&self, mime_type: &str) -> TestResult> { + let (read, write) = uapi::pipe2(c::O_CLOEXEC)?; + self.tran.send(Receive { + self_id: self.id, + mime_type, + fd: Rc::new(write), + })?; + Ok(Rc::new(read)) + } + + fn handle_offer(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let ev = Offer::parse_full(parser)?; + self.offers.borrow_mut().insert(ev.mime_type.to_string()); + Ok(()) + } +} + +impl Drop for TestDataControlOffer { + fn drop(&mut self) { + let _ = self.destroy(); + } +} + +test_object! { + TestDataControlOffer, ZwlrDataControlOfferV1; + + OFFER => handle_offer, +} + +impl TestObject for TestDataControlOffer {} diff --git a/src/it/test_ifs/test_data_control_source.rs b/src/it/test_ifs/test_data_control_source.rs new file mode 100644 index 00000000..6274d1f5 --- /dev/null +++ b/src/it/test_ifs/test_data_control_source.rs @@ -0,0 +1,67 @@ +use { + crate::{ + it::{ + test_error::{TestError, TestResult}, + test_object::TestObject, + test_transport::TestTransport, + test_utils::test_expected_event::TEEH, + testrun::ParseFull, + }, + utils::buffd::MsgParser, + wire::{zwlr_data_control_source_v1::*, ZwlrDataControlSourceV1Id}, + }, + std::{cell::Cell, rc::Rc}, + uapi::OwnedFd, +}; + +pub struct TestDataControlSource { + pub id: ZwlrDataControlSourceV1Id, + pub tran: Rc, + pub destroyed: Cell, + pub cancelled: Cell, + pub sends: TEEH<(String, Rc)>, +} + +impl TestDataControlSource { + pub fn destroy(&self) -> TestResult { + if !self.destroyed.replace(true) { + self.tran.send(Destroy { self_id: self.id })?; + } + Ok(()) + } + + pub fn offer(&self, mime_type: &str) -> TestResult { + self.tran.send(Offer { + self_id: self.id, + mime_type, + })?; + Ok(()) + } + + fn handle_send(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let ev = Send::parse_full(parser)?; + self.sends.push((ev.mime_type.to_string(), ev.fd)); + Ok(()) + } + + fn handle_cancelled(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let _ev = Cancelled::parse_full(parser)?; + self.cancelled.set(true); + Ok(()) + } +} + +impl Drop for TestDataControlSource { + fn drop(&mut self) { + let _ = self.destroy(); + } +} + +test_object! { + TestDataControlSource, ZwlrDataControlSourceV1; + + SEND => handle_send, + CANCELLED => handle_cancelled, +} + +impl TestObject for TestDataControlSource {} diff --git a/src/it/test_ifs/test_data_device_manager.rs b/src/it/test_ifs/test_data_device_manager.rs index c2c838ef..59dcc8a1 100644 --- a/src/it/test_ifs/test_data_device_manager.rs +++ b/src/it/test_ifs/test_data_device_manager.rs @@ -32,6 +32,7 @@ impl TestDataDeviceManager { id: self.tran.id(), tran: self.tran.clone(), destroyed: Cell::new(false), + sends: Rc::new(Default::default()), }); self.tran.add_obj(data_source.clone())?; self.tran.send(CreateDataSource { diff --git a/src/it/test_ifs/test_data_source.rs b/src/it/test_ifs/test_data_source.rs index bd07b450..4c2ff9f7 100644 --- a/src/it/test_ifs/test_data_source.rs +++ b/src/it/test_ifs/test_data_source.rs @@ -2,18 +2,20 @@ use { crate::{ it::{ test_error::TestResult, test_object::TestObject, test_transport::TestTransport, - testrun::ParseFull, + test_utils::test_expected_event::TEEH, testrun::ParseFull, }, utils::buffd::MsgParser, wire::{wl_data_source::*, WlDataSourceId}, }, std::{cell::Cell, rc::Rc}, + uapi::OwnedFd, }; pub struct TestDataSource { pub id: WlDataSourceId, pub tran: Rc, pub destroyed: Cell, + pub sends: TEEH<(String, Rc)>, } impl TestDataSource { @@ -48,7 +50,8 @@ impl TestDataSource { } fn handle_send(&self, parser: MsgParser<'_, '_>) -> TestResult { - let _ev = Send::parse_full(parser)?; + let ev = Send::parse_full(parser)?; + self.sends.push((ev.mime_type.to_string(), ev.fd)); Ok(()) } diff --git a/src/it/test_ifs/test_registry.rs b/src/it/test_ifs/test_registry.rs index a093c59b..3acd3c09 100644 --- a/src/it/test_ifs/test_registry.rs +++ b/src/it/test_ifs/test_registry.rs @@ -7,6 +7,7 @@ use { test_ifs::{ test_compositor::TestCompositor, test_content_type_manager::TestContentTypeManager, test_cursor_shape_manager::TestCursorShapeManager, + test_data_control_manager::TestDataControlManager, test_data_device_manager::TestDataDeviceManager, test_ext_foreign_toplevel_list::TestExtForeignToplevelList, test_jay_compositor::TestJayCompositor, test_shm::TestShm, @@ -45,6 +46,7 @@ pub struct TestRegistrySingletons { pub wp_cursor_shape_manager_v1: u32, pub wp_linux_drm_syncobj_manager_v1: u32, pub wp_content_type_manager_v1: u32, + pub zwlr_data_control_manager_v1: u32, } pub struct TestRegistry { @@ -65,6 +67,7 @@ pub struct TestRegistry { pub cursor_shape_manager: CloneCell>>, pub syncobj_manager: CloneCell>>, pub content_type_manager: CloneCell>>, + pub data_control_manager: CloneCell>>, pub seats: CopyHashMap>, } @@ -129,6 +132,7 @@ impl TestRegistry { wp_cursor_shape_manager_v1, wp_linux_drm_syncobj_manager_v1, wp_content_type_manager_v1, + zwlr_data_control_manager_v1, }; self.singletons.set(Some(singletons.clone())); Ok(singletons) @@ -201,6 +205,13 @@ impl TestRegistry { 1, TestContentTypeManager ); + create_singleton!( + get_data_control_manager, + data_control_manager, + zwlr_data_control_manager_v1, + 2, + TestDataControlManager + ); pub fn bind( &self, diff --git a/src/it/test_transport.rs b/src/it/test_transport.rs index 6a76936f..b2b9398b 100644 --- a/src/it/test_transport.rs +++ b/src/it/test_transport.rs @@ -65,6 +65,7 @@ impl TestTransport { cursor_shape_manager: Default::default(), syncobj_manager: Default::default(), content_type_manager: Default::default(), + data_control_manager: Default::default(), seats: Default::default(), }); self.send(wl_display::GetRegistry { diff --git a/src/it/tests.rs b/src/it/tests.rs index cfb51ecf..01fcc6b5 100644 --- a/src/it/tests.rs +++ b/src/it/tests.rs @@ -63,6 +63,7 @@ mod t0029_double_click_float; mod t0030_cursor_shape; mod t0031_syncobj; mod t0032_content_type; +mod t0032_data_control; pub trait TestCase: Sync { fn name(&self) -> &'static str; @@ -113,5 +114,6 @@ pub fn tests() -> Vec<&'static dyn TestCase> { t0029_double_click_float, t0030_cursor_shape, t0031_syncobj, + t0032_data_control, } } diff --git a/src/it/tests/t0032_data_control.rs b/src/it/tests/t0032_data_control.rs new file mode 100644 index 00000000..043c3587 --- /dev/null +++ b/src/it/tests/t0032_data_control.rs @@ -0,0 +1,87 @@ +use { + crate::it::{ + test_error::{TestErrorExt, TestResult}, + testrun::TestRun, + }, + std::{ + io::{Read, Write}, + rc::Rc, + }, +}; + +testcase!(); + +async fn test(run: Rc) -> TestResult { + let _ds = run.create_default_setup().await?; + + let client1 = run.create_client().await?; + let seat1 = client1.get_default_seat().await?; + let dev1 = client1.data_device_manager.get_data_device(&seat1.seat)?; + let entered = seat1.kb.enter.expect()?; + let win1 = client1.create_window().await?; + win1.map2().await?; + let serial = entered.next()?.serial; + let source1 = client1.data_device_manager.create_data_source()?; + source1.offer("image")?; + let sends1 = source1.sends.expect()?; + + let client2 = run.create_client().await?; + let seat2 = client2.get_default_seat().await?; + let data_control2 = client2.registry.get_data_control_manager().await?; + let dev2 = data_control2.get_data_device(&seat2.seat)?; + let source2 = data_control2.create_data_source()?; + source2.offer("text")?; + let sends2 = source2.sends.expect()?; + + let client3 = run.create_client().await?; + let seat3 = client3.get_default_seat().await?; + let data_control3 = client3.registry.get_data_control_manager().await?; + let dev3 = data_control3.get_data_device(&seat3.seat)?; + let selection = dev3.selection.expect()?; + + dev2.set_selection(&source2)?; + client2.sync().await; + client3.sync().await; + + let Some(sel) = selection.last().with_context(|| "selection 1")? else { + bail!("no selection (1)"); + }; + tassert!(sel.offers.borrow().contains("text")); + { + let rfd = sel.receive("text")?; + client3.sync().await; + client2.sync().await; + let (mime, sfd) = sends2.next().with_context(|| "sends2")?; + tassert_eq!(mime, "text"); + sfd.borrow().write_all(b"abcd")?; + drop(sfd); + let mut buf = vec![]; + rfd.borrow().read_to_end(&mut buf)?; + tassert_eq!(buf, b"abcd"); + } + + tassert_eq!(source2.cancelled.get(), false); + dev1.set_selection(&source1, serial)?; + client1.sync().await; + client2.sync().await; + tassert_eq!(source2.cancelled.get(), true); + + let Some(sel) = selection.last().with_context(|| "selection 2")? else { + bail!("no selection (2)"); + }; + tassert!(sel.offers.borrow().contains("image")); + { + let rfd = sel.receive("image")?; + client3.sync().await; + client1.sync().await; + let (mime, sfd) = sends1.next().with_context(|| "sends1")?; + tassert_eq!(mime, "image"); + sfd.borrow().write_all(b"xyz")?; + drop(sfd); + let mut buf = vec![]; + rfd.borrow().read_to_end(&mut buf)?; + tassert_eq!(buf, b"xyz"); + } + + Ok(()) +}