From 7ade0bb69a5437b441b037ac851d019cee380bc7 Mon Sep 17 00:00:00 2001 From: Jonah Graham Date: Fri, 17 Oct 2025 17:51:22 -0400 Subject: [PATCH] Improve Clipboard tests As I prepare my final Clipboard PR for #2126 I split out this test simplification and refactor to ensure that the tests still work as expected before my Clipboard changes land. Changes include: - Splitting out RemoteClipboard wrapper so that we can test other controls clipboard fully by remotely controlling the clipboard. - Moving some useful test methods (runOperationInThread and waitAllEvents) to SwtTestUtil - Improving SwtTestUtil.openShell on GTK4 so that all events needed to ensure shell is fully opened are received. In particular we used to just wait for SWT.Paint, however some operations, such as clipboard, cannot happen until SWT.Activate are also received. - Added SwtTestUtil.waitAllEvents to support SwtTestUtil.openShell change - Cleaned up Test_org_eclipse_swt_dnd_Clipboard now that I have better understanding how Clipboard works - Added tests for selection clipboard by parameterizing most tests and adding new tests that check interaction between selection and normal clipboard - Remove unneeded processEvents calls from Test_org_eclipse_swt_dnd_Clipboard Part of #2126 --- .../swt/tests/junit/RemoteClipboard.java | 208 ++++++++++ .../eclipse/swt/tests/junit/SwtTestUtil.java | 102 ++++- .../Test_org_eclipse_swt_dnd_Clipboard.java | 371 +++++------------- .../data/clipboard/ClipboardCommands.java | 21 +- .../data/clipboard/ClipboardCommandsImpl.java | 29 +- .../data/clipboard/ClipboardTest.java | 14 +- 6 files changed, 444 insertions(+), 301 deletions(-) create mode 100644 tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/RemoteClipboard.java diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/RemoteClipboard.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/RemoteClipboard.java new file mode 100644 index 0000000000..b68c4a115e --- /dev/null +++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/RemoteClipboard.java @@ -0,0 +1,208 @@ +/******************************************************************************* + * Copyright (c) 2025 Kichwa Coders Canada, Inc. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.swt.tests.junit; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.ProcessBuilder.Redirect; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import clipboard.ClipboardCommands; + +public class RemoteClipboard implements ClipboardCommands { + private ClipboardCommands remote; + private Process remoteClipboardProcess; + private Path remoteClipboardTempDir; + + public void start() throws Exception { + assertNull(remote, "Create a new instance to restart"); + /* + * The below copy using getPath may be redundant (i.e. it may be possible to run + * the class files from where they currently reside in the bin folder or the + * jar), but this method of setting up the class files is very simple and is + * done the same way that other files are extracted for tests. + * + * If the ClipboardTest starts to get more complicated, or other tests want to + * replicate this design element, then refactoring this is an option. + */ + remoteClipboardTempDir = Files.createTempDirectory("swt-test-Clipboard"); + List.of( // + "ClipboardTest", // + "ClipboardCommands", // + "ClipboardCommandsImpl", // + "ClipboardTest$LocalHostOnlySocketFactory" // + ).forEach((f) -> { + // extract the files and put them in the temp directory + SwtTestUtil.copyFile("/clipboard/" + f + ".class", + remoteClipboardTempDir.resolve("clipboard/" + f + ".class")); + }); + + String javaHome = System.getProperty("java.home"); + String javaExe = javaHome + "/bin/java" + (SwtTestUtil.isWindowsOS ? ".exe" : ""); + assertTrue(Files.exists(Path.of(javaExe))); + + ProcessBuilder pb = new ProcessBuilder(javaExe, "clipboard.ClipboardTest") + .directory(remoteClipboardTempDir.toFile()); + pb.inheritIO(); + pb.redirectOutput(Redirect.PIPE); + remoteClipboardProcess = pb.start(); + + // Read server output to find the port + int port = SwtTestUtil.runOperationInThread(() -> { + BufferedReader reader = new BufferedReader(new InputStreamReader(remoteClipboardProcess.getInputStream())); + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith(ClipboardCommands.PORT_MESSAGE)) { + String[] parts = line.split(":"); + return Integer.parseInt(parts[1].trim()); + } + } + throw new RuntimeException("Failed to get port"); + }); + assertNotEquals(0, port); + try { + Registry reg = LocateRegistry.getRegistry("127.0.0.1", port); + long stopTime = System.currentTimeMillis() + 10000; + do { + try { + remote = (ClipboardCommands) reg.lookup(ClipboardCommands.ID); + break; + } catch (NotBoundException e) { + // try again because the remote app probably hasn't bound yet + } + } while (System.currentTimeMillis() < stopTime); + } catch (RemoteException e) { + + Integer exitValue = null; + boolean waitFor = false; + try { + waitFor = remoteClipboardProcess.waitFor(5, TimeUnit.SECONDS); + if (waitFor) { + exitValue = remoteClipboardProcess.exitValue(); + } + } catch (InterruptedException e1) { + Thread.interrupted(); + } + + String message = "Failed to get remote clipboards command, this seems to happen on macOS on I-build tests. Exception: " + + e.toString() + " waitFor: " + waitFor + " exitValue: " + exitValue; + + // Give some diagnostic information to help track down why this fails on build + // machine. We only hard error on Linux, for other platforms we allow test to + // just be skipped until we track down what is causing + // https://github.com/eclipse-platform/eclipse.platform.swt/issues/2553 + assumeTrue(SwtTestUtil.isGTK, message); + throw new RuntimeException(message, e); + } + assertNotNull(remote); + + // Run a no-op on the Swing event loop so that we know it is idle + // and we can continue startup + remote.waitUntilReady(); + remote.setFocus(); + remote.waitUntilReady(); + } + + @Override + public void stop() throws RemoteException { + try { + stopProcess(); + } catch (InterruptedException e) { + Thread.interrupted(); + } finally { + deleteRemoteTempDir(); + } + } + + private void stopProcess() throws RemoteException, InterruptedException { + try { + if (remote != null) { + remote.stop(); + remote = null; + } + } finally { + if (remoteClipboardProcess != null) { + try { + remoteClipboardProcess.destroy(); + assertTrue(remoteClipboardProcess.waitFor(10, TimeUnit.SECONDS)); + } finally { + remoteClipboardProcess.destroyForcibly(); + assertTrue(remoteClipboardProcess.waitFor(10, TimeUnit.SECONDS)); + remoteClipboardProcess = null; + } + } + } + } + + private void deleteRemoteTempDir() { + if (remoteClipboardTempDir != null) { + // At this point the process is ideally destroyed - or at least the test will + // report a failure if it isn't. Clean up the extracted files, but don't + // fail test if we fail to delete + try { + Files.walkFileTree(remoteClipboardTempDir, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + System.err.println("SWT Warning: Failed to clean up temp directory " + remoteClipboardTempDir + + " Error:" + e.toString()); + e.printStackTrace(); + } + } + } + + @Override + public void setContents(String string, int clipboardId) throws RemoteException { + remote.setContents(string, clipboardId); + } + + @Override + public void setFocus() throws RemoteException { + remote.setFocus(); + } + + @Override + public String getStringContents(int clipboardId) throws RemoteException { + return remote.getStringContents(clipboardId); + } + + @Override + public void waitUntilReady() throws RemoteException { + remote.waitUntilReady(); + } +} \ No newline at end of file diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/SwtTestUtil.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/SwtTestUtil.java index bf0de69dcf..cd0fb25191 100644 --- a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/SwtTestUtil.java +++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/SwtTestUtil.java @@ -24,6 +24,9 @@ import java.io.PrintStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BooleanSupplier; @@ -203,7 +206,12 @@ public static boolean isBidi() { public static void openShell(Shell shell) { if (shell != null && !shell.getVisible()) { if (isGTK) { - waitEvent(() -> shell.open(), shell, SWT.Paint, 1000); + if (isGTK4()) { + waitAllEvents(() -> shell.open(), shell, Set.of(SWT.Paint, SWT.Activate, SWT.FocusIn), 1000); + } else { + waitEvent(() -> shell.open(), shell, SWT.Paint, 1000); + } + processEvents(); } else { shell.open(); } @@ -485,6 +493,45 @@ public static boolean waitEvent(Runnable trigger, Control control, int swtEvent, return true; } +/** + * Wait until specified control receives all the specified event. + * + * @param trigger may be null. Code that is expected to send event. + * Note that if you trigger it outside, then event may + * arrive *before* you call this function, and it will + * fail to receive event. + * @param control control expected to receive the event + * @param swtEvents events, such as SWT.Paint + * @param timeoutMsec how long to wait for event + * @return true if event was received + */ +public static boolean waitAllEvents(Runnable trigger, Control control, Set swtEvents, int timeoutMsec) { + Map eventsLeftToReceive = new HashMap<>(); + for (Integer swtEvent : swtEvents) { + Listener listener = event -> { + control.removeListener(swtEvent, eventsLeftToReceive.get(swtEvent)); + eventsLeftToReceive.remove(swtEvent); + }; + eventsLeftToReceive.put(swtEvent, listener); + control.addListener(swtEvent, listener); + } + try { + if (trigger != null) + trigger.run(); + + long start = System.currentTimeMillis(); + while (!eventsLeftToReceive.isEmpty()) { + if (System.currentTimeMillis() - start > timeoutMsec) + return false; + processEvents(); + } + } finally { + eventsLeftToReceive.forEach((swtEvent, listener) -> control.removeListener(swtEvent, listener)); + } + + return true; +} + /** * Wait until specified Shell becomes active, or internal timeout elapses. * @@ -642,4 +689,57 @@ public static Path copyFile(String sourceFilename, Path destinationPath) { return destinationPath; } +@FunctionalInterface +public interface ExceptionalSupplier { + T get() throws Exception; +} + +/** + * When running some operations, such as requesting remote process read the + * clipboard, we need to have the event queue processing otherwise the remote + * won't be able to read our clipboard contribution. + * + * This method starts the supplier in a new thread and runs the event loop until + * the thread completes, or until a timeout is reached. + */ +static T runOperationInThread(ExceptionalSupplier supplier) throws RuntimeException { + return runOperationInThread(10000, supplier); +} + +/** + * When running some operations, such as requesting remote process read the + * clipboard, we need to have the event queue processing otherwise the remote + * won't be able to read our clipboard contribution. + * + * This method starts the supplier in a new thread and runs the event loop until + * the thread completes, or until a timeout is reached. + */ +static T runOperationInThread(int timeoutMs, ExceptionalSupplier supplier) throws RuntimeException { + Object[] supplierValue = new Object[1]; + Exception[] supplierException = new Exception[1]; + Runnable task = () -> { + try { + supplierValue[0] = supplier.get(); + } catch (Exception e) { + supplierValue[0] = null; + supplierException[0] = e; + } + }; + Thread thread = new Thread(task, SwtTestUtil.class.getName() + ".runOperationInThread"); + thread.setDaemon(true); + thread.start(); + BooleanSupplier done = () -> !thread.isAlive(); + try { + processEvents(timeoutMs, done); + } catch (InterruptedException e) { + throw new RuntimeException("Failed while running thread", e); + } + assertTrue(done.getAsBoolean()); + if (supplierException[0] != null) { + throw new RuntimeException("Failed while running thread", supplierException[0]); + } + @SuppressWarnings("unchecked") + T result = (T) supplierValue[0]; + return result; +} } diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_dnd_Clipboard.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_dnd_Clipboard.java index a912fb4c70..ae1fde8001 100644 --- a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_dnd_Clipboard.java +++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_dnd_Clipboard.java @@ -11,30 +11,13 @@ package org.eclipse.swt.tests.junit; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.lang.ProcessBuilder.Redirect; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.rmi.NotBoundException; -import java.rmi.RemoteException; -import java.rmi.registry.LocateRegistry; -import java.rmi.registry.Registry; import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.function.BooleanSupplier; import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.DND; import org.eclipse.swt.dnd.RTFTransfer; import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.dnd.Transfer; @@ -43,9 +26,10 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; - -import clipboard.ClipboardCommands; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; /** * Automated Test Suite for class org.eclipse.swt.dnd.Clipboard @@ -54,18 +38,16 @@ * @see Test_org_eclipse_swt_custom_StyledText StyledText tests as it also does * some clipboard tests */ +@Tag("clipboard") public class Test_org_eclipse_swt_dnd_Clipboard { - private static final int DEFAULT_TIMEOUT_MS = 10000; - static int uniqueId = 1; + private static int uniqueId = 1; private Display display; private Shell shell; private Clipboard clipboard; private TextTransfer textTransfer; private RTFTransfer rtfTransfer; - private ClipboardCommands remote; - private Process remoteClipboardProcess; - private Path remoteClipboardTempDir; + private RemoteClipboard remote; @BeforeEach public void setUp() { @@ -73,34 +55,19 @@ public void setUp() { if (display == null) { display = Display.getDefault(); } - clipboard = new Clipboard(display); textTransfer = TextTransfer.getInstance(); rtfTransfer = RTFTransfer.getInstance(); } - private void sleep() throws InterruptedException { - if (SwtTestUtil.isGTK4()) { - /** - * TODO remove all uses of sleep and change them to processEvents with the - * suitable conditional, or entirely remove them - */ - SwtTestUtil.processEvents(100, null); - } else { - SwtTestUtil.processEvents(); - } - } - /** * Note: Wayland backend does not allow access to system clipboard from * non-focussed windows. So we have to create/open and focus a window here so * that clipboard operations work. */ - private void openAndFocusShell() throws InterruptedException { + private void openAndFocusShell() { shell = new Shell(display); - shell.open(); - shell.setFocus(); - sleep(); + SwtTestUtil.openShell(shell); } /** @@ -108,18 +75,18 @@ private void openAndFocusShell() throws InterruptedException { * non-focussed windows. So we have to open and focus remote here so that * clipboard operations work. */ - private void openAndFocusRemote() throws Exception { - startRemoteClipboardCommands(); - remote.setFocus(); - remote.waitUntilReady(); - sleep(); + public void openAndFocusRemote() throws Exception { + assertNull(remote); + remote = new RemoteClipboard(); + remote.start(); } @AfterEach public void tearDown() throws Exception { - sleep(); try { - stopRemoteClipboardCommands(); + if (remote != null) { + remote.stop(); + } } finally { if (clipboard != null) { clipboard.dispose(); @@ -131,147 +98,6 @@ public void tearDown() throws Exception { } } - private void startRemoteClipboardCommands() throws Exception { - /* - * The below copy using getPath may be redundant (i.e. it may be possible to run - * the class files from where they currently reside in the bin folder or the - * jar), but this method of setting up the class files is very simple and is - * done the same way that other files are extracted for tests. - * - * If the ClipboardTest starts to get more complicated, or other tests want to - * replicate this design element, then refactoring this is an option. - */ - remoteClipboardTempDir = Files.createTempDirectory("swt-test-Clipboard"); - List.of( // - "ClipboardTest", // - "ClipboardCommands", // - "ClipboardCommandsImpl", // - "ClipboardTest$LocalHostOnlySocketFactory" // - ).forEach((f) -> { - // extract the files and put them in the temp directory - SwtTestUtil.copyFile("/clipboard/" + f + ".class", - remoteClipboardTempDir.resolve("clipboard/" + f + ".class")); - }); - - String javaHome = System.getProperty("java.home"); - String javaExe = javaHome + "/bin/java" + (SwtTestUtil.isWindowsOS ? ".exe" : ""); - assertTrue(Files.exists(Path.of(javaExe))); - - ProcessBuilder pb = new ProcessBuilder(javaExe, "clipboard.ClipboardTest") - .directory(remoteClipboardTempDir.toFile()); - pb.inheritIO(); - pb.redirectOutput(Redirect.PIPE); - remoteClipboardProcess = pb.start(); - - // Read server output to find the port - int port = runOperationInThread(() -> { - BufferedReader reader = new BufferedReader(new InputStreamReader(remoteClipboardProcess.getInputStream())); - String line; - while ((line = reader.readLine()) != null) { - if (line.startsWith(ClipboardCommands.PORT_MESSAGE)) { - String[] parts = line.split(":"); - return Integer.parseInt(parts[1].trim()); - } - } - throw new RuntimeException("Failed to get port"); - }); - assertNotEquals(0, port); - try { - Registry reg = LocateRegistry.getRegistry("127.0.0.1", port); - long stopTime = System.currentTimeMillis() + DEFAULT_TIMEOUT_MS; - do { - try { - remote = (ClipboardCommands) reg.lookup(ClipboardCommands.ID); - break; - } catch (NotBoundException e) { - // try again because the remote app probably hasn't bound yet - } - } while (System.currentTimeMillis() < stopTime); - } catch (RemoteException e) { - - Integer exitValue = null; - boolean waitFor = false; - try { - waitFor = remoteClipboardProcess.waitFor(5, TimeUnit.SECONDS); - if (waitFor) { - exitValue = remoteClipboardProcess.exitValue(); - } - } catch (InterruptedException e1) { - Thread.interrupted(); - } - - String message = "Failed to get remote clipboards command, this seems to happen on macOS on I-build tests. Exception: " - + e.toString() + " waitFor: " + waitFor + " exitValue: " + exitValue; - - // Give some diagnostic information to help track down why this fails on build - // machine. We only hard error on Linux, for other platforms we allow test to - // just be skipped until we track down what is causing - // https://github.com/eclipse-platform/eclipse.platform.swt/issues/2553 - assumeTrue(SwtTestUtil.isGTK, message); - throw new RuntimeException(message, e); - } - assertNotNull(remote); - - // Run a no-op on the Swing event loop so that we know it is idle - // and we can continue startup - remote.waitUntilReady(); - } - - private void stopRemoteClipboardCommands() throws Exception { - try { - stopRemoteProcess(); - } finally { - deleteRemoteTempDir(); - } - } - - private void deleteRemoteTempDir() { - if (remoteClipboardTempDir != null) { - // At this point the process is ideally destroyed - or at least the test will - // report a failure if it isn't. Clean up the extracted files, but don't - // fail test if we fail to delete - try { - Files.walkFileTree(remoteClipboardTempDir, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Files.delete(file); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } - }); - } catch (IOException e) { - System.err.println("SWT Warning: Failed to clean up temp directory " + remoteClipboardTempDir - + " Error:" + e.toString()); - e.printStackTrace(); - } - } - } - - private void stopRemoteProcess() throws RemoteException, InterruptedException { - try { - if (remote != null) { - remote.stop(); - remote = null; - } - } finally { - if (remoteClipboardProcess != null) { - try { - remoteClipboardProcess.destroy(); - assertTrue(remoteClipboardProcess.waitFor(10, TimeUnit.SECONDS)); - } finally { - remoteClipboardProcess.destroyForcibly(); - assertTrue(remoteClipboardProcess.waitFor(10, TimeUnit.SECONDS)); - remoteClipboardProcess = null; - } - } - } - } - /** * Make sure to always copy/paste unique strings - this ensures that tests run * under {@link RepeatedTest}s don't false pass because of clipboard value on @@ -281,39 +107,54 @@ private String getUniqueTestString() { return "Hello World " + uniqueId++; } + /** + * Return the set of clipboards that are supported on this platform, this method + * is referenced in the {@link MethodSource} annotations on the + * {@link ParameterizedTest}s within this class. + */ + public static List supportedClipboardIds() { + if (SwtTestUtil.isGTK) { + return List.of(DND.CLIPBOARD, DND.SELECTION_CLIPBOARD); + } + return List.of(DND.CLIPBOARD); + } + /** * Test that the remote application clipboard works */ - @Test - public void test_Remote() throws Exception { + @ParameterizedTest + @MethodSource("supportedClipboardIds") + public void test_Remote(int clipboardId) throws Exception { openAndFocusRemote(); String helloWorld = getUniqueTestString(); - remote.setContents(helloWorld); - assertEquals(helloWorld, remote.getStringContents()); + remote.setContents(helloWorld, clipboardId); + assertEquals(helloWorld, remote.getStringContents(clipboardId)); } /** * This tests set + get on local clipboard. Remote clipboard can have different * behaviours and has additional tests. */ - @Test - public void test_LocalClipboard() throws Exception { + @ParameterizedTest + @MethodSource("supportedClipboardIds") + public void test_LocalClipboard(int clipboardId) { openAndFocusShell(); String helloWorld = getUniqueTestString(); - clipboard.setContents(new Object[] { helloWorld }, new Transfer[] { textTransfer }); - assertEquals(helloWorld, clipboard.getContents(textTransfer)); - assertNull(clipboard.getContents(rtfTransfer)); + clipboard.setContents(new Object[] { helloWorld }, new Transfer[] { textTransfer }, clipboardId); + assertEquals(helloWorld, clipboard.getContents(textTransfer, clipboardId)); + assertNull(clipboard.getContents(rtfTransfer, clipboardId)); helloWorld = getUniqueTestString(); String helloWorldRtf = "{\\rtf1\\b\\i " + helloWorld + "}"; - clipboard.setContents(new Object[] { helloWorld, helloWorldRtf }, new Transfer[] { textTransfer, rtfTransfer }); - assertEquals(helloWorld, clipboard.getContents(textTransfer)); - assertEquals(helloWorldRtf, clipboard.getContents(rtfTransfer)); + clipboard.setContents(new Object[] { helloWorld, helloWorldRtf }, new Transfer[] { textTransfer, rtfTransfer }, + clipboardId); + assertEquals(helloWorld, clipboard.getContents(textTransfer, clipboardId)); + assertEquals(helloWorldRtf, clipboard.getContents(rtfTransfer, clipboardId)); helloWorld = getUniqueTestString(); helloWorldRtf = "{\\rtf1\\b\\i " + helloWorld + "}"; - clipboard.setContents(new Object[] { helloWorldRtf }, new Transfer[] { rtfTransfer }); + clipboard.setContents(new Object[] { helloWorldRtf }, new Transfer[] { rtfTransfer }, clipboardId); if (SwtTestUtil.isCocoa) { /* * macOS's pasteboard has some extra functionality that even if you don't @@ -330,100 +171,68 @@ public void test_LocalClipboard() throws Exception { * multiple data formats to the pasteboard to ensure interoperability with other * apps. */ - assertEquals(helloWorld, clipboard.getContents(textTransfer)); + assertEquals(helloWorld, clipboard.getContents(textTransfer, clipboardId)); } else { - assertNull(clipboard.getContents(textTransfer)); + assertNull(clipboard.getContents(textTransfer, clipboardId)); } - assertEquals(helloWorldRtf, clipboard.getContents(rtfTransfer)); + assertEquals(helloWorldRtf, clipboard.getContents(rtfTransfer, clipboardId)); } - @Test - public void test_setContents() throws Exception { - try { - openAndFocusShell(); - String helloWorld = getUniqueTestString(); + @ParameterizedTest + @MethodSource("supportedClipboardIds") + public void test_setContents(int clipboardId) throws Exception { + openAndFocusShell(); + String helloWorld = getUniqueTestString(); - clipboard.setContents(new Object[] { helloWorld }, new Transfer[] { textTransfer }); - sleep(); + clipboard.setContents(new Object[] { helloWorld }, new Transfer[] { textTransfer }, clipboardId); - openAndFocusRemote(); - SwtTestUtil.processEvents(1000, () -> helloWorld.equals(runOperationInThread(remote::getStringContents))); - String result = runOperationInThread(remote::getStringContents); - assertEquals(helloWorld, result); - } catch (Exception | AssertionError e) { - if (SwtTestUtil.isGTK4() && !SwtTestUtil.isX11()) { - // TODO make the code + test stable - throw new RuntimeException( - "This test is really unstable on wayland backend, at least with Ubuntu 25.04", e); - } - throw e; - } + openAndFocusRemote(); + String result = SwtTestUtil.runOperationInThread(() -> remote.getStringContents(clipboardId)); + assertEquals(helloWorld, result); } - @Test - public void test_getContents() throws Exception { + @ParameterizedTest + @MethodSource("supportedClipboardIds") + public void test_getContents(int clipboardId) throws Exception { openAndFocusRemote(); String helloWorld = getUniqueTestString(); - remote.setContents(helloWorld); + remote.setContents(helloWorld, clipboardId); openAndFocusShell(); - SwtTestUtil.processEvents(1000, () -> { - return helloWorld.equals(clipboard.getContents(textTransfer)); - }); - assertEquals(helloWorld, clipboard.getContents(textTransfer)); + assertEquals(helloWorld, clipboard.getContents(textTransfer, clipboardId)); } - @FunctionalInterface - public interface ExceptionalSupplier { - T get() throws Exception; - } - /** - * When running some operations, such as requesting remote process read the - * clipboard, we need to have the event queue processing otherwise the remote - * won't be able to read our clipboard contribution. - * - * This method starts the supplier in a new thread and runs the event loop until - * the thread completes, or until a timeout is reached. - */ - private T runOperationInThread(ExceptionalSupplier supplier) throws RuntimeException { - return runOperationInThread(DEFAULT_TIMEOUT_MS, supplier); + @Test + public void test_getContentsBothClipboards() throws Exception { + assumeTrue(SwtTestUtil.isGTK); + + openAndFocusRemote(); + String helloWorldClipboard = getUniqueTestString(); + remote.setContents(helloWorldClipboard, DND.CLIPBOARD); + String helloWorldSelection = getUniqueTestString(); + remote.setContents(helloWorldSelection, DND.SELECTION_CLIPBOARD); + + openAndFocusShell(); + assertEquals(helloWorldClipboard, clipboard.getContents(textTransfer, DND.CLIPBOARD)); + assertEquals(helloWorldSelection, clipboard.getContents(textTransfer, DND.SELECTION_CLIPBOARD)); } - /** - * When running some operations, such as requesting remote process read the - * clipboard, we need to have the event queue processing otherwise the remote - * won't be able to read our clipboard contribution. - * - * This method starts the supplier in a new thread and runs the event loop until - * the thread completes, or until a timeout is reached. - */ - private T runOperationInThread(int timeoutMs, ExceptionalSupplier supplier) throws RuntimeException { - Object[] supplierValue = new Object[1]; - Exception[] supplierException = new Exception[1]; - Runnable task = () -> { - try { - supplierValue[0] = supplier.get(); - } catch (Exception e) { - supplierValue[0] = null; - supplierException[0] = e; - } - }; - Thread thread = new Thread(task, this.getClass().getName() + ".runOperationInThread"); - thread.setDaemon(true); - thread.start(); - BooleanSupplier done = () -> !thread.isAlive(); - try { - SwtTestUtil.processEvents(timeoutMs, done); - } catch (InterruptedException e) { - throw new RuntimeException("Failed while running thread", e); - } - assertTrue(done.getAsBoolean()); - if (supplierException[0] != null) { - throw new RuntimeException("Failed while running thread", supplierException[0]); - } - @SuppressWarnings("unchecked") - T result = (T) supplierValue[0]; - return result; + @Test + public void test_setContentsBothClipboards() throws Exception { + assumeTrue(SwtTestUtil.isGTK); + + openAndFocusShell(); + String helloWorldClipboard = getUniqueTestString(); + clipboard.setContents(new Object[] { helloWorldClipboard }, new Transfer[] { textTransfer }, DND.CLIPBOARD); + String helloWorldSelection = getUniqueTestString(); + clipboard.setContents(new Object[] { helloWorldSelection }, new Transfer[] { textTransfer }, + DND.SELECTION_CLIPBOARD); + + openAndFocusRemote(); + assertEquals(helloWorldClipboard, + SwtTestUtil.runOperationInThread(() -> remote.getStringContents(DND.CLIPBOARD))); + assertEquals(helloWorldSelection, + SwtTestUtil.runOperationInThread(() -> remote.getStringContents(DND.SELECTION_CLIPBOARD))); } } diff --git a/tests/org.eclipse.swt.tests/data/clipboard/ClipboardCommands.java b/tests/org.eclipse.swt.tests/data/clipboard/ClipboardCommands.java index adf595f842..ababe24e0c 100644 --- a/tests/org.eclipse.swt.tests/data/clipboard/ClipboardCommands.java +++ b/tests/org.eclipse.swt.tests/data/clipboard/ClipboardCommands.java @@ -17,13 +17,30 @@ public interface ClipboardCommands extends Remote { String PORT_MESSAGE = "ClipboardCommands Registry Port: "; String ID = "ClipboardCommands"; + /** + * Same value as SWT's DND.CLIPBOARD + */ + int CLIPBOARD = 1 << 0; + /** + * Same value as SWT's DND.SELECTION_CLIPBOARD + */ + int SELECTION_CLIPBOARD = 1 << 1; + + void stop() throws RemoteException; - void setContents(String string) throws RemoteException; + /** + * @param string string to set as contents + * @param clipboardId {@link #CLIPBOARD} or {@value #SELECTION_CLIPBOARD} + */ + void setContents(String string, int clipboardId) throws RemoteException; void setFocus() throws RemoteException; - String getStringContents() throws RemoteException; + /** + * @param clipboardId {@link #CLIPBOARD} or {@value #SELECTION_CLIPBOARD} + */ + String getStringContents(int clipboardId) throws RemoteException; void waitUntilReady() throws RemoteException; } diff --git a/tests/org.eclipse.swt.tests/data/clipboard/ClipboardCommandsImpl.java b/tests/org.eclipse.swt.tests/data/clipboard/ClipboardCommandsImpl.java index 28123f10fa..72fe1334c0 100644 --- a/tests/org.eclipse.swt.tests/data/clipboard/ClipboardCommandsImpl.java +++ b/tests/org.eclipse.swt.tests/data/clipboard/ClipboardCommandsImpl.java @@ -58,27 +58,40 @@ public void stop() throws RemoteException { } @Override - public void setContents(String text) throws RemoteException { + public void setContents(String text, int clipboardId) throws RemoteException { invokeAndWait(() -> { - clipboardTest.log("setContents(\"" + text + "\")"); + String display = text == null ? "null" : ("\"" + text + "\""); + clipboardTest.log("setContents(" + display + ", " + clipboardId + ")"); StringSelection selection = new StringSelection(text); - Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, null); + getClipboard(clipboardId).setContents(selection, null); }); } + private Clipboard getClipboard(int clipboardId) { + Clipboard clipboard; + if (clipboardId == SELECTION_CLIPBOARD) { + clipboard = Toolkit.getDefaultToolkit().getSystemSelection(); + } else if (clipboardId == CLIPBOARD) { + clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + } else { + throw new RuntimeException("Invalid clipboardId " + clipboardId); + } + return clipboard; + } + @Override - public String getStringContents() throws RemoteException { + public String getStringContents(int clipboardId) throws RemoteException { String[] data = new String[] { null }; invokeAndWait(() -> { - Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + Clipboard clipboard = getClipboard(clipboardId); try { data[0] = (String) clipboard.getData(DataFlavor.stringFlavor); - clipboardTest.log("getStringContents() returned " + data[0]); + clipboardTest.log("getStringContents(" + clipboardId + ") returned " + data[0]); } catch (Exception e) { data[0] = null; DataFlavor[] availableDataFlavors = clipboard.getAvailableDataFlavors(); - clipboardTest.log("getStringContents() threw " + e.toString() + clipboardTest.log("getStringContents(" + clipboardId + ") threw " + e.toString() + " and returned null. The clipboard had availableDataFlavors = " + Arrays.asList(availableDataFlavors)); } @@ -102,4 +115,4 @@ private void invokeAndWait(Runnable run) throws RemoteException { } } -} \ No newline at end of file +} diff --git a/tests/org.eclipse.swt.tests/data/clipboard/ClipboardTest.java b/tests/org.eclipse.swt.tests/data/clipboard/ClipboardTest.java index 18187bb670..d42a006bac 100644 --- a/tests/org.eclipse.swt.tests/data/clipboard/ClipboardTest.java +++ b/tests/org.eclipse.swt.tests/data/clipboard/ClipboardTest.java @@ -45,12 +45,12 @@ public class ClipboardTest extends JFrame { private static final class LocalHostOnlySocketFactory extends RMISocketFactory { @Override public ServerSocket createServerSocket(int port) throws IOException { - return new ServerSocket(port, 50, InetAddress.getLoopbackAddress()); + return new ServerSocket(port, 50, InetAddress.getLoopbackAddress()); } @Override public Socket createSocket(String host, int port) throws IOException { - return new Socket(InetAddress.getLoopbackAddress(), port); + return new Socket(InetAddress.getLoopbackAddress(), port); } } @@ -63,8 +63,6 @@ public ClipboardTest() throws RemoteException { commands = new ClipboardCommandsImpl(this); rmiRegistry.rebind(ClipboardCommands.ID, commands); - - textArea = new JTextArea(10, 40); JScrollPane scrollPane = new JScrollPane(textArea); @@ -111,16 +109,14 @@ public void log(String log) { } public static void main(String[] args) throws IOException { - System.setProperty("java.rmi.server.hostname", "127.0.0.1"); + System.setProperty("java.rmi.server.hostname", "127.0.0.1"); - // Make sure RMI is localhost only - RMISocketFactory.setSocketFactory(new LocalHostOnlySocketFactory()); + // Make sure RMI is localhost only + RMISocketFactory.setSocketFactory(new LocalHostOnlySocketFactory()); int chosenPort = getAvailablePort(); rmiRegistry = LocateRegistry.createRegistry(chosenPort); System.out.println(ClipboardCommands.PORT_MESSAGE + chosenPort); - - SwingUtilities.invokeLater(() -> { try { new ClipboardTest();