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();