janhoy commented on code in PR #4738: URL: https://github.com/apache/solr/pull/4738#discussion_r3829469787
########## solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java: ########## @@ -0,0 +1,534 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.webapp; + +import com.carrotsearch.randomizedtesting.ThreadFilter; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakLingering; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.function.BooleanSupplier; +import java.util.function.Function; +import java.util.logging.Level; +import org.apache.lucene.tests.util.QuickPatchThreadsFilter; +import org.apache.lucene.util.SuppressForbidden; +import org.apache.solr.SolrIgnoredThreadsFilter; +import org.apache.solr.SolrTestCaseJ4; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.client.solrj.request.GenericSolrRequest; +import org.apache.solr.cloud.SolrCloudTestCase; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.common.util.EnvUtils; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.embedded.JettyConfig; +import org.apache.solr.embedded.JettySolrRunner; +import org.apache.solr.util.ExternalPaths; +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.junit.AfterClass; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.rules.TestRule; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.StaleElementReferenceException; +import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.logging.LogEntry; +import org.openqa.selenium.logging.LogType; +import org.openqa.selenium.logging.LoggingPreferences; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for browser-based tests of the AngularJS Admin UI. + * + * <p>Starts a {@link SolrCloudTestCase} mini-cluster whose Jetty nodes also serve the Admin UI + * static files (see {@code JettyConfig.Builder#enableAdminUi(boolean)}), then drives the UI with a + * headless Chrome via Selenium WebDriver. + * + * <p>The tests require a locally installed Chrome/Chromium browser. Discovery order: the {@code + * tests.ui.chrome.binary} system property, the {@code CHROME_BIN} environment variable, then a list + * of well-known install locations. When no browser is found, all tests in the class are skipped via + * {@link Assume}. The matching chromedriver is provisioned by Selenium Manager, which may download + * it on first use (cached under {@code ~/.cache/selenium}); if that fails (e.g. offline), tests are + * likewise skipped. + */ [email protected](bugUrl = "Admin UI browser tests drive plain http") +@ThreadLeakFilters( + defaultFilters = true, + filters = { + SolrIgnoredThreadsFilter.class, + QuickPatchThreadsFilter.class, + AdminUiTestBase.WebDriverThreadsFilter.class + }) +@ThreadLeakLingering(linger = 5000) +public abstract class AdminUiTestBase extends SolrCloudTestCase { + + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + protected static final Duration WAIT_TIMEOUT = Duration.ofSeconds(15); + + protected static WebDriver driver; + + /** Base url of the first node, e.g. {@code http://127.0.0.1:PORT/solr} */ + protected static String baseUrl; + + /** + * Optional security.json for the cluster. Subclasses assign this in their {@code @BeforeClass} + * (which runs after this class's browser-starting one, but before the cluster starts lazily on + * first use). Never assign it in a {@code static} block: test runners may load all test classes + * up front, so static initializers of one class can run long before its suite executes. + */ + protected static String securityJson; + + /** + * When true (set by {@code AdminUiStandaloneTestBase}), no cloud cluster is started; the test + * class starts its own standalone {@link JettySolrRunner}(s), assigns {@link #standaloneJetty} + * and {@link #baseUrl}, and stops them again. + */ + protected static boolean standaloneMode = false; + + /** The standalone node backing {@link #adminApi} when {@link #standaloneMode} is set. */ + protected static JettySolrRunner standaloneJetty; + + /** + * Serves a minimal stand-in for the generated js-client bundle ({@code libs/solr/index.js}), + * which only exists inside the built webapp, not in the source tree tests serve from. The + * AngularJS {@code CollectionsV2} service fails to instantiate without the {@code solrApi} + * global, taking the whole Collections screen down with it. Only the small API surface the + * AngularJS UI actually uses is stubbed. + */ + public static class StubJsClientServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.setContentType("text/javascript"); + resp.getOutputStream() + .write( + ("var solrApi = {\n" + + " ApiClient: { instance: { basePath: '/api', defaultHeaders: {} } },\n" + + " CollectionsApi: function() {\n" + + " this.reloadCollection = function(name, callback) {\n" + + " var xhr = new XMLHttpRequest();\n" + + " xhr.open('POST', '/api/collections/' + name + '/reload');\n" + + " xhr.setRequestHeader('Content-Type', 'application/json');\n" + + " xhr.onload = function() { callback(null, null, {status: xhr.status}); };\n" + + " xhr.onerror = function() { callback(new Error('reload failed'), null, {status: xhr.status}); };\n" + + " xhr.send('{}');\n" + + " };\n" + + " }\n" + + "};\n") + .getBytes(StandardCharsets.UTF_8)); + } + } + + /** Ignores threads spawned by Selenium and the JDK http client it uses. */ + public static class WebDriverThreadsFilter implements ThreadFilter { + @Override + public boolean reject(Thread t) { + String name = t.getName(); + // JDK java.net.http client worker/selector threads (used by Selenium) are daemon + // threads in a shared pool that outlive WebDriver.quit() + return name.startsWith("HttpClient-") + // reaps the external chromedriver/chrome processes + || name.startsWith("process reaper") + // selenium driver-service startup checker pool, terminates on its own + || name.startsWith("UrlChecker-") + // selenium's chromedriver stdout/stderr pump, stops when the process exits + || name.startsWith("External Process Output Forwarder") + // JDK-internal scheduler backing CompletableFuture timeouts, lives forever + || name.equals("CompletableFutureDelayScheduler"); + } + } + + @BeforeClass + @SuppressForbidden(reason = "Selenium's logging preferences API uses java.util.logging levels") + public static void startClusterAndBrowser() throws Exception { + Path chrome = findChromeBinary(); + Assume.assumeTrue( + "No Chrome/Chromium binary found (set -Dtests.ui.chrome.binary=...), skipping UI tests", + chrome != null); + + // metrics are off by default in test clusters, but UI screens (e.g. Plugins) need them; + // restored after the class by SolrTestCase's SystemPropertiesRestoreRule + System.setProperty("metricsEnabled", "true"); + // the cluster starts lazily via ensureCloudCluster(), after subclass @BeforeClass + // methods have had the chance to configure securityJson or standalone mode + + ChromeOptions options = new ChromeOptions(); + options.setBinary(chrome.toString()); + options.addArguments( + "--headless=new", + "--window-size=1440,1024", + "--disable-gpu", + "--no-sandbox", + "--disable-dev-shm-usage"); + LoggingPreferences logPrefs = new LoggingPreferences(); + logPrefs.enable(LogType.BROWSER, Level.ALL); + options.setCapability("goog:loggingPrefs", logPrefs); + try { + driver = new ChromeDriver(options); + } catch (WebDriverException e) { + Assume.assumeNoException( + "Could not start ChromeDriver (chromedriver missing and not downloadable?)", e); + } + driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30)); + } + + /** Starts the 2-node cloud cluster serving the UI, unless already started. */ + protected static void ensureCloudCluster() { + if (standaloneMode || cluster != null) { + return; + } + try { + var clusterBuilder = + configureCluster(2).withJettyConfig(AdminUiTestBase::configureJettyForUi); + if (securityJson != null) { + clusterBuilder.withSecurityJson(securityJson); + } + clusterBuilder.configure(); + baseUrl = cluster.getJettySolrRunner(0).getBaseUrl().toString(); + } catch (Exception e) { + throw new RuntimeException("Could not start UI test cluster", e); + } + } + + /** Configures a Jetty node to serve the Admin UI plus the js-client stub. */ + protected static void configureJettyForUi(JettyConfig.Builder jetty) { + jetty + .enableAdminUi(true) + // exact-path mapping takes precedence over the static /libs/* servlet + .withServlet(new ServletHolder(new StubJsClientServlet()), "/libs/solr/index.js"); + } + + @AfterClass + public static void stopBrowser() { + // reset the static per-class configuration: several test classes run in the same + // JVM, and flags set by one class's static initializer must not leak into the next + standaloneMode = false; + standaloneJetty = null; + securityJson = null; + if (driver != null) { + try { + driver.quit(); + } finally { + driver = null; + baseUrl = null; + } + } + } + + /** Captures a screenshot and the page source when a test fails, for post-mortem debugging. */ + @Rule + public final TestRule screenshotOnFailure = + new TestWatcher() { + @Override + protected void failed(Throwable e, Description description) { + if (driver == null) return; + try { + Path dir = createTempDir("ui-failure-" + description.getMethodName()); + byte[] png = ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES); + Files.write(dir.resolve("screenshot.png"), png); + Files.writeString(dir.resolve("page.html"), driver.getPageSource()); + StringBuilder console = new StringBuilder(); + for (LogEntry entry : driver.manage().logs().get(LogType.BROWSER).getAll()) { + console.append(entry.getLevel()).append(' ').append(entry.getMessage()).append('\n'); + } + Files.writeString(dir.resolve("console.log"), console.toString()); + log.error("UI test failure artifacts saved to {}", dir); + } catch (Exception suppressed) { + log.warn("Could not save UI failure artifacts", suppressed); + } + } + }; + + /** + * Navigates to an Admin UI page and waits for a screen-specific anchor element to be visible. + * + * @param route the Angular hash route without leading {@code #/}, e.g. {@code ""} (dashboard), + * {@code "~cloud"} or {@code "collection1/query"} + * @param anchor a locator for an element that indicates the screen has rendered + * @return the anchor element + */ + protected static WebElement openPage(String route, By anchor) { + ensureCloudCluster(); + driver.get(baseUrl + "/index.html#/" + route); + return waitFor(anchor); + } + + /** Waits for the given element to be visible, up to {@link #WAIT_TIMEOUT}. */ + protected static WebElement waitFor(By locator) { + return poll(locator, el -> el.isDisplayed() ? el : null, "visible element"); + } + + /** Waits until the given element has non-blank text, and returns the text. */ + protected static String waitForText(By locator) { + return poll( + locator, + el -> { + String text = el.getText(); + return el.isDisplayed() && !text.isBlank() ? text : null; + }, + "non-empty text"); + } + + /** + * Polls the given element until {@code condition} returns non-null (a fresh lookup each round, so + * elements replaced by Angular re-renders are tolerated), failing after {@link #WAIT_TIMEOUT}. + */ + private static <T> T poll(By locator, Function<WebElement, T> condition, String description) { + long deadlineNanos = System.nanoTime() + WAIT_TIMEOUT.toNanos(); + WebDriverException lastException = null; + while (System.nanoTime() < deadlineNanos) { + try { + T result = condition.apply(driver.findElement(locator)); + if (result != null) { + return result; + } + lastException = null; + } catch (NoSuchElementException | StaleElementReferenceException e) { + lastException = e; + } + try { + Thread.sleep(200); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + throw new AssertionError( + "Timed out waiting for " + description + " at " + locator, lastException); + } + + /** + * Issues a GET request to the given admin path (e.g. {@code /admin/info/system}) on the same node + * the browser talks to, and returns the parsed response. Used to fetch the expected values that + * the UI should display. + */ + protected static NamedList<Object> adminApi(String path, SolrParams params) + throws IOException, SolrServerException { + ensureCloudCluster(); + JettySolrRunner jetty = standaloneMode ? standaloneJetty : cluster.getJettySolrRunner(0); + try (SolrClient client = jetty.newClient()) { + return client.request(new GenericSolrRequest(SolrRequest.METHOD.GET, path, params)); + } + } + + /** + * Uploads the default configset under the collection's name and creates the collection. A + * single-replica collection is pinned to the node the browser talks to, so core-level screens + * find its core locally. + */ + protected static void createFixtureCollection(String name, int numShards, int numReplicas) + throws Exception { + ensureCloudCluster(); + cluster.uploadConfigSet(ExternalPaths.DEFAULT_CONFIGSET, name); + CollectionAdminRequest.Create create = Review Comment: Kept `CollectionAdminRequest` here: it gives `setCreateNodeSet()` and pairs with `cluster.waitForActiveCollection()`. The generated `CreateCollection` would be more verbose for a test fixture. ########## solr/test-framework/src/java/org/apache/solr/embedded/JettyConfig.java: ########## @@ -37,6 +37,9 @@ public class JettyConfig { public final boolean enableV2; public final boolean enableGracefulShutdown; + /** If true, serve the Admin UI static files and index.html like the production web.xml does. */ + public final boolean enableAdminUi; + private JettyConfig( Review Comment: Agreed, but pre-existing and private — this PR only adds one flag. Worth a separate refactor JIRA; happy to file one. ########## solr/test-framework/src/java/org/apache/solr/embedded/JettySolrRunner.java: ########## Review Comment: Agreed — when we have a container-based harness this can go away; until then the opt-in flag keeps it in-JVM and fast. ########## solr/webapp/build.gradle: ########## @@ -43,6 +54,39 @@ dependencies { if (gradle.ext.withUiModule) { generatedUIBundle project(path: ":solr:ui", configuration: "wasmJsUIBundle") } + + // Browser-based tests of the AngularJS Admin UI (see src/test) + testImplementation project(':solr:core') + testImplementation project(':solr:solrj') + testImplementation project(':solr:test-framework') + // puts the /sql handler on the in-JVM server classpath so the SQL screen works + testRuntimeOnly project(':solr:modules:sql') + testImplementation libs.carrotsearch.randomizedtesting.runner + testImplementation libs.eclipse.jetty.ee10.servlet + testImplementation libs.jakarta.servlet.api + testImplementation libs.junit.junit + testImplementation(libs.selenium.chromedriver, { + // solr-core already provides the opentelemetry api/sdk that selenium's optional + // tracing uses; the remaining excludes are compile-time-only annotation jars + exclude group: 'io.opentelemetry' + exclude group: 'io.opentelemetry.instrumentation' + exclude group: 'io.opentelemetry.semconv' + exclude group: 'com.google.auto.service' + exclude group: 'org.jspecify' Review Comment: Tried it: Selenium pulls jspecify 1.0.1 while the rest of the build resolves 1.0.0, so this module's lockfile would carry two versions plus a new `licenses/jspecify-1.0.1.jar.sha1` for a test-only annotation jar. Kept the exclusion. ########## solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java: ########## @@ -0,0 +1,534 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.webapp; + +import com.carrotsearch.randomizedtesting.ThreadFilter; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakLingering; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.function.BooleanSupplier; +import java.util.function.Function; +import java.util.logging.Level; +import org.apache.lucene.tests.util.QuickPatchThreadsFilter; +import org.apache.lucene.util.SuppressForbidden; +import org.apache.solr.SolrIgnoredThreadsFilter; +import org.apache.solr.SolrTestCaseJ4; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.client.solrj.request.GenericSolrRequest; +import org.apache.solr.cloud.SolrCloudTestCase; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.common.util.EnvUtils; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.embedded.JettyConfig; +import org.apache.solr.embedded.JettySolrRunner; +import org.apache.solr.util.ExternalPaths; +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.junit.AfterClass; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.rules.TestRule; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.StaleElementReferenceException; +import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.logging.LogEntry; +import org.openqa.selenium.logging.LogType; +import org.openqa.selenium.logging.LoggingPreferences; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for browser-based tests of the AngularJS Admin UI. + * + * <p>Starts a {@link SolrCloudTestCase} mini-cluster whose Jetty nodes also serve the Admin UI + * static files (see {@code JettyConfig.Builder#enableAdminUi(boolean)}), then drives the UI with a + * headless Chrome via Selenium WebDriver. + * + * <p>The tests require a locally installed Chrome/Chromium browser. Discovery order: the {@code + * tests.ui.chrome.binary} system property, the {@code CHROME_BIN} environment variable, then a list + * of well-known install locations. When no browser is found, all tests in the class are skipped via + * {@link Assume}. The matching chromedriver is provisioned by Selenium Manager, which may download + * it on first use (cached under {@code ~/.cache/selenium}); if that fails (e.g. offline), tests are + * likewise skipped. + */ [email protected](bugUrl = "Admin UI browser tests drive plain http") +@ThreadLeakFilters( + defaultFilters = true, + filters = { + SolrIgnoredThreadsFilter.class, + QuickPatchThreadsFilter.class, + AdminUiTestBase.WebDriverThreadsFilter.class + }) +@ThreadLeakLingering(linger = 5000) +public abstract class AdminUiTestBase extends SolrCloudTestCase { + + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + protected static final Duration WAIT_TIMEOUT = Duration.ofSeconds(15); + + protected static WebDriver driver; + + /** Base url of the first node, e.g. {@code http://127.0.0.1:PORT/solr} */ + protected static String baseUrl; + + /** + * Optional security.json for the cluster. Subclasses assign this in their {@code @BeforeClass} + * (which runs after this class's browser-starting one, but before the cluster starts lazily on + * first use). Never assign it in a {@code static} block: test runners may load all test classes + * up front, so static initializers of one class can run long before its suite executes. + */ + protected static String securityJson; + + /** + * When true (set by {@code AdminUiStandaloneTestBase}), no cloud cluster is started; the test + * class starts its own standalone {@link JettySolrRunner}(s), assigns {@link #standaloneJetty} + * and {@link #baseUrl}, and stops them again. + */ + protected static boolean standaloneMode = false; + + /** The standalone node backing {@link #adminApi} when {@link #standaloneMode} is set. */ + protected static JettySolrRunner standaloneJetty; + + /** + * Serves a minimal stand-in for the generated js-client bundle ({@code libs/solr/index.js}), + * which only exists inside the built webapp, not in the source tree tests serve from. The + * AngularJS {@code CollectionsV2} service fails to instantiate without the {@code solrApi} + * global, taking the whole Collections screen down with it. Only the small API surface the + * AngularJS UI actually uses is stubbed. + */ + public static class StubJsClientServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.setContentType("text/javascript"); + resp.getOutputStream() + .write( + ("var solrApi = {\n" Review Comment: Done in 40730ba. ########## solr/webapp/src/test/org/apache/solr/webapp/AdminUiTestBase.java: ########## @@ -0,0 +1,534 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.webapp; + +import com.carrotsearch.randomizedtesting.ThreadFilter; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakLingering; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.function.BooleanSupplier; +import java.util.function.Function; +import java.util.logging.Level; +import org.apache.lucene.tests.util.QuickPatchThreadsFilter; +import org.apache.lucene.util.SuppressForbidden; +import org.apache.solr.SolrIgnoredThreadsFilter; +import org.apache.solr.SolrTestCaseJ4; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.client.solrj.request.GenericSolrRequest; +import org.apache.solr.cloud.SolrCloudTestCase; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.common.util.EnvUtils; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.embedded.JettyConfig; +import org.apache.solr.embedded.JettySolrRunner; +import org.apache.solr.util.ExternalPaths; +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.junit.AfterClass; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.rules.TestRule; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.StaleElementReferenceException; +import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.logging.LogEntry; +import org.openqa.selenium.logging.LogType; +import org.openqa.selenium.logging.LoggingPreferences; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for browser-based tests of the AngularJS Admin UI. + * + * <p>Starts a {@link SolrCloudTestCase} mini-cluster whose Jetty nodes also serve the Admin UI + * static files (see {@code JettyConfig.Builder#enableAdminUi(boolean)}), then drives the UI with a + * headless Chrome via Selenium WebDriver. + * + * <p>The tests require a locally installed Chrome/Chromium browser. Discovery order: the {@code + * tests.ui.chrome.binary} system property, the {@code CHROME_BIN} environment variable, then a list + * of well-known install locations. When no browser is found, all tests in the class are skipped via + * {@link Assume}. The matching chromedriver is provisioned by Selenium Manager, which may download + * it on first use (cached under {@code ~/.cache/selenium}); if that fails (e.g. offline), tests are + * likewise skipped. + */ [email protected](bugUrl = "Admin UI browser tests drive plain http") +@ThreadLeakFilters( + defaultFilters = true, + filters = { + SolrIgnoredThreadsFilter.class, + QuickPatchThreadsFilter.class, + AdminUiTestBase.WebDriverThreadsFilter.class + }) +@ThreadLeakLingering(linger = 5000) +public abstract class AdminUiTestBase extends SolrCloudTestCase { + + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + protected static final Duration WAIT_TIMEOUT = Duration.ofSeconds(15); + + protected static WebDriver driver; + + /** Base url of the first node, e.g. {@code http://127.0.0.1:PORT/solr} */ + protected static String baseUrl; + + /** + * Optional security.json for the cluster. Subclasses assign this in their {@code @BeforeClass} + * (which runs after this class's browser-starting one, but before the cluster starts lazily on + * first use). Never assign it in a {@code static} block: test runners may load all test classes + * up front, so static initializers of one class can run long before its suite executes. + */ + protected static String securityJson; + + /** + * When true (set by {@code AdminUiStandaloneTestBase}), no cloud cluster is started; the test + * class starts its own standalone {@link JettySolrRunner}(s), assigns {@link #standaloneJetty} + * and {@link #baseUrl}, and stops them again. + */ + protected static boolean standaloneMode = false; + + /** The standalone node backing {@link #adminApi} when {@link #standaloneMode} is set. */ + protected static JettySolrRunner standaloneJetty; + + /** + * Serves a minimal stand-in for the generated js-client bundle ({@code libs/solr/index.js}), + * which only exists inside the built webapp, not in the source tree tests serve from. The + * AngularJS {@code CollectionsV2} service fails to instantiate without the {@code solrApi} + * global, taking the whole Collections screen down with it. Only the small API surface the + * AngularJS UI actually uses is stubbed. + */ + public static class StubJsClientServlet extends HttpServlet { Review Comment: Done in 40730ba — the build now hands the bundle built by `:solr:webapp:js-client` to the test JVM via `tests.ui.jsclient.bundle`, so tests serve the real thing. The stub only remains as fallback for `-PdisableJsClient=true`. Full suite green against the real bundle. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
