Skip to content

Commit 1164958

Browse files
committed
New test for Clipboard
Testing the Clipboard can be difficult because there are a lot of system issues that can interfere, but of particular concern are: 1. System clipboard managers which may take ownership of the clipboard at unexpected times. 2. Limitations as to when processes can access clipboard, such as on Wayland where only active window can access clipboard. 3. Different behaviour when copying within a single process than between processes. These tests aim to resolve these issues. For the system clipboard manager there are a lot of extra sleep calls to allow clipboard manager to complete operations before continuing tests. In addition, we run all the tests multiple times by default to ensure stability. For the process limitations, we carefully control when the shell is created because we often cannot get focus back when shell ends up in the background. See the openAndFocusShell and openAndFocusRemote methods. For the different behaviours, we spin up a simple Swing app in a new process (the "remote" in openAndFocusRemote above). This app can be directed, over RMI, to access the clipboard. This allows our test to place data on the clipboard and ensure that the remote app can read the data successfully. For now this test only covers basic text (and a little of RTF). Adding Image and other transfers is part of the future work as such functionality is added in GTK4 while working on #2126 For the changes to SwtTestUtil that we required: 1. isGTK4 moved from test_getImeInputMode to the Utils 2. processEvents was limited to 20 readAndDispatch calls per second due to the ordering of the targetTimestamp. This change allows full speed readAndDispatching. 3. getPath was refactored to allow better control of source and destination of files extracted. See extracting of class files for remote Swing app in startRemoteClipboardCommands method Part of #2126 Split out of #2538
1 parent 43f54cc commit 1164958

File tree

6 files changed

+621
-20
lines changed

6 files changed

+621
-20
lines changed

tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/SwtTestUtil.java

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@ public class SwtTestUtil {
105105

106106
public final static boolean isX11 = isGTK
107107
&& "x11".equals(System.getProperty("org.eclipse.swt.internal.gdk.backend"));
108-
108+
public final static boolean isGTK4 = isGTK
109+
&& System.getProperty("org.eclipse.swt.internal.gtk.version", "").startsWith("4");
109110

110111
/**
111112
* The palette used by images. See {@link #getAllPixels(Image)} and {@link #createImage}
@@ -400,13 +401,16 @@ public static void processEvents(int timeoutMs, BooleanSupplier breakCondition)
400401
long targetTimestamp = System.currentTimeMillis() + timeoutMs;
401402
Display display = Display.getCurrent();
402403
while (!breakCondition.getAsBoolean()) {
403-
if (!display.readAndDispatch()) {
404-
if (System.currentTimeMillis() < targetTimestamp) {
405-
Thread.sleep(50);
406-
} else {
404+
while (display.readAndDispatch()) {
405+
if (System.currentTimeMillis() >= targetTimestamp) {
407406
return;
408407
}
409408
}
409+
if (System.currentTimeMillis() < targetTimestamp) {
410+
Thread.sleep(50);
411+
} else {
412+
return;
413+
}
410414
}
411415
}
412416

@@ -583,18 +587,24 @@ public static boolean hasPixelNotMatching(Image image, Color nonMatchingColor, R
583587
}
584588

585589
public static Path getPath(String fileName, TemporaryFolder tempFolder) {
586-
Path filePath = tempFolder.getRoot().toPath().resolve("image-resources").resolve(Path.of(fileName));
587-
if (!Files.isRegularFile(filePath)) {
590+
Path path = tempFolder.getRoot().toPath();
591+
Path filePath = path.resolve("image-resources").resolve(Path.of(fileName));
592+
return getPath(fileName, filePath);
593+
}
594+
595+
public static Path getPath(String sourceFilename, Path destinationPath) {
596+
if (!Files.isRegularFile(destinationPath)) {
588597
// Extract resource on the classpath to a temporary file to ensure it's
589598
// available as plain file, even if this bundle is packed as jar
590-
try (InputStream inStream = SwtTestUtil.class.getResourceAsStream(fileName)) {
591-
assertNotNull(inStream, "InputStream == null for file " + fileName);
592-
Files.createDirectories(filePath.getParent());
593-
Files.copy(inStream, filePath);
599+
try (InputStream inStream = SwtTestUtil.class.getResourceAsStream(sourceFilename)) {
600+
assertNotNull(inStream, "InputStream == null for file " + sourceFilename);
601+
Files.createDirectories(destinationPath.getParent());
602+
Files.copy(inStream, destinationPath);
594603
} catch (IOException e) {
595604
throw new IllegalArgumentException(e);
596605
}
597606
}
598-
return filePath;
607+
return destinationPath;
599608
}
609+
600610
}
Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2000, 2025 IBM Corporation and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* IBM Corporation - initial API and implementation
13+
*******************************************************************************/
14+
package org.eclipse.swt.tests.junit;
15+
16+
import static org.junit.jupiter.api.Assertions.assertEquals;
17+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
18+
import static org.junit.jupiter.api.Assertions.assertNull;
19+
import static org.junit.jupiter.api.Assertions.assertTrue;
20+
21+
import java.io.BufferedReader;
22+
import java.io.InputStreamReader;
23+
import java.lang.ProcessBuilder.Redirect;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
26+
import java.rmi.NotBoundException;
27+
import java.rmi.registry.LocateRegistry;
28+
import java.rmi.registry.Registry;
29+
import java.util.List;
30+
import java.util.concurrent.TimeUnit;
31+
import java.util.function.BooleanSupplier;
32+
33+
import org.eclipse.swt.dnd.Clipboard;
34+
import org.eclipse.swt.dnd.RTFTransfer;
35+
import org.eclipse.swt.dnd.TextTransfer;
36+
import org.eclipse.swt.dnd.Transfer;
37+
import org.eclipse.swt.widgets.Display;
38+
import org.eclipse.swt.widgets.Shell;
39+
import org.junit.jupiter.api.AfterEach;
40+
import org.junit.jupiter.api.BeforeEach;
41+
import org.junit.jupiter.api.RepeatedTest;
42+
import org.junit.jupiter.api.Test;
43+
import org.junit.jupiter.api.io.TempDir;
44+
45+
import clipboard.ClipboardCommands;
46+
47+
/**
48+
* Automated Test Suite for class org.eclipse.swt.dnd.Clipboard
49+
*
50+
* @see org.eclipse.swt.dnd.Clipboard
51+
* @see Test_org_eclipse_swt_custom_StyledText StyledText tests as it also does
52+
* some clipboard tests
53+
*/
54+
public class Test_org_eclipse_swt_dnd_Clipboard {
55+
56+
private static final int REPEAT_COUNT = 3;
57+
@TempDir
58+
static Path tempFolder;
59+
static int uniqueId = 1;
60+
private Display display;
61+
private Shell shell;
62+
private Clipboard clipboard;
63+
private TextTransfer textTransfer;
64+
private RTFTransfer rtfTransfer;
65+
private ClipboardCommands remote;
66+
private Process remoteClipboardProcess;
67+
68+
@BeforeEach
69+
public void setUp() {
70+
display = Display.getCurrent();
71+
if (display == null) {
72+
display = Display.getDefault();
73+
}
74+
75+
clipboard = new Clipboard(display);
76+
textTransfer = TextTransfer.getInstance();
77+
rtfTransfer = RTFTransfer.getInstance();
78+
}
79+
80+
/**
81+
* TODO remove all uses of sleep and change them to processEvents with the
82+
* suitable conditional, or entirely remove them
83+
*/
84+
private void sleep() throws InterruptedException {
85+
SwtTestUtil.processEvents(100, null);
86+
}
87+
88+
/**
89+
* Note: Wayland backend does not allow access to system clipboard from
90+
* non-focussed windows. So we have to create/open and focus a window here so
91+
* that clipboard operations work.
92+
*/
93+
private void openAndFocusShell() throws InterruptedException {
94+
shell = new Shell(display);
95+
shell.open();
96+
shell.setFocus();
97+
sleep();
98+
}
99+
100+
/**
101+
* Note: Wayland backend does not allow access to system clipboard from
102+
* non-focussed windows. So we have to open and focus remote here so that
103+
* clipboard operations work.
104+
*/
105+
private void openAndFocusRemote() throws Exception {
106+
startRemoteClipboardCommands();
107+
remote.setFocus();
108+
remote.waitUntilReady();
109+
sleep();
110+
}
111+
112+
@AfterEach
113+
public void tearDown() throws Exception {
114+
sleep();
115+
try {
116+
stopRemoteClipboardCommands();
117+
} finally {
118+
if (clipboard != null) {
119+
clipboard.dispose();
120+
}
121+
if (shell != null) {
122+
shell.dispose();
123+
}
124+
SwtTestUtil.processEvents();
125+
}
126+
}
127+
128+
private void startRemoteClipboardCommands() throws Exception {
129+
// TODO: Can I get the jar and run it without extracting it?
130+
// TODO: Can I get all the files without having to list them?
131+
List.of( //
132+
"ClipboardTest", //
133+
"ClipboardCommands", //
134+
"ClipboardCommandsImpl", //
135+
"ClipboardTest$LocalHostOnlySocketFactory" //
136+
).forEach((f) -> {
137+
// extract the files and put them in the temp directory
138+
SwtTestUtil.getPath("/clipboard/" + f + ".class", tempFolder.resolve("clipboard/" + f + ".class"));
139+
});
140+
141+
String javaHome = System.getProperty("java.home");
142+
String javaExe = javaHome + "/bin/java";
143+
assertTrue(Files.exists(Path.of(javaExe)));
144+
145+
ProcessBuilder pb = new ProcessBuilder(javaExe, "clipboard.ClipboardTest").directory(tempFolder.toFile());
146+
pb.inheritIO();
147+
pb.redirectOutput(Redirect.PIPE);
148+
remoteClipboardProcess = pb.start();
149+
150+
// Read server output to find the port
151+
BufferedReader reader = new BufferedReader(new InputStreamReader(remoteClipboardProcess.getInputStream()));
152+
int port = 0;
153+
String line;
154+
// TODO: add a timeout here
155+
while ((line = reader.readLine()) != null) {
156+
if (line.startsWith(ClipboardCommands.PORT_MESSAGE)) {
157+
String[] parts = line.split(":");
158+
port = Integer.parseInt(parts[1].trim());
159+
break;
160+
}
161+
}
162+
assertNotEquals(0, port);
163+
Registry reg = LocateRegistry.getRegistry("127.0.0.1", port);
164+
long stopTime = System.currentTimeMillis() + 1000;
165+
do {
166+
try {
167+
remote = (ClipboardCommands) reg.lookup(ClipboardCommands.ID);
168+
break;
169+
} catch (NotBoundException e) {
170+
// try again because the remote app probably hasn't bound yet
171+
}
172+
} while (System.currentTimeMillis() < stopTime);
173+
174+
// Run a no-op on the Swing event loop so that we know it is idle
175+
// and we can continue startup
176+
remote.waitUntilReady();
177+
}
178+
179+
private void stopRemoteClipboardCommands() throws Exception {
180+
try {
181+
if (remote != null) {
182+
remote.stop();
183+
remote = null;
184+
}
185+
} finally {
186+
if (remoteClipboardProcess != null) {
187+
try {
188+
remoteClipboardProcess.waitFor(1, TimeUnit.SECONDS);
189+
} finally {
190+
remoteClipboardProcess.destroyForcibly();
191+
remoteClipboardProcess = null;
192+
}
193+
}
194+
}
195+
}
196+
197+
/**
198+
* Make sure to always copy/paste unique strings - this ensures that tests run
199+
* under {@link RepeatedTest}s don't false pass because of clipboard value on
200+
* previous iteration.
201+
*/
202+
private String getUniqueTestString() {
203+
return "Hello World " + uniqueId++;
204+
}
205+
206+
/**
207+
* Test that the remote application clipboard works
208+
*/
209+
@Test
210+
public void test_Remote() throws Exception {
211+
openAndFocusRemote();
212+
String helloWorld = getUniqueTestString();
213+
remote.setContents(helloWorld);
214+
assertEquals(helloWorld, remote.getStringContents());
215+
}
216+
217+
/**
218+
* This tests set + get on local clipboard. Remote clipboard can have different
219+
* behaviours and has additional tests.
220+
*/
221+
@RepeatedTest(value = REPEAT_COUNT)
222+
public void test_LocalClipboard() throws Exception {
223+
openAndFocusShell();
224+
225+
String helloWorld = getUniqueTestString();
226+
clipboard.setContents(new Object[] { helloWorld }, new Transfer[] { textTransfer });
227+
assertEquals(helloWorld, clipboard.getContents(textTransfer));
228+
assertNull(clipboard.getContents(rtfTransfer));
229+
230+
helloWorld = getUniqueTestString();
231+
String helloWorldRtf = "{\\rtf1\\b\\i " + helloWorld + "}";
232+
clipboard.setContents(new Object[] { helloWorld, helloWorldRtf }, new Transfer[] { textTransfer, rtfTransfer });
233+
assertEquals(helloWorld, clipboard.getContents(textTransfer));
234+
assertEquals(helloWorldRtf, clipboard.getContents(rtfTransfer));
235+
236+
helloWorld = getUniqueTestString();
237+
helloWorldRtf = "{\\rtf1\\b\\i " + helloWorld + "}";
238+
clipboard.setContents(new Object[] { helloWorldRtf }, new Transfer[] { rtfTransfer });
239+
assertNull(clipboard.getContents(textTransfer));
240+
assertEquals(helloWorldRtf, clipboard.getContents(rtfTransfer));
241+
}
242+
243+
@RepeatedTest(value = REPEAT_COUNT)
244+
public void test_setContents() throws Exception {
245+
try {
246+
openAndFocusShell();
247+
String helloWorld = getUniqueTestString();
248+
249+
clipboard.setContents(new Object[] { helloWorld }, new Transfer[] { textTransfer });
250+
sleep();
251+
252+
openAndFocusRemote();
253+
SwtTestUtil.processEvents(1000, () -> helloWorld.equals(runOperationInThread(remote::getStringContents)));
254+
String result = runOperationInThread(remote::getStringContents);
255+
assertEquals(helloWorld, result);
256+
} catch (Exception | AssertionError e) {
257+
if (SwtTestUtil.isGTK4 && !SwtTestUtil.isX11) {
258+
// TODO make the code + test stable
259+
throw new RuntimeException(
260+
"This test is really unstable on wayland backend, at least with Ubuntu 25.04", e);
261+
}
262+
throw e;
263+
}
264+
}
265+
266+
@RepeatedTest(value = REPEAT_COUNT)
267+
public void test_getContents() throws Exception {
268+
openAndFocusRemote();
269+
String helloWorld = getUniqueTestString();
270+
remote.setContents(helloWorld);
271+
272+
openAndFocusShell();
273+
SwtTestUtil.processEvents(1000, () -> {
274+
return helloWorld.equals(clipboard.getContents(textTransfer));
275+
});
276+
assertEquals(helloWorld, clipboard.getContents(textTransfer));
277+
}
278+
279+
@FunctionalInterface
280+
public interface ExceptionalSupplier<T> {
281+
T get() throws Exception;
282+
}
283+
284+
/**
285+
* When running some operations, such as requesting remote process read the
286+
* clipboard, we need to have the event queue processing otherwise the remote
287+
* won't be able to read our clipboard contribution.
288+
*
289+
* This method starts the supplier in a new thread and runs the event loop until
290+
* the thread completes.
291+
*
292+
* @throws InterruptedException
293+
*/
294+
private <T> T runOperationInThread(ExceptionalSupplier<T> supplier) throws RuntimeException {
295+
Object[] supplierValue = new Object[1];
296+
Exception[] supplierException = new Exception[1];
297+
Runnable task = () -> {
298+
try {
299+
supplierValue[0] = supplier.get();
300+
} catch (Exception e) {
301+
supplierValue[0] = null;
302+
supplierException[0] = e;
303+
}
304+
};
305+
Thread thread = new Thread(task, this.getClass().getName() + ".runOperationInThread");
306+
thread.setDaemon(true);
307+
thread.start();
308+
BooleanSupplier done = () -> !thread.isAlive();
309+
try {
310+
SwtTestUtil.processEvents(2000, done);
311+
} catch (InterruptedException e) {
312+
throw new RuntimeException("Failed while running thread", e);
313+
}
314+
assertTrue(done.getAsBoolean());
315+
if (supplierException[0] != null) {
316+
throw new RuntimeException("Failed while running thread", supplierException[0]);
317+
}
318+
@SuppressWarnings("unchecked")
319+
T result = (T) supplierValue[0];
320+
return result;
321+
}
322+
}

0 commit comments

Comments
 (0)