From 65052b4abf8f7cd974202046b027c2e29715d2d8 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 29 Dec 2023 10:45:33 -0800 Subject: [PATCH 01/10] Update Selenium and force firefox115 --- gradle.properties | 2 +- src/org/labkey/test/TestProperties.java | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 029eae2bdb..27e7dd9ba8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ hamcrestCoreVersion=1.3 lookfirstSardineVersion=5.7 jettyVersion=12.0.4 -seleniumVersion=4.11.0 +seleniumVersion=4.16.0 mockserverNettyVersion=5.15.0 labkeySchemasTestVersion=23.7-SNAPSHOT diff --git a/src/org/labkey/test/TestProperties.java b/src/org/labkey/test/TestProperties.java index 5c15d264cb..db6e95f82a 100644 --- a/src/org/labkey/test/TestProperties.java +++ b/src/org/labkey/test/TestProperties.java @@ -40,6 +40,9 @@ public abstract class TestProperties static { + //TODO: DO NOT MERGE! FORCING FIREFOX VERSION FOR VALIDATION + System.setProperty("selenium.firefox.binary", System.getenv("FIREFOX115")); + // https://github.com/SeleniumHQ/selenium/issues/11750#issuecomment-1470357124 System.setProperty("webdriver.http.factory", "jdk-http-client"); From fd2514e4d74bd626cb7e28d9159bc12a51d26725 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 29 Dec 2023 10:46:07 -0800 Subject: [PATCH 02/10] Newer schemas version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 27e7dd9ba8..63cf4dfa6a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,4 +10,4 @@ jettyVersion=12.0.4 seleniumVersion=4.16.0 mockserverNettyVersion=5.15.0 -labkeySchemasTestVersion=23.7-SNAPSHOT +labkeySchemasTestVersion=23.11-SNAPSHOT From 6b773b82abc4f94ed5e1fef8ebc7ad17c292fceb Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 29 Dec 2023 15:13:23 -0800 Subject: [PATCH 03/10] Remove custom http factory --- build.gradle | 2 -- src/org/labkey/test/TestProperties.java | 3 --- 2 files changed, 5 deletions(-) diff --git a/build.gradle b/build.gradle index 3f825c9121..0ccdfbd9ad 100644 --- a/build.gradle +++ b/build.gradle @@ -33,8 +33,6 @@ project.dependencies { implementation("org.seleniumhq.selenium:selenium-remote-driver:${seleniumVersion}") implementation("org.seleniumhq.selenium:selenium-chrome-driver:${seleniumVersion}") implementation "org.seleniumhq.selenium:selenium-ie-driver:${seleniumVersion}" - // https://github.com/SeleniumHQ/selenium/issues/11750#issuecomment-1470357124 - implementation "org.seleniumhq.selenium:selenium-http-jdk-client:${seleniumVersion}" implementation("org.eclipse.jetty:jetty-util:${jettyVersion}") implementation("com.google.guava:guava:${guavaVersion}") diff --git a/src/org/labkey/test/TestProperties.java b/src/org/labkey/test/TestProperties.java index db6e95f82a..449ceea696 100644 --- a/src/org/labkey/test/TestProperties.java +++ b/src/org/labkey/test/TestProperties.java @@ -43,9 +43,6 @@ public abstract class TestProperties //TODO: DO NOT MERGE! FORCING FIREFOX VERSION FOR VALIDATION System.setProperty("selenium.firefox.binary", System.getenv("FIREFOX115")); - // https://github.com/SeleniumHQ/selenium/issues/11750#issuecomment-1470357124 - System.setProperty("webdriver.http.factory", "jdk-http-client"); - final File propFile = new File(TestFileUtils.getTestRoot(), "test.properties"); final File propFileTemplate = new File(TestFileUtils.getTestRoot(), "test.properties.template"); if (!propFile.exists()) From 44212a95a2e42ba24981972370823eb6bd8d5533 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Tue, 2 Jan 2024 10:12:35 -0800 Subject: [PATCH 04/10] Decouple Firefox upgrade from selenium upgrade --- src/org/labkey/test/TestProperties.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/org/labkey/test/TestProperties.java b/src/org/labkey/test/TestProperties.java index 449ceea696..579fba5dfb 100644 --- a/src/org/labkey/test/TestProperties.java +++ b/src/org/labkey/test/TestProperties.java @@ -40,9 +40,6 @@ public abstract class TestProperties static { - //TODO: DO NOT MERGE! FORCING FIREFOX VERSION FOR VALIDATION - System.setProperty("selenium.firefox.binary", System.getenv("FIREFOX115")); - final File propFile = new File(TestFileUtils.getTestRoot(), "test.properties"); final File propFileTemplate = new File(TestFileUtils.getTestRoot(), "test.properties.template"); if (!propFile.exists()) From 382c215f713ca47b8ac29b54143bde598f57e573 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Tue, 2 Jan 2024 10:16:29 -0800 Subject: [PATCH 05/10] Minimize change --- src/org/labkey/test/TestProperties.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/org/labkey/test/TestProperties.java b/src/org/labkey/test/TestProperties.java index 579fba5dfb..1d6579a4d2 100644 --- a/src/org/labkey/test/TestProperties.java +++ b/src/org/labkey/test/TestProperties.java @@ -40,6 +40,7 @@ public abstract class TestProperties static { + final File propFile = new File(TestFileUtils.getTestRoot(), "test.properties"); final File propFileTemplate = new File(TestFileUtils.getTestRoot(), "test.properties.template"); if (!propFile.exists()) From ce54dbe141d099c059abc20eb1d5d284aa6a4f68 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 3 Jan 2024 14:06:08 -0800 Subject: [PATCH 06/10] Timing fix for domain form --- src/org/labkey/test/components/domain/DomainFormPanel.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/org/labkey/test/components/domain/DomainFormPanel.java b/src/org/labkey/test/components/domain/DomainFormPanel.java index 93d36f71b5..b704b830e8 100644 --- a/src/org/labkey/test/components/domain/DomainFormPanel.java +++ b/src/org/labkey/test/components/domain/DomainFormPanel.java @@ -625,6 +625,7 @@ private List findFieldRows() private DomainFieldRow findFieldRow(String name) { + WebDriverWrapper.waitFor(() -> !findFieldRows().isEmpty(), 1_000); List fieldRows = findFieldRows(); for (int i = 0; i < fieldRows.size(); i++) { From 6e3a22eb0ad928edee327345cb4e71c81b0736eb Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 3 Jan 2024 16:04:34 -0800 Subject: [PATCH 07/10] Make JUnitTest shut down its browser more reliably --- .gitignore | 5 + src/org/labkey/test/BaseWebDriverTest.java | 51 +++++- src/org/labkey/test/ExtraSiteWrapper.java | 7 + src/org/labkey/test/tests/JUnitTest.java | 40 ++--- .../labkey/test/util/ApiBootstrapHelper.java | 170 ++++++++++++++++++ .../labkey/test/util/ArtifactCollector.java | 95 +++------- src/org/labkey/test/util/Crawler.java | 2 +- .../test/util/QuickBootstrapPseudoTest.java | 154 +--------------- 8 files changed, 268 insertions(+), 256 deletions(-) create mode 100644 src/org/labkey/test/util/ApiBootstrapHelper.java diff --git a/.gitignore b/.gitignore index edbd6a5aec..d3f8690df1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,11 @@ out/ # Transform jars built by Gradle data/qc/*.jar +# Logs and temp files created by tests +data/**/*.log +data/**/assaydata/ +data/**/drt_temp/ + # Generated by test runner remainingTests.txt diff --git a/src/org/labkey/test/BaseWebDriverTest.java b/src/org/labkey/test/BaseWebDriverTest.java index dfae090ce3..4156fd68a5 100644 --- a/src/org/labkey/test/BaseWebDriverTest.java +++ b/src/org/labkey/test/BaseWebDriverTest.java @@ -142,6 +142,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.labkey.test.TestProperties.isHeapDumpCollectionEnabled; import static org.labkey.test.TestProperties.isInjectionCheckEnabled; import static org.labkey.test.TestProperties.isLeakCheckSkipped; import static org.labkey.test.TestProperties.isLinkCheckEnabled; @@ -154,9 +155,11 @@ import static org.labkey.test.WebTestHelper.GC_ATTEMPT_LIMIT; import static org.labkey.test.WebTestHelper.MAX_LEAK_LIMIT; import static org.labkey.test.WebTestHelper.buildURL; +import static org.labkey.test.WebTestHelper.isLocalServer; import static org.labkey.test.WebTestHelper.logToServer; import static org.labkey.test.components.ext4.Window.Window; import static org.labkey.test.components.html.RadioButton.RadioButton; +import static org.labkey.test.teamcity.TeamCityUtils.publishArtifact; /** * This class should be used as the base for all functional test classes @@ -189,6 +192,7 @@ public abstract class BaseWebDriverTest extends LabKeySiteWrapper implements Cle private String _lastPageText = null; protected static boolean _testFailed = false; protected static boolean _anyTestFailed = false; + private static boolean _dumpedHeap = false; private final ArtifactCollector _artifactCollector; private final DeferredErrorCollector _errorCollector; @@ -1067,7 +1071,7 @@ private void handleFailure(Throwable error, @LoggedParam String testName) .filter(s -> !s.isEmpty()).toList(); TestLogger.error("Remaining files after attempting to delete: " + path + "\n\t" + String.join("\t\n", subdirs), notEmpty); } - getArtifactCollector().dumpHeap(); + dumpHeap(); } catch (IOException e) { @@ -1114,6 +1118,47 @@ private Throwable unwrapRuntimeException(Throwable throwable) return null; } + public void dumpHeap() + { + if (_dumpedHeap || // Only one heap dump per suite (don't want to overload TeamCity) + !isLocalServer() || + isGuestModeTest() || + !isHeapDumpCollectionEnabled() + ) + { + return; + } + + pushLocation(); + + // Use dumpHeapAction rather that touching file so that we can get file name and publish artifact. + beginAt(WebTestHelper.buildURL("admin", "dumpHeap")); + String dumpMsg = Locators.bodyPanel().childTag("div").findElement(getDriver()).getText(); + String filePrefix = "Heap dumped to "; + int prefixIndex = dumpMsg.indexOf(filePrefix); + if (prefixIndex < 0) + { + checker().error("Unable to extract heap dump filename from page body.\n" + dumpMsg); + return; + } + String filename = dumpMsg.substring(prefixIndex + filePrefix.length()); + File heapDump = new File(filename); + File destFile = new File(getArtifactCollector().ensureDumpDir(), heapDump.getName()); + try + { + Files.move(heapDump.toPath(), destFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + publishArtifact(destFile, null); + _dumpedHeap = true; + } + catch (IOException e) + { + TestLogger.error("Failed to move HeapDump file to test logs directory."); + e.printStackTrace(); + } + + popLocation(); // go back to get screenshot if needed. + } + private void dumpBrowserConsole() { List logEntries = executeScript("return console.everything;", List.class); @@ -1349,7 +1394,7 @@ protected void checkLeaks() if (newLeak != null) { - getArtifactCollector().dumpHeap(); + dumpHeap(); ArtifactCollector.dumpThreads(); fail(String.format("Found memory leak: %s [1 of %d, MAX:%d]\nSee test artifacts for more information.", newLeak, leakCount, MAX_LEAK_LIMIT)); } @@ -2689,7 +2734,7 @@ private File getDownloadDir() private void setUp(BaseWebDriverTest test) { WebDriver oldWebDriver = getWebDriver(); - File newDownloadDir = new File(test.getArtifactCollector().ensureDumpDir(test.getClass().getSimpleName()), "downloads"); + File newDownloadDir = new File(ArtifactCollector.ensureDumpDir(test.getClass().getSimpleName()), "downloads"); _driverAndService = test.createNewWebDriver(_driverAndService, test.BROWSER_TYPE, newDownloadDir); if (getWebDriver() != oldWebDriver) // downloadDir only changes when a new WebDriver is started. _downloadDir = newDownloadDir; diff --git a/src/org/labkey/test/ExtraSiteWrapper.java b/src/org/labkey/test/ExtraSiteWrapper.java index f1788e6b92..39e2c7da18 100644 --- a/src/org/labkey/test/ExtraSiteWrapper.java +++ b/src/org/labkey/test/ExtraSiteWrapper.java @@ -15,6 +15,7 @@ */ package org.labkey.test; +import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import org.openqa.selenium.WebDriver; import org.openqa.selenium.remote.service.DriverService; @@ -31,6 +32,12 @@ public ExtraSiteWrapper(BrowserType browserType, File downloadDir) this.extraDriver = createNewWebDriver(browserType, downloadDir); } + public ExtraSiteWrapper(WebDriver driver) + { + super(); + this.extraDriver = new ImmutablePair<>(driver, null); + } + @Override public void close() { diff --git a/src/org/labkey/test/tests/JUnitTest.java b/src/org/labkey/test/tests/JUnitTest.java index 1cc391cbd4..f915d90d61 100644 --- a/src/org/labkey/test/tests/JUnitTest.java +++ b/src/org/labkey/test/tests/JUnitTest.java @@ -43,18 +43,19 @@ import org.labkey.remoteapi.SimplePostCommand; import org.labkey.remoteapi.collections.CaseInsensitiveHashMap; import org.labkey.test.BaseWebDriverTest; +import org.labkey.test.ExtraSiteWrapper; import org.labkey.test.Runner; import org.labkey.test.SuiteFactory; import org.labkey.test.TestProperties; -import org.labkey.test.TestTimeoutException; +import org.labkey.test.WebDriverWrapper; import org.labkey.test.WebTestHelper; import org.labkey.test.categories.BVT; import org.labkey.test.categories.UnitTests; +import org.labkey.test.util.ApiBootstrapHelper; import org.labkey.test.util.ArtifactCollector; import org.labkey.test.util.JUnitFooter; import org.labkey.test.util.JUnitHeader; import org.labkey.test.util.LogMethod; -import org.labkey.test.util.QuickBootstrapPseudoTest; import org.labkey.test.util.TestLogger; import java.io.IOException; @@ -108,47 +109,36 @@ public String toString() return getClass().getName(); } - public static class JUnitSeleniumHelper extends BaseWebDriverTest - { - @Override - protected String getProjectName() {return null;} - @Override - protected void doCleanup(boolean afterTest) throws TestTimeoutException - { } - @Override - public List getAssociatedModules() { return null; } - - @Override public BrowserType bestBrowser() {return BrowserType.CHROME;} - } - // Use WebDriver to ensure we're upgraded @LogMethod private static void upgradeHelper(boolean skipInitialUserChecks) { // TODO: remove upgrade helper from JUnitTest and run before suite starts. - BaseWebDriverTest helper; + + ExtraSiteWrapper bootstrapBrowser = new ExtraSiteWrapper(WebDriverWrapper.BrowserType.FIREFOX, + ArtifactCollector.ensureDumpDir(JUnitTest.class.getSimpleName())); if (skipInitialUserChecks) { - helper = new QuickBootstrapPseudoTest(); - } - else - { - helper = new JUnitSeleniumHelper(); + bootstrapBrowser = new ApiBootstrapHelper(bootstrapBrowser.getDriver()); } + try { - helper.setUp(); // sign in performs upgrade if necessary - helper.signIn(); + bootstrapBrowser.signIn(); } catch (Throwable t) { - if (helper.getWrappedDriver() != null) + if (bootstrapBrowser.getWrappedDriver() != null) { - helper.getArtifactCollector().dumpPageSnapshot("ServerBootstrap", null); + new ArtifactCollector(bootstrapBrowser, JUnitTest.class.getSimpleName()).dumpPageSnapshot("ServerBootstrap", null); } throw t; } + finally + { + bootstrapBrowser.close(); + } } public static TestSuite dynamicSuite(Collection categories, Collection excludedCategories) diff --git a/src/org/labkey/test/util/ApiBootstrapHelper.java b/src/org/labkey/test/util/ApiBootstrapHelper.java new file mode 100644 index 0000000000..a8bfe21809 --- /dev/null +++ b/src/org/labkey/test/util/ApiBootstrapHelper.java @@ -0,0 +1,170 @@ +package org.labkey.test.util; + +import org.json.JSONObject; +import org.labkey.remoteapi.CommandException; +import org.labkey.remoteapi.CommandResponse; +import org.labkey.remoteapi.Connection; +import org.labkey.remoteapi.GuestCredentialsProvider; +import org.labkey.remoteapi.SimpleGetCommand; +import org.labkey.remoteapi.SimplePostCommand; +import org.labkey.test.ExtraSiteWrapper; +import org.labkey.test.LabKeySiteWrapper; +import org.labkey.test.WebTestHelper; +import org.labkey.test.components.core.login.SetPasswordForm; +import org.openqa.selenium.WebDriver; + +import java.io.IOException; +import java.time.Duration; + +/** + * Bootstrap a server without the initial user validation done by {@link LabKeySiteWrapper#signIn()} + * Requires that we are able to create the initial user via API. + */ +public class ApiBootstrapHelper extends ExtraSiteWrapper +{ + public ApiBootstrapHelper(WebDriver driver) + { + super(driver); + } + + @Override + public void signIn() + { + waitForStartup(); + if (!isInitialUserCreated()) + { + createInitialUser(); + waitForBootstrap(); + new APIUserHelper(this).setInjectionDisplayName(PasswordUtil.getUsername()); + } + else + { + TestLogger.log("Initial user is already created. Nothing to do."); + } + } + + @LogMethod + private void waitForStartup() + { + Connection cn = new Connection(WebTestHelper.getBaseURL(), new GuestCredentialsProvider()); + SimpleGetCommand command = new SimpleGetCommand("admin", "healthCheck"); + Exception lastException = null; + + Timer timer = new Timer(Duration.ofMinutes(5)); + Duration timeOfLastLog = null; + do + { + if (timeOfLastLog == null || timer.elapsed().minus(timeOfLastLog).compareTo(Duration.ofSeconds(10)) > 0) + { + timeOfLastLog = timer.elapsed(); + StringBuilder msg = new StringBuilder("Waiting for server to finish starting up."); + if (lastException != null) + { + msg.append(" [").append(lastException.getMessage()).append("]"); + } + TestLogger.log(msg.toString()); + } + try + { + CommandResponse response = command.execute(cn, null); + if ((Boolean) response.getParsedData().getOrDefault("healthy", false)) + { + return; + } + } + catch (CommandException | IOException e) + { + lastException = e; + } + sleep(500); + } while (!timer.isTimedOut()); + + throw new RuntimeException("Server not done starting up.", lastException); + } + + @LogMethod + private boolean isInitialUserCreated() + { + Connection cn = new Connection(WebTestHelper.getBaseURL(), new GuestCredentialsProvider()); + SimplePostCommand command = new SimplePostCommand("admin", "configurationSummary"); + + try + { + command.execute(cn, null); + return false; // ConfigurationSummaryAction is accessible by guest only before initial user is created. + } + catch (IOException | CommandException e) + { + if (e instanceof CommandException && ((CommandException)e).getStatusCode() == 401) + { + return true; + } + else + { + throw new RuntimeException(e); + } + } + } + + private void createInitialUser() + { + beginAt(WebTestHelper.buildURL("login", "initialUser")); + new SetPasswordForm(getDriver()) + .setEmail(PasswordUtil.getUsername()) + .setNewPassword(PasswordUtil.getPassword()) + .clickSubmit(90_000); + } + + /** + * TODO: Make this work so that we don't have to open a browser. + * The POST just ends up redirecting to the normal initial user view. + */ + @LogMethod + private void createInitialUser_API() + { + Connection cn = createDefaultConnection(); + SimplePostCommand initialUserCommand = new SimplePostCommand("login", "initialUser"); + JSONObject params = new JSONObject(); + params.put("email", PasswordUtil.getUsername()); + params.put("password", PasswordUtil.getPassword()); + params.put("password2", PasswordUtil.getPassword()); + initialUserCommand.setJsonObject(params); + + try + { + initialUserCommand.execute(cn, null); + } + catch (IOException | CommandException e) + { + throw new RuntimeException("Failed to create initial user.", e); + } + } + + @LogMethod + private void waitForBootstrap() + { + Connection cn = WebTestHelper.getRemoteApiConnection(false); + SimpleGetCommand command = new SimpleGetCommand("admin", "startupStatus"); + Exception lastException = null; + + Timer timer = new Timer(Duration.ofMinutes(5)); + do + { + try + { + CommandResponse response = command.execute(cn, null); + if ((Boolean) response.getParsedData().getOrDefault("startupComplete", false)) + { + return; + } + } + catch (CommandException | IOException e) + { + lastException = e; + } + } while (!timer.isTimedOut()); + + throw new RuntimeException("Server didn't finish starting.", lastException); + } + +} diff --git a/src/org/labkey/test/util/ArtifactCollector.java b/src/org/labkey/test/util/ArtifactCollector.java index 2a7958f468..7eaf1ba78b 100644 --- a/src/org/labkey/test/util/ArtifactCollector.java +++ b/src/org/labkey/test/util/ArtifactCollector.java @@ -16,13 +16,13 @@ package org.labkey.test.util; import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.labkey.test.BaseWebDriverTest; import org.labkey.test.Locator; -import org.labkey.test.Locators; import org.labkey.test.TestFileUtils; import org.labkey.test.TestProperties; import org.labkey.test.WebDriverWrapper; @@ -46,8 +46,6 @@ import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Base64; import java.util.HashMap; @@ -55,31 +53,34 @@ import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; -import static org.labkey.test.TestProperties.isHeapDumpCollectionEnabled; import static org.labkey.test.TestProperties.isTestRunningOnTeamCity; import static org.labkey.test.WebTestHelper.isLocalServer; public class ArtifactCollector { - private static final Map, Integer> _shotCounters = new HashMap<>(); + private static final Map _shotCounters = new HashMap<>(); - private final BaseWebDriverTest _test; private final WebDriverWrapper _driver; + private final String _dumpDirName; // Use CopyOnWriteArrayList to avoid ConcurrentModificationException private static List> pipelineDirs = new CopyOnWriteArrayList<>(); private static long _testStart; - private static boolean _dumpedHeap = false; - public ArtifactCollector(BaseWebDriverTest test) + public ArtifactCollector(WebDriverWrapper driver, String dumpDirName) { - this(test, test); + _driver = driver; + _dumpDirName = dumpDirName; } - public ArtifactCollector(BaseWebDriverTest test, WebDriverWrapper driver) + public ArtifactCollector(WebDriverWrapper driver, ArtifactCollector otherBrowserCollector) { - _test = test; - _driver = driver; + this(driver, otherBrowserCollector._dumpDirName); + } + + public ArtifactCollector(BaseWebDriverTest test) + { + this(test, test.getClass().getSimpleName()); } public static void init() @@ -90,19 +91,10 @@ public static void init() public File ensureDumpDir() { - String currentTestClassName; - try - { - currentTestClassName = _test.getClass().getSimpleName(); - } - catch (NullPointerException e) - { - currentTestClassName = "UnknownTest"; - } - return ensureDumpDir(currentTestClassName); + return ensureDumpDir(_dumpDirName); } - public File ensureDumpDir(String testClassName) + public static File ensureDumpDir(String testClassName) { File dumpDir = new File(TestProperties.getDumpDir(), testClassName); if ( !dumpDir.exists() ) @@ -161,8 +153,8 @@ private String buildBaseName(@NotNull String suffix) private int getAndIncrementShotCounter() { - Integer shotCounter = _shotCounters.getOrDefault(_test.getClass(), 0); - _shotCounters.put(_test.getClass(), shotCounter + 1); + Integer shotCounter = _shotCounters.getOrDefault(_dumpDirName, 0); + _shotCounters.put(_dumpDirName, shotCounter + 1); return shotCounter; } @@ -174,7 +166,7 @@ public String dumpPageSnapshot(String snapshotName) public String dumpPageSnapshot(String snapshotName, @Nullable String subdir) { File dumpDir = ensureDumpDir(); - if (subdir != null && subdir.length() > 0) + if (!StringUtils.isBlank(subdir)) { dumpDir = new File(dumpDir, subdir); if ( !dumpDir.exists() ) @@ -191,47 +183,6 @@ public String dumpPageSnapshot(String snapshotName, @Nullable String subdir) return baseName; } - public void dumpHeap() - { - if (_dumpedHeap || // Only one heap dump per suite (don't want to overload TeamCity) - !isLocalServer() || - _test.isGuestModeTest() || - !isHeapDumpCollectionEnabled() - ) - { - return; - } - - _driver.pushLocation(); - - // Use dumpHeapAction rather that touching file so that we can get file name and publish artifact. - _driver.beginAt("/admin/dumpHeap.view"); - String dumpMsg = Locators.bodyPanel().childTag("div").findElement(_driver.getDriver()).getText(); - String filePrefix = "Heap dumped to "; - int prefixIndex = dumpMsg.indexOf(filePrefix); - if (prefixIndex < 0) - { - _test.checker().error("Unable to extract heap dump filename from page body. 'ArtifactCollector.dumpHeap' may need to be updated.\n" + dumpMsg); - return; - } - String filename = dumpMsg.substring(prefixIndex + filePrefix.length()); - File heapDump = new File(filename); - File destFile = new File(ensureDumpDir(), heapDump.getName()); - try - { - Files.move(heapDump.toPath(), destFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - publishArtifact(destFile); - _dumpedHeap = true; - } - catch (IOException e) - { - TestLogger.error("Failed to move HeapDump file to test logs directory."); - e.printStackTrace(); - } - - _driver.popLocation(); // go back to get screenshot if needed. - } - public File dumpScreen(File dir, String baseName) { File screenFile = new File(dir, baseName + ".png"); @@ -251,12 +202,6 @@ public File dumpScreen(File dir, String baseName) public File dumpPdf(File dir, String baseName) { - if (_test != _driver || _test.getBrowserType() == WebDriverWrapper.BrowserType.CHROME && !TestProperties.isRunWebDriverHeadless()) - { - // Avoid error: "PrintToPDF is only supported in headless mode" - return null; - } - File pdfFile = new File(dir, baseName + ".pdf"); try { @@ -302,8 +247,8 @@ public File dumpFullScreen(File dir, String baseName) public File dumpHtml(File dir, String baseName) { String pageHtml; - if (_test == _driver) - pageHtml = _test.getLastPageText(); + if (_driver instanceof BaseWebDriverTest test) + pageHtml = test.getLastPageText(); else pageHtml = _driver.getHtmlSource(); diff --git a/src/org/labkey/test/util/Crawler.java b/src/org/labkey/test/util/Crawler.java index 200827fc7e..822bd4a59a 100644 --- a/src/org/labkey/test/util/Crawler.java +++ b/src/org/labkey/test/util/Crawler.java @@ -1110,7 +1110,7 @@ private List crawlLink(final UrlToCheck urlToCheck) { originBrowser.simpleSignIn(); originBrowser.beginAt(origin.toString()); - ArtifactCollector collector = new ArtifactCollector(BaseWebDriverTest.getCurrentTest(), originBrowser); + ArtifactCollector collector = new ArtifactCollector(originBrowser, _test.getArtifactCollector()); collector.dumpPageSnapshot("crawler", "crawlOrigin"); } } diff --git a/src/org/labkey/test/util/QuickBootstrapPseudoTest.java b/src/org/labkey/test/util/QuickBootstrapPseudoTest.java index e35a795da0..5cce847ef8 100644 --- a/src/org/labkey/test/util/QuickBootstrapPseudoTest.java +++ b/src/org/labkey/test/util/QuickBootstrapPseudoTest.java @@ -1,22 +1,10 @@ package org.labkey.test.util; -import org.json.JSONObject; import org.junit.Test; import org.junit.experimental.categories.Category; -import org.labkey.remoteapi.CommandException; -import org.labkey.remoteapi.CommandResponse; -import org.labkey.remoteapi.Connection; -import org.labkey.remoteapi.GuestCredentialsProvider; -import org.labkey.remoteapi.SimpleGetCommand; -import org.labkey.remoteapi.SimplePostCommand; import org.labkey.test.BaseWebDriverTest; import org.labkey.test.LabKeySiteWrapper; -import org.labkey.test.TestTimeoutException; -import org.labkey.test.WebTestHelper; -import org.labkey.test.components.core.login.SetPasswordForm; -import java.io.IOException; -import java.time.Duration; import java.util.Arrays; import java.util.List; @@ -29,150 +17,12 @@ @Category({}) public class QuickBootstrapPseudoTest extends BaseWebDriverTest { - @Override - protected void doCleanup(boolean afterTest) throws TestTimeoutException - { - super.doCleanup(afterTest); - } + final ApiBootstrapHelper _bootstrapHelper = new ApiBootstrapHelper(getDriver()); @Override public void signIn() { - waitForStartup(); - if (!isInitialUserCreated()) - { - createInitialUser(); - waitForBootstrap(); - new APIUserHelper(this).setInjectionDisplayName(PasswordUtil.getUsername()); - } - else - { - TestLogger.log("Initial user is already created. Nothing to do."); - } - } - - @LogMethod - private void waitForStartup() - { - Connection cn = new Connection(WebTestHelper.getBaseURL(), new GuestCredentialsProvider()); - SimpleGetCommand command = new SimpleGetCommand("admin", "healthCheck"); - Exception lastException = null; - - Timer timer = new Timer(Duration.ofMinutes(5)); - Duration timeOfLastLog = null; - do - { - if (timeOfLastLog == null || timer.elapsed().minus(timeOfLastLog).compareTo(Duration.ofSeconds(10)) > 0) - { - timeOfLastLog = timer.elapsed(); - StringBuilder msg = new StringBuilder("Waiting for server to finish starting up."); - if (lastException != null) - { - msg.append(" [").append(lastException.getMessage()).append("]"); - } - TestLogger.log(msg.toString()); - } - try - { - CommandResponse response = command.execute(cn, null); - if ((Boolean) response.getParsedData().getOrDefault("healthy", false)) - { - return; - } - } - catch (CommandException | IOException e) - { - lastException = e; - } - sleep(500); - } while (!timer.isTimedOut()); - - throw new RuntimeException("Server not done starting up.", lastException); - } - - @LogMethod - private boolean isInitialUserCreated() - { - Connection cn = new Connection(WebTestHelper.getBaseURL(), new GuestCredentialsProvider()); - SimplePostCommand command = new SimplePostCommand("admin", "configurationSummary"); - - try - { - command.execute(cn, null); - return false; // ConfigurationSummaryAction is accessible by guest only before initial user is created. - } - catch (IOException | CommandException e) - { - if (e instanceof CommandException && ((CommandException)e).getStatusCode() == 401) - { - return true; - } - else - { - throw new RuntimeException(e); - } - } - } - - private void createInitialUser() - { - beginAt(WebTestHelper.buildURL("login", "initialUser")); - new SetPasswordForm(getDriver()) - .setEmail(PasswordUtil.getUsername()) - .setNewPassword(PasswordUtil.getPassword()) - .clickSubmit(90_000); - } - - /** - * TODO: Make this work so that we don't have to open a browser. - * The POST just ends up redirecting to the normal initial user view. - */ - @LogMethod - private void createInitialUser_API() - { - Connection cn = createDefaultConnection(); - SimplePostCommand initialUserCommand = new SimplePostCommand("login", "initialUser"); - JSONObject params = new JSONObject(); - params.put("email", PasswordUtil.getUsername()); - params.put("password", PasswordUtil.getPassword()); - params.put("password2", PasswordUtil.getPassword()); - initialUserCommand.setJsonObject(params); - - try - { - initialUserCommand.execute(cn, null); - } - catch (IOException | CommandException e) - { - throw new RuntimeException("Failed to create initial user.", e); - } - } - - @LogMethod - private void waitForBootstrap() - { - Connection cn = WebTestHelper.getRemoteApiConnection(false); - SimpleGetCommand command = new SimpleGetCommand("admin", "startupStatus"); - Exception lastException = null; - - Timer timer = new Timer(Duration.ofMinutes(5)); - do - { - try - { - CommandResponse response = command.execute(cn, null); - if ((Boolean) response.getParsedData().getOrDefault("startupComplete", false)) - { - return; - } - } - catch (CommandException | IOException e) - { - lastException = e; - } - } while (!timer.isTimedOut()); - - throw new RuntimeException("Server didn't finish starting.", lastException); + _bootstrapHelper.signIn(); } @Test From cfabdb1fb5f5dd261c369eadd14a727f02c5e812 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 18 Jan 2024 14:15:31 -0800 Subject: [PATCH 08/10] Fix Locator for Chrome --- src/org/labkey/test/Locator.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/org/labkey/test/Locator.java b/src/org/labkey/test/Locator.java index 63191ebe36..eed4ffd7be 100644 --- a/src/org/labkey/test/Locator.java +++ b/src/org/labkey/test/Locator.java @@ -1364,17 +1364,18 @@ public XPathLocator withPredicate(XPathLocator descendant) public XPathLocator withPredicate(@Language("XPath") String predicate) { - return this.append("[" + getRelativeXPath(predicate) + "]"); + // Make compatible with Chrome "/../self::*[predicate]" instead of "/..[predicate]" + return this.append((getLoc().endsWith("/..") ? "/self::*" : "") + "[" + getRelativeXPath(predicate) + "]"); } public XPathLocator withoutPredicate(@Language("XPath") String predicate) { - return this.append("[not(" + getRelativeXPath(predicate) + ")]"); + return this.withPredicate("not(" + getRelativeXPath(predicate) + ")"); } public XPathLocator withoutPredicate(XPathLocator predicate) { - return this.append("[not(" + getRelativeXPath(predicate.toXpath()) + ")]"); + return this.withoutPredicate(getRelativeXPath(predicate.toXpath())); } public XPathLocator attributeStartsWith(String attribute, String text) From 4e916ec8433f4af18416ffd7d6376dcbe4ecde2d Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 13 Mar 2024 14:52:22 -0700 Subject: [PATCH 09/10] Newest selenium version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index fff1cea485..79be19ead5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ awaitilityVersion=4.2.0 lookfirstSardineVersion=5.7 jettyVersion=12.0.4 -seleniumVersion=4.16.0 +seleniumVersion=4.18.0 mockserverNettyVersion=5.15.0 labkeySchemasTestVersion=23.11-SNAPSHOT From cdf5fe89548164184c61df2160c40964e3bb536f Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 14 Mar 2024 17:21:36 -0700 Subject: [PATCH 10/10] Create initial user via API when appropriate --- src/org/labkey/test/tests/JUnitTest.java | 9 +- src/org/labkey/test/util/APIUserHelper.java | 20 +++-- .../labkey/test/util/ApiBootstrapHelper.java | 82 +++++++++++-------- .../test/util/QuickBootstrapPseudoTest.java | 34 +------- 4 files changed, 73 insertions(+), 72 deletions(-) diff --git a/src/org/labkey/test/tests/JUnitTest.java b/src/org/labkey/test/tests/JUnitTest.java index 0629531a1c..cf9a46ce5b 100644 --- a/src/org/labkey/test/tests/JUnitTest.java +++ b/src/org/labkey/test/tests/JUnitTest.java @@ -44,6 +44,7 @@ import org.labkey.remoteapi.collections.CaseInsensitiveHashMap; import org.labkey.test.BaseWebDriverTest; import org.labkey.test.ExtraSiteWrapper; +import org.labkey.test.LabKeySiteWrapper; import org.labkey.test.Runner; import org.labkey.test.SuiteFactory; import org.labkey.test.TestProperties; @@ -115,13 +116,15 @@ private static void upgradeHelper(boolean skipInitialUserChecks) { // TODO: remove upgrade helper from JUnitTest and run before suite starts. - ExtraSiteWrapper bootstrapBrowser = new ExtraSiteWrapper(WebDriverWrapper.BrowserType.FIREFOX, - ArtifactCollector.ensureDumpDir(JUnitTest.class.getSimpleName())); if (skipInitialUserChecks) { - bootstrapBrowser = new ApiBootstrapHelper(bootstrapBrowser.getDriver()); + new ApiBootstrapHelper().signIn(); + return; } + ExtraSiteWrapper bootstrapBrowser = new ExtraSiteWrapper(WebDriverWrapper.BrowserType.FIREFOX, + ArtifactCollector.ensureDumpDir(JUnitTest.class.getSimpleName())); + try { // sign in performs upgrade if necessary diff --git a/src/org/labkey/test/util/APIUserHelper.java b/src/org/labkey/test/util/APIUserHelper.java index c1dfbe6a57..52b08084a6 100644 --- a/src/org/labkey/test/util/APIUserHelper.java +++ b/src/org/labkey/test/util/APIUserHelper.java @@ -37,6 +37,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.function.Supplier; import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; @@ -45,9 +46,17 @@ public class APIUserHelper extends AbstractUserHelper { + private final Supplier connectionSupplier; + public APIUserHelper(Supplier connectionSupplier) + { + super(null); + this.connectionSupplier = connectionSupplier; + } + public APIUserHelper(WebDriverWrapper driver) { super(driver); + connectionSupplier = driver::createDefaultConnection; } @Override @@ -82,7 +91,7 @@ public void _setDisplayName(String email, String newDisplayName) try { - new QueryApiHelper(getWrapper().createDefaultConnection(), "/", "core", "siteusers") + new QueryApiHelper(connectionSupplier.get(), "/", "core", "siteusers") .updateRows(List.of(Maps.of("userId", userId, "DisplayName", newDisplayName))); } catch (IOException | CommandException e) @@ -122,7 +131,7 @@ public JSONObject getJsonObject() } }; command.setSendEmail(sendEmail); - Connection connection = getWrapper().createDefaultConnection(); + Connection connection = connectionSupplier.get(); try { CreateUserResponse response = command.execute(connection, ""); @@ -157,8 +166,8 @@ public GetUsersResponse getUsers(boolean includeInactive) { GetUsersCommand command = new GetUsersCommand(); command.setIncludeInactive(includeInactive); - Connection connection = getWrapper().createDefaultConnection(); - if (getWrapper().isImpersonating()) + Connection connection = connectionSupplier.get(); + if (getWrapper() != null && getWrapper().isImpersonating()) { // Don't use browser session. Tests often call 'getDisplayNameForEmail' while impersonating non-admins. connection = WebTestHelper.getRemoteApiConnection(false); @@ -205,11 +214,10 @@ protected void _deleteUser(String userEmail) private void deleteUser(@NotNull Integer userId) { - Connection connection = getWrapper().createDefaultConnection(); DeleteUserCommand command = new DeleteUserCommand(userId); try { - command.execute(connection, "/"); + command.execute(connectionSupplier.get(), "/"); } catch (IOException|CommandException e) { diff --git a/src/org/labkey/test/util/ApiBootstrapHelper.java b/src/org/labkey/test/util/ApiBootstrapHelper.java index a8bfe21809..5d0696122d 100644 --- a/src/org/labkey/test/util/ApiBootstrapHelper.java +++ b/src/org/labkey/test/util/ApiBootstrapHelper.java @@ -1,33 +1,38 @@ package org.labkey.test.util; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; +import org.apache.hc.core5.http.message.BasicNameValuePair; import org.json.JSONObject; import org.labkey.remoteapi.CommandException; import org.labkey.remoteapi.CommandResponse; import org.labkey.remoteapi.Connection; import org.labkey.remoteapi.GuestCredentialsProvider; +import org.labkey.remoteapi.PostCommand; import org.labkey.remoteapi.SimpleGetCommand; import org.labkey.remoteapi.SimplePostCommand; import org.labkey.test.ExtraSiteWrapper; import org.labkey.test.LabKeySiteWrapper; +import org.labkey.test.WebDriverWrapper; import org.labkey.test.WebTestHelper; import org.labkey.test.components.core.login.SetPasswordForm; import org.openqa.selenium.WebDriver; import java.io.IOException; +import java.net.URI; import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import static org.labkey.test.WebTestHelper.getBaseURL; /** * Bootstrap a server without the initial user validation done by {@link LabKeySiteWrapper#signIn()} - * Requires that we are able to create the initial user via API. */ -public class ApiBootstrapHelper extends ExtraSiteWrapper +public class ApiBootstrapHelper { - public ApiBootstrapHelper(WebDriver driver) - { - super(driver); - } + public ApiBootstrapHelper() { } - @Override public void signIn() { waitForStartup(); @@ -35,7 +40,7 @@ public void signIn() { createInitialUser(); waitForBootstrap(); - new APIUserHelper(this).setInjectionDisplayName(PasswordUtil.getUsername()); + new APIUserHelper(this::createDefaultConnection).setInjectionDisplayName(PasswordUtil.getUsername()); } else { @@ -46,7 +51,7 @@ public void signIn() @LogMethod private void waitForStartup() { - Connection cn = new Connection(WebTestHelper.getBaseURL(), new GuestCredentialsProvider()); + Connection cn = new Connection(getBaseURL(), new GuestCredentialsProvider()); SimpleGetCommand command = new SimpleGetCommand("admin", "healthCheck"); Exception lastException = null; @@ -76,7 +81,7 @@ private void waitForStartup() { lastException = e; } - sleep(500); + WebDriverWrapper.sleep(500); } while (!timer.isTimedOut()); throw new RuntimeException("Server not done starting up.", lastException); @@ -85,7 +90,7 @@ private void waitForStartup() @LogMethod private boolean isInitialUserCreated() { - Connection cn = new Connection(WebTestHelper.getBaseURL(), new GuestCredentialsProvider()); + Connection cn = new Connection(getBaseURL(), new GuestCredentialsProvider()); SimplePostCommand command = new SimplePostCommand("admin", "configurationSummary"); try @@ -106,29 +111,11 @@ private boolean isInitialUserCreated() } } - private void createInitialUser() - { - beginAt(WebTestHelper.buildURL("login", "initialUser")); - new SetPasswordForm(getDriver()) - .setEmail(PasswordUtil.getUsername()) - .setNewPassword(PasswordUtil.getPassword()) - .clickSubmit(90_000); - } - - /** - * TODO: Make this work so that we don't have to open a browser. - * The POST just ends up redirecting to the normal initial user view. - */ @LogMethod - private void createInitialUser_API() + private void createInitialUser() { - Connection cn = createDefaultConnection(); - SimplePostCommand initialUserCommand = new SimplePostCommand("login", "initialUser"); - JSONObject params = new JSONObject(); - params.put("email", PasswordUtil.getUsername()); - params.put("password", PasswordUtil.getPassword()); - params.put("password2", PasswordUtil.getPassword()); - initialUserCommand.setJsonObject(params); + Connection cn = new Connection(getBaseURL(), new GuestCredentialsProvider()); + CreateInitialUserCommand initialUserCommand = new CreateInitialUserCommand(); try { @@ -167,4 +154,35 @@ private void waitForBootstrap() throw new RuntimeException("Server didn't finish starting.", lastException); } + public Connection createDefaultConnection() + { + return new Connection(getBaseURL(), PasswordUtil.getUsername(), PasswordUtil.getPassword()); + } +} + +class CreateInitialUserCommand extends PostCommand +{ + public CreateInitialUserCommand() + { + super("login", "initialUser"); + } + + protected List getPostData() + { + List postData = new ArrayList<>(); + postData.add(new BasicNameValuePair("email", PasswordUtil.getUsername())); + postData.add(new BasicNameValuePair("password", PasswordUtil.getPassword())); + postData.add(new BasicNameValuePair("password2", PasswordUtil.getPassword())); + + return postData; + } + + @Override + protected HttpPost createRequest(URI uri) + { + // InitialUserAction is not a real API action, so we POST form data instead of JSON + HttpPost request = new HttpPost(uri); + request.setEntity(new UrlEncodedFormEntity(getPostData())); + return request; + } } diff --git a/src/org/labkey/test/util/QuickBootstrapPseudoTest.java b/src/org/labkey/test/util/QuickBootstrapPseudoTest.java index 5cce847ef8..ad7a4aa7e4 100644 --- a/src/org/labkey/test/util/QuickBootstrapPseudoTest.java +++ b/src/org/labkey/test/util/QuickBootstrapPseudoTest.java @@ -11,41 +11,13 @@ /** * Bootstrap a server without the initial user validation done by {@link LabKeySiteWrapper#signIn()} * Not actually a test. Just piggy-backing on the test harness to make it easier to run. - * TODO: Make this class extend {@link LabKeySiteWrapper} so that we don't open a browser. - * Requires that we are able to create the initial user via API. */ @Category({}) -public class QuickBootstrapPseudoTest extends BaseWebDriverTest +public class QuickBootstrapPseudoTest { - final ApiBootstrapHelper _bootstrapHelper = new ApiBootstrapHelper(getDriver()); - - @Override - public void signIn() - { - _bootstrapHelper.signIn(); - } - @Test - public void testNothing() - { - TestLogger.log(whoAmI().getParsedData().toString()); - } - - @Override - protected BrowserType bestBrowser() - { - return BrowserType.CHROME; - } - - @Override - protected String getProjectName() - { - return null; - } - - @Override - public List getAssociatedModules() + public void bootstrap() { - return Arrays.asList(); + new ApiBootstrapHelper().signIn(); } }