Skip to content

Commit ba112f0

Browse files
committed
Add explicit activation (by tester) to Clipboard tests on Wayland
On Wayland apps are not allowed to take ownership of the system clipboard unless the Wayland compositer (e.g KWin, mutter or others) believes that the user has initiated the request for ownership. This means that activating and focusing the Shell may not be sufficient. Therefore we require the person running the tests to press a button in the shell so the compositer believes that the clipboard access is legitimate. This commit adds such a button, and a short timeout to press it. If not pressed in time the tests requiring SWT to take ownership of the clipboard will be skipped. The button must be pressed multiple times throughout the test, therefore we group the affected tests to run first with TestMethodOrder annotation. On GitHub actions + Jenkins these tests are skipped by default. Note: At the moment activating + focusing the shell is sufficient to read from the system clipboard. In addition, X11 (and therefore the Swing remote clipboard test app) run under Xwayland do not require such explicit activation, so the key presses are only on the couple of tests that require ownership of system clipboard to operate properly. Part of #2126
1 parent 199a644 commit ba112f0

File tree

6 files changed

+154
-12
lines changed

6 files changed

+154
-12
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,9 @@ public String getStringContents(int clipboardId) throws RemoteException {
205205
public void waitUntilReady() throws RemoteException {
206206
remote.waitUntilReady();
207207
}
208+
209+
@Override
210+
public void waitForButtonPress() throws RemoteException {
211+
remote.waitForButtonPress();
212+
}
208213
}

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,28 @@ public final static boolean isX11() {
125125
throw new IllegalStateException("unreachable");
126126
}
127127

128+
/**
129+
* Return whether running on Wayland. This is dynamically set at runtime and cannot
130+
* be accessed before the corresponding property is initialized in Display.
131+
*
132+
* <strong>Note:</strong> this method still must be called after the first
133+
* Display is created to be valid
134+
*/
135+
public final static boolean isWayland() {
136+
if (!isGTK) {
137+
return false;
138+
}
139+
String backend = System.getProperty("org.eclipse.swt.internal.gdk.backend");
140+
141+
if ("wayland".equals(backend)) {
142+
return true;
143+
} else if ("x11".equals(backend)) {
144+
return false;
145+
}
146+
fail("org.eclipse.swt.internal.gdk.backend System property is not set yet. Create a new Display before calling isWayland");
147+
throw new IllegalStateException("unreachable");
148+
}
149+
128150
/**
129151
* Return whether running on GTK4. This is dynamically set at runtime and cannot
130152
* be accessed before the corresponding property is initialized in OS.

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

Lines changed: 98 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,32 @@
1212

1313
import static org.junit.jupiter.api.Assertions.assertEquals;
1414
import static org.junit.jupiter.api.Assertions.assertNull;
15+
import static org.junit.jupiter.api.Assumptions.assumeFalse;
1516
import static org.junit.jupiter.api.Assumptions.assumeTrue;
1617

1718
import java.util.List;
1819
import java.util.concurrent.CompletableFuture;
20+
import java.util.concurrent.atomic.AtomicBoolean;
1921

22+
import org.eclipse.swt.SWT;
2023
import org.eclipse.swt.dnd.Clipboard;
2124
import org.eclipse.swt.dnd.DND;
2225
import org.eclipse.swt.dnd.RTFTransfer;
2326
import org.eclipse.swt.dnd.TextTransfer;
2427
import org.eclipse.swt.dnd.Transfer;
28+
import org.eclipse.swt.layout.RowLayout;
29+
import org.eclipse.swt.widgets.Button;
2530
import org.eclipse.swt.widgets.Display;
31+
import org.eclipse.swt.widgets.Label;
2632
import org.eclipse.swt.widgets.Shell;
2733
import org.junit.jupiter.api.AfterEach;
2834
import org.junit.jupiter.api.BeforeEach;
35+
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
36+
import org.junit.jupiter.api.Order;
2937
import org.junit.jupiter.api.RepeatedTest;
3038
import org.junit.jupiter.api.Tag;
3139
import org.junit.jupiter.api.Test;
40+
import org.junit.jupiter.api.TestMethodOrder;
3241
import org.junit.jupiter.params.ParameterizedTest;
3342
import org.junit.jupiter.params.provider.MethodSource;
3443

@@ -40,8 +49,20 @@
4049
* some clipboard tests
4150
*/
4251
@Tag("clipboard")
52+
@TestMethodOrder(OrderAnnotation.class) // run tests needing button presses first
4353
public class Test_org_eclipse_swt_dnd_Clipboard {
4454

55+
/**
56+
* See {@link #openAndFocusShell(boolean)} - some tests require user to actually
57+
* interact with the shell.
58+
*
59+
* Default to skipping tests requiring "real" activation on GHA and Jenkins.
60+
*
61+
* <code>true</code>: skip tests <code>false</code>: don't skip tests
62+
* <code>null</code>: unknown whether to skip tests yet
63+
*/
64+
private static Boolean skipTestsRequiringButtonPress = (Boolean.parseBoolean(System.getenv("GITHUB_ACTIONS"))
65+
|| System.getenv("JOB_NAME") != null) ? true : null;
4566
private static int uniqueId = 1;
4667
private Display display;
4768
private Shell shell;
@@ -65,21 +86,86 @@ public void setUp() {
6586
* Note: Wayland backend does not allow access to system clipboard from
6687
* non-focussed windows. So we have to create/open and focus a window here so
6788
* that clipboard operations work.
89+
*
90+
* Additionally, if we want to provide data to the clipboard, we require user
91+
* interaction on the created shell. Therefore if forSetContents is true the
92+
* tester needs to press a button for test to work.
93+
*
94+
* If there is no user interaction (button not pressed) then the test is
95+
* skipped, rather than failed, and subsequent tests requiring user interaction
96+
* as skipped too. See {@link #skipTestsRequiringButtonPress}
6897
*/
69-
private void openAndFocusShell() {
98+
private void openAndFocusShell(boolean forSetContents) throws InterruptedException {
99+
assertNull(shell);
70100
shell = new Shell(display);
71-
SwtTestUtil.openShell(shell);
101+
102+
boolean requireUserPress = forSetContents && SwtTestUtil.isWayland();
103+
if (requireUserPress) {
104+
assumeFalse(skipTestsRequiringButtonPress != null && skipTestsRequiringButtonPress,
105+
"Skipping tests that require user input");
106+
107+
AtomicBoolean pressed = new AtomicBoolean(false);
108+
shell.setLayout(new RowLayout(SWT.VERTICAL));
109+
Button button = new Button(shell, SWT.PUSH);
110+
button.setText("Press me!");
111+
button.addListener(SWT.Selection, (e) -> pressed.set(true));
112+
button.setSize(200, 50);
113+
Label label = new Label(shell, SWT.NONE);
114+
label.setText("""
115+
Press the button to tell Wayland that you really want this window to have access to clipboard.
116+
This is needed on Wayland because only really focussed programs are allowed to write to the
117+
global keyboard.
118+
119+
If you don't press this button soon, the test will be skipped and you won't be asked again.
120+
""");
121+
Label timeleft = new Label(shell, SWT.NONE);
122+
timeleft.setText("Time left to press button: XXXXXXXXXXXXXXXXXXXX seconds");
123+
124+
SwtTestUtil.openShell(shell);
125+
126+
// If we know there is a tester pressing the buttons, allow them
127+
// a little grace on the timeout. If we don't know if there is a
128+
// tester around, skip tests fairly quickly and don't
129+
// ask again.
130+
int timeout = skipTestsRequiringButtonPress == null ? 1500 : 10000;
131+
long startTime = System.nanoTime();
132+
SwtTestUtil.processEvents(timeout, () -> {
133+
long nowTime = System.nanoTime();
134+
long timeLeft = nowTime - startTime;
135+
long timeLeftMs = timeout - (timeLeft / 1_000_000);
136+
double timeLeftS = timeLeftMs / 1_000.0d;
137+
timeleft.setText("Time left to press button: " + timeLeftS + " seconds");
138+
return pressed.get();
139+
});
140+
boolean userPressedButton = pressed.get();
141+
if (userPressedButton) {
142+
skipTestsRequiringButtonPress = false;
143+
} else {
144+
skipTestsRequiringButtonPress = true;
145+
assumeTrue(false, "Skipping tests that require user input");
146+
}
147+
} else {
148+
SwtTestUtil.openShell(shell);
149+
}
150+
72151
}
73152

74153
/**
75154
* Note: Wayland backend does not allow access to system clipboard from
76155
* non-focussed windows. So we have to open and focus remote here so that
77156
* clipboard operations work.
78157
*/
79-
public void openAndFocusRemote() throws Exception {
158+
private void openAndFocusRemote() throws Exception {
80159
assertNull(remote);
81160
remote = new RemoteClipboard();
82161
remote.start();
162+
163+
/*
164+
* If/when OpenJDK Project Wakefield gets merged then we may need to wait for
165+
* button pressed on the swing app just like the SWT app. This may also be
166+
* needed if Wayland implementations get more restrictive on X apps too.
167+
*/
168+
// remote.waitForButtonPress();
83169
}
84170

85171
@AfterEach
@@ -138,8 +224,8 @@ public void test_Remote(int clipboardId) throws Exception {
138224
*/
139225
@ParameterizedTest
140226
@MethodSource("supportedClipboardIds")
141-
public void test_LocalClipboard(int clipboardId) {
142-
openAndFocusShell();
227+
public void test_LocalClipboard(int clipboardId) throws InterruptedException {
228+
openAndFocusShell(false);
143229

144230
String helloWorld = getUniqueTestString();
145231
clipboard.setContents(new Object[] { helloWorld }, new Transfer[] { textTransfer }, clipboardId);
@@ -179,10 +265,11 @@ public void test_LocalClipboard(int clipboardId) {
179265
assertEquals(helloWorldRtf, clipboard.getContents(rtfTransfer, clipboardId));
180266
}
181267

268+
@Order(2)
182269
@ParameterizedTest
183270
@MethodSource("supportedClipboardIds")
184271
public void test_setContents(int clipboardId) throws Exception {
185-
openAndFocusShell();
272+
openAndFocusShell(true);
186273
String helloWorld = getUniqueTestString();
187274

188275
clipboard.setContents(new Object[] { helloWorld }, new Transfer[] { textTransfer }, clipboardId);
@@ -199,11 +286,10 @@ public void test_getContents(int clipboardId) throws Exception {
199286
String helloWorld = getUniqueTestString();
200287
remote.setContents(helloWorld, clipboardId);
201288

202-
openAndFocusShell();
289+
openAndFocusShell(false);
203290
assertEquals(helloWorld, clipboard.getContents(textTransfer, clipboardId));
204291
}
205292

206-
207293
@Test
208294
public void test_getContentsBothClipboards() throws Exception {
209295
assumeTrue(SwtTestUtil.isGTK);
@@ -214,16 +300,17 @@ public void test_getContentsBothClipboards() throws Exception {
214300
String helloWorldSelection = getUniqueTestString();
215301
remote.setContents(helloWorldSelection, DND.SELECTION_CLIPBOARD);
216302

217-
openAndFocusShell();
303+
openAndFocusShell(false);
218304
assertEquals(helloWorldClipboard, clipboard.getContents(textTransfer, DND.CLIPBOARD));
219305
assertEquals(helloWorldSelection, clipboard.getContents(textTransfer, DND.SELECTION_CLIPBOARD));
220306
}
221307

308+
@Order(1)
222309
@Test
223310
public void test_setContentsBothClipboards() throws Exception {
224311
assumeTrue(SwtTestUtil.isGTK);
225312

226-
openAndFocusShell();
313+
openAndFocusShell(true);
227314
String helloWorldClipboard = getUniqueTestString();
228315
clipboard.setContents(new Object[] { helloWorldClipboard }, new Transfer[] { textTransfer }, DND.CLIPBOARD);
229316
String helloWorldSelection = getUniqueTestString();
@@ -244,7 +331,7 @@ public void test_getContentsAsync(int clipboardId) throws Exception {
244331
String helloWorld = getUniqueTestString();
245332
remote.setContents(helloWorld, clipboardId);
246333

247-
openAndFocusShell();
334+
openAndFocusShell(false);
248335

249336
// Multiple ways of using the API
250337
// 1: Spin the event loop manually waiting for future to complete

tests/org.eclipse.swt.tests/data/clipboard/ClipboardCommands.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,6 @@ public interface ClipboardCommands extends Remote {
4343
String getStringContents(int clipboardId) throws RemoteException;
4444

4545
void waitUntilReady() throws RemoteException;
46+
47+
void waitForButtonPress() throws RemoteException;
4648
}

tests/org.eclipse.swt.tests/data/clipboard/ClipboardCommandsImpl.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@
1818
import java.rmi.RemoteException;
1919
import java.rmi.server.UnicastRemoteObject;
2020
import java.util.Arrays;
21+
import java.util.concurrent.CountDownLatch;
22+
import java.util.concurrent.TimeUnit;
2123

2224
import javax.swing.SwingUtilities;
2325

2426
public class ClipboardCommandsImpl extends UnicastRemoteObject implements ClipboardCommands {
2527
private static final long serialVersionUID = 330098269086266134L;
2628
private ClipboardTest clipboardTest;
29+
CountDownLatch buttonPressed;
2730

2831
protected ClipboardCommandsImpl(ClipboardTest clipboardTest) throws RemoteException {
2932
super();
@@ -107,6 +110,23 @@ public void setFocus() throws RemoteException {
107110
});
108111
}
109112

113+
@Override
114+
public void waitForButtonPress() throws RemoteException {
115+
clipboardTest.log("waitForButtonPress() - START");
116+
buttonPressed = new CountDownLatch(1);
117+
try {
118+
if (buttonPressed.await(10, TimeUnit.SECONDS)) {
119+
clipboardTest.log("waitForButtonPress() - SUCCESS");
120+
} else {
121+
clipboardTest.log("waitForButtonPress() - FAILED Timeout");
122+
throw new RemoteException("Button not pressed in time");
123+
}
124+
} catch (InterruptedException e) {
125+
clipboardTest.log("waitForButtonPress() - FAILED Interrupted");
126+
throw new RemoteException("Interrupted while waiting for button press", e);
127+
}
128+
}
129+
110130
private void invokeAndWait(Runnable run) throws RemoteException {
111131
try {
112132
SwingUtilities.invokeAndWait(run);

tests/org.eclipse.swt.tests/data/clipboard/ClipboardTest.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public Socket createSocket(String host, int port) throws IOException {
5656

5757
private static Registry rmiRegistry;
5858
private JTextArea textArea;
59-
private ClipboardCommands commands;
59+
private ClipboardCommandsImpl commands;
6060

6161
public ClipboardTest() throws RemoteException {
6262
super("ClipboardTest");
@@ -68,6 +68,7 @@ public ClipboardTest() throws RemoteException {
6868

6969
JButton copyButton = new JButton("Copy");
7070
JButton pasteButton = new JButton("Paste");
71+
JButton pressToContinue = new JButton("Press To Continue Test");
7172

7273
copyButton.addActionListener(e -> {
7374
String text = textArea.getSelectedText();
@@ -88,9 +89,14 @@ public ClipboardTest() throws RemoteException {
8889
}
8990
});
9091

92+
pressToContinue.addActionListener(e -> {
93+
commands.buttonPressed.countDown();
94+
});
95+
9196
JPanel buttonPanel = new JPanel();
9297
buttonPanel.add(copyButton);
9398
buttonPanel.add(pasteButton);
99+
buttonPanel.add(pressToContinue);
94100

95101
add(scrollPane, BorderLayout.CENTER);
96102
add(buttonPanel, BorderLayout.SOUTH);

0 commit comments

Comments
 (0)