diff --git a/citrus-remote-sample/pom.xml b/citrus-remote-sample/pom.xml index 016fadb..2e573a5 100644 --- a/citrus-remote-sample/pom.xml +++ b/citrus-remote-sample/pom.xml @@ -16,6 +16,8 @@ 4.3.1 6.1.12 + + false @@ -96,6 +98,9 @@ verify verify + + ${docker} + @@ -104,28 +109,30 @@ + + org.citrusframework + citrus-bom + ${citrus.version} + pom + import + org.apache.logging.log4j log4j-bom ${log4j2.version} + pom + import - - - - jakarta.servlet - jakarta.servlet-api - 6.1.0 - - org.citrusframework citrus-remote-server ${project.version} + test @@ -133,52 +140,57 @@ org.springframework spring-test ${spring.version} + test org.apache.logging.log4j log4j-api + test org.apache.logging.log4j log4j-slf4j2-impl + test org.apache.logging.log4j log4j-iostreams - ${log4j2.version} + test org.citrusframework citrus-base + test org.citrusframework citrus-spring ${citrus.version} + test org.citrusframework citrus-endpoint-catalog - ${citrus.version} + test org.citrusframework citrus-testng - ${citrus.version} + test org.citrusframework citrus-http - ${citrus.version} + test org.citrusframework citrus-validation-text - ${citrus.version} + test @@ -193,7 +205,9 @@ false false false + true + diff --git a/citrus-remote-server/pom.xml b/citrus-remote-server/pom.xml index 34accf5..3aefd56 100644 --- a/citrus-remote-server/pom.xml +++ b/citrus-remote-server/pom.xml @@ -13,83 +13,10 @@ Citrus :: Tools :: Remote Server Citrus Remote Server - - - - org.apache.maven.plugins - maven-shade-plugin - 3.6.0 - - - package - - shade - - - - - org.eclipse.jetty* - jakarta.servlet:jakarta.servlet-api - com.sparkjava* - org.citrusframework:citrus-remote - - - ${project.build.directory}/dependency-reduced-pom.xml - - - org.eclipse.jetty - shaded.org.eclipse.jetty - - META-INF/*.MF - - - - javax.servlet - shaded.javax.servlet - - - - - org.eclipse.jetty* - - META-INF/*.MF - META-INF/LICENSE - META-INF/NOTICE.txt - about.html - - - - jakarta.servlet - - META-INF/*.MF - - - - com.sparkjava* - - META-INF/*.MF - - - - - - - true - - - - - - - - com.sparkjava - spark-core - - - jakarta.servlet - jakarta.servlet-api + io.vertx + vertx-web diff --git a/citrus-remote-server/src/main/java/org/citrusframework/remote/CitrusRemoteApplication.java b/citrus-remote-server/src/main/java/org/citrusframework/remote/CitrusRemoteApplication.java index 2011285..19582f2 100644 --- a/citrus-remote-server/src/main/java/org/citrusframework/remote/CitrusRemoteApplication.java +++ b/citrus-remote-server/src/main/java/org/citrusframework/remote/CitrusRemoteApplication.java @@ -16,11 +16,20 @@ package org.citrusframework.remote; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.AbstractVerticle; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.BodyHandler; import org.citrusframework.Citrus; import org.citrusframework.CitrusInstanceManager; import org.citrusframework.CitrusInstanceStrategy; import org.citrusframework.TestClass; -import org.citrusframework.exceptions.CitrusRuntimeException; import org.citrusframework.main.CitrusAppConfiguration; import org.citrusframework.main.TestRunConfiguration; import org.citrusframework.remote.controller.RunController; @@ -31,11 +40,8 @@ import org.citrusframework.remote.transformer.JsonResponseTransformer; import org.citrusframework.report.JUnitReporter; import org.citrusframework.report.LoggingReporter; -import org.citrusframework.util.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import spark.Filter; -import spark.servlet.SparkApplication; import java.io.File; import java.net.URLDecoder; @@ -45,18 +51,16 @@ import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.concurrent.*; +import java.util.concurrent.TimeUnit; import java.util.stream.Stream; -import static spark.Spark.*; - /** * Remote application creates routes for this web application. * * @author Christoph Deppisch - * @since 2.7.4 + * @since 2..4 */ -public class CitrusRemoteApplication implements SparkApplication { +public class CitrusRemoteApplication extends AbstractVerticle { /** Logger */ private static final Logger logger = LoggerFactory.getLogger(CitrusRemoteApplication.class); @@ -72,7 +76,6 @@ public class CitrusRemoteApplication implements SparkApplication { private final CitrusRemoteConfiguration configuration; /** Single thread job scheduler */ - private final ExecutorService jobs = Executors.newSingleThreadExecutor(); private Future> remoteResultFuture; /** Latest test reports */ @@ -98,154 +101,206 @@ public CitrusRemoteApplication(CitrusRemoteConfiguration configuration) { } @Override - public void init() { + public void start() { CitrusInstanceManager.mode(CitrusInstanceStrategy.SINGLETON); - CitrusInstanceManager.addInstanceProcessor(citrus -> citrus.addTestReporter(remoteTestResultReporter)); - - before((Filter) (request, response) -> logger.info("{} {}{}", request.requestMethod(), request.url(), Optional.ofNullable(request.queryString()).map(query -> "?" + query).orElse(""))); - - get("/health", (req, res) -> { - res.type(APPLICATION_JSON); - return "{ \"status\": \"UP\" }"; + CitrusInstanceManager.addInstanceProcessor(citrus -> + citrus.addTestReporter(remoteTestResultReporter)); + + Router router = Router.router(getVertx()); + router.route().handler(BodyHandler.create()); + router.route().handler(ctx -> { + logger.info( + "{} {}", + ctx.request().method(), + ctx.request().uri()); + ctx.next(); }); + addHealthEndpoint(router); + addFilesEndpoint(router); + addResultsEndpoints(router); + addRunEndpoints(router); + addConfigEndpoints(router); + + getVertx().createHttpServer() + .requestHandler(router) + .listen(configuration.getPort()) + .onSuccess(unused -> + logger.info("Server started on port {}", configuration.getPort())); + } - get("/files/:name", (req, res) -> { - res.type(APPLICATION_OCTET_STREAM); - String fileName = req.params(":name"); - Path file = Path.of(fileName); - - if (Files.isRegularFile(file)) { - res.header( - "Content-Disposition", - "attachment; filename=\"" + file.getFileName() + "\""); - return Files.readAllBytes(file); - } - return null; - }); - - path("/results", () -> { - get("", APPLICATION_JSON, (req, res) -> { - res.type(APPLICATION_JSON); - - long timeout = Optional.ofNullable(req.queryParams("timeout")) - .map(Long::valueOf) - .orElse(10000L); + private static void addHealthEndpoint(Router router) { + router.get("/health") + .handler(wrapThrowingHandler(ctx -> + ctx.response().putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .end("{ \"status\": \"UP\" }"))); + } - if (remoteResultFuture != null) { - try { - return remoteResultFuture.get(timeout, TimeUnit.MILLISECONDS); - } catch (TimeoutException e) { - res.status(206); // partial content + private static void addFilesEndpoint(Router router) { + router.get("/files/:name") + .handler(wrapThrowingHandler(ctx -> { + HttpServerResponse response = ctx.response(); + response.putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_OCTET_STREAM); + String fileName = ctx.pathParam("name"); + Path file = Path.of(fileName); + if (Files.isRegularFile(file)) { + response.putHeader( + HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"" + file.getFileName() + "\"") + .sendFile(fileName); } - } - - List results = new ArrayList<>(); - remoteTestResultReporter.getLatestResults().doWithResults(result -> results.add(RemoteResult.fromTestResult(result))); - return results; - }, responseTransformer); - - get("", (req, res) -> remoteTestResultReporter.getTestReport()); - - get("/files", (req, res) -> { - res.type(APPLICATION_JSON); - File junitReportsFolder = new File(getJUnitReportsFolder()); - - if (junitReportsFolder.exists()) { - return Optional.ofNullable(junitReportsFolder.list()) - .stream(). - flatMap(Stream::of) - .toList(); - } - - return Collections.emptyList(); - }, responseTransformer); - - get("/file/:name", (req, res) -> { - res.type(APPLICATION_XML); - File junitReportsFolder = new File(getJUnitReportsFolder()); - File testResultFile = new File(junitReportsFolder, req.params(":name")); - - if (junitReportsFolder.exists() && testResultFile.exists()) { - return FileUtils.readToString(testResultFile); - } - - throw halt(404, "Failed to find test result file: " + req.params(":name")); - }); - - get("/suite", (req, res) -> { - res.type(APPLICATION_XML); - JUnitReporter jUnitReporter = new JUnitReporter(); - File citrusReportsFolder = new File(jUnitReporter.getReportDirectory()); - File suiteResultFile = new File(citrusReportsFolder, String.format(jUnitReporter.getReportFileNamePattern(), jUnitReporter.getSuiteName())); - - if (citrusReportsFolder.exists() && suiteResultFile.exists()) { - return FileUtils.readToString(suiteResultFile); - } - - throw halt(404, "Failed to find suite result file: " + suiteResultFile.getPath()); - }); - }); - - path("/run", () -> { - get("", (req, res) -> { - TestRunConfiguration runConfiguration = new TestRunConfiguration(); - - if (req.queryParams().contains("engine")) { - runConfiguration.setEngine(URLDecoder.decode(req.queryParams("engine"), ENCODING)); - } else { - runConfiguration.setEngine(configuration.getEngine()); - } - - if (req.queryParams().contains("includes")) { - runConfiguration.setIncludes(URLDecoder.decode(req.queryParams("includes"), ENCODING).split(",")); - } - - if (req.queryParams().contains("package")) { - runConfiguration.setPackages(Collections.singletonList(URLDecoder.decode(req.queryParams("package"), ENCODING))); - } + })); + } - if (req.queryParams().contains("class")) { - runConfiguration.setTestSources(Collections.singletonList(TestClass.fromString(URLDecoder.decode(req.queryParams("class"), ENCODING)))); - } + private void addResultsEndpoints(Router router) { + router.get("/results") + .produces(APPLICATION_JSON) + .handler(wrapThrowingHandler(ctx -> { + long timeout = Optional.ofNullable(ctx.request().params().get("timeout")) + .map(Long::valueOf) + .orElse(10000L); + + HttpServerResponse response = ctx.response(); + if (remoteResultFuture != null) { + response.putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON); + remoteResultFuture.timeout(timeout, TimeUnit.MILLISECONDS) + .onSuccess(results -> + response.end(responseTransformer.render(results))) + .onFailure(throwable -> response + .setStatusCode(HttpResponseStatus.PARTIAL_CONTENT.code()) + .end(responseTransformer.render(Collections.emptyList()))); + } else { + final List results = new ArrayList<>(); + remoteTestResultReporter.getLatestResults().doWithResults(result -> + results.add(RemoteResult.fromTestResult(result))); + response.end(responseTransformer.render(results)); + } + })); + router.get("/results") + .handler(ctx -> ctx.response().end( + responseTransformer.render(remoteTestResultReporter.getTestReport()))); + router.get("/results/files") + .handler(wrapThrowingHandler(ctx -> { + File junitReportsFolder = new File(getJUnitReportsFolder()); + + List result = Collections.emptyList(); + if (junitReportsFolder.exists()) { + result = Optional.ofNullable(junitReportsFolder.list()) + .stream() + .flatMap(Stream::of) + .toList(); + } + ctx.response().putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .end(responseTransformer.render(result)); + })); + router.get("/results/file/:name") + .handler(wrapThrowingHandler(ctx -> { + HttpServerResponse response = ctx.response(); + response.putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_XML); + String fileName = ctx.pathParam("name"); + String reportsFolder = getJUnitReportsFolder(); + Path testResultFile = Path.of(reportsFolder).resolve(fileName); + + if (Files.exists(testResultFile)) { + response.sendFile(testResultFile.toString()); + } else { + response.setStatusCode(HttpResponseStatus.NOT_FOUND.code()) + .end("Failed to find test result file: %s".formatted(fileName)); + } + })); + router.get("/results/suite") + .handler(wrapThrowingHandler(ctx -> { + HttpServerResponse response = ctx.response(); + response.putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON); + JUnitReporter jUnitReporter = new JUnitReporter(); + Path suiteResultFile = Path.of(jUnitReporter.getReportDirectory()) + .resolve(String.format( + jUnitReporter.getReportFileNamePattern(), + jUnitReporter.getSuiteName())); + if (Files.exists(suiteResultFile)) { + response.sendFile(suiteResultFile.toString()); + } else { + response.setStatusCode(HttpResponseStatus.NOT_FOUND.code()) + .end("Failed to find suite result file: %s".formatted(suiteResultFile)); + } + })); + } - res.type(APPLICATION_JSON); + private void addRunEndpoints(Router router) { + router.get("/run") + .handler(wrapThrowingHandler(ctx -> { + TestRunConfiguration runConfiguration = new TestRunConfiguration(); + MultiMap queryParams = ctx.request().params(); + if (queryParams.contains("engine")) { + String engine = queryParams.get("engine"); + runConfiguration.setEngine(URLDecoder.decode(engine, ENCODING)); + } else { + runConfiguration.setEngine(configuration.getEngine()); + } - return runTests(runConfiguration); - }, responseTransformer); + if (queryParams.contains("includes")) { + String value = queryParams.get("includes"); + runConfiguration.setIncludes(URLDecoder.decode(value, ENCODING) + .split(",")); + } - put("", (req, res) -> { - remoteResultFuture = jobs.submit(new RunJob(requestTransformer.read(req.body(), TestRunConfiguration.class)) { - @Override - public List run(TestRunConfiguration runConfiguration) { - return runTests(runConfiguration); + if (queryParams.contains("package")) { + String value = queryParams.get("package"); + runConfiguration.setPackages(Collections.singletonList( + URLDecoder.decode(value, ENCODING))); } - }); - return ""; - }); + if (queryParams.contains("class")) { + String value = queryParams.get("class"); + runConfiguration.setTestSources(Collections.singletonList( + TestClass.fromString(URLDecoder.decode(value, ENCODING)))); + } - post("", (req, res) -> { - TestRunConfiguration runConfiguration = requestTransformer.read(req.body(), TestRunConfiguration.class); - return runTests(runConfiguration); - }, responseTransformer); - }); + ctx.response().putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .end(responseTransformer.render(runTests(runConfiguration))); + })); + router.put("/run") + .handler(wrapThrowingHandler(ctx -> { + remoteResultFuture = getVertx().executeBlocking( + new RunJob(requestTransformer.read(ctx.body().asString(), TestRunConfiguration.class)) { + @Override + public List run(TestRunConfiguration runConfiguration) { + return runTests(runConfiguration); + } + }); + ctx.response().end(""); + })); + router.post("/run") + .handler(wrapThrowingHandler(ctx -> { + TestRunConfiguration runConfiguration = requestTransformer.read( + ctx.body().asString(), + TestRunConfiguration.class); + ctx.response().end(responseTransformer.render(runTests(runConfiguration))); + })); + } - path("/configuration", () -> { - get("", (req, res) -> { - res.type(APPLICATION_JSON); - return configuration; - }, responseTransformer); + private void addConfigEndpoints(Router router) { + router.get("/configuration") + .handler(wrapThrowingHandler(ctx -> + ctx.response() + .putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .end(responseTransformer.render(configuration)))); + router.put("/configuration") + .handler(wrapThrowingHandler(ctx -> + configuration.apply(requestTransformer.read( + ctx.body().asString(), + CitrusAppConfiguration.class)))); + } - put("", (req, res) -> { - configuration.apply(requestTransformer.read(req.body(), CitrusAppConfiguration.class)); - return ""; - }); - }); - exception(CitrusRuntimeException.class, (exception, request, response) -> { - response.status(500); - response.body(exception.getMessage()); - }); + private static Handler wrapThrowingHandler(ThrowingHandler handler) { + return ctx -> { + try { + handler.handle(ctx); + } catch (Exception e) { + ctx.response().setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()) + .end(e.getMessage()); + } + }; } /** @@ -297,12 +352,13 @@ private String getJUnitReportsFolder() { } @Override - public void destroy() { + public void stop() { Optional citrus = CitrusInstanceManager.get(); if (citrus.isPresent()) { logger.info("Closing Citrus and its application context"); citrus.get().close(); } + getVertx().close(); } // TODO: Check if this is equivalent to @@ -315,4 +371,8 @@ private boolean isPresent(String className) { return false; } } + + interface ThrowingHandler { + void handle(T t) throws Exception; + } } diff --git a/citrus-remote-server/src/main/java/org/citrusframework/remote/CitrusRemoteServer.java b/citrus-remote-server/src/main/java/org/citrusframework/remote/CitrusRemoteServer.java index 076db9a..98b2db5 100644 --- a/citrus-remote-server/src/main/java/org/citrusframework/remote/CitrusRemoteServer.java +++ b/citrus-remote-server/src/main/java/org/citrusframework/remote/CitrusRemoteServer.java @@ -16,10 +16,10 @@ package org.citrusframework.remote; +import io.vertx.core.Vertx; import org.citrusframework.remote.controller.RunController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import spark.Spark; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -27,7 +27,6 @@ import java.util.concurrent.TimeoutException; import static java.lang.Thread.currentThread; -import static spark.Spark.port; /** * @author Christoph Deppisch @@ -35,7 +34,7 @@ public class CitrusRemoteServer { /** Logger */ - private static Logger logger = LoggerFactory.getLogger(CitrusRemoteServer.class); + private static final Logger logger = LoggerFactory.getLogger(CitrusRemoteServer.class); /** Endpoint configuration */ private final CitrusRemoteConfiguration configuration; @@ -100,8 +99,7 @@ public static void main(String[] args) { */ public void start() { application = new CitrusRemoteApplication(configuration); - port(configuration.getPort()); - application.init(); + Vertx.vertx().deployVerticle(application); if (!configuration.isSkipTests()) { new RunController(configuration).run(); @@ -116,9 +114,8 @@ public void start() { * Stops the server instance. */ public void stop() { - application.destroy(); + application.stop(); complete(); - Spark.stop(); } /** diff --git a/citrus-remote-server/src/main/java/org/citrusframework/remote/transformer/JsonResponseTransformer.java b/citrus-remote-server/src/main/java/org/citrusframework/remote/transformer/JsonResponseTransformer.java index b939809..613722d 100644 --- a/citrus-remote-server/src/main/java/org/citrusframework/remote/transformer/JsonResponseTransformer.java +++ b/citrus-remote-server/src/main/java/org/citrusframework/remote/transformer/JsonResponseTransformer.java @@ -20,13 +20,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import org.citrusframework.exceptions.CitrusRuntimeException; -import spark.ResponseTransformer; /** * @author Christoph Deppisch * @since 2.7.4 */ -public class JsonResponseTransformer implements ResponseTransformer { +public class JsonResponseTransformer { private final ObjectMapper mapper; @@ -38,7 +37,6 @@ public JsonResponseTransformer() { mapper.enable(SerializationFeature.INDENT_OUTPUT); } - @Override public String render(Object model) { try { return mapper.writeValueAsString(model); diff --git a/pom.xml b/pom.xml index cf1b641..dc899bc 100644 --- a/pom.xml +++ b/pom.xml @@ -55,8 +55,7 @@ 2.17.2 2.22.1 2.0.11 - 4.0.4 - 2.9.4 + 4.5.9 -Xdoclint:none @@ -126,10 +125,14 @@ httpclient5 ${httpclient.version} + + - com.sparkjava - spark-core - ${sparkjava.core.version} + io.vertx + vertx-stack-depchain + ${vertx.version} + pom + import @@ -149,12 +152,6 @@ ${jackson.version} - - jakarta.servlet - jakarta.servlet-api - ${jakarta.servlet-api.version} - - org.slf4j