Skip to content

Commit 7831aa5

Browse files
committed
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
1 parent d2de8e3 commit 7831aa5

File tree

6 files changed

+444
-301
lines changed

6 files changed

+444
-301
lines changed
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Kichwa Coders Canada, Inc.
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+
package org.eclipse.swt.tests.junit;
12+
13+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
14+
import static org.junit.jupiter.api.Assertions.assertNotNull;
15+
import static org.junit.jupiter.api.Assertions.assertNull;
16+
import static org.junit.jupiter.api.Assertions.assertTrue;
17+
import static org.junit.jupiter.api.Assumptions.assumeTrue;
18+
19+
import java.io.BufferedReader;
20+
import java.io.IOException;
21+
import java.io.InputStreamReader;
22+
import java.lang.ProcessBuilder.Redirect;
23+
import java.nio.file.FileVisitResult;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
26+
import java.nio.file.SimpleFileVisitor;
27+
import java.nio.file.attribute.BasicFileAttributes;
28+
import java.rmi.NotBoundException;
29+
import java.rmi.RemoteException;
30+
import java.rmi.registry.LocateRegistry;
31+
import java.rmi.registry.Registry;
32+
import java.util.List;
33+
import java.util.concurrent.TimeUnit;
34+
35+
import clipboard.ClipboardCommands;
36+
37+
public class RemoteClipboard implements ClipboardCommands {
38+
private ClipboardCommands remote;
39+
private Process remoteClipboardProcess;
40+
private Path remoteClipboardTempDir;
41+
42+
public void start() throws Exception {
43+
assertNull(remote, "Create a new instance to restart");
44+
/*
45+
* The below copy using getPath may be redundant (i.e. it may be possible to run
46+
* the class files from where they currently reside in the bin folder or the
47+
* jar), but this method of setting up the class files is very simple and is
48+
* done the same way that other files are extracted for tests.
49+
*
50+
* If the ClipboardTest starts to get more complicated, or other tests want to
51+
* replicate this design element, then refactoring this is an option.
52+
*/
53+
remoteClipboardTempDir = Files.createTempDirectory("swt-test-Clipboard");
54+
List.of( //
55+
"ClipboardTest", //
56+
"ClipboardCommands", //
57+
"ClipboardCommandsImpl", //
58+
"ClipboardTest$LocalHostOnlySocketFactory" //
59+
).forEach((f) -> {
60+
// extract the files and put them in the temp directory
61+
SwtTestUtil.copyFile("/clipboard/" + f + ".class",
62+
remoteClipboardTempDir.resolve("clipboard/" + f + ".class"));
63+
});
64+
65+
String javaHome = System.getProperty("java.home");
66+
String javaExe = javaHome + "/bin/java" + (SwtTestUtil.isWindowsOS ? ".exe" : "");
67+
assertTrue(Files.exists(Path.of(javaExe)));
68+
69+
ProcessBuilder pb = new ProcessBuilder(javaExe, "clipboard.ClipboardTest")
70+
.directory(remoteClipboardTempDir.toFile());
71+
pb.inheritIO();
72+
pb.redirectOutput(Redirect.PIPE);
73+
remoteClipboardProcess = pb.start();
74+
75+
// Read server output to find the port
76+
int port = SwtTestUtil.runOperationInThread(() -> {
77+
BufferedReader reader = new BufferedReader(new InputStreamReader(remoteClipboardProcess.getInputStream()));
78+
String line;
79+
while ((line = reader.readLine()) != null) {
80+
if (line.startsWith(ClipboardCommands.PORT_MESSAGE)) {
81+
String[] parts = line.split(":");
82+
return Integer.parseInt(parts[1].trim());
83+
}
84+
}
85+
throw new RuntimeException("Failed to get port");
86+
});
87+
assertNotEquals(0, port);
88+
try {
89+
Registry reg = LocateRegistry.getRegistry("127.0.0.1", port);
90+
long stopTime = System.currentTimeMillis() + 10000;
91+
do {
92+
try {
93+
remote = (ClipboardCommands) reg.lookup(ClipboardCommands.ID);
94+
break;
95+
} catch (NotBoundException e) {
96+
// try again because the remote app probably hasn't bound yet
97+
}
98+
} while (System.currentTimeMillis() < stopTime);
99+
} catch (RemoteException e) {
100+
101+
Integer exitValue = null;
102+
boolean waitFor = false;
103+
try {
104+
waitFor = remoteClipboardProcess.waitFor(5, TimeUnit.SECONDS);
105+
if (waitFor) {
106+
exitValue = remoteClipboardProcess.exitValue();
107+
}
108+
} catch (InterruptedException e1) {
109+
Thread.interrupted();
110+
}
111+
112+
String message = "Failed to get remote clipboards command, this seems to happen on macOS on I-build tests. Exception: "
113+
+ e.toString() + " waitFor: " + waitFor + " exitValue: " + exitValue;
114+
115+
// Give some diagnostic information to help track down why this fails on build
116+
// machine. We only hard error on Linux, for other platforms we allow test to
117+
// just be skipped until we track down what is causing
118+
// https://github.com/eclipse-platform/eclipse.platform.swt/issues/2553
119+
assumeTrue(SwtTestUtil.isGTK, message);
120+
throw new RuntimeException(message, e);
121+
}
122+
assertNotNull(remote);
123+
124+
// Run a no-op on the Swing event loop so that we know it is idle
125+
// and we can continue startup
126+
remote.waitUntilReady();
127+
remote.setFocus();
128+
remote.waitUntilReady();
129+
}
130+
131+
@Override
132+
public void stop() throws RemoteException {
133+
try {
134+
stopProcess();
135+
} catch (InterruptedException e) {
136+
Thread.interrupted();
137+
} finally {
138+
deleteRemoteTempDir();
139+
}
140+
}
141+
142+
private void stopProcess() throws RemoteException, InterruptedException {
143+
try {
144+
if (remote != null) {
145+
remote.stop();
146+
remote = null;
147+
}
148+
} finally {
149+
if (remoteClipboardProcess != null) {
150+
try {
151+
remoteClipboardProcess.destroy();
152+
assertTrue(remoteClipboardProcess.waitFor(10, TimeUnit.SECONDS));
153+
} finally {
154+
remoteClipboardProcess.destroyForcibly();
155+
assertTrue(remoteClipboardProcess.waitFor(10, TimeUnit.SECONDS));
156+
remoteClipboardProcess = null;
157+
}
158+
}
159+
}
160+
}
161+
162+
private void deleteRemoteTempDir() {
163+
if (remoteClipboardTempDir != null) {
164+
// At this point the process is ideally destroyed - or at least the test will
165+
// report a failure if it isn't. Clean up the extracted files, but don't
166+
// fail test if we fail to delete
167+
try {
168+
Files.walkFileTree(remoteClipboardTempDir, new SimpleFileVisitor<Path>() {
169+
@Override
170+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
171+
Files.delete(file);
172+
return FileVisitResult.CONTINUE;
173+
}
174+
175+
@Override
176+
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
177+
Files.delete(dir);
178+
return FileVisitResult.CONTINUE;
179+
}
180+
});
181+
} catch (IOException e) {
182+
System.err.println("SWT Warning: Failed to clean up temp directory " + remoteClipboardTempDir
183+
+ " Error:" + e.toString());
184+
e.printStackTrace();
185+
}
186+
}
187+
}
188+
189+
@Override
190+
public void setContents(String string, int clipboardId) throws RemoteException {
191+
remote.setContents(string, clipboardId);
192+
}
193+
194+
@Override
195+
public void setFocus() throws RemoteException {
196+
remote.setFocus();
197+
}
198+
199+
@Override
200+
public String getStringContents(int clipboardId) throws RemoteException {
201+
return remote.getStringContents(clipboardId);
202+
}
203+
204+
@Override
205+
public void waitUntilReady() throws RemoteException {
206+
remote.waitUntilReady();
207+
}
208+
}

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

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
import java.io.PrintStream;
2525
import java.nio.file.Files;
2626
import java.nio.file.Path;
27+
import java.util.HashMap;
28+
import java.util.Map;
29+
import java.util.Set;
2730
import java.util.concurrent.atomic.AtomicBoolean;
2831
import java.util.function.BooleanSupplier;
2932

@@ -203,7 +206,12 @@ public static boolean isBidi() {
203206
public static void openShell(Shell shell) {
204207
if (shell != null && !shell.getVisible()) {
205208
if (isGTK) {
206-
waitEvent(() -> shell.open(), shell, SWT.Paint, 1000);
209+
if (isGTK4()) {
210+
waitAllEvents(() -> shell.open(), shell, Set.of(SWT.Paint, SWT.Activate, SWT.FocusIn), 1000);
211+
} else {
212+
waitEvent(() -> shell.open(), shell, SWT.Paint, 1000);
213+
}
214+
processEvents();
207215
} else {
208216
shell.open();
209217
}
@@ -485,6 +493,45 @@ public static boolean waitEvent(Runnable trigger, Control control, int swtEvent,
485493
return true;
486494
}
487495

496+
/**
497+
* Wait until specified control receives all the specified event.
498+
*
499+
* @param trigger may be null. Code that is expected to send event.
500+
* Note that if you trigger it outside, then event may
501+
* arrive *before* you call this function, and it will
502+
* fail to receive event.
503+
* @param control control expected to receive the event
504+
* @param swtEvents events, such as SWT.Paint
505+
* @param timeoutMsec how long to wait for event
506+
* @return <code>true</code> if event was received
507+
*/
508+
public static boolean waitAllEvents(Runnable trigger, Control control, Set<Integer> swtEvents, int timeoutMsec) {
509+
Map<Integer, Listener> eventsLeftToReceive = new HashMap<>();
510+
for (Integer swtEvent : swtEvents) {
511+
Listener listener = event -> {
512+
control.removeListener(swtEvent, eventsLeftToReceive.get(swtEvent));
513+
eventsLeftToReceive.remove(swtEvent);
514+
};
515+
eventsLeftToReceive.put(swtEvent, listener);
516+
control.addListener(swtEvent, listener);
517+
}
518+
try {
519+
if (trigger != null)
520+
trigger.run();
521+
522+
long start = System.currentTimeMillis();
523+
while (!eventsLeftToReceive.isEmpty()) {
524+
if (System.currentTimeMillis() - start > timeoutMsec)
525+
return false;
526+
processEvents();
527+
}
528+
} finally {
529+
eventsLeftToReceive.forEach((swtEvent, listener) -> control.removeListener(swtEvent, listener));
530+
}
531+
532+
return true;
533+
}
534+
488535
/**
489536
* Wait until specified Shell becomes active, or internal timeout elapses.
490537
*
@@ -642,4 +689,57 @@ public static Path copyFile(String sourceFilename, Path destinationPath) {
642689
return destinationPath;
643690
}
644691

692+
@FunctionalInterface
693+
public interface ExceptionalSupplier<T> {
694+
T get() throws Exception;
695+
}
696+
697+
/**
698+
* When running some operations, such as requesting remote process read the
699+
* clipboard, we need to have the event queue processing otherwise the remote
700+
* won't be able to read our clipboard contribution.
701+
*
702+
* This method starts the supplier in a new thread and runs the event loop until
703+
* the thread completes, or until a timeout is reached.
704+
*/
705+
static <T> T runOperationInThread(ExceptionalSupplier<T> supplier) throws RuntimeException {
706+
return runOperationInThread(10000, supplier);
707+
}
708+
709+
/**
710+
* When running some operations, such as requesting remote process read the
711+
* clipboard, we need to have the event queue processing otherwise the remote
712+
* won't be able to read our clipboard contribution.
713+
*
714+
* This method starts the supplier in a new thread and runs the event loop until
715+
* the thread completes, or until a timeout is reached.
716+
*/
717+
static <T> T runOperationInThread(int timeoutMs, ExceptionalSupplier<T> supplier) throws RuntimeException {
718+
Object[] supplierValue = new Object[1];
719+
Exception[] supplierException = new Exception[1];
720+
Runnable task = () -> {
721+
try {
722+
supplierValue[0] = supplier.get();
723+
} catch (Exception e) {
724+
supplierValue[0] = null;
725+
supplierException[0] = e;
726+
}
727+
};
728+
Thread thread = new Thread(task, SwtTestUtil.class.getName() + ".runOperationInThread");
729+
thread.setDaemon(true);
730+
thread.start();
731+
BooleanSupplier done = () -> !thread.isAlive();
732+
try {
733+
processEvents(timeoutMs, done);
734+
} catch (InterruptedException e) {
735+
throw new RuntimeException("Failed while running thread", e);
736+
}
737+
assertTrue(done.getAsBoolean());
738+
if (supplierException[0] != null) {
739+
throw new RuntimeException("Failed while running thread", supplierException[0]);
740+
}
741+
@SuppressWarnings("unchecked")
742+
T result = (T) supplierValue[0];
743+
return result;
744+
}
645745
}

0 commit comments

Comments
 (0)