From 3b5bbb84671631f3219e70b0e382c6a35f29b310 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Sat, 15 Aug 2026 20:49:40 +1000 Subject: [PATCH] [client] tests: cover Caja clipboard MIME handling Teach the X11 fixture about the MATE atom and verify import preference, source target advertisement, and copied-files delivery. Cover the same Wayland offer and source behavior. Assert that MATE reuses the single GNOME-style payload instead of fetching a duplicate representation. Exercise the core parser and generator with the MATE copied-files MIME, including invalid operation handling and byte-identical remote output. --- client/tests/clipboard_files_test.c | 38 ++++++++++++- client/tests/wayland_clipboard_test.c | 79 ++++++++++++++++++++++++--- client/tests/x11_clipboard_test.c | 78 ++++++++++++++++++++++++-- 3 files changed, 181 insertions(+), 14 deletions(-) diff --git a/client/tests/clipboard_files_test.c b/client/tests/clipboard_files_test.c index 4eff6bcb..88125e17 100644 --- a/client/tests/clipboard_files_test.c +++ b/client/tests/clipboard_files_test.c @@ -271,6 +271,13 @@ static void testLocalLifecycle(void) CHECK(notices == 2); CHECK(datasets[1] != datasets[0]); + clipboardFiles_clearLocal(); + CHECK(clipboardFiles_setLocal("x-special/mate-copied-files", + gnome, (size_t)gnomeLength)); + CHECK(notices == 3); + CHECK(datasets[2] != datasets[0]); + CHECK(datasets[2] != datasets[1]); + static const char invalid[] = "file:///does/not/exist\n"; CHECK(!clipboardFiles_setLocal( "text/uri-list", invalid, sizeof(invalid) - 1U)); @@ -281,9 +288,10 @@ static void testLocalLifecycle(void) CHECK(clipboardFiles_testInit(UINT64_C(0x5678000056780000))); CHECK(clipboardFiles_setLocal( "text/uri-list", uri, (size_t)uriLength)); - CHECK(notices == 3); - CHECK(datasets[2] != datasets[0]); - CHECK(datasets[2] != datasets[1]); + CHECK(notices == 4); + CHECK(datasets[3] != datasets[0]); + CHECK(datasets[3] != datasets[1]); + CHECK(datasets[3] != datasets[2]); clipboardFiles_free(); CHECK(unlink(path) == 0); @@ -381,6 +389,8 @@ static void testLocalUriValidation(void) CHECK(length > 0 && (size_t)length < sizeof(uri)); CHECK(!clipboardFiles_setLocal( "x-special/gnome-copied-files", uri, (size_t)length)); + CHECK(!clipboardFiles_setLocal( + "x-special/mate-copied-files", uri, (size_t)length)); CHECK(notices == before + 2U); clipboardFiles_free(); @@ -721,6 +731,27 @@ static void checkRemoteUri(uint64_t presentation, const char * name) free(uri); } +static void checkRemoteMate(uint64_t presentation, const char * name) +{ + char * gnome = NULL; + char * mate = NULL; + size_t gnomeSize = 0; + size_t mateSize = 0; + CHECK(clipboardFiles_getRemotePresentation(presentation, + "x-special/gnome-copied-files", &gnome, &gnomeSize)); + CHECK(clipboardFiles_getRemotePresentation(presentation, + "x-special/mate-copied-files", &mate, &mateSize)); + CHECK(gnome); + CHECK(mate); + CHECK(gnomeSize == mateSize); + CHECK(memcmp(gnome, mate, mateSize) == 0); + CHECK(mateSize >= sizeof("copy\n") - 1U); + CHECK(memcmp(mate, "copy\n", sizeof("copy\n") - 1U) == 0); + CHECK(strstr(mate, name)); + free(mate); + free(gnome); +} + typedef struct ReleasePresentationTask { pthread_barrier_t * barrier; @@ -791,6 +822,7 @@ static void testRemoteLifecycle(void) clipboardFiles_remotePresentationAcquire(); CHECK(firstPresentation == first); checkRemoteUri(firstPresentation, "first.txt"); + checkRemoteMate(firstPresentation, "first.txt"); const uint64_t secondDataset = KVMFR_CLIPBOARD_TRANSFER_HELPER | UINT64_C(0x73110002); diff --git a/client/tests/wayland_clipboard_test.c b/client/tests/wayland_clipboard_test.c index d71e9307..83aad25a 100644 --- a/client/tests/wayland_clipboard_test.c +++ b/client/tests/wayland_clipboard_test.c @@ -164,8 +164,10 @@ struct Log unsigned int presentationAcquireN; unsigned int presentationDeliveredN; unsigned int presentationReleaseN; + unsigned int presentationMimeN; uint64_t presentationDelivered[MAX_SOURCE]; uint64_t presentationReleased[MAX_SOURCE]; + char presentationMime[MAX_MIME][80]; char fileMime[80]; uint8_t fileData[MAX_TRANSFER]; size_t fileSize; @@ -598,10 +600,14 @@ bool clipboardFiles_getRemotePresentation(uint64_t presentation, const char * mime, char ** data, size_t * size) { CHECK(presentation >= 1001U); + CHECK(rec.presentationMimeN < MAX_MIME); + snprintf(rec.presentationMime[rec.presentationMimeN++], + sizeof(rec.presentationMime[0]), "%s", mime); const char * value; if (!strcmp(mime, "text/uri-list")) value = "file:///run/user/1000/looking-glass/guest.txt\r\n"; - else if (!strcmp(mime, "x-special/gnome-copied-files")) + else if (!strcmp(mime, "x-special/gnome-copied-files") || + !strcmp(mime, "x-special/mate-copied-files")) value = "copy\nfile:///run/user/1000/looking-glass/guest.txt\r\n"; else if (!strcmp(mime, "application/x-kde-cutselection")) value = "0"; @@ -803,20 +809,59 @@ static void testFileImport(void) finish(); } +static void testFileImportMate(void) +{ + start(); + const char payload[] = + "copy\nfile:///home/user/first.txt\r\n" + "file:///home/user/second.txt\r\n"; + proto.receiveData = payload; + proto.receiveSize = sizeof(payload) - 1U; + + struct Proxy * offer = newOffer(); + offerMime(offer, "text/plain;charset=utf-8"); + offerMime(offer, "text/uri-list"); + offerMime(offer, "x-special/mate-copied-files"); + selectOffer(offer); + + CHECK(rec.noticeN == 0); + CHECK(proto.receiveN == 1); + CHECK(strcmp(proto.receiveMime, + "x-special/mate-copied-files") == 0); + CHECK(rec.pollN == 1); + pollFire(0, EPOLLIN); + CHECK(rec.fileSetN == 1); + CHECK(strcmp(rec.fileMime, + "x-special/mate-copied-files") == 0); + CHECK(rec.fileSize == sizeof(payload) - 1U); + CHECK(memcmp(rec.fileData, payload, sizeof(payload) - 1U) == 0); + + finish(); +} + static void testFileSource(void) { start(); waylandCBNotice(LG_CLIPBOARD_DATA_FILES); CHECK(rec.presentationAcquireN == 1); CHECK(rec.presentationReleaseN == 0); + CHECK(rec.presentationMimeN == 3); + CHECK(strcmp(rec.presentationMime[0], "text/uri-list") == 0); + CHECK(strcmp(rec.presentationMime[1], + "x-special/gnome-copied-files") == 0); + CHECK(strcmp(rec.presentationMime[2], + "application/x-kde-cutselection") == 0); CHECK(proto.sourceN == 1); struct Proxy * source = &proto.source[0]; - CHECK(source->mimeN == 4); + CHECK(source->mimeN == 5); CHECK(strcmp(source->mime[0], "x-special/gnome-copied-files") == 0); - CHECK(strcmp(source->mime[1], "text/uri-list") == 0); - CHECK(strcmp(source->mime[2], + CHECK(strcmp(source->mime[1], + "x-special/mate-copied-files") == 0); + CHECK(strcmp(source->mime[2], "text/uri-list") == 0); + CHECK(strcmp(source->mime[3], "application/x-kde-cutselection") == 0); + CHECK(strcmp(source->mime[4], wlCb.lgMimetype) == 0); int fds[2]; CHECK(pipe(fds) == 0); @@ -836,14 +881,33 @@ static void testFileSource(void) CHECK(read(fds[0], actual, 1) == 0); CHECK(close(fds[0]) == 0); + int mate[2]; + CHECK(pipe(mate) == 0); + sourceListener(source)->send(source->data, + (struct wl_data_source *)source, + "x-special/mate-copied-files", mate[1]); + CHECK(rec.pollN == 2); + pollFire(1, EPOLLOUT); + CHECK(rec.presentationDeliveredN == 2); + CHECK(rec.presentationDelivered[1] == 1001U); + const char mateExpected[] = + "copy\nfile:///run/user/1000/looking-glass/guest.txt\r\n"; + char mateActual[sizeof(mateExpected)] = { 0 }; + CHECK(read(mate[0], mateActual, sizeof(mateActual)) == + (ssize_t)(sizeof(mateExpected) - 1U)); + CHECK(memcmp(mateActual, + mateExpected, sizeof(mateExpected) - 1U) == 0); + CHECK(read(mate[0], mateActual, 1) == 0); + CHECK(close(mate[0]) == 0); + int kde[2]; CHECK(pipe(kde) == 0); sourceListener(source)->send(source->data, (struct wl_data_source *)source, "application/x-kde-cutselection", kde[1]); - CHECK(rec.pollN == 2); - pollFire(1, EPOLLOUT); - CHECK(rec.presentationDeliveredN == 1); + CHECK(rec.pollN == 3); + pollFire(2, EPOLLOUT); + CHECK(rec.presentationDeliveredN == 2); CHECK(read(kde[0], actual, sizeof(actual)) == 1); CHECK(actual[0] == '0'); CHECK(read(kde[0], actual, 1) == 0); @@ -1573,6 +1637,7 @@ static const struct Test tests[] = { { "mime" , testMime }, { "file-import", testFileImport }, + { "file-mate-import", testFileImportMate }, { "file-source", testFileSource }, { "file-replace", testFileSourceReplacement }, { "replace" , testReplace }, diff --git a/client/tests/x11_clipboard_test.c b/client/tests/x11_clipboard_test.c index 4cb09f40..7172c866 100644 --- a/client/tests/x11_clipboard_test.c +++ b/client/tests/x11_clipboard_test.c @@ -60,7 +60,8 @@ #define A_JPEG 104UL #define A_URI 105UL #define A_GNOME 106UL -#define A_KDE 107UL +#define A_MATE 107UL +#define A_KDE 108UL struct WindowLog { @@ -223,6 +224,8 @@ Atom XInternAtom(Display * display, const char * name, Bool onlyIfExists) return A_URI; if (!strcmp(name, "x-special/gnome-copied-files")) return A_GNOME; + if (!strcmp(name, "x-special/mate-copied-files")) + return A_MATE; if (!strcmp(name, "application/x-kde-cutselection")) return A_KDE; CHECK(false); @@ -609,7 +612,8 @@ bool clipboardFiles_getRemotePresentation(uint64_t presentation, const char * value; if (!strcmp(mime, "text/uri-list")) value = "file:///run/user/1000/looking-glass/guest.txt\r\n"; - else if (!strcmp(mime, "x-special/gnome-copied-files")) + else if (!strcmp(mime, "x-special/gnome-copied-files") || + !strcmp(mime, "x-special/mate-copied-files")) value = "copy\nfile:///run/user/1000/looking-glass/guest.txt\r\n"; else if (!strcmp(mime, "application/x-kde-cutselection")) value = "0"; @@ -887,6 +891,29 @@ static void testFileImportIncr(void) finish(); } +static void testFileImportMate(void) +{ + start(); + const unsigned long targets[] = { A_TEXT, A_URI, A_MATE }; + discover(603, targets, ARRAY_LENGTH(targets)); + CHECK(rec.convertN == 2); + const struct ConvertLog * convert = &rec.convert[1]; + CHECK(convert->target == A_MATE); + CHECK(convert->property == x11atoms.SEL_DATA); + + const char payload[] = + "copy\nfile:///home/user/first.txt\r\n" + "file:///home/user/second.txt\r\n"; + prop8(A_MATE, payload, sizeof(payload) - 1U); + selection(convert->requestor, A_MATE, x11atoms.SEL_DATA); + CHECK(rec.fileSetN == 1); + CHECK(strcmp(rec.fileMime, "x-special/mate-copied-files") == 0); + CHECK(rec.fileSize == sizeof(payload) - 1U); + CHECK(memcmp(rec.fileData, payload, sizeof(payload) - 1U) == 0); + CHECK(rec.noticeN == 0); + finish(); +} + static void testFileSource(void) { start(); @@ -898,12 +925,13 @@ static void testFileSource(void) selectionRequest(x11atoms.TARGETS, 710); CHECK(rec.changeN == 1); CHECK(rec.change[0].format == 32); - CHECK(rec.change[0].count == 4); + CHECK(rec.change[0].count == 5); const Atom * targets = (const Atom *)rec.change[0].data; CHECK(targets[0] == x11atoms.TARGETS); CHECK(targets[1] == A_URI); CHECK(targets[2] == A_GNOME); - CHECK(targets[3] == A_KDE); + CHECK(targets[3] == A_MATE); + CHECK(targets[4] == A_KDE); selectionRequest(A_URI, 711); CHECK(rec.presentationAcquireN == 2); @@ -958,6 +986,46 @@ static void testFileSource(void) CHECK(rec.presentationReleaseN == 3); } +static void testFileSourceMate(void) +{ + start(); + x11CBNotice(LG_CLIPBOARD_DATA_FILES); + CHECK(rec.presentationAcquireN == 1); + CHECK(rec.presentationReleaseN == 0); + + selectionRequest(A_MATE, 713); + CHECK(rec.presentationAcquireN == 2); + CHECK(rec.structureSelectN == 1); + CHECK(rec.changeN == 1); + CHECK(rec.change[0].type == x11atoms.INCR); + CHECK(rec.change[0].format == 32); + CHECK(rec.change[0].count == 1); + CHECK(rec.sendN == 1); + + propertyState(900, 713, PropertyDelete); + CHECK(rec.changeN == 2); + CHECK(rec.change[1].type == A_MATE); + CHECK(rec.change[1].format == 8); + const char expected[] = + "copy\nfile:///run/user/1000/looking-glass/guest.txt\r\n"; + CHECK(rec.change[1].size == sizeof(expected) - 1U); + CHECK(memcmp(rec.change[1].data, + expected, sizeof(expected) - 1U) == 0); + + propertyState(900, 713, PropertyDelete); + CHECK(rec.changeN == 3); + CHECK(rec.change[2].type == A_MATE); + CHECK(rec.change[2].count == 0); + CHECK(rec.presentationDeliveredN == 1); + CHECK(rec.presentationDelivered[0] == 2001U); + CHECK(rec.presentationReleaseN == 1); + CHECK(rec.presentationReleased[0] == 2001U); + + finish(); + CHECK(rec.presentationReleaseN == 2); + CHECK(rec.presentationReleased[1] == 2001U); +} + static void testFileSourceActiveReplacement(void) { start(); @@ -1434,7 +1502,9 @@ static const struct Test tests[] = { "targets" , testTargets }, { "file-import" , testFileImport }, { "file-incr" , testFileImportIncr }, + { "file-mate-import" , testFileImportMate }, { "file-source" , testFileSource }, + { "file-mate-source" , testFileSourceMate }, { "file-source-active-replace", testFileSourceActiveReplacement }, { "normal" , testNormal }, { "incr" , testIncr },