Skip to content

Commit 2a31ddf

Browse files
committed
Fix race condition in RCPTestWorkbenchAdvisor test harness
Introduce a Phaser to ensure async/sync operations are scheduled before postStartup marks the workbench as started. This prevents timing issues where test variables (asyncWithDisplayAccess, syncWithDisplayAccess, etc.) might not be set correctly due to the workbench being marked as started before async runnables execute. Changes: - Add Phaser to coordinate async/sync thread operations - Register/deregister threads when scheduling display operations - Wait for all operations to be scheduled in postStartup (with 5s timeout) - Process event loop to ensure async runnables execute before marking started - Simplify Boolean assignments (remove unnecessary ternary operators) This ensures deterministic behavior in test harness initialization and prevents flaky test failures due to race conditions.
1 parent 9c171a0 commit 2a31ddf

File tree

1 file changed

+57
-17
lines changed

1 file changed

+57
-17
lines changed

tests/org.eclipse.ui.tests.harness/src/org/eclipse/ui/tests/harness/util/RCPTestWorkbenchAdvisor.java

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
*******************************************************************************/
1414
package org.eclipse.ui.tests.harness.util;
1515

16+
import java.util.concurrent.Phaser;
17+
import java.util.concurrent.TimeUnit;
18+
import java.util.concurrent.TimeoutException;
19+
1620
import org.eclipse.jface.preference.IPreferenceStore;
1721
import org.eclipse.swt.SWTException;
1822
import org.eclipse.swt.widgets.Display;
@@ -44,6 +48,10 @@ public class RCPTestWorkbenchAdvisor extends WorkbenchAdvisor {
4448

4549
private static boolean started = false;
4650

51+
// Use a Phaser to ensure async operations are scheduled before postStartup
52+
// Each thread registers itself and arrives when the operation is scheduled
53+
private static final Phaser asyncPhaser = new Phaser(1); // 1 for the main thread
54+
4755
public static boolean isSTARTED() {
4856
synchronized (RCPTestWorkbenchAdvisor.class) {
4957
return started;
@@ -122,12 +130,10 @@ public void eventLoopIdle(final Display display) {
122130
public void preStartup() {
123131
super.preStartup();
124132
final Display display = Display.getCurrent();
133+
125134
if (display != null) {
126135
display.asyncExec(() -> {
127-
if (isSTARTED())
128-
asyncDuringStartup = Boolean.FALSE;
129-
else
130-
asyncDuringStartup = Boolean.TRUE;
136+
asyncDuringStartup = !isSTARTED();
131137
});
132138
}
133139

@@ -151,24 +157,28 @@ public void preStartup() {
151157
}
152158

153159
private void setupSyncDisplayThread(final boolean callDisplayAccess, final Display display) {
160+
// Register this thread with the phaser
161+
asyncPhaser.register();
154162
Thread syncThread = new Thread() {
155163
@Override
156164
public void run() {
157165
if (callDisplayAccess)
158166
DisplayAccess.accessDisplayDuringStartup();
159167
try {
160168
display.syncExec(() -> {
161-
synchronized (RCPTestWorkbenchAdvisor.class) {
162-
if (callDisplayAccess)
163-
syncWithDisplayAccess = !isSTARTED() ? Boolean.TRUE : Boolean.FALSE;
164-
else
165-
syncWithoutDisplayAccess = !isSTARTED() ? Boolean.TRUE : Boolean.FALSE;
169+
if (callDisplayAccess) {
170+
syncWithDisplayAccess = !isSTARTED();
171+
} else {
172+
syncWithoutDisplayAccess = !isSTARTED();
166173
}
167174
});
168175
} catch (SWTException e) {
169176
// this can happen because we shut down the workbench just
170177
// as soon as we're initialized - ie: when we're trying to
171178
// run this runnable in the deferred case.
179+
} finally {
180+
// Signal that this operation has completed
181+
asyncPhaser.arriveAndDeregister();
172182
}
173183
}
174184
};
@@ -177,19 +187,26 @@ public void run() {
177187
}
178188

179189
private void setupAsyncDisplayThread(final boolean callDisplayAccess, final Display display) {
190+
// Register this thread with the phaser
191+
asyncPhaser.register();
180192
Thread asyncThread = new Thread() {
181193
@Override
182194
public void run() {
183195
if (callDisplayAccess)
184196
DisplayAccess.accessDisplayDuringStartup();
185-
display.asyncExec(() -> {
186-
synchronized (RCPTestWorkbenchAdvisor.class) {
187-
if (callDisplayAccess)
188-
asyncWithDisplayAccess = !isSTARTED() ? Boolean.TRUE : Boolean.FALSE;
189-
else
190-
asyncWithoutDisplayAccess = !isSTARTED() ? Boolean.TRUE : Boolean.FALSE;
191-
}
192-
});
197+
try {
198+
display.asyncExec(() -> {
199+
if (callDisplayAccess) {
200+
asyncWithDisplayAccess = !isSTARTED();
201+
} else {
202+
asyncWithoutDisplayAccess = !isSTARTED();
203+
}
204+
});
205+
} finally {
206+
// Signal that this operation has been scheduled (not necessarily executed yet)
207+
// This avoids deadlock since we're not waiting for execution on the UI thread
208+
asyncPhaser.arriveAndDeregister();
209+
}
193210
}
194211
};
195212
asyncThread.setDaemon(true);
@@ -199,6 +216,29 @@ public void run() {
199216
@Override
200217
public void postStartup() {
201218
super.postStartup();
219+
220+
// Wait for all async/sync operations to be scheduled (not blocking UI thread)
221+
try {
222+
// Wait up to 5 seconds for all operations to be scheduled
223+
// The main thread arrives and deregisters, waiting for all other registered threads
224+
asyncPhaser.awaitAdvanceInterruptibly(asyncPhaser.arrive(), 5, TimeUnit.SECONDS);
225+
} catch (TimeoutException e) {
226+
// Log warning but don't throw - we need to mark as started to avoid breaking subsequent tests
227+
System.err.println("WARNING: Not all async/sync operations were scheduled within timeout");
228+
e.printStackTrace();
229+
} catch (InterruptedException e) {
230+
Thread.currentThread().interrupt();
231+
System.err.println("WARNING: Interrupted while waiting for async/sync operations");
232+
}
233+
234+
// Pump the event loop to ensure async runnables execute before marking as started
235+
// This prevents the original race condition where async variables might not be set yet
236+
// Wait until the variables that should be set during startup are actually set to TRUE
237+
UITestUtil.processEventsUntil(() -> Boolean.TRUE.equals(syncWithDisplayAccess) && Boolean.TRUE.equals(asyncWithDisplayAccess), 5000);
238+
// Process any remaining events to allow variables that should NOT be set during startup
239+
// to accidentally execute (to detect regression)
240+
UITestUtil.processEvents();
241+
202242
synchronized (RCPTestWorkbenchAdvisor.class) {
203243
started = true;
204244
}

0 commit comments

Comments
 (0)