diff --git a/com.avaloq.tools.ddk.check.ui.test/src/com/avaloq/tools/ddk/check/ui/test/quickfix/CheckQuickfixTest.java b/com.avaloq.tools.ddk.check.ui.test/src/com/avaloq/tools/ddk/check/ui/test/quickfix/CheckQuickfixTest.java index 25778498e..9ec0fbe20 100644 --- a/com.avaloq.tools.ddk.check.ui.test/src/com/avaloq/tools/ddk/check/ui/test/quickfix/CheckQuickfixTest.java +++ b/com.avaloq.tools.ddk.check.ui.test/src/com/avaloq/tools/ddk/check/ui/test/quickfix/CheckQuickfixTest.java @@ -10,22 +10,30 @@ *******************************************************************************/ package com.avaloq.tools.ddk.check.ui.test.quickfix; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Predicate; +import java.util.stream.Collectors; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.emf.common.util.WrappedException; +import org.eclipse.swtbot.swt.finder.utils.SWTBotPreferences; import org.eclipse.swtbot.swt.finder.widgets.SWTBotTree; import org.eclipse.swtbot.swt.finder.widgets.SWTBotTreeItem; -import org.eclipse.swtbot.swt.finder.widgets.TimeoutException; import org.eclipse.xtext.diagnostics.Diagnostic; import org.eclipse.xtext.validation.Issue; import org.junit.jupiter.api.Test; -import com.avaloq.tools.ddk.check.CheckConstants; import com.avaloq.tools.ddk.check.ui.quickfix.Messages; import com.avaloq.tools.ddk.check.validation.IssueCodes; import com.avaloq.tools.ddk.test.core.jupiter.BugTest; @@ -44,9 +52,10 @@ public class CheckQuickfixTest extends AbstractCheckQuickfixTest { private static final String PACKAGE_NAME = "com.avaloq.test"; + private static final String RESOURCE_COLUMN_NAME = "Resource"; private final SwtWorkbenchBot bot = new SwtWorkbenchBot(); - private boolean oldAutoBuildState; + private final AtomicBoolean oldAutoBuildState = new AtomicBoolean(); public String getTestSourceFileName(final String catalogName) { return PACKAGE_NAME.replace(".", "/") + '/' + catalogName + '.' + getXtextTestUtil().getFileExtension(); @@ -69,13 +78,13 @@ protected XtextTestSource getTestSource() { @Override protected void beforeEachTest() { super.beforeEachTest(); - oldAutoBuildState = getTestProjectManager().setAutobuild(true); + oldAutoBuildState.set(getTestProjectManager().setAutobuild(true)); cleanUp(); } @Override protected void afterEachTest() { - getTestProjectManager().setAutobuild(oldAutoBuildState); + getTestProjectManager().setAutobuild(oldAutoBuildState.get()); cleanUp(); super.afterEachTest(); } @@ -170,9 +179,11 @@ public void testBulkApplyingQuickfix() { ProblemsViewTestUtil.showAllErrors(bot); ProblemsViewTestUtil.groupByNone(bot); + // Disable autobuilding so source creation cannot trigger a build before the explicit incremental build below. + getTestProjectManager().setAutobuild(false); + // Add catalogs containing multiple instances of the same quickfixable marker - for (final String catalogName : catalogNames) { - createTestSource(getTestSourceFileName(catalogName), """ + final List catalogSources = catalogNames.stream().map(catalogName -> createTestSource(getTestSourceFileName(catalogName), """ package %s catalog %s @@ -186,23 +197,22 @@ public void testBulkApplyingQuickfix() { message "%s" { } } - """.formatted(PACKAGE_NAME, catalogName, label1, label1, label2, label2)); - } + """.formatted(PACKAGE_NAME, catalogName, label1, label1, label2, label2))).toList(); // Build the catalogs, and wait for the expected markers to appear getTestProjectManager().build(); + waitForWorkspaceMarkers(catalogSources, expectedMarkers); final SWTBotTree markersTreeBot = ProblemsViewTestUtil.getMarkersTree(bot); - Predicate checkResourceFilter = i -> i.row().get(1).endsWith('.' + CheckConstants.FILE_EXTENSION); - bot.waitUntil(new WaitForEquals<>("Not all expected markers appeared.", () -> expectedMarkers, () -> Arrays.stream(markersTreeBot.getAllItems()).filter(checkResourceFilter).toList().size())); + final Set sourceFileNames = catalogSources.stream().map(source -> source.getiFile().getName()).collect(Collectors.toSet()); + final int resourceColumnIndex = markersTreeBot.columns().indexOf(RESOURCE_COLUMN_NAME); + assertTrue(resourceColumnIndex >= 0, "Problems view has no Resource column."); + Predicate checkResourceFilter = item -> sourceFileNames.contains(item.cell(resourceColumnIndex)); + final SWTBotTreeItem[] checkMarkers = waitForProblemsViewMarkers(markersTreeBot, checkResourceFilter, expectedMarkers, "Not all expected markers appeared in the Problems view."); // ACT - // Disable autobuilding, to avoid losing focus while selecting markers - getTestProjectManager().setAutobuild(false); - getTestProjectManager().build(); - // Bulk-apply quickfixes on all markers, ensuring that all markers remain selected - ProblemsViewTestUtil.bulkApplyQuickfix(bot, Messages.CheckQuickfixProvider_ADD_ID_LABEL, Arrays.stream(markersTreeBot.getAllItems()).filter(checkResourceFilter).toArray(SWTBotTreeItem[]::new)); - bot.waitUntil(new WaitForEquals<>("Not all markers are still selected.", () -> expectedMarkers, () -> markersTreeBot.selectionCount())); + ProblemsViewTestUtil.bulkApplyQuickfix(bot, Messages.CheckQuickfixProvider_ADD_ID_LABEL, checkMarkers); + bot.waitUntil(new WaitForEquals<>("Not all markers are still selected.", () -> expectedMarkers, () -> markersTreeBot.selectionCount()), ProblemsViewTestUtil.ASYNC_UPDATE_TIMEOUT); // Save all modified files and build the catalogs bot.editors().forEach(editor -> editor.save()); @@ -211,11 +221,56 @@ public void testBulkApplyingQuickfix() { // ASSERT // Check that all markers are fixed + waitForWorkspaceMarkers(catalogSources, 0); + waitForProblemsViewMarkers(markersTreeBot, checkResourceFilter, 0, "Some markers were not removed from the Problems view."); + } + + /** + * Waits until the exact expected number of relevant workspace markers exists. + */ + private void waitForWorkspaceMarkers(final List catalogSources, final int expectedMarkers) { + final long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(ProblemsViewTestUtil.ASYNC_UPDATE_TIMEOUT); + int markerCount; + do { + markerCount = countWorkspaceMarkers(catalogSources); + if (markerCount == expectedMarkers) { + return; + } + bot.sleep(SWTBotPreferences.DEFAULT_POLL_DELAY); + } while (System.nanoTime() < deadline); + assertEquals(expectedMarkers, markerCount, "Workspace marker count did not reach the expected value."); + } + + /** + * Counts the relevant problem markers attached directly to the given source files. + */ + private int countWorkspaceMarkers(final List catalogSources) { + int markerCount = 0; try { - bot.waitUntil(new WaitForEquals<>("Some markers were not quickfixed.", () -> 0, () -> Arrays.stream(markersTreeBot.getAllItems()).filter(checkResourceFilter).toList().size())); - } catch (TimeoutException exception) { - fail(exception.getMessage()); + for (XtextTestSource source : catalogSources) { + for (IMarker marker : source.getiFile().findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_ZERO)) { + if (IssueCodes.MISSING_ID_ON_CHECK.equals(marker.getAttribute(Issue.CODE_KEY))) { + markerCount++; + } + } + } + return markerCount; + } catch (CoreException e) { + throw new WrappedException("Could not read workspace markers.", e); } } + + /** + * Waits for, captures, and returns the exact expected set of Problems view marker items. + */ + private SWTBotTreeItem[] waitForProblemsViewMarkers(final SWTBotTree markersTreeBot, final Predicate markerFilter, final int expectedMarkers, final String failureMessage) { + final AtomicReference matchingMarkers = new AtomicReference<>(); + bot.waitUntil(new WaitForEquals<>(failureMessage, () -> expectedMarkers, () -> { + final SWTBotTreeItem[] items = Arrays.stream(markersTreeBot.getAllItems()).filter(markerFilter).toArray(SWTBotTreeItem[]::new); + matchingMarkers.set(items); + return items.length; + }), ProblemsViewTestUtil.ASYNC_UPDATE_TIMEOUT); + return matchingMarkers.get(); + } } -// CHECKSTYLE:CONSTANTS-ON \ No newline at end of file +// CHECKSTYLE:CONSTANTS-ON diff --git a/com.avaloq.tools.ddk.test.ui/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.test.ui/META-INF/MANIFEST.MF index 182b880a7..51a399401 100644 --- a/com.avaloq.tools.ddk.test.ui/META-INF/MANIFEST.MF +++ b/com.avaloq.tools.ddk.test.ui/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: com.avaloq.tools.ddk.test.ui Bundle-SymbolicName: com.avaloq.tools.ddk.test.ui;singleton:=true -Bundle-Version: 17.3.2.qualifier +Bundle-Version: 17.3.3.qualifier Bundle-Vendor: Avaloq Group AG Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-Activator: com.avaloq.tools.ddk.test.ui.Activator diff --git a/com.avaloq.tools.ddk.test.ui/pom.xml b/com.avaloq.tools.ddk.test.ui/pom.xml index 4cf42f03d..64119659b 100644 --- a/com.avaloq.tools.ddk.test.ui/pom.xml +++ b/com.avaloq.tools.ddk.test.ui/pom.xml @@ -6,8 +6,8 @@ 18.0.1-SNAPSHOT ../ddk-parent - 17.3.2-SNAPSHOT + 17.3.3-SNAPSHOT com.avaloq.tools.ddk.test.ui eclipse-plugin - \ No newline at end of file + diff --git a/com.avaloq.tools.ddk.test.ui/src/com/avaloq/tools/ddk/test/ui/swtbot/util/ProblemsViewTestUtil.java b/com.avaloq.tools.ddk.test.ui/src/com/avaloq/tools/ddk/test/ui/swtbot/util/ProblemsViewTestUtil.java index a526f3af7..724cc16d0 100644 --- a/com.avaloq.tools.ddk.test.ui/src/com/avaloq/tools/ddk/test/ui/swtbot/util/ProblemsViewTestUtil.java +++ b/com.avaloq.tools.ddk.test.ui/src/com/avaloq/tools/ddk/test/ui/swtbot/util/ProblemsViewTestUtil.java @@ -11,13 +11,19 @@ package com.avaloq.tools.ddk.test.ui.swtbot.util; +import static org.eclipse.swtbot.swt.finder.matchers.WidgetMatcherFactory.widgetOfType; +import static org.eclipse.swtbot.swt.finder.matchers.WidgetMatcherFactory.withLabel; import static org.eclipse.ui.IPageLayout.ID_PROBLEM_VIEW; +import static org.hamcrest.Matchers.allOf; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.eclipse.swt.widgets.Table; import org.eclipse.swtbot.eclipse.finder.waits.Conditions; +import org.eclipse.swtbot.swt.finder.SWTBot; +import org.eclipse.swtbot.swt.finder.waits.WaitForObjectCondition; import org.eclipse.swtbot.swt.finder.widgets.SWTBotShell; import org.eclipse.swtbot.swt.finder.widgets.SWTBotTable; import org.eclipse.swtbot.swt.finder.widgets.SWTBotTableItem; @@ -33,6 +39,9 @@ * Utility class with handy methods for testing Problems view. */ public final class ProblemsViewTestUtil { + /** Maximum time to wait for asynchronous Problems view and Quick Fix dialog updates. */ + public static final long ASYNC_UPDATE_TIMEOUT = 30000; + public static final String GROUP_BY = "&Group By"; //$NON-NLS-1$ public static final String NONE = "&None"; //$NON-NLS-1$ public static final String SHOW = "&Show"; //$NON-NLS-1$ @@ -139,27 +148,43 @@ public static void bulkApplyQuickfix(final SwtWorkbenchBot bot, final String qui // Open the Quick Fix dialog final SWTBotTree markersTreeBot = getMarkersTree(bot); DynamicContextActionUiTestUtil.clickContextMenu(() -> { - // Select markers. Keep retrying if focus is lost at an inopportune moment. - do { + // Select markers. Keep retrying for a bounded time if focus is lost at an inopportune moment. + bot.waitUntil(new WaitForEquals<>("Could not select all markers in the Problems view.", () -> markers.length, () -> { markersTreeBot.select(markers); - } while (markersTreeBot.selectionCount() != markers.length); + return markersTreeBot.selectionCount(); + }), ASYNC_UPDATE_TIMEOUT); }, markersTreeBot, DynamicMenuPredicate.ALWAYS_WAITING, QUICK_FIX_CONTEXT_MENU_ITEM_LABEL); - bot.waitUntil(Conditions.shellIsActive(QUICK_FIX_DIALOG_TEXT)); + bot.waitUntil(Conditions.shellIsActive(QUICK_FIX_DIALOG_TEXT), ASYNC_UPDATE_TIMEOUT); final SWTBotShell quickFixShell = bot.shell(QUICK_FIX_DIALOG_TEXT); + final SWTBot quickFixBot = quickFixShell.bot(); // Select the quickfix - bot.tableWithLabel(SELECT_A_FIX_TABLE_LABEL).select(quickfixLabel); + final SWTBotTable selectFixTable = waitForTable(bot, quickFixShell, SELECT_A_FIX_TABLE_LABEL); + bot.waitUntil(new WaitForEquals<>("Quick Fix dialog did not list the requested fix.", () -> true, () -> selectFixTable.containsItem(quickfixLabel)), ASYNC_UPDATE_TIMEOUT); //$NON-NLS-1$ + selectFixTable.select(quickfixLabel); // Selecting the fix repopulates the Problems table asynchronously; tick every expected row and retry until all are // checked, tolerating the transient table resizes that happen while the table is still being repopulated. final int locationColumnIndex = markersTreeBot.columns().indexOf(LOCATION_COLUMN_NAME); + if (locationColumnIndex < 0) { + throw new IllegalStateException("Problems view has no Location column."); //$NON-NLS-1$ + } final Set markerLocations = Stream.of(markers).map(marker -> marker.cell(locationColumnIndex)).collect(Collectors.toSet()); - final SWTBotTable tableBot = bot.tableWithLabel(PROBLEMS_TABLE_LABEL); - bot.waitUntil(new WaitForEquals<>("Quick Fix dialog did not list all expected markers.", () -> markers.length, () -> checkMatchingRows(tableBot, markerLocations))); //$NON-NLS-1$ + final SWTBotTable tableBot = waitForTable(bot, quickFixShell, PROBLEMS_TABLE_LABEL); + bot.waitUntil(new WaitForEquals<>("Quick Fix dialog did not list all expected markers.", () -> markers.length, () -> checkMatchingRows(tableBot, markerLocations)), ASYNC_UPDATE_TIMEOUT); //$NON-NLS-1$ - bot.clickButton(FINISH_BUTTON_LABEL); + bot.clickButton(quickFixBot.button(FINISH_BUTTON_LABEL), ASYNC_UPDATE_TIMEOUT); // Block until the wizard has actually closed, so callers do not save/build while the resolution is still being applied. - bot.waitUntil(Conditions.shellCloses(quickFixShell)); + bot.waitUntil(Conditions.shellCloses(quickFixShell), ASYNC_UPDATE_TIMEOUT); + } + + /** + * Waits for the table with the given label inside the given shell. + */ + private static SWTBotTable waitForTable(final SwtWorkbenchBot bot, final SWTBotShell shell, final String label) { + final WaitForObjectCondition tableAppears = Conditions.waitForWidget(allOf(widgetOfType(Table.class), withLabel(label)), shell.widget); + bot.waitUntil(tableAppears, ASYNC_UPDATE_TIMEOUT); + return new SWTBotTable(tableAppears.get(0)); } /*