From cd53f1a1a69cb7bfebe7f0a848541a39bcbddb89 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Thu, 16 Apr 2026 19:57:27 +0200 Subject: [PATCH 1/3] DEBUG: Repeat ResourceInitialSelectionTest flaky tests with diagnostics Adds @RepeatedTest(50) to the tests reported as flaky in issue #294, plus per-run logging of waitForDialogRefresh stability and table state at assertion time. Linux cannot reproduce the flake locally (200/200 pass under DISPLAY=:1); this branch exists only to trigger CI on macOS/Windows runners via a fork-internal PR so we can capture itemCount, selection, and the time spent in waitForDialogRefresh on a failing run. Will be reverted before any fix is merged. --- .../dialogs/ResourceInitialSelectionTest.java | 52 ++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/dialogs/ResourceInitialSelectionTest.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/dialogs/ResourceInitialSelectionTest.java index a4c6fb2603a..e24b13687a1 100644 --- a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/dialogs/ResourceInitialSelectionTest.java +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/dialogs/ResourceInitialSelectionTest.java @@ -43,6 +43,7 @@ import org.eclipse.ui.tests.harness.util.DisplayHelper; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; /** @@ -73,7 +74,7 @@ public void doSetUp() throws Exception { /** * Test that a resource is selected by default even without initial selection. */ - @Test + @RepeatedTest(50) public void testSingleSelectionAndNoInitialSelectionWithInitialPattern() { boolean hasMultiSelection = false; dialog = createDialog(hasMultiSelection); @@ -93,7 +94,7 @@ public void testSingleSelectionAndNoInitialSelectionWithInitialPattern() { /** * Test that a specific resource can be selected by default. */ - @Test + @RepeatedTest(50) public void testSingleSelectionAndOneInitialSelectionWithInitialPattern() { boolean hasMultiSelection = false; dialog = createDialog(hasMultiSelection); @@ -222,7 +223,7 @@ public void testMultiSelectionAndNoInitialSelectionWithInitialPattern() { * Test that a specified resource can be selected by default when multi * selection is enabled. */ - @Test + @RepeatedTest(50) public void testMultiSelectionAndOneInitialSelectionWithInitialPattern() { boolean hasMultiSelection = true; dialog = createDialog(hasMultiSelection); @@ -309,7 +310,7 @@ public void testMultiSelectionAndSomeInitialNonExistingSelectionWithInitialPatte /** * Test that several specified resources can be selected by default. */ - @Test + @RepeatedTest(50) public void testMultiSelectionAndTwoInitialSelectionsWithInitialPattern() { boolean hasMultiSelection = true; @@ -335,7 +336,7 @@ public void testMultiSelectionAndTwoInitialSelectionsWithInitialPattern() { * Test that several specified resources can be selected by default but are * ignored if the initial pattern does not match. */ - @Test + @RepeatedTest(50) public void testMultiSelectionAndTwoInitialFilteredSelections() { boolean hasMultiSelection = true; @@ -462,7 +463,8 @@ private void waitForDialogRefresh() { // Selection is applied only in the final refresh, so we must wait for // stability rather than just for any non-zero count. int[] lastCount = { -1 }; - DisplayHelper.waitForCondition(display, 5000, () -> { + long startNanos = System.nanoTime(); + boolean stable = DisplayHelper.waitForCondition(display, 5000, () -> { processUIEvents(); try { Table table = (Table) ((Composite) ((Composite) ((Composite) dialog.getShell().getChildren()[0]) @@ -477,9 +479,47 @@ private void waitForDialogRefresh() { return false; } }); + long waitedMs = (System.nanoTime() - startNanos) / 1_000_000L; + System.out.println("[ResourceInitialSelectionTest] waitForDialogRefresh: stable=" + stable + + " lastCount=" + lastCount[0] + " waitedMs=" + waitedMs); // Final event loop processing to pick up any trailing async tasks processUIEvents(); + dumpDialogState("after waitForDialogRefresh"); + } + + /** + * Diagnostic helper: dump table item count, selection, and item labels. + * Used to investigate flake on CI (issue #294). Remove before merge. + */ + private void dumpDialogState(String label) { + try { + Table table = (Table) ((Composite) ((Composite) ((Composite) dialog.getShell().getChildren()[0]) + .getChildren()[0]).getChildren()[0]).getChildren()[3]; + int count = table.getItemCount(); + int[] selIdx = table.getSelectionIndices(); + TableItem[] selItems = table.getSelection(); + StringBuilder sb = new StringBuilder("[ResourceInitialSelectionTest] ").append(label) + .append(": itemCount=").append(count) + .append(" selectionIndices=").append(java.util.Arrays.toString(selIdx)) + .append(" selectionData=["); + for (int i = 0; i < selItems.length; i++) { + if (i > 0) sb.append(", "); + Object data = selItems[i].getData(); + sb.append(data == null ? "null" : data.toString()); + } + sb.append("] firstItems=["); + int dump = Math.min(count, 5); + for (int i = 0; i < dump; i++) { + if (i > 0) sb.append(", "); + TableItem it = table.getItem(i); + sb.append(it == null ? "null" : it.getText()); + } + sb.append("]"); + System.out.println(sb); + } catch (Exception e) { + System.out.println("[ResourceInitialSelectionTest] " + label + " dump failed: " + e); + } } /** From 91ee2e2b1c47c9905f3e161c2ddbadd99e09d279 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Thu, 16 Apr 2026 20:57:51 +0200 Subject: [PATCH 2/3] DEBUG: bump to 500 reps + per-failure dump with test name --- .../dialogs/ResourceInitialSelectionTest.java | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/dialogs/ResourceInitialSelectionTest.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/dialogs/ResourceInitialSelectionTest.java index e24b13687a1..c5615485df8 100644 --- a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/dialogs/ResourceInitialSelectionTest.java +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/dialogs/ResourceInitialSelectionTest.java @@ -45,6 +45,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; /** * Tests that FilteredResourcesSelectionDialog selects its initial selection @@ -64,9 +65,12 @@ public class ResourceInitialSelectionTest { private IProject project; + private String currentTestName; + @BeforeEach - public void doSetUp() throws Exception { + public void doSetUp(TestInfo info) throws Exception { + currentTestName = info.getDisplayName(); FILES.clear(); createProject(); } @@ -74,7 +78,7 @@ public void doSetUp() throws Exception { /** * Test that a resource is selected by default even without initial selection. */ - @RepeatedTest(50) + @RepeatedTest(500) public void testSingleSelectionAndNoInitialSelectionWithInitialPattern() { boolean hasMultiSelection = false; dialog = createDialog(hasMultiSelection); @@ -88,13 +92,13 @@ public void testSingleSelectionAndNoInitialSelectionWithInitialPattern() { List selected = getSelectedItems(dialog); - assertFalse(selected.isEmpty(), "One file should be selected by default"); + assertWithDump(() -> assertFalse(selected.isEmpty(), "One file should be selected by default")); } /** * Test that a specific resource can be selected by default. */ - @RepeatedTest(50) + @RepeatedTest(500) public void testSingleSelectionAndOneInitialSelectionWithInitialPattern() { boolean hasMultiSelection = false; dialog = createDialog(hasMultiSelection); @@ -109,7 +113,7 @@ public void testSingleSelectionAndOneInitialSelectionWithInitialPattern() { List selected = getSelectedItems(dialog); - assertEquals(asList(FILES.get("foo.txt")), selected, "One file should be selected by default"); + assertWithDump(() -> assertEquals(asList(FILES.get("foo.txt")), selected, "One file should be selected by default")); } /** @@ -216,14 +220,14 @@ public void testMultiSelectionAndNoInitialSelectionWithInitialPattern() { List selected = getSelectedItems(dialog); - assertFalse(selected.isEmpty(), "One file should be selected by default"); + assertWithDump(() -> assertFalse(selected.isEmpty(), "One file should be selected by default")); } /** * Test that a specified resource can be selected by default when multi * selection is enabled. */ - @RepeatedTest(50) + @RepeatedTest(500) public void testMultiSelectionAndOneInitialSelectionWithInitialPattern() { boolean hasMultiSelection = true; dialog = createDialog(hasMultiSelection); @@ -238,7 +242,7 @@ public void testMultiSelectionAndOneInitialSelectionWithInitialPattern() { List selected = getSelectedItems(dialog); - assertEquals(asList(FILES.get("foo.txt")), selected, "One file should be selected by default"); + assertWithDump(() -> assertEquals(asList(FILES.get("foo.txt")), selected, "One file should be selected by default")); } /** @@ -310,7 +314,7 @@ public void testMultiSelectionAndSomeInitialNonExistingSelectionWithInitialPatte /** * Test that several specified resources can be selected by default. */ - @RepeatedTest(50) + @RepeatedTest(500) public void testMultiSelectionAndTwoInitialSelectionsWithInitialPattern() { boolean hasMultiSelection = true; @@ -329,14 +333,14 @@ public void testMultiSelectionAndTwoInitialSelectionsWithInitialPattern() { boolean initialElementsAreSelected = selected.containsAll(initialSelection) && initialSelection.containsAll(selected); - assertTrue(initialElementsAreSelected, "Two files should be selected by default"); + assertWithDump(() -> assertTrue(initialElementsAreSelected, "Two files should be selected by default")); } /** * Test that several specified resources can be selected by default but are * ignored if the initial pattern does not match. */ - @RepeatedTest(50) + @RepeatedTest(500) public void testMultiSelectionAndTwoInitialFilteredSelections() { boolean hasMultiSelection = true; @@ -355,7 +359,7 @@ public void testMultiSelectionAndTwoInitialFilteredSelections() { boolean initialElementsAreSelected = selected.containsAll(expectedSelection) && expectedSelection.containsAll(selected); - assertTrue(initialElementsAreSelected, "Two files should be selected by default"); + assertWithDump(() -> assertTrue(initialElementsAreSelected, "Two files should be selected by default")); } private FilteredResourcesSelectionDialog createDialog(boolean multiSelection) { @@ -480,12 +484,28 @@ private void waitForDialogRefresh() { } }); long waitedMs = (System.nanoTime() - startNanos) / 1_000_000L; - System.out.println("[ResourceInitialSelectionTest] waitForDialogRefresh: stable=" + stable - + " lastCount=" + lastCount[0] + " waitedMs=" + waitedMs); + // Only log when something looks suspicious to keep CI logs manageable. + if (!stable || waitedMs > 500 || lastCount[0] <= 0) { + System.out.println("[#294] " + currentTestName + " waitForDialogRefresh: stable=" + stable + + " lastCount=" + lastCount[0] + " waitedMs=" + waitedMs); + dumpDialogState("after waitForDialogRefresh"); + } // Final event loop processing to pick up any trailing async tasks processUIEvents(); - dumpDialogState("after waitForDialogRefresh"); + } + + /** + * Diagnostic helper: assert and dump table state with test name on failure. + */ + private void assertWithDump(Runnable assertion) { + try { + assertion.run(); + } catch (Throwable t) { + System.out.println("[#294] FAIL " + currentTestName + ": " + t.getMessage()); + dumpDialogState("on failure"); + throw t; + } } /** From c600a68a89dbc41d631722b59a8ab22241ed1196 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Fri, 17 Apr 2026 05:29:05 +0200 Subject: [PATCH 3/3] Fix flaky ResourceInitialSelectionTest via pipeline scheduling rule FilteredItemsSelectionDialog's background pipeline has three jobs that read and write the same ContentProvider state: FilterHistoryJob mutates it via contentProvider.reset() and contentProvider.addHistoryItems(), FilterJob populates it via contentProvider.add() (inside fillContentProvider), and RefreshCacheJob reads it in contentProvider.reloadCache(). Without a shared scheduling rule they can run concurrently on different worker threads. Under the right timing, FilterHistoryJob.contentProvider.reset() clears the items set on one worker while FilterJob is iterating members() and adding items on another - wiping some or all of the populated items before the table is rendered. This manifested as the long-standing ResourceInitialSelectionTest flake (issue #294): sometimes the table was empty, sometimes a random subset of items survived in the wrong order. The race is reliably triggered by the ModifyListener on the pattern Text firing applyFilter() twice in quick succession during createDialogArea on slow runners, which schedules two FilterHistoryJobs that overlap with the FilterJob. Fix: share a per-dialog ISchedulingRule across filterHistoryJob, filterJob and refreshCacheJob. They now serialize; the race cannot occur. Separate dialogs still run in parallel. Also expose JOB_FAMILY (@noreference) and tag all four pipeline jobs with it, so tests can deterministically wait for the pipeline via Job.getJobManager().find(JOB_FAMILY). The item-count stability probe in ResourceInitialSelectionTest.waitForDialogRefresh() is replaced with that family-based wait - this also gets rid of the unreliable 5 s timeout that was the visible symptom. Fixes https://github.com/eclipse-platform/eclipse.platform.ui/issues/294 --- .../.settings/.api_filters | 8 + .../dialogs/FilteredItemsSelectionDialog.java | 58 +++++++ .../dialogs/ResourceInitialSelectionTest.java | 141 +++++------------- 3 files changed, 103 insertions(+), 104 deletions(-) diff --git a/bundles/org.eclipse.ui.workbench/.settings/.api_filters b/bundles/org.eclipse.ui.workbench/.settings/.api_filters index 4614ab0685b..d3d918f2683 100644 --- a/bundles/org.eclipse.ui.workbench/.settings/.api_filters +++ b/bundles/org.eclipse.ui.workbench/.settings/.api_filters @@ -1,5 +1,13 @@ + + + + + + + + diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/dialogs/FilteredItemsSelectionDialog.java b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/dialogs/FilteredItemsSelectionDialog.java index 5da18768f29..065b4cdc8e4 100644 --- a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/dialogs/FilteredItemsSelectionDialog.java +++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/dialogs/FilteredItemsSelectionDialog.java @@ -50,6 +50,7 @@ import org.eclipse.core.runtime.ProgressMonitorWrapper; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.core.runtime.jobs.ISchedulingRule; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.ActionContributionItem; @@ -236,6 +237,17 @@ public abstract class FilteredItemsSelectionDialog extends SelectionStatusDialog */ private boolean isShownForTheFirstTime = true; + /** + * Job family under which all background filtering and refresh jobs of this + * dialog are grouped. Tests may use this with + * {@link org.eclipse.core.runtime.jobs.IJobManager#join(Object, org.eclipse.core.runtime.IProgressMonitor)} + * to deterministically wait for the filter/refresh pipeline to settle. + * + * @noreference This field is not intended to be referenced by clients. + * @since 3.138 + */ + public static final Object JOB_FAMILY = new Object(); + /** * Creates a new instance of the class. * @@ -250,10 +262,36 @@ public FilteredItemsSelectionDialog(Shell shell, boolean multi) { filterJob = new FilterJob(); contentProvider = new ContentProvider(); refreshCacheJob = new RefreshCacheJob(); + // All three jobs mutate / read the same ContentProvider state (items, + // duplicates, lastFilteredItems, ...). Without a common scheduling rule + // they can run concurrently on different worker threads, racing + // FilterHistoryJob.contentProvider.reset() against FilterJob's + // fillContentProvider() and silently emptying or corrupting the visible + // items. The rule is per-dialog, so separate dialogs still run in + // parallel. + ISchedulingRule pipelineRule = new ContentProviderSerialRule(); + filterHistoryJob.setRule(pipelineRule); + filterJob.setRule(pipelineRule); + refreshCacheJob.setRule(pipelineRule); itemsListSeparator = new ItemsListSeparator(WorkbenchMessages.FilteredItemsSelectionDialog_separatorLabel); selectionMode = NONE; } + /** + * Per-dialog mutex rule used to serialize the filter/refresh pipeline jobs. + */ + private static final class ContentProviderSerialRule implements ISchedulingRule { + @Override + public boolean contains(ISchedulingRule rule) { + return rule == this; + } + + @Override + public boolean isConflicting(ISchedulingRule rule) { + return rule == this; + } + } + /** * Creates a new instance of the class. Created dialog won't allow to select * more than one item. @@ -1315,6 +1353,11 @@ public IStatus runInUIThread(IProgressMonitor monitor) { return new Status(IStatus.OK, PlatformUI.PLUGIN_ID, IStatus.OK, EMPTY_STRING, null); } + @Override + public boolean belongsTo(Object family) { + return family == JOB_FAMILY; + } + } /** @@ -1420,6 +1463,11 @@ protected void canceling() { contentProvider.stopReloadingCache(); } + @Override + public boolean belongsTo(Object family) { + return family == JOB_FAMILY; + } + } private class RemoveHistoryItemAction extends Action { @@ -1854,6 +1902,11 @@ protected IStatus run(IProgressMonitor monitor) { return Status.OK_STATUS; } + @Override + public boolean belongsTo(Object family) { + return family == JOB_FAMILY; + } + } /** @@ -1977,6 +2030,11 @@ protected void filterContent(GranualProgressMonitor monitor) throws CoreExceptio } + @Override + public boolean belongsTo(Object family) { + return family == JOB_FAMILY; + } + } /** diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/dialogs/ResourceInitialSelectionTest.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/dialogs/ResourceInitialSelectionTest.java index c5615485df8..0dc1be9f588 100644 --- a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/dialogs/ResourceInitialSelectionTest.java +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/dialogs/ResourceInitialSelectionTest.java @@ -38,14 +38,13 @@ import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableItem; import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.dialogs.FilteredItemsSelectionDialog; import org.eclipse.ui.dialogs.FilteredResourcesSelectionDialog; import org.eclipse.ui.internal.decorators.DecoratorManager; import org.eclipse.ui.tests.harness.util.DisplayHelper; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInfo; /** * Tests that FilteredResourcesSelectionDialog selects its initial selection @@ -65,12 +64,9 @@ public class ResourceInitialSelectionTest { private IProject project; - private String currentTestName; - @BeforeEach - public void doSetUp(TestInfo info) throws Exception { - currentTestName = info.getDisplayName(); + public void doSetUp() throws Exception { FILES.clear(); createProject(); } @@ -78,7 +74,7 @@ public void doSetUp(TestInfo info) throws Exception { /** * Test that a resource is selected by default even without initial selection. */ - @RepeatedTest(500) + @Test public void testSingleSelectionAndNoInitialSelectionWithInitialPattern() { boolean hasMultiSelection = false; dialog = createDialog(hasMultiSelection); @@ -92,13 +88,13 @@ public void testSingleSelectionAndNoInitialSelectionWithInitialPattern() { List selected = getSelectedItems(dialog); - assertWithDump(() -> assertFalse(selected.isEmpty(), "One file should be selected by default")); + assertFalse(selected.isEmpty(), "One file should be selected by default"); } /** * Test that a specific resource can be selected by default. */ - @RepeatedTest(500) + @Test public void testSingleSelectionAndOneInitialSelectionWithInitialPattern() { boolean hasMultiSelection = false; dialog = createDialog(hasMultiSelection); @@ -113,7 +109,7 @@ public void testSingleSelectionAndOneInitialSelectionWithInitialPattern() { List selected = getSelectedItems(dialog); - assertWithDump(() -> assertEquals(asList(FILES.get("foo.txt")), selected, "One file should be selected by default")); + assertEquals(asList(FILES.get("foo.txt")), selected, "One file should be selected by default"); } /** @@ -130,8 +126,11 @@ public void testSingleSelectionAndOneInitialNonExistingSelectionWithInitialPatte dialog.open(); dialog.refresh(); - // Don't wait for full refresh - this test checks that invalid initial - // selections don't cause a selection before dialog is fully loaded + // Intentionally no waitForDialogRefresh: this asserts that during + // initial load, the dialog does not pre-select anything for an + // invalid initial element. After the refresh pipeline drains the + // dialog falls back to selecting row 0, which is a separate + // behavior not under test here. List selected = getSelectedItems(dialog); @@ -172,8 +171,10 @@ public void testSingleSelectionAndOneFilteredSelection() { dialog.open(); dialog.refresh(); - // Don't wait for full refresh - this test checks that filtered initial - // selections don't cause a selection before dialog is fully loaded + // Intentionally no waitForDialogRefresh: foofoo is filtered out by + // the *.txt pattern, so during initial load nothing is selected. + // After the refresh pipeline drains the dialog falls back to row 0, + // which is a separate behavior not under test here. List selected = getSelectedItems(dialog); @@ -220,14 +221,14 @@ public void testMultiSelectionAndNoInitialSelectionWithInitialPattern() { List selected = getSelectedItems(dialog); - assertWithDump(() -> assertFalse(selected.isEmpty(), "One file should be selected by default")); + assertFalse(selected.isEmpty(), "One file should be selected by default"); } /** * Test that a specified resource can be selected by default when multi * selection is enabled. */ - @RepeatedTest(500) + @Test public void testMultiSelectionAndOneInitialSelectionWithInitialPattern() { boolean hasMultiSelection = true; dialog = createDialog(hasMultiSelection); @@ -242,7 +243,7 @@ public void testMultiSelectionAndOneInitialSelectionWithInitialPattern() { List selected = getSelectedItems(dialog); - assertWithDump(() -> assertEquals(asList(FILES.get("foo.txt")), selected, "One file should be selected by default")); + assertEquals(asList(FILES.get("foo.txt")), selected, "One file should be selected by default"); } /** @@ -279,8 +280,11 @@ public void testMultiSelectionAndTwoInitialNonExistingSelectionWithInitialPatter dialog.open(); dialog.refresh(); - // Don't wait for full refresh - this test checks that invalid initial - // selections don't cause a selection before dialog is fully loaded + // Intentionally no waitForDialogRefresh: this asserts that during + // initial load, the dialog does not pre-select anything for invalid + // initial elements. After the refresh pipeline drains the dialog + // falls back to selecting row 0, which is a separate behavior not + // under test here. List selected = getSelectedItems(dialog); @@ -314,7 +318,7 @@ public void testMultiSelectionAndSomeInitialNonExistingSelectionWithInitialPatte /** * Test that several specified resources can be selected by default. */ - @RepeatedTest(500) + @Test public void testMultiSelectionAndTwoInitialSelectionsWithInitialPattern() { boolean hasMultiSelection = true; @@ -333,14 +337,14 @@ public void testMultiSelectionAndTwoInitialSelectionsWithInitialPattern() { boolean initialElementsAreSelected = selected.containsAll(initialSelection) && initialSelection.containsAll(selected); - assertWithDump(() -> assertTrue(initialElementsAreSelected, "Two files should be selected by default")); + assertTrue(initialElementsAreSelected, "Two files should be selected by default"); } /** * Test that several specified resources can be selected by default but are * ignored if the initial pattern does not match. */ - @RepeatedTest(500) + @Test public void testMultiSelectionAndTwoInitialFilteredSelections() { boolean hasMultiSelection = true; @@ -359,7 +363,7 @@ public void testMultiSelectionAndTwoInitialFilteredSelections() { boolean initialElementsAreSelected = selected.containsAll(expectedSelection) && expectedSelection.containsAll(selected); - assertWithDump(() -> assertTrue(initialElementsAreSelected, "Two files should be selected by default")); + assertTrue(initialElementsAreSelected, "Two files should be selected by default"); } private FilteredResourcesSelectionDialog createDialog(boolean multiSelection) { @@ -453,95 +457,24 @@ private void processUIEvents() { } /** - * Wait for dialog refresh jobs to complete and process UI events. - * This ensures background jobs finish before assertions are made. + * Wait for the dialog's background filter/refresh jobs to complete. + *

+ * The dialog schedules a chain of jobs on open/refresh: + * {@code FilterHistoryJob → FilterJob → RefreshCacheJob → RefreshJob}. All + * four are tagged with {@link FilteredItemsSelectionDialog#JOB_FAMILY}, so + * we wait for that family to drain. The 30 s ceiling is a deadlock guard; + * the pipeline usually completes within a few hundred milliseconds. */ private void waitForDialogRefresh() { Display display = PlatformUI.getWorkbench().getDisplay(); - - // The dialog performs async operations (FilterHistoryJob → FilterJob → - // RefreshCacheJob → RefreshJob) to filter and populate the table after refresh() - // We need to wait for the table item count to stabilize before checking - // selection state. The count can temporarily be non-zero after the history - // refresh, then change again when FilterJob populates actual results. - // Selection is applied only in the final refresh, so we must wait for - // stability rather than just for any non-zero count. - int[] lastCount = { -1 }; - long startNanos = System.nanoTime(); - boolean stable = DisplayHelper.waitForCondition(display, 5000, () -> { + DisplayHelper.waitForCondition(display, 30_000L, () -> { processUIEvents(); - try { - Table table = (Table) ((Composite) ((Composite) ((Composite) dialog.getShell().getChildren()[0]) - .getChildren()[0]).getChildren()[0]).getChildren()[3]; - int count = table.getItemCount(); - if (count > 0 && count == lastCount[0]) { - return true; // stable non-zero count: all refreshes have completed - } - lastCount[0] = count; - return false; - } catch (Exception e) { - return false; - } + return Job.getJobManager().find(FilteredItemsSelectionDialog.JOB_FAMILY).length == 0; }); - long waitedMs = (System.nanoTime() - startNanos) / 1_000_000L; - // Only log when something looks suspicious to keep CI logs manageable. - if (!stable || waitedMs > 500 || lastCount[0] <= 0) { - System.out.println("[#294] " + currentTestName + " waitForDialogRefresh: stable=" + stable - + " lastCount=" + lastCount[0] + " waitedMs=" + waitedMs); - dumpDialogState("after waitForDialogRefresh"); - } - - // Final event loop processing to pick up any trailing async tasks + // Final event loop processing to pick up any trailing asyncExecs. processUIEvents(); } - /** - * Diagnostic helper: assert and dump table state with test name on failure. - */ - private void assertWithDump(Runnable assertion) { - try { - assertion.run(); - } catch (Throwable t) { - System.out.println("[#294] FAIL " + currentTestName + ": " + t.getMessage()); - dumpDialogState("on failure"); - throw t; - } - } - - /** - * Diagnostic helper: dump table item count, selection, and item labels. - * Used to investigate flake on CI (issue #294). Remove before merge. - */ - private void dumpDialogState(String label) { - try { - Table table = (Table) ((Composite) ((Composite) ((Composite) dialog.getShell().getChildren()[0]) - .getChildren()[0]).getChildren()[0]).getChildren()[3]; - int count = table.getItemCount(); - int[] selIdx = table.getSelectionIndices(); - TableItem[] selItems = table.getSelection(); - StringBuilder sb = new StringBuilder("[ResourceInitialSelectionTest] ").append(label) - .append(": itemCount=").append(count) - .append(" selectionIndices=").append(java.util.Arrays.toString(selIdx)) - .append(" selectionData=["); - for (int i = 0; i < selItems.length; i++) { - if (i > 0) sb.append(", "); - Object data = selItems[i].getData(); - sb.append(data == null ? "null" : data.toString()); - } - sb.append("] firstItems=["); - int dump = Math.min(count, 5); - for (int i = 0; i < dump; i++) { - if (i > 0) sb.append(", "); - TableItem it = table.getItem(i); - sb.append(it == null ? "null" : it.getText()); - } - sb.append("]"); - System.out.println(sb); - } catch (Exception e) { - System.out.println("[ResourceInitialSelectionTest] " + label + " dump failed: " + e); - } - } - /** * Delete project with retry mechanism to handle cases where background jobs * are still using the project resources.