Skip to content

Commit

Permalink
Adapt enabling of continuous testing to new DEV UI
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Apr 14, 2023
1 parent ee81290 commit 509738a
Show file tree
Hide file tree
Showing 11 changed files with 134 additions and 125 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand Down
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
<checkstyle.version>10.9.2</checkstyle.version>
<!-- Code Coverage Properties-->
<jacoco.agent.argLine />
<playwright.version>1.32.0</playwright.version>
</properties>
<distributionManagement>
<snapshotRepository>
Expand Down Expand Up @@ -191,6 +192,12 @@
<artifactId>simpleclient_pushgateway</artifactId>
<version>${prometheus.simpleclient_pushgateway.version}</version>
</dependency>
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<!-- TODO: upstream is also considering using of 'playwright', use managed version if that happens -->
<version>${playwright.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -85,11 +83,11 @@ public QuarkusCliRestService createApplication(String name, CreateApplicationReq
List<String> 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
Expand All @@ -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());
}
Expand All @@ -122,11 +124,11 @@ public QuarkusCliDefaultService createExtension(String name, CreateExtensionRequ
List<String> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
4 changes: 2 additions & 2 deletions quarkus-test-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
<type>pom</type>
</dependency>
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> 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<String, String> modifier) {
try {
File targetFile = getServiceFolder().resolve(file).toFile();
Expand All @@ -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<HtmlElement> 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<HtmlElement> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -34,11 +32,13 @@ public void push(String serviceName, Map<String, String> 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());
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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<String> containsExpectedLog = createExpectedLogPredicate(expectedLogs);
AwaitilityUtils.untilAsserted(() -> {
List<String> 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<String> 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<String> actualLogs = service.getLogs();
Assertions.assertTrue(actualLogs.stream().noneMatch(line -> line.contains(unexpectedLog)),
Expand Down
Loading

0 comments on commit 509738a

Please sign in to comment.