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