diff --git a/examples/greetings/src/test/java/io/quarkus/qe/DevModeGreetingResourceIT.java b/examples/greetings/src/test/java/io/quarkus/qe/DevModeGreetingResourceIT.java
index fd09b10e3..a72861839 100644
--- a/examples/greetings/src/test/java/io/quarkus/qe/DevModeGreetingResourceIT.java
+++ b/examples/greetings/src/test/java/io/quarkus/qe/DevModeGreetingResourceIT.java
@@ -3,7 +3,6 @@
import static org.hamcrest.Matchers.is;
import org.apache.http.HttpStatus;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
@@ -16,11 +15,9 @@
import io.quarkus.test.services.DevModeQuarkusApplication;
import io.quarkus.test.utils.AwaitilityUtils;
-// TODO: mvavrik enable and adapt to new continuous testing page
-@Disabled("Disabled as DEV UI continuous testing is currently re-worked")
@QuarkusScenario
@DisabledOnNative
-@DisabledOnQuarkusVersion(version = "1\\..*", reason = "Continuous Testing was entered in 2.x")
+@DisabledOnQuarkusVersion(version = "3.0.0.CR2", reason = "Continuous Testing page was added to DEV UI in Quarkus 3.0.0.Final")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class DevModeGreetingResourceIT {
diff --git a/pom.xml b/pom.xml
index f9d119fad..28e8cdaec 100644
--- a/pom.xml
+++ b/pom.xml
@@ -59,6 +59,7 @@
10.9.2
+ 1.32.0
@@ -191,6 +192,12 @@
simpleclient_pushgateway
${prometheus.simpleclient_pushgateway.version}
+
+ com.microsoft.playwright
+ playwright
+
+ ${playwright.version}
+
diff --git a/quarkus-test-cli/src/main/java/io/quarkus/test/bootstrap/QuarkusCliClient.java b/quarkus-test-cli/src/main/java/io/quarkus/test/bootstrap/QuarkusCliClient.java
index b323e7d38..88f07219a 100644
--- a/quarkus-test-cli/src/main/java/io/quarkus/test/bootstrap/QuarkusCliClient.java
+++ b/quarkus-test-cli/src/main/java/io/quarkus/test/bootstrap/QuarkusCliClient.java
@@ -12,8 +12,6 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import org.apache.commons.lang3.StringUtils;
-
import io.quarkus.test.configuration.PropertyLookup;
import io.quarkus.test.logging.FileLoggingHandler;
import io.quarkus.test.logging.Log;
@@ -85,11 +83,11 @@ public QuarkusCliRestService createApplication(String name, CreateApplicationReq
List args = new ArrayList<>();
args.addAll(Arrays.asList("create", "app", name));
// Platform Bom
- if (StringUtils.isNotEmpty(request.platformBom)) {
+ if (isNotEmpty(request.platformBom)) {
args.add("--platform-bom=" + request.platformBom);
}
// Stream
- if (StringUtils.isNotEmpty(request.stream)) {
+ if (isNotEmpty(request.stream)) {
args.add("--stream=" + request.stream);
}
// Extensions
@@ -107,6 +105,10 @@ public QuarkusCliRestService createApplication(String name, CreateApplicationReq
return service;
}
+ private static boolean isNotEmpty(String str) {
+ return str != null && !str.isEmpty();
+ }
+
public QuarkusCliDefaultService createExtension(String name) {
return createExtension(name, CreateExtensionRequest.defaults());
}
@@ -122,11 +124,11 @@ public QuarkusCliDefaultService createExtension(String name, CreateExtensionRequ
List args = new ArrayList<>();
args.addAll(Arrays.asList("create", "extension", name));
// Platform Bom
- if (StringUtils.isNotEmpty(request.platformBom)) {
+ if (isNotEmpty(request.platformBom)) {
args.add("--platform-bom=" + request.platformBom);
}
// Stream
- if (StringUtils.isNotEmpty(request.stream)) {
+ if (isNotEmpty(request.stream)) {
args.add("--stream=" + request.stream);
}
// Extra args
diff --git a/quarkus-test-cli/src/main/java/io/quarkus/test/services/quarkus/CliDevModeLocalhostQuarkusApplicationManagedResource.java b/quarkus-test-cli/src/main/java/io/quarkus/test/services/quarkus/CliDevModeLocalhostQuarkusApplicationManagedResource.java
index fa623b47d..85bbd61b0 100644
--- a/quarkus-test-cli/src/main/java/io/quarkus/test/services/quarkus/CliDevModeLocalhostQuarkusApplicationManagedResource.java
+++ b/quarkus-test-cli/src/main/java/io/quarkus/test/services/quarkus/CliDevModeLocalhostQuarkusApplicationManagedResource.java
@@ -7,8 +7,6 @@
import java.util.List;
import java.util.Map;
-import org.apache.commons.lang3.StringUtils;
-
import io.quarkus.test.bootstrap.Protocol;
import io.quarkus.test.bootstrap.QuarkusCliClient;
import io.quarkus.test.bootstrap.ServiceContext;
@@ -124,7 +122,7 @@ private void assignPorts() {
private int getOrAssignPortByProperty(String property) {
return serviceContext.getOwner().getProperty(property)
- .filter(StringUtils::isNotEmpty)
+ .filter(str -> !str.isEmpty())
.map(Integer::parseInt)
.orElseGet(SocketUtils::findAvailablePort);
}
diff --git a/quarkus-test-core/pom.xml b/quarkus-test-core/pom.xml
index 2ae172e8c..abbf9c283 100644
--- a/quarkus-test-core/pom.xml
+++ b/quarkus-test-core/pom.xml
@@ -38,8 +38,8 @@
pom
- net.sourceforge.htmlunit
- htmlunit
+ com.microsoft.playwright
+ playwright
io.opentelemetry
diff --git a/quarkus-test-core/src/main/java/io/quarkus/test/bootstrap/DevModeQuarkusService.java b/quarkus-test-core/src/main/java/io/quarkus/test/bootstrap/DevModeQuarkusService.java
index 9ec02e4f9..265d09856 100644
--- a/quarkus-test-core/src/main/java/io/quarkus/test/bootstrap/DevModeQuarkusService.java
+++ b/quarkus-test-core/src/main/java/io/quarkus/test/bootstrap/DevModeQuarkusService.java
@@ -1,45 +1,107 @@
package io.quarkus.test.bootstrap;
-import static java.util.stream.Collectors.toList;
-import static org.junit.jupiter.api.Assertions.assertEquals;
+import static io.quarkus.test.utils.AwaitilityUtils.untilAsserted;
+import static io.quarkus.test.utils.AwaitilityUtils.AwaitilitySettings.usingTimeout;
+import static java.time.Duration.ofSeconds;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
-import java.util.List;
+import java.util.Set;
import java.util.function.Function;
import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.Assertions;
-import com.gargoylesoftware.htmlunit.BrowserVersion;
-import com.gargoylesoftware.htmlunit.NicelyResynchronizingAjaxController;
-import com.gargoylesoftware.htmlunit.SilentCssErrorHandler;
-import com.gargoylesoftware.htmlunit.WebClient;
-import com.gargoylesoftware.htmlunit.html.HtmlElement;
-import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import com.microsoft.playwright.Browser;
+import com.microsoft.playwright.Page;
+import com.microsoft.playwright.Playwright;
public class DevModeQuarkusService extends RestService {
- public static final String DEV_UI_PATH = "/q/dev";
-
- private static final int JAVASCRIPT_WAIT_TIMEOUT_MILLIS = 10000;
- private static final String XPATH_BTN_CLASS = "contains(@class, 'btn')";
- private static final String XPATH_BTN_ON_OFF_CLASS = "contains(@class, 'btnPowerOnOffButton')";
- private static final String CONTINUOUS_TESTING_BTN = "//a[" + XPATH_BTN_CLASS + " and " + XPATH_BTN_ON_OFF_CLASS + "]";
- private static final String CONTINUOUS_TESTING_LABEL_DISABLED = "Tests not running";
+ private static final int WAITING_TIMEOUT_SEC = 15;
+ private static final String DEV_UI_CONTINUOUS_TESTING_PATH = "/q/dev-ui/continuous-testing";
+ private static final String START_CONTINUOUS_TESTING_BTN_CSS_ID = "#start-cnt-testing-btn";
+ private static final String NO_TESTS_FOUND = "No tests found";
+ private static final String TESTS_PAUSED = "Tests paused";
+ private static final String TESTS_ARE_PASSING = "tests are passing";
+ private static final String TESTS_IS_PASSING = "test is passing";
+ private static final String RUNNING_TESTS_FOR_1ST_TIME = "Running tests for the first time";
+ /**
+ * Following hooks are currently logged by {@link io.quarkus.deployment.dev.testing.TestConsoleHandler}.
+ * They should only be present if {@link #RUNNING_TESTS_FOR_1ST_TIME} is also logged, but testing for all of them
+ * make detection of the state of the continuous testing less error-prone.
+ */
+ private static final Set CONTINUOUS_TESTING_ENABLED_HOOKS = Set.of("Starting tests",
+ "Starting test run", NO_TESTS_FOUND, TESTS_IS_PASSING, TESTS_ARE_PASSING,
+ "All tests are now passing");
+
+ /**
+ * Enables continuous testing if and only if it wasn't enabled already (no matter what the current state is)
+ * and there are no tests to run or all tests are passing. Logic required for second re-enabling of continuous
+ * testing is more robust, and we don't really need it. Same goes for scenario when testing is enabled and tests fail.
+ *
+ * @return DevModeQuarkusService
+ */
public DevModeQuarkusService enableContinuousTesting() {
- HtmlPage webDevUi = webDevUiPage();
- // If the "enable continuous testing" btn is not found, we assume it's already enabled it.
- if (isContinuousTestingBtnDisabled(webDevUi)) {
- clickOnElement(getContinuousTestingBtn(webDevUi));
+ // check if continuous testing is disabled
+ if (testsArePaused()) {
+
+ // go to 'continuous-testing' page and click on 'Start' button which enables continuous testing
+ try (Playwright playwright = Playwright.create()) {
+ try (Browser browser = playwright.chromium().launch()) {
+ Page page = browser.newContext().newPage();
+ page.navigate(getContinuousTestingPath());
+ page.locator(START_CONTINUOUS_TESTING_BTN_CSS_ID).click();
+
+ // wait till enabling of continuous testing is finished
+ untilAsserted(() -> logs().assertContains(NO_TESTS_FOUND, TESTS_IS_PASSING, TESTS_ARE_PASSING),
+ usingTimeout(ofSeconds(WAITING_TIMEOUT_SEC)));
+ }
+ }
}
return this;
}
+ private String getContinuousTestingPath() {
+ return getURI(Protocol.HTTP).withPath(DEV_UI_CONTINUOUS_TESTING_PATH).toString();
+ }
+
+ private boolean testsArePaused() {
+ boolean testsArePaused = false;
+ for (String entry : getLogs()) {
+ if (entry.contains(TESTS_PAUSED)) {
+ testsArePaused = true;
+ // we intentionally continue looking as we need to be sure testing wasn't enabled in past
+ continue;
+ }
+
+ if (entry.contains(RUNNING_TESTS_FOR_1ST_TIME)) {
+ // continuous testing is already enabled
+ return false;
+ }
+
+ if (CONTINUOUS_TESTING_ENABLED_HOOKS.stream().anyMatch(entry::contains)) {
+ throw new IllegalStateException(String.format(
+ "Implementation of continuous testing in Quarkus application has changed as we detected "
+ + "'%s' log message, but message '%s' wasn't logged",
+ entry, RUNNING_TESTS_FOR_1ST_TIME));
+ }
+ }
+
+ if (testsArePaused) {
+ // continuous testing disabled
+ return true;
+ }
+
+ // we only get here if implementation has changed (e.g. hooks are different now),
+ // or there is a bug in continuous testing
+ throw new IllegalStateException("State of continuous testing couldn't be recognized");
+ }
+
public void modifyFile(String file, Function modifier) {
try {
File targetFile = getServiceFolder().resolve(file).toFile();
@@ -59,81 +121,12 @@ public void copyFile(String file, String target) {
FileUtils.deleteQuietly(targetPath);
FileUtils.copyFile(sourcePath.toFile(), targetPath);
- targetPath.setLastModified(System.currentTimeMillis());
+ if (!targetPath.setLastModified(System.currentTimeMillis())) {
+ throw new IllegalStateException("Failed to set the last-modified time of the file: " + targetPath.getPath());
+ }
} catch (IOException e) {
Assertions.fail("Error copying file. Caused by " + e.getMessage());
}
}
- public HtmlElement getContinuousTestingBtn(HtmlPage page) {
- List btn = getElementsByXPath(page, CONTINUOUS_TESTING_BTN);
- assertEquals(1, btn.size(), "Should be only one button to enable continuous testing");
- return btn.get(0);
- }
-
- public boolean isContinuousTestingBtnDisabled(HtmlPage page) {
- HtmlElement btn = getContinuousTestingBtn(page);
- return btn.getTextContent().trim().equals(CONTINUOUS_TESTING_LABEL_DISABLED);
- }
-
- public HtmlPage clickOnElement(HtmlElement elem) {
- try {
- return elem.click();
- } catch (IOException e) {
- Assertions.fail("Can't click on element. Caused by: " + e.getMessage());
- }
-
- return null;
- }
-
- public List getElementsByXPath(HtmlPage htmlPage, String path) {
- return htmlPage.getByXPath(path).stream()
- .filter(elem -> elem instanceof HtmlElement)
- .map(elem -> (HtmlElement) elem)
- .collect(toList());
- }
-
- public HtmlPage webDevUiPage() {
- try {
- HtmlPage page = (HtmlPage) webPage(DEV_UI_PATH).refresh();
- waitUntilLoaded(page);
- return page;
- } catch (IOException e) {
- return null;
- }
- }
-
- public HtmlPage webPage(String path) {
- try {
- var uri = getURI(Protocol.HTTP).withPath(path);
- return webClient().getPage(uri.toString());
- } catch (IOException e) {
- Assertions.fail("Page with path " + path + " does not exist");
- }
-
- return null;
- }
-
- public WebClient webClient() {
- WebClient webClient = new WebClient(BrowserVersion.CHROME);
- webClient.getCookieManager().clearCookies();
- webClient.getCache().clear();
- webClient.getCache().setMaxSize(0);
- webClient.setCssErrorHandler(new SilentCssErrorHandler());
- // re-synchronize asynchronous XHR.
- webClient.setAjaxController(new NicelyResynchronizingAjaxController());
- webClient.getOptions().setUseInsecureSSL(true);
- webClient.getOptions().setDownloadImages(false);
- webClient.getOptions().setGeolocationEnabled(false);
- webClient.getOptions().setAppletEnabled(false);
- webClient.getOptions().setCssEnabled(false);
- webClient.getOptions().setThrowExceptionOnScriptError(false);
- webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
- webClient.getOptions().setRedirectEnabled(true);
- return webClient;
- }
-
- private void waitUntilLoaded(HtmlPage page) {
- page.getEnclosingWindow().getJobManager().waitForJobs(JAVASCRIPT_WAIT_TIMEOUT_MILLIS);
- }
}
diff --git a/quarkus-test-core/src/main/java/io/quarkus/test/metrics/exporters/FileMetricsExporter.java b/quarkus-test-core/src/main/java/io/quarkus/test/metrics/exporters/FileMetricsExporter.java
index a805f4ad2..7b6483758 100644
--- a/quarkus-test-core/src/main/java/io/quarkus/test/metrics/exporters/FileMetricsExporter.java
+++ b/quarkus-test-core/src/main/java/io/quarkus/test/metrics/exporters/FileMetricsExporter.java
@@ -7,8 +7,6 @@
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
-import org.apache.commons.text.TextStringBuilder;
-
import io.quarkus.test.configuration.PropertyLookup;
public class FileMetricsExporter implements MetricsExporter {
@@ -34,11 +32,13 @@ public void push(String serviceName, Map labels) throws IOExcept
allMetrics.putAll(labels);
allMetrics.putAll(metrics);
- TextStringBuilder sw = new TextStringBuilder();
+ StringBuilder sb = new StringBuilder();
allMetrics.keySet().stream()
.sorted()
- .forEach(metricId -> sw.appendln(String.format("%s=%s", metricId, allMetrics.get(metricId))));
+ .forEach(metricId -> sb
+ .append(String.format("%s=%s", metricId, allMetrics.get(metricId)))
+ .append(System.lineSeparator()));
- Files.write(Path.of(METRICS_EXPORT_FILE_OUTPUT.get()), sw.toString().getBytes());
+ Files.write(Path.of(METRICS_EXPORT_FILE_OUTPUT.get()), sb.toString().getBytes());
}
}
diff --git a/quarkus-test-core/src/main/java/io/quarkus/test/utils/LogsVerifier.java b/quarkus-test-core/src/main/java/io/quarkus/test/utils/LogsVerifier.java
index 1d26fc30b..5a572834f 100644
--- a/quarkus-test-core/src/main/java/io/quarkus/test/utils/LogsVerifier.java
+++ b/quarkus-test-core/src/main/java/io/quarkus/test/utils/LogsVerifier.java
@@ -1,6 +1,8 @@
package io.quarkus.test.utils;
+import java.util.Arrays;
import java.util.List;
+import java.util.function.Predicate;
import org.junit.jupiter.api.Assertions;
@@ -18,14 +20,25 @@ public QuarkusLogsVerifier forQuarkus() {
return new QuarkusLogsVerifier(service);
}
- public void assertContains(String expectedLog) {
+ /**
+ * Asserts log contains any of {@code expectedLogs}.
+ */
+ public void assertContains(String... expectedLogs) {
+ Predicate containsExpectedLog = createExpectedLogPredicate(expectedLogs);
AwaitilityUtils.untilAsserted(() -> {
List actualLogs = service.getLogs();
- Assertions.assertTrue(actualLogs.stream().anyMatch(line -> line.contains(expectedLog)),
- "Log does not contain " + expectedLog + ". Full logs: " + actualLogs);
+ Assertions.assertTrue(actualLogs.stream().anyMatch(containsExpectedLog),
+ "Log does not contain any of '" + Arrays.toString(expectedLogs) + "'. Full logs: " + actualLogs);
});
}
+ private Predicate createExpectedLogPredicate(String[] expectedLogs) {
+ if (expectedLogs.length == 1) {
+ return log -> log.contains(expectedLogs[0]);
+ }
+ return log -> Arrays.stream(expectedLogs).anyMatch(log::contains);
+ }
+
public void assertDoesNotContain(String unexpectedLog) {
List actualLogs = service.getLogs();
Assertions.assertTrue(actualLogs.stream().noneMatch(line -> line.contains(unexpectedLog)),
diff --git a/quarkus-test-knative-events/root/src/main/java/io/quarkus/test/services/knative/eventing/FunqyKnativeEventsService.java b/quarkus-test-knative-events/root/src/main/java/io/quarkus/test/services/knative/eventing/FunqyKnativeEventsService.java
index 34868db67..891bef674 100644
--- a/quarkus-test-knative-events/root/src/main/java/io/quarkus/test/services/knative/eventing/FunqyKnativeEventsService.java
+++ b/quarkus-test-knative-events/root/src/main/java/io/quarkus/test/services/knative/eventing/FunqyKnativeEventsService.java
@@ -1,6 +1,7 @@
package io.quarkus.test.services.knative.eventing;
import static java.util.Objects.requireNonNull;
+import static org.apache.http.HttpStatus.SC_NOT_FOUND;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@@ -9,7 +10,6 @@
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
-import org.eclipse.jetty.http.HttpStatus;
import org.hamcrest.Matcher;
import io.fabric8.knative.client.KnativeClient;
@@ -289,7 +289,7 @@ public ForwardResponseValidator get() {
}
private ForwardResponseValidator validate(Response response) {
- if (response.statusCode() == HttpStatus.NOT_FOUND_404) {
+ if (response.statusCode() == SC_NOT_FOUND) {
// We need Funqy function that forward cloud events to the broker. Brokers are internal by design.
// We need a way to send events to the broker. We could expose another service, or use 'DomainMapping', but
// that's less efficient than using existing app. 'clusterEndpoint' is a Funqy function that we call directly.
diff --git a/quarkus-test-service-consul/src/main/java/io/quarkus/test/bootstrap/ConsulService.java b/quarkus-test-service-consul/src/main/java/io/quarkus/test/bootstrap/ConsulService.java
index 5c3d0a995..9f8593d27 100644
--- a/quarkus-test-service-consul/src/main/java/io/quarkus/test/bootstrap/ConsulService.java
+++ b/quarkus-test-service-consul/src/main/java/io/quarkus/test/bootstrap/ConsulService.java
@@ -4,8 +4,7 @@
import java.io.IOException;
import java.nio.charset.StandardCharsets;
-
-import org.apache.commons.io.IOUtils;
+import java.util.Objects;
import com.google.common.net.HostAndPort;
import com.orbitz.consul.Consul;
@@ -21,9 +20,11 @@ public void loadPropertiesFromString(String key, String content) {
public void loadPropertiesFromFile(String key, String file) {
KeyValueClient kvClient = consulClient().keyValueClient();
try {
- String properties = IOUtils.toString(
- ConsulService.class.getClassLoader().getResourceAsStream(file),
- StandardCharsets.UTF_8);
+ String properties;
+ try (var is = ConsulService.class.getClassLoader().getResourceAsStream(file)) {
+ Objects.requireNonNull(is, "Failed to find file " + file);
+ properties = new String(is.readAllBytes(), StandardCharsets.UTF_8);
+ }
kvClient.putValue(key, properties);
} catch (IOException e) {
fail("Failed to load properties. Caused by " + e.getMessage());
diff --git a/quarkus-test-service-infinispan/src/main/java/io/quarkus/test/bootstrap/InfinispanService.java b/quarkus-test-service-infinispan/src/main/java/io/quarkus/test/bootstrap/InfinispanService.java
index 3662c9fae..eac5fb3e9 100644
--- a/quarkus-test-service-infinispan/src/main/java/io/quarkus/test/bootstrap/InfinispanService.java
+++ b/quarkus-test-service-infinispan/src/main/java/io/quarkus/test/bootstrap/InfinispanService.java
@@ -7,8 +7,6 @@
import java.util.Arrays;
import java.util.List;
-import org.apache.commons.lang3.StringUtils;
-
public class InfinispanService extends BaseService {
public static final String USERNAME_DEFAULT = "my_username";
@@ -63,7 +61,7 @@ public InfinispanService onPreStart(Action action) {
withProperty("USER", getUsername());
withProperty("PASS", getPassword());
- if (StringUtils.isNotEmpty(configFile)) {
+ if (configFile != null && !configFile.isEmpty()) {
// legacy -> Infinispan previous to version 14
withProperty("CONFIG_PATH", RESOURCE_PREFIX + SLASH + configFile);
// Infinispan 14+ configuration setup