From 597e309e4454f912a042ae03df925e8eca0377b7 Mon Sep 17 00:00:00 2001 From: Karl Lum Date: Tue, 28 Apr 2026 19:06:17 -0700 Subject: [PATCH 1/5] Regression for issue #936 (#2944) #### Rationale Regression test for https://github.com/LabKey/internal-issues/issues/936 --- src/org/labkey/test/tests/CustomizeViewTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/org/labkey/test/tests/CustomizeViewTest.java b/src/org/labkey/test/tests/CustomizeViewTest.java index fb4384de1d..db679bfe3b 100644 --- a/src/org/labkey/test/tests/CustomizeViewTest.java +++ b/src/org/labkey/test/tests/CustomizeViewTest.java @@ -303,7 +303,7 @@ public void saveFilterTest() FieldKey fieldKey = FieldKey.fromParts(LAST_NAME_COLUMN); String op = "Starts With"; String value = "J"; - String[] viewNames = {TRICKY_CHARACTERS + "view", "AAC", "aaa", "aad", "zzz"}; + String[] viewNames = {TRICKY_CHARACTERS + "view", "AAC", "aaa", "aad", "zzz", "view,with,comma"}; setColumns(LAST_NAME_COLUMN); for(String name : viewNames) @@ -311,6 +311,10 @@ public void saveFilterTest() _customizeViewsHelper.openCustomizeViewPanel(); _customizeViewsHelper.addFilter(fieldKey, op, value); _customizeViewsHelper.saveCustomView(name); + _customizeViewsHelper.openCustomizeViewPanel(); + // GitHub Issue #936 : ensure custom view can be edited + assertElementNotPresent(Locator.tagWithClass("div", "alert-warning").withText(String.format("Custom Grid View '%s' not found.", name))); + _customizeViewsHelper.closePanel(); } DataRegionTable drt = new DataRegionTable("query", getDriver()); From 0134ce283167ef65aa07b4bbfd43f44bfcb49c88 Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Mon, 4 May 2026 14:48:04 -0700 Subject: [PATCH 2/5] Fix test grouping on TeamCity for Gradle 9.3 (#2973) - Fix test grouping on TeamCity for Gradle 9.3 (`Runner.flattenSuiteInto`) - Fix test logging to look nice with new test grouping - Remove workaround for https://github.com/gradle/gradle/issues/36451 --- build.gradle | 3 - src/org/labkey/test/Runner.java | 119 ++++++++++++++++------- src/org/labkey/test/tests/JUnitTest.java | 35 ++++--- 3 files changed, 103 insertions(+), 54 deletions(-) diff --git a/build.gradle b/build.gradle index aa002ab263..1cd4499b30 100644 --- a/build.gradle +++ b/build.gradle @@ -182,9 +182,6 @@ project.tasks.named("uiTests").configure { project.parent.parent.tasks.named("ijConfigure").configure { dependsOn(initPropertiesTask) } -project.tasks.withType(RunTestSuite).configureEach { it -> - scanForTestClasses = true // Required for Gradle 9.3 -} if (project.hasProperty('doPublishing')) { diff --git a/src/org/labkey/test/Runner.java b/src/org/labkey/test/Runner.java index bacc2d7a6b..39826754ba 100644 --- a/src/org/labkey/test/Runner.java +++ b/src/org/labkey/test/Runner.java @@ -129,21 +129,24 @@ private void updateRemainingTests(Test test, boolean failed, boolean errored) Class testClass = getTestClass(test); _remainingTests.remove(testClass); if (failed) - _failedTests.add(test.toString()); + _failedTests.add(getTestName(test)); else if (errored) - _erroredTests.add(test.toString()); + _erroredTests.add(getTestName(test)); else - _passedTests.add(test.toString()); + _passedTests.add(getTestName(test)); } private static void writeRemainingTests() { - ArrayList failedAndRemaining = new ArrayList<>(); - failedAndRemaining.addAll(_failedTests); - failedAndRemaining.addAll(_erroredTests); - for (Class clazz : _remainingTests) - failedAndRemaining.add(clazz.getName()); - writeClasses(failedAndRemaining, getRemainingTestsFile()); + if (!TestProperties.isTestRunningOnTeamCity()) // Not useful on TeamCity + { + ArrayList failedAndRemaining = new ArrayList<>(); + failedAndRemaining.addAll(_failedTests); + failedAndRemaining.addAll(_erroredTests); + for (Class clazz : _remainingTests) + failedAndRemaining.add(clazz.getName()); + writeClasses(failedAndRemaining, getRemainingTestsFile()); + } } private static void writeClasses(List tests, File file) @@ -203,7 +206,7 @@ private static Class[] readClasses(File recentlyFailedTestsFile, List currentTestClass = getTestClass(test); - final String currentTestName = currentTestClass.getSimpleName(); + final String currentTestName = getTestName(test); TestListener classFailListener = new TestListener() { @@ -298,13 +300,16 @@ public void endTest(Test _test) { } }; testResult.addListener(classFailListener); - logToServer("=== Starting " + currentTestName + getProgress() + " ==="); - LOG.info("=============== Starting " + currentTestName + getProgress() + " ================="); + if (!(test instanceof JUnitTest.RemoteTest)) + { + logToServer("=== Starting " + currentTestName + getProgress() + " ==="); + LOG.info("=============== Starting " + currentTestName + getProgress() + " ================="); + } // This stub matches the failure generated by JUnit when it fails during static setup/teardown (e.g. @BeforeClass) // Without this TeamCity has no way of knowing when a setup/teardown failure has been resolved - final Test loggingStub = test instanceof JUnit4TestAdapter ? - new TestSuite(currentTestClass) : + final Test loggingStub = test instanceof JUnit4TestAdapter jUnit4TestAdapter ? + new TestSuite(jUnit4TestAdapter.getTestClass()) : null; if (loggingStub != null) @@ -317,10 +322,13 @@ public void endTest(Test _test) { } testResult.removeListener(classFailListener); - String result = failed.booleanValue() || errored.booleanValue() ? "Failed " : "Completed "; - TestLogger.resetLogger(); - LOG.info("=============== " + result + currentTestName + getProgress() + " ================="); - logToServer("=== " + result + currentTestName + getProgress() + " ==="); + if (!(test instanceof JUnitTest.RemoteTest)) + { + String result = failed.booleanValue() || errored.booleanValue() ? "Failed " : "Completed "; + TestLogger.resetLogger(); + LOG.info("=============== " + result + currentTestName + getProgress() + " ================="); + logToServer("=== " + result + currentTestName + getProgress() + " ==="); + } } else @@ -547,7 +555,7 @@ public String describe() } else if (test.countTestCases() > 0) { - suite.addTest(test); + flattenSuiteInto(suite, (TestSuite) test); foundServerSideTest = true; } } @@ -570,7 +578,7 @@ else if (specifiedSuite.startsWith("?") && specifiedSuite.length() > 1) } TestSuite dynamicSuite = JUnitTest.dynamicSuite(requestedSuites, excludedSuites); if (dynamicSuite.countTestCases() > 0) - suite.addTest(dynamicSuite); + flattenSuiteInto(suite, dynamicSuite); } } @@ -603,9 +611,9 @@ public void run(TestResult testResult) private static void writeTimeReport() { - int width = 60; + int width = 64; long total = 0; - LOG.info("======================= Time Report ========================"); + LOG.info(getCenteredText("Time Report", '=', width)); Duration totalCrawlTime = Duration.ZERO; int totalUniquePages = 0; @@ -619,7 +627,7 @@ private static void writeTimeReport() } for (Map.Entry entry : _testStats.entrySet()) { - String testName = entry.getKey().toString(); + String testName = getTestName(entry.getKey()); long duration = entry.getValue(); long percent = Math.round(100.0 * (duration / (double) total)); @@ -630,7 +638,6 @@ private static void writeTimeReport() (_erroredTests.contains(testName) ? "ERROR" : "not run"))) + " - " + formatDuration(duration) + " " + percentStr; - testName = testName.substring(testName.lastIndexOf('.') + 1); LOG.info(getFixedWidthString(testName, durationAndPercent, width)); @@ -686,7 +693,7 @@ private static void writeTimeReport() } if (!TeamCityUtils.getBuildStatistics().isEmpty()) { - LOG.info("--------------------- Build Statistics ---------------------"); + LOG.info(getCenteredText("Build Statistics", '-', width)); for (String stat : TeamCityUtils.getBuildStatistics().keySet()) { List values = TeamCityUtils.getBuildStatistics().get(stat); @@ -702,7 +709,7 @@ private static void writeTimeReport() Map> actionWarnings = WebDriverWrapper.getActionWarnings(); if (!actionWarnings.isEmpty()) { - LOG.info("---------------------- Test Warnings -----------------------"); + LOG.info(getCenteredText("Test Warnings", '-', width)); for (String warning : actionWarnings.keySet()) { LOG.info(" " + warning + ":"); @@ -714,7 +721,7 @@ private static void writeTimeReport() } } } - LOG.info("------------------------------------------------------------"); + LOG.info("-".repeat(width)); LOG.info(getFixedWidthString("Total duration:", formatDuration(total), width) + "\n"); LOG.info("Completed " + FastDateFormat.getInstance("yyyy-MM-dd HH:mm").format(new Date())); } @@ -744,10 +751,23 @@ private static String getRowString(String[] list, int columnWidth) private static String getFixedWidthString(String prefix, String suffix, int length) { int contentLength = prefix.length() + suffix.length(); - int padding = Math.max(0, length - contentLength); + int padding = Math.max(1, length - contentLength); return prefix + " ".repeat(padding) + suffix; } + private static String getCenteredText(String text, char paddingChar, int width) + { + text = StringUtils.trimToEmpty(text); + if (text.length() > width) + return text; + + text = text.isEmpty() ? "" : " " + text + " "; + int paddingLength = width - text.length(); + int leftPadding = paddingLength / 2; + int rightPadding = paddingLength - leftPadding; + return Character.toString(paddingChar).repeat(leftPadding) + text + Character.toString(paddingChar).repeat(rightPadding); + } + private static TestSet getCompositeTestSet(List suitesColl) { if (suitesColl.isEmpty()) @@ -1021,7 +1041,7 @@ else if (testNames.isEmpty()) Test test = e.nextElement(); Class testClass = getTestClass(test); _remainingTests.add(testClass); - LOG.info(" " + testClass.getSimpleName()); + LOG.info(" " + getTestName(test)); for (String testMethod : specifiedTestMethods.getOrDefault(testClass, Collections.emptyList())) { LOG.info(" ." + testMethod); @@ -1126,10 +1146,43 @@ private static String getModuleNameFromPath(File path) return null; } + private static void flattenSuiteInto(TestSuite destination, TestSuite source) + { + if (TestProperties.isTestRunningOnTeamCity()) + { + // Flatten test suites so that TeamCity will correlate with results from before Gradle 9.3 + Enumeration tests = source.tests(); + while (tests.hasMoreElements()) + { + Test t = tests.nextElement(); + if (t instanceof TestSuite nested) + flattenSuiteInto(destination, nested); + else + destination.addTest(t); + } + } + else + { + destination.addTest(source); + } + } + + private static String getTestName(Test test) + { + if (test instanceof JUnit4TestAdapter testAdapter) + return testAdapter.getTestClass().getSimpleName(); + else if (test instanceof JUnitTest.RemoteTest remoteTest) + return remoteTest.getName(); + else + return test.getClass().getSimpleName(); + } + private static Class getTestClass(Test test) { - if (test instanceof JUnit4TestAdapter) - return ((JUnit4TestAdapter) test).getTestClass(); + if (test instanceof JUnit4TestAdapter testAdapter) + return testAdapter.getTestClass(); + else if (test instanceof JUnitTest.RemoteTest) + return JUnitTest.class; else return test.getClass(); } diff --git a/src/org/labkey/test/tests/JUnitTest.java b/src/org/labkey/test/tests/JUnitTest.java index 2b785e1fee..8016ef095d 100644 --- a/src/org/labkey/test/tests/JUnitTest.java +++ b/src/org/labkey/test/tests/JUnitTest.java @@ -84,7 +84,7 @@ public JUnitTest() public static TestSuite suite() throws Exception { - return JUnitTest._suite((p) -> true, false); + return JUnitTest._suite(_ -> true, false); } private static String getWhen(Map test) @@ -136,12 +136,9 @@ private static void upgradeHelper(boolean skipInitialUserChecks) } catch (Throwable t) { - if (bootstrapBrowser.getWrappedDriver() != null) - { - ArtifactCollector artifactCollector = new ArtifactCollector(bootstrapBrowser, JUnitTest.class.getSimpleName()); - artifactCollector.dumpPageSnapshot("ServerBootstrap", null); - artifactCollector.publishDumpedArtifacts(); - } + ArtifactCollector artifactCollector = new ArtifactCollector(bootstrapBrowser, JUnitTest.class.getSimpleName()); + artifactCollector.dumpPageSnapshot("ServerBootstrap", null); + artifactCollector.publishDumpedArtifacts(); throw t; } finally @@ -176,8 +173,7 @@ public static TestSuite dynamicSuite(Collection categories, CollectionUpgrade Status") || catch (Throwable t) { upgradeError = t; - t.printStackTrace(); + LOG.warn("Error during upgrade/bootstrap", t); } TestSuite testSuite; try @@ -298,7 +294,7 @@ else if (responseBody.contains("Upgrade Status") || } catch (Exception retryException) { - retryException.printStackTrace(); + LOG.warn("Error fetching remote test suite", retryException); testSuite = new TestSuite(); testSuite.addTest(new Runner.ErrorTest("", retryException)); } @@ -321,7 +317,7 @@ else if (responseBody.contains("Upgrade Status") || for (String key : json.keySet()) { AtomicInteger ioeCounter = new AtomicInteger(0); - TestSuite testsuite = new TestSuite(key); + TestSuite moduleSuite = new TestSuite(key + " Tests"); JSONArray testClassArray = json.getJSONArray(key); // Individual tests include both the class name and the requested timeout for (int i = 0; i < testClassArray.length(); i++) @@ -334,17 +330,20 @@ else if (responseBody.contains("Upgrade Status") || // Timeout is represented in seconds int timeout = testClass.getInt("timeout"); if (accept.test(testClass.toMap())) - testsuite.addTest(new RemoteTest(className, timeout, ioeCounter)); + moduleSuite.addTest(new RemoteTest(className, timeout, ioeCounter)); } } - if (!addedHeader && testsuite.countTestCases() > 0) + if (moduleSuite.countTestCases() > 0) { - BaseJUnitTestWrapper.extraSetup = !skipInitialUserChecks; - remotesuite.addTest(new JUnit4TestAdapter(JUnitHeader.class)); - addedHeader = true; + if (!addedHeader) + { + BaseJUnitTestWrapper.extraSetup = !skipInitialUserChecks; + remotesuite.addTest(new JUnit4TestAdapter(JUnitHeader.class)); + addedHeader = true; + } + remotesuite.addTest(moduleSuite); } - remotesuite.addTest(testsuite); } if (addedHeader) { From ad4083435c4a9aa654e91ff91b6849b62e57546b Mon Sep 17 00:00:00 2001 From: Josh Eckels Date: Mon, 11 May 2026 08:16:05 -0700 Subject: [PATCH 3/5] Improve cache clearing/GC/heap dump behavior (#2986) - New utility method to clear caches using `ClearCachesCommand` - Eliminate deprecated method --- .../remoteapi/admin/ClearCachesCommand.java | 31 +++++++++++++++++++ src/org/labkey/test/BaseWebDriverTest.java | 28 ++++++++++++----- .../tests/filecontent/FilesQueryTest.java | 2 +- .../tests/perf/SchemaBrowserPerfTest.java | 6 ++-- 4 files changed, 55 insertions(+), 12 deletions(-) create mode 100644 src/org/labkey/remoteapi/admin/ClearCachesCommand.java diff --git a/src/org/labkey/remoteapi/admin/ClearCachesCommand.java b/src/org/labkey/remoteapi/admin/ClearCachesCommand.java new file mode 100644 index 0000000000..a9851f2e64 --- /dev/null +++ b/src/org/labkey/remoteapi/admin/ClearCachesCommand.java @@ -0,0 +1,31 @@ +package org.labkey.remoteapi.admin; + +import org.labkey.remoteapi.CommandResponse; +import org.labkey.remoteapi.PostCommand; + +import java.util.HashMap; +import java.util.Map; + +public class ClearCachesCommand extends PostCommand +{ + private final boolean _clearCaches; + private final boolean _gc; + + public ClearCachesCommand(boolean clearCaches, boolean gc) + { + super("admin", "clearCaches"); + _clearCaches = clearCaches; + _gc = gc; + } + + @Override + protected Map createParameterMap() + { + Map params = new HashMap<>(); + if (_clearCaches) + params.put("clearCaches", true); + if (_gc) + params.put("gc", true); + return params; + } +} diff --git a/src/org/labkey/test/BaseWebDriverTest.java b/src/org/labkey/test/BaseWebDriverTest.java index 10d9d5ad98..039872a8db 100644 --- a/src/org/labkey/test/BaseWebDriverTest.java +++ b/src/org/labkey/test/BaseWebDriverTest.java @@ -44,6 +44,7 @@ import org.labkey.remoteapi.CommandException; import org.labkey.remoteapi.CommandResponse; import org.labkey.remoteapi.Connection; +import org.labkey.remoteapi.admin.ClearCachesCommand; import org.labkey.remoteapi.SimpleGetCommand; import org.labkey.remoteapi.SimplePostCommand; import org.labkey.remoteapi.collections.CaseInsensitiveHashMap; @@ -99,7 +100,6 @@ import org.labkey.test.util.UIPermissionsHelper; import org.labkey.test.util.core.webdav.WebDavUploadHelper; import org.labkey.test.util.ext4cmp.Ext4FieldRef; -import org.labkey.test.util.query.QueryUtils; import org.labkey.test.util.selenium.WebDriverUtils; import org.openqa.selenium.By; import org.openqa.selenium.ElementClickInterceptedException; @@ -1174,6 +1174,7 @@ public void dumpHeap() // Use dumpHeapAction rather that touching file so that we can get file name and publish artifact. beginAt(WebTestHelper.buildURL("admin", "dumpHeap")); + clickButton("OK"); String dumpMsg = Locators.bodyPanel().childTag("div").findElement(getDriver()).getText(); String filePrefix = "Heap dumped to "; int prefixIndex = dumpMsg.indexOf(filePrefix); @@ -1389,6 +1390,22 @@ public static File getDownloadDir() return SingletonWebDriver.getInstance().getDownloadDir(); } + /** + * Clears all server caches and runs garbage collection via the admin ClearCachesAction. + * Leaves the browser on the MemTracker page. + */ + protected void clearCaches() + { + try + { + new ClearCachesCommand(true, true).execute(createDefaultConnection(), "/"); + } + catch (IOException | CommandException e) + { + throw new RuntimeException("Failed to clear caches", e); + } + } + protected void checkLeaks() { if (isLeakCheckSkipped()) @@ -1415,7 +1432,8 @@ protected void checkLeaks() } } msSinceTestStart = System.currentTimeMillis() - previousLeakCheck; - beginAt(WebTestHelper.buildURL("admin", "memTracker", Map.of("gc", 1, "clearCaches", 1)), 120000); + clearCaches(); + beginAt(WebTestHelper.buildURL("admin", "memTracker")); if (!isTextPresent("In-Use Objects")) throw new IllegalStateException("Asserts must be enabled to track memory leaks; add -ea to your server VM params and restart or add -DmemCheck=false to your test VM params."); leakCount = getImageWithAltTextCount("expand/collapse"); @@ -2379,12 +2397,6 @@ public void validateQueries(boolean validateSubfolders) validateQueries(validateSubfolders, 120000); } - @Deprecated - public void deleteAllRows(String projectName, String schema, String table) throws IOException, CommandException - { - QueryUtils.truncateTable(projectName, schema, table); - } - // This class makes it easier to start a specimen import early in a test and wait for completion later. public class SpecimenImporter { diff --git a/src/org/labkey/test/tests/filecontent/FilesQueryTest.java b/src/org/labkey/test/tests/filecontent/FilesQueryTest.java index dbcd52fbf0..2fafacdeca 100644 --- a/src/org/labkey/test/tests/filecontent/FilesQueryTest.java +++ b/src/org/labkey/test/tests/filecontent/FilesQueryTest.java @@ -173,7 +173,7 @@ public void testNonFileBrowserFileRecords() private void ensureFilesUpToDate() { log("Clear cache so that exp.files will do a sync immediately"); - beginAt(WebTestHelper.buildURL("admin", "caches", Map.of("clearCaches", "1")), 120000); + clearCaches(); goToProjectHome(); } diff --git a/src/org/labkey/test/tests/perf/SchemaBrowserPerfTest.java b/src/org/labkey/test/tests/perf/SchemaBrowserPerfTest.java index af48928236..05a938bcfa 100644 --- a/src/org/labkey/test/tests/perf/SchemaBrowserPerfTest.java +++ b/src/org/labkey/test/tests/perf/SchemaBrowserPerfTest.java @@ -88,7 +88,7 @@ private long[] studyBaselineEmptyCache() { long[] emptyCacheOpenStudyTimes = new long[5]; // run tests for (int x = 0 ; x < 5; x++) { - beginAt(WebTestHelper.buildURL("admin", "caches", Map.of("clearCaches", 1)), 120000); + clearCaches(); goToHome(); clickProject(getProjectName()); goToSchemaBrowser(); @@ -103,7 +103,7 @@ private long[] studyBaselineEmptyCache() { private long[] studyBaselineFullCache() { long[] fullCacheOpenStudyTimes = new long[5]; // prepare cache - beginAt(WebTestHelper.buildURL("admin", "caches", Map.of("clearCaches", 1)), 120000); + clearCaches(); goToHome(); clickProject(getProjectName()); goToSchemaBrowser(); @@ -125,7 +125,7 @@ private long[] studyBaselineFullCache() { private long[] studyDataBaselineFullCache() { long[] emptyCacheOpenStudyDataTimes = new long[5]; // prepare cache - beginAt(WebTestHelper.buildURL("admin", "caches", Map.of("clearCaches", 1)), 120000); + clearCaches(); goToHome(); clickProject(getProjectName()); goToSchemaBrowser(); From fa9919302a4220c4ae88285be6344e1a4c2dc2fd Mon Sep 17 00:00:00 2001 From: Karl Lum Date: Tue, 12 May 2026 10:50:04 -0700 Subject: [PATCH 4/5] Supress stack trace display from error pages (#2985) #### Rationale Updates tests to handle the work to remove the stack trace from the error pages. https://github.com/LabKey/platform/pull/7654 --- src/org/labkey/test/pages/LabkeyErrorPage.java | 6 ++++++ src/org/labkey/test/tests/LabkeyErrorPageTest.java | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/org/labkey/test/pages/LabkeyErrorPage.java b/src/org/labkey/test/pages/LabkeyErrorPage.java index 49937d6cb4..b9b415419d 100644 --- a/src/org/labkey/test/pages/LabkeyErrorPage.java +++ b/src/org/labkey/test/pages/LabkeyErrorPage.java @@ -51,6 +51,12 @@ public String getErrorImage() { return elementCache().errorImage.getAttribute("src"); } + + public boolean isShowDetailsPresent() + { + return !Locator.button("View Details").findElements(getDriver()).isEmpty(); + } + @Override protected ElementCache newElementCache() { diff --git a/src/org/labkey/test/tests/LabkeyErrorPageTest.java b/src/org/labkey/test/tests/LabkeyErrorPageTest.java index 4f05da9bb2..0cbfddf7fe 100644 --- a/src/org/labkey/test/tests/LabkeyErrorPageTest.java +++ b/src/org/labkey/test/tests/LabkeyErrorPageTest.java @@ -45,6 +45,8 @@ public void testGeneralErrors() checker().verifyEquals("Incorrect error heading message", "404: page not found", errorPage.getErrorHeading()); checker().verifyThat("Incorrect error image", errorPage.getErrorImage(), CoreMatchers.containsString(imageTitle)); + checker().verifyTrue("'Show Details' button should appear on not found error page", + errorPage.isShowDetailsPresent()); beginAt(WebTestHelper.buildRelativeUrl("project", getCurrentContainerPath(), "beginning")); errorPage = new LabkeyErrorPage(getDriver()); @@ -88,6 +90,8 @@ public void testServerConfigurationErrors() checker().verifyEquals("Incorrect error sub-heading message", "The requested page cannot be found. You have a configuration problem.", errorPage.getSubErrorHeading()); checker().verifyThat("Incorrect error image", errorPage.getErrorImage(), CoreMatchers.containsString(imageTitle)); + checker().verifyTrue("'Show Details' button should appear on configuration error page", + errorPage.isShowDetailsPresent()); checkExpectedErrors(1); } @@ -103,9 +107,11 @@ public void testExecutionErrors() checker().verifyEquals("Incorrect error heading message", "Oops! An error has occurred.", errorPage.getErrorHeading()); checker().verifyEquals("Incorrect error instructions", "You can find help resources here and may " + - "find troubleshooting hints by reading the full stack trace in the View Details section below.", + "find troubleshooting hints by reading the full stack trace in the server logs.", errorPage.getErrorInstruction()); checker().verifyThat("Incorrect error image", errorPage.getErrorImage(), CoreMatchers.containsString(imageTitle)); + checker().verifyFalse("'Show Details' button should not appear on execution error page", + errorPage.isShowDetailsPresent()); checkExpectedErrors(2); } From 32489341be8cf34e568a447c0cc072edee10c4e8 Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Wed, 13 May 2026 09:24:06 -0700 Subject: [PATCH 5/5] Upgrade test for "limit unsuccessful logins" settings (#2991) - Upgrade test for "limit unsuccessful logins" settings - Update `BaseUpgradeTest` annotations to work at the class level --- .../ComplianceLoginSettingsPage.java | 20 +++++++++++++++++ .../test/tests/upgrade/BaseUpgradeTest.java | 22 ++++++++++++++----- src/org/labkey/test/util/VersionRange.java | 4 +--- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/org/labkey/test/pages/compliance/ComplianceLoginSettingsPage.java b/src/org/labkey/test/pages/compliance/ComplianceLoginSettingsPage.java index a7a08d90aa..2892a3e32e 100644 --- a/src/org/labkey/test/pages/compliance/ComplianceLoginSettingsPage.java +++ b/src/org/labkey/test/pages/compliance/ComplianceLoginSettingsPage.java @@ -35,6 +35,11 @@ public void disableLoginControls() shortWait().until(wd -> !elementCache().loginAttemptCountCombo.isEnabled()); } + public boolean isLoginLimitEnabled() + { + return elementCache().enableLoginChk.isSelected(); + } + public void setLoginAttemptCount(String count) { setFormElement(elementCache().loginAttemptCountInput, count); @@ -45,6 +50,21 @@ public void selectLoginAttemptCount(String count) elementCache().loginAttemptCountCombo.selectComboBoxItem(count); } + public String getLoginAttemptCount() + { + return getFormElement(elementCache().loginAttemptCountInput); + } + + public String getLoginAttemptPeriod() + { + return getFormElement(elementCache().loginAttemptPeriodInput); + } + + public String getLoginAttemptRecoveryTime() + { + return getFormElement(elementCache().loginAttemptRecoveryTimeInput); + } + public void selectLoginAttemptPeriod(String period) { elementCache().loginAttemptPeriodCombo.selectComboBoxItem(period); diff --git a/src/org/labkey/test/tests/upgrade/BaseUpgradeTest.java b/src/org/labkey/test/tests/upgrade/BaseUpgradeTest.java index a7e3b20568..12e1e2bc29 100644 --- a/src/org/labkey/test/tests/upgrade/BaseUpgradeTest.java +++ b/src/org/labkey/test/tests/upgrade/BaseUpgradeTest.java @@ -50,13 +50,23 @@ public static void setupProject() throws Exception { BaseUpgradeTest currentTest = BaseWebDriverTest.getCurrentTest(); + Class testClass = currentTest.getClass(); + String earliestVersion = Optional.ofNullable(testClass.getAnnotation(EarliestVersion.class)) + .map(EarliestVersion::value).orElse(null); + String latestVersion = Optional.ofNullable(testClass.getAnnotation(LatestVersion.class)) + .map(LatestVersion::value).orElse(null); + + Assume.assumeTrue("Test class not valid when upgrading from version: " + setupVersion, + VersionRange.versionRange(earliestVersion, latestVersion).contains(setupVersion) + ); + if (isUpgradeSetupPhase) { currentTest.doSetup(); } else { - TestLogger.info("Skipping setup for %s. Verifying upgrade.". formatted(currentTest.getClass().getSimpleName())); + TestLogger.info("Skipping setup for %s. Verifying upgrade.". formatted(testClass.getSimpleName())); } } @@ -100,19 +110,19 @@ protected boolean wasSetupWithin(String earliestVersion, String latestVersion) * Specifies the earliest version of the test class that performed the required setup for the annotated method. */ @Retention(RetentionPolicy.RUNTIME) - @Target({ElementType.METHOD}) + @Target({ElementType.METHOD, ElementType.TYPE}) protected @interface EarliestVersion { String value(); } /** - * Annotates test methods that should only run when upgrading from particular LabKey versions, as specified in - * {@code webtest.upgradePreviousVersion}.
- * Specifies the latest version of the test class that performed the required setup for the annotated method. + * Annotates test methods or classes that should only run when upgrading from particular LabKey versions, as + * specified in {@code webtest.upgradePreviousVersion}.
+ * Specifies the latest version of the test class that performed the required setup for the annotated method or class. */ @Retention(RetentionPolicy.RUNTIME) - @Target({ElementType.METHOD}) + @Target({ElementType.METHOD, ElementType.TYPE}) protected @interface LatestVersion { String value(); } diff --git a/src/org/labkey/test/util/VersionRange.java b/src/org/labkey/test/util/VersionRange.java index 865974783e..083c425336 100644 --- a/src/org/labkey/test/util/VersionRange.java +++ b/src/org/labkey/test/util/VersionRange.java @@ -14,15 +14,13 @@ public class VersionRange * * @param earliestVersion The earliest version in the range (inclusive). If null, there is no lower bound. * @param latestVersion The latest version in the range (inclusive). If null, there is no upper bound. - * @throws IllegalArgumentException if both versions are null, or if earliestVersion is after latestVersion. + * @throws IllegalArgumentException if earliestVersion is after latestVersion. */ public VersionRange(Version earliestVersion, Version latestVersion) { this.earliestVersion = earliestVersion; this.latestVersion = latestVersion; - if (earliestVersion == null && latestVersion == null) - throw new IllegalArgumentException("Version range requires at least one version"); if (earliestVersion != null && latestVersion != null && earliestVersion.compareTo(latestVersion) > 0) throw new IllegalArgumentException("%s is after %s".formatted(earliestVersion, latestVersion));