Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WireMock config changes are taken into account for live-reload (dev mode only) #85

Merged
merged 1 commit into from
Jan 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,29 @@

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.jboss.logging.Logger;

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;

import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem;
import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Consume;
import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem;
import io.quarkus.deployment.builditem.DevServicesResultBuildItem;
import io.quarkus.deployment.builditem.DevServicesResultBuildItem.RunningDevService;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem;
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
import io.quarkus.deployment.builditem.LiveReloadBuildItem;
import io.quarkus.deployment.dev.devservices.DevServiceDescriptionBuildItem;
Expand All @@ -30,6 +39,8 @@ class WireMockServerProcessor {
private static final Logger LOGGER = Logger.getLogger(WireMockServerProcessor.class);
private static final String FEATURE_NAME = "wiremock";
private static final String DEV_SERVICE_NAME = "WireMock";
private static final String MAPPINGS = "mappings";
private static final String FILES = "__files";
private static final int MIN_PORT = 1025;
private static final int MAX_PORT = 65535;
static volatile RunningDevService devService;
Expand Down Expand Up @@ -69,10 +80,19 @@ DevServicesResultBuildItem setup(LaunchModeBuildItem launchMode, LiveReloadBuild

@BuildStep(onlyIf = { WireMockServerEnabled.class, GlobalDevServicesConfig.Enabled.class })
@Consume(DevServicesResultBuildItem.class)
DevServiceDescriptionBuildItem renderDevServiceDevUICard(WireMockServerBuildTimeConfig config) {
DevServiceDescriptionBuildItem renderDevServiceDevUICard() {
return new DevServiceDescriptionBuildItem(DEV_SERVICE_NAME, null, devService.getConfig());
}

@BuildStep(onlyIf = { WireMockServerEnabled.class, GlobalDevServicesConfig.Enabled.class, IsDevelopment.class })
void watchWireMockConfigFiles(WireMockServerBuildTimeConfig config,
BuildProducer<HotDeploymentWatchedFileBuildItem> items) {
listFiles(Paths.get(config.filesMapping(), MAPPINGS), Paths.get(config.filesMapping(), FILES)).forEach(file -> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to https://quarkus.io/guides/all-builditems , HotDeploymentWatchedFileBuildItem actually supports passing a Glob pattern, hence you could probably instruct it to watch all files under the Mappings and Files directories (including when new ones added?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've analyzed the glob-pattern feature back in the days. However, it does not convince me since the API is not very intuitive. Furthermore, it also doesn't work for new files (please see quarkusio/quarkus#25338). Anyway, the current implementation already watches all files under the Mappings and Files directories.

LOGGER.debugf("Watching [%s] for hot deployment!", file);
items.produce(new HotDeploymentWatchedFileBuildItem(file));
});
}

private static RunningDevService startWireMockDevService(WireMockServerBuildTimeConfig config) {

final WireMockConfiguration configuration = options().usingFilesUnderDirectory(config.filesMapping())
Expand All @@ -83,15 +103,13 @@ private static RunningDevService startWireMockDevService(WireMockServerBuildTime
server.start();
LOGGER.debugf("WireMock server listening on port [%s]", server.port());

return new RunningDevService(DEV_SERVICE_NAME, null, server::shutdown, PORT,
String.valueOf(server.port()));
return new RunningDevService(DEV_SERVICE_NAME, null, server::shutdown, PORT, String.valueOf(server.port()));
}

private static synchronized void stopWireMockDevService() {
try {
if (devService != null) {
LOGGER.debugf("Stopping WireMock server running on port %s",
devService.getConfig().get(PORT));
LOGGER.debugf("Stopping WireMock server running on port %s", devService.getConfig().get(PORT));
devService.close();
}
} catch (IOException e) {
Expand All @@ -110,4 +128,18 @@ private static boolean isPortConfigInvalid(WireMockServerBuildTimeConfig config)
return port < MIN_PORT || port > MAX_PORT;
}

private static Set<String> listFiles(Path... dirs) {
return Arrays.stream(dirs).filter(Files::isDirectory).map(WireMockServerProcessor::fileWalk)
.flatMap(Set::stream).collect(Collectors.toSet());
}

private static Set<String> fileWalk(Path start) {
try (Stream<Path> stream = Files.walk(start)) {
return stream.filter(Files::isRegularFile).map(Path::toAbsolutePath).map(Path::toString)
.collect(Collectors.toSet());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

}
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
package io.quarkiverse.wiremock.devservice;

import static io.quarkiverse.wiremock.devservice.ConfigProviderResource.BASE_URL;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

import org.eclipse.microprofile.config.ConfigProvider;

@Path("/quarkus/wiremock")
@Path(BASE_URL)
@ApplicationScoped
class ConfigProviderResource {

@Path("/devservices/config")
static final String BASE_URL = "/quarkus/wiremock/devservices";

@GET
@Path("/reload")
public Response reload() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be added to the docs too, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't get your point. It's just an endpoint for testing purposes. What's your intention to add this to the doc?

return Response.ok().build();
}

@GET
@Path("/config")
@Produces(MediaType.TEXT_PLAIN)
public String getConfigValue(@QueryParam("name") String propertyName) {
public String getConfigValue(@QueryParam("propertyName") String propertyName) {
return ConfigProvider.getConfig().getValue(propertyName, String.class);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package io.quarkiverse.wiremock.devservice;

import static io.quarkiverse.wiremock.devservice.ConfigProviderResource.BASE_URL;
import static io.quarkiverse.wiremock.devservice.WireMockConfigKey.PORT;
import static java.lang.String.format;
import static org.hamcrest.Matchers.is;
import static org.jboss.resteasy.reactive.RestResponse.StatusCode.OK;

Expand All @@ -12,18 +15,19 @@
import io.restassured.RestAssured;

@SuppressWarnings("java:S5786")
public class WireMockDevModeTest {
public class WireMockBasicDevModeTest {

private static final String APP_PROPERTIES = "application-static-port.properties";

@RegisterExtension
static final QuarkusDevModeTest DEV_MODE_TEST = new QuarkusDevModeTest().setArchiveProducer(
() -> ShrinkWrap.create(JavaArchive.class)
() -> ShrinkWrap.create(JavaArchive.class).addClass(ConfigProviderResource.class)
.addAsResource(APP_PROPERTIES, "application.properties"));

@Test
void testJsonMappingFilesRecognition() {
RestAssured.when().get("http://localhost:50200/wiremock").then().statusCode(OK)
String port = RestAssured.get(format("%s/config?propertyName=%s", BASE_URL, PORT)).then().extract().asString();
RestAssured.when().get(format("http://localhost:%s/basic", port)).then().statusCode(OK)
.body(is("Everything was just fine!"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class WireMockBasicTest {
@Test
void testWireMockMappingsFolder() {
final int port = ConfigProvider.getConfig().getValue(PORT, Integer.class);
RestAssured.when().get(String.format("http://localhost:%d/wiremock", port)).then().statusCode(OK)
RestAssured.when().get(String.format("http://localhost:%d/basic", port)).then().statusCode(OK)
.body(is("Everything was just fine!"));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
package io.quarkiverse.wiremock.devservice;

import static io.quarkiverse.wiremock.devservice.ConfigProviderResource.BASE_URL;
import static io.quarkiverse.wiremock.devservice.WireMockConfigKey.FILES_MAPPING;
import static io.quarkiverse.wiremock.devservice.WireMockConfigKey.PORT;
import static java.lang.String.format;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.jboss.resteasy.reactive.RestResponse.StatusCode.OK;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.net.ServerSocket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
Expand All @@ -16,18 +24,17 @@

import io.quarkus.test.QuarkusDevModeTest;
import io.restassured.RestAssured;
import io.restassured.response.Response;

/**
* Write your dev mode tests here - see the <a href="https://quarkus.io/guides/writing-extensions#testing-hot-reload">testing
* extension guide</a> for more information.
*/
@SuppressWarnings("java:S5786")
public class WireMockLiveReloadTest {
private static final String BASE_URL = "/quarkus/wiremock/devservices";
private static final int TARGET_STATIC_WIREMOCK_PORT = 50000;
private static final String APP_PROPERTIES = "application.properties";

// Start hot reload (DevMode) test with your extension loaded
@RegisterExtension
static final QuarkusDevModeTest DEV_MODE_TEST = new QuarkusDevModeTest().setArchiveProducer(
() -> ShrinkWrap.create(JavaArchive.class).addClass(ConfigProviderResource.class)
Expand All @@ -36,21 +43,76 @@ public class WireMockLiveReloadTest {
@Test
void testPortModificationViaLiveReload() {

assertFalse(isInUse(TARGET_STATIC_WIREMOCK_PORT),
"Port" + TARGET_STATIC_WIREMOCK_PORT + " is already in use!");
Given.portIsNotInUse(TARGET_STATIC_WIREMOCK_PORT);

// add port configuration to the properties file
DEV_MODE_TEST.modifyResourceFile(APP_PROPERTIES,
s -> s.concat(System.lineSeparator() + PORT + "=" + TARGET_STATIC_WIREMOCK_PORT));
When.addApplicationProperty(System.lineSeparator() + PORT + "=" +
TARGET_STATIC_WIREMOCK_PORT); // add port configuration to the properties file
When.callConfigProvider(PORT).then().body(equalTo(String.valueOf(TARGET_STATIC_WIREMOCK_PORT)));

// Currently, the live-reload can only be triggered via a http call.
// See https://github.com/quarkiverse/quarkus-wiremock/issues/69 for more details.
// This will be enhanced with the next development iteration.
RestAssured.get(format("%s/config?name=%s", BASE_URL, PORT)).then()
.body(equalTo(String.valueOf(TARGET_STATIC_WIREMOCK_PORT)));
Then.portIsInUse(TARGET_STATIC_WIREMOCK_PORT);
}

@Test
void testWireMockConfigChangeHotDeployment() throws IOException {

var tempDir = Files.createTempDirectory(Path.of("target"), "wiremock-");
var wireMockConfig = Given.wireMockConfigFile(tempDir); // create temporary WireMock config

// specify temp dir as files-mapping location
When.addApplicationProperty(System.lineSeparator() + FILES_MAPPING + "=target/" + tempDir.getFileName());
When.triggerHttpLiveReload(); // --> At this point, Quarkus watches the new config file
When.callWireMock().then().statusCode(OK).body(is("Modify me at runtime!"));

When.modifyResponseBody(wireMockConfig);
When.callWireMock().then().statusCode(OK).body(is("Live reload rockz!"));
}

private static class Given {

private static Path wireMockConfigFile(Path rootLocation) throws IOException {
var mapLocation = Paths.get(rootLocation.toFile().getAbsolutePath(), "mappings");
Files.createDirectories(mapLocation);
var configFile = Paths.get(mapLocation.toFile().getAbsolutePath(), "modify.json");
Files.write(configFile,
"{\"request\":{\"method\":\"GET\",\"url\": \"/modify\"},\"response\":{\"status\": 200,\"body\":\"Modify me at runtime!\"}}"
.getBytes(
StandardCharsets.UTF_8));
return configFile;
}

private static void portIsNotInUse(int port) {
assertFalse(isInUse(port), "Port " + port + " is already in use!");
}
}

private static class When {
private static void triggerHttpLiveReload() {
RestAssured.get(format("%s/reload", BASE_URL));
}

private static Response callWireMock() {
return RestAssured.when().get(format("http://localhost:%d/modify", getDynamicPort()));
}

private static Response callConfigProvider(String propertyName) {
return RestAssured.get(format("%s/config?propertyName=%s", BASE_URL, propertyName));
}

private static void addApplicationProperty(String property) {
DEV_MODE_TEST.modifyResourceFile(APP_PROPERTIES, s -> s.concat(property));
}

assertTrue(isInUse(TARGET_STATIC_WIREMOCK_PORT),
"WireMock Dev Service doesn't listen on port " + TARGET_STATIC_WIREMOCK_PORT);
private static void modifyResponseBody(Path configFile) throws IOException {
Files.writeString(configFile,
Files.readString(configFile).replace("Modify me at runtime!", "Live reload rockz!"));
}
}

private static class Then {

private static void portIsInUse(int port) {
assertTrue(isInUse(port), "No Service is listening on port " + port);
}
}

private static boolean isInUse(int port) {
Expand All @@ -60,4 +122,8 @@ private static boolean isInUse(int port) {
return true;
}
}

private static int getDynamicPort() {
return Integer.parseInt(When.callConfigProvider(PORT).then().extract().asString());
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"request": {
"method": "GET",
"url": "/wiremock"
"url": "/basic"
},
"response": {
"status": 200,
Expand Down