From ba08625b88b97ff114417ec4a9c4f5298db074fd Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 11 Feb 2021 12:21:29 +0200 Subject: [PATCH] Introduce @QuarkusIntegration test Resolves: #13818 --- .../main/asciidoc/building-native-image.adoc | 6 + .../asciidoc/getting-started-testing.adoc | 17 ++ .../quarkus-standard-way/pom.xml | 72 ++++- .../container/image/GreetingResourceIT.java | 21 ++ .../PanacheFunctionalityInGraalITCase.java | 4 +- .../it/panache/PanacheFunctionalityTest.java | 3 +- .../PanacheFunctionalityInGraalITCase.java | 4 +- .../it/FastJarQuarkusIntegrationTestIT.java | 15 + .../it/LegacyJarQuarkusIntegrationTestIT.java | 15 + .../io/quarkus/maven/it/QuarkusITBase.java | 43 +++ .../it/UberJarQuarkusIntegrationTestIT.java | 15 + .../projects/reactive-routes/pom.xml | 194 +++++++++++++ .../reactive/routes/MyDeclarativeRoutes.java | 25 ++ .../org/acme/reactive/routes/MyFilter.java | 17 ++ .../org/acme/reactive/routes/RouteIT.java | 8 + .../org/acme/reactive/routes/RouteTest.java | 31 ++ .../quarkus/test/common/ArtifactLauncher.java | 14 + .../test/common/DockerContainerLauncher.java | 117 ++++++++ .../io/quarkus/test/common/JarLauncher.java | 99 +++++++ .../io/quarkus/test/common/LauncherUtil.java | 105 +++++++ .../test/common/NativeImageLauncher.java | 215 +------------- .../common/PortCapturingProcessReader.java | 79 +++++ .../io/quarkus/test/common/ProcessReader.java | 42 +++ .../test/junit/DisabledOnIntegrationTest.java | 39 +++ .../DisabledOnIntegrationTestCondition.java | 57 ++++ .../junit/IntegrationTestExtensionState.java | 54 ++++ .../test/junit/IntegrationTestUtil.java | 108 +++++++ .../test/junit/NativeTestExtension.java | 144 ++-------- .../test/junit/QuarkusIntegrationTest.java | 34 +++ .../QuarkusIntegrationTestExtension.java | 271 ++++++++++++++++++ .../test/junit/QuarkusTestExtension.java | 28 +- 31 files changed, 1548 insertions(+), 348 deletions(-) create mode 100644 integration-tests/container-image/quarkus-standard-way/src/test/java/io/quarkus/it/container/image/GreetingResourceIT.java create mode 100644 integration-tests/maven/src/test/java/io/quarkus/maven/it/FastJarQuarkusIntegrationTestIT.java create mode 100644 integration-tests/maven/src/test/java/io/quarkus/maven/it/LegacyJarQuarkusIntegrationTestIT.java create mode 100644 integration-tests/maven/src/test/java/io/quarkus/maven/it/QuarkusITBase.java create mode 100644 integration-tests/maven/src/test/java/io/quarkus/maven/it/UberJarQuarkusIntegrationTestIT.java create mode 100644 integration-tests/maven/src/test/resources/projects/reactive-routes/pom.xml create mode 100644 integration-tests/maven/src/test/resources/projects/reactive-routes/src/main/java/org/acme/reactive/routes/MyDeclarativeRoutes.java create mode 100644 integration-tests/maven/src/test/resources/projects/reactive-routes/src/main/java/org/acme/reactive/routes/MyFilter.java create mode 100644 integration-tests/maven/src/test/resources/projects/reactive-routes/src/test/java/org/acme/reactive/routes/RouteIT.java create mode 100644 integration-tests/maven/src/test/resources/projects/reactive-routes/src/test/java/org/acme/reactive/routes/RouteTest.java create mode 100644 test-framework/common/src/main/java/io/quarkus/test/common/ArtifactLauncher.java create mode 100644 test-framework/common/src/main/java/io/quarkus/test/common/DockerContainerLauncher.java create mode 100644 test-framework/common/src/main/java/io/quarkus/test/common/JarLauncher.java create mode 100644 test-framework/common/src/main/java/io/quarkus/test/common/LauncherUtil.java create mode 100644 test-framework/common/src/main/java/io/quarkus/test/common/PortCapturingProcessReader.java create mode 100644 test-framework/common/src/main/java/io/quarkus/test/common/ProcessReader.java create mode 100644 test-framework/junit5/src/main/java/io/quarkus/test/junit/DisabledOnIntegrationTest.java create mode 100644 test-framework/junit5/src/main/java/io/quarkus/test/junit/DisabledOnIntegrationTestCondition.java create mode 100644 test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestExtensionState.java create mode 100644 test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java create mode 100644 test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTest.java create mode 100644 test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java diff --git a/docs/src/main/asciidoc/building-native-image.adoc b/docs/src/main/asciidoc/building-native-image.adoc index 1da4117ac99f27..1b3354b1b53045 100644 --- a/docs/src/main/asciidoc/building-native-image.adoc +++ b/docs/src/main/asciidoc/building-native-image.adoc @@ -329,6 +329,12 @@ duration can be changed using the `quarkus.test.native-image-wait-time` system p to 300 seconds, use: `./mvnw verify -Pnative -Dquarkus.test.native-image-wait-time=300`. ==== +[WARNING] +==== +In the future, `@NativeImageTest` will be deprecated in favor of `@QuarkusIntegrationTest` which provides a superset of the testing +capabilities of `@NativeImageTest`. More information about `@QuarkusIntegrationTest` can be found in the link:getting-started-testing#quarkus-integration-test[Testing Guide]. +==== + By default, native tests runs using the `prod` profile. This can be overridden using the `quarkus.test.native-image-profile` property. For example, in your `application.properties` file, add: `quarkus.test.native-image-profile=test`. diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index 8f69be4a2c16eb..62d511c97223c0 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -1011,6 +1011,23 @@ guide except injecting into tests (and the native executable runs in a separate This is covered in the link:building-native-image[Native Executable Guide]. +[WARNING] +==== +Although `@NativeImageTest` is not yet deprecated, it will be in the future as it's functionality is covered by `@QuarkusIntegrationTest` +which is described in the following section. +==== + +[#quarkus-integration-test] +== Using @QuarkusIntegrationTest + +`@QuarkusIntegrationTest` should be used to launch and test the artifact produced by the Quarkus build, and supports testing a jar (of whichever type), a native image or container-image. +Put simply, this means that if the result of a Quarkus build (`mvn package` or `gradle build`) is a jar, that jar will be launched as `java -jar ...` and tests run against it. +If instead a native image was build, then the application is launched as `./application ...` and again the tests run against the running application. +Finally, if a container image was created during the build (by using including the `quarkus-container-image-jib` or `quarkus-container-image-docker` extensions and having the +`quarkus.container-image.build=true` property configured), then a container is created and run (this requires the `docker` executable being present). + +As is the case with `@NativeImageTest`, this is a black box test that supports the same set features and has the same limitations. + [[test-from-ide]] == Running `@QuarkusTest` from an IDE diff --git a/integration-tests/container-image/quarkus-standard-way/pom.xml b/integration-tests/container-image/quarkus-standard-way/pom.xml index 7a3b12517bfb60..2ee96f3e28b142 100644 --- a/integration-tests/container-image/quarkus-standard-way/pom.xml +++ b/integration-tests/container-image/quarkus-standard-way/pom.xml @@ -10,12 +10,12 @@ quarkus-integration-test-container-image-standard Quarkus - Integration Tests - Container Image - Standard - Container Image integration tests that use @QuarkusProdModeTest + Container Image integration tests that use '@QuarkusProdModeTest' and '@QuarkusIntegrationTests' io.quarkus - quarkus-resteasy + quarkus-resteasy-reactive @@ -40,6 +40,16 @@ assertj-core test + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-junit5 + test + @@ -70,7 +80,7 @@ io.quarkus - quarkus-resteasy-deployment + quarkus-resteasy-reactive-deployment ${project.version} pom test @@ -83,4 +93,60 @@ + + + test-quarkus-integration-test + + + test-containers + + + + true + + + + io.quarkus + quarkus-container-image-jib + compile + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + maven-failsafe-plugin + + false + + + + + integration-test + verify + + + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + + diff --git a/integration-tests/container-image/quarkus-standard-way/src/test/java/io/quarkus/it/container/image/GreetingResourceIT.java b/integration-tests/container-image/quarkus-standard-way/src/test/java/io/quarkus/it/container/image/GreetingResourceIT.java new file mode 100644 index 00000000000000..c4c565ea22012d --- /dev/null +++ b/integration-tests/container-image/quarkus-standard-way/src/test/java/io/quarkus/it/container/image/GreetingResourceIT.java @@ -0,0 +1,21 @@ +package io.quarkus.it.container.image; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class GreetingResourceIT { + + @Test + public void testHelloEndpoint() { + given() + .when().get("/greeting") + .then() + .statusCode(200) + .body(is("hello")); + } +} diff --git a/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheFunctionalityInGraalITCase.java b/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheFunctionalityInGraalITCase.java index f67a867480d57d..cf1335df76c6bd 100644 --- a/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheFunctionalityInGraalITCase.java +++ b/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheFunctionalityInGraalITCase.java @@ -1,11 +1,11 @@ package io.quarkus.it.panache; -import io.quarkus.test.junit.NativeImageTest; +import io.quarkus.test.junit.QuarkusIntegrationTest; /** * Test various Panache operations running in native mode */ -@NativeImageTest +@QuarkusIntegrationTest public class PanacheFunctionalityInGraalITCase extends PanacheFunctionalityTest { } diff --git a/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheFunctionalityTest.java b/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheFunctionalityTest.java index d6a62d946eb011..bd5878b28b20fc 100644 --- a/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheFunctionalityTest.java +++ b/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheFunctionalityTest.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil; +import io.quarkus.test.junit.DisabledOnIntegrationTest; import io.quarkus.test.junit.DisabledOnNativeImage; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; @@ -135,7 +136,7 @@ public void jaxbDeserializationHasAllFields() throws JsonProcessingException, JA /** * This test is disabled in native mode as there is no interaction with the quarkus integration test endpoint. */ - @DisabledOnNativeImage + @DisabledOnIntegrationTest @Test public void jsonbDeserializationHasAllFields() throws JsonProcessingException { // set Up diff --git a/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityInGraalITCase.java b/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityInGraalITCase.java index 03711c07dcf3b8..78d2b13010a09e 100644 --- a/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityInGraalITCase.java +++ b/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityInGraalITCase.java @@ -1,11 +1,11 @@ package io.quarkus.it.panache.reactive; -import io.quarkus.test.junit.NativeImageTest; +import io.quarkus.test.junit.QuarkusIntegrationTest; /** * Test various Panache operations running in native mode */ -@NativeImageTest +@QuarkusIntegrationTest public class PanacheFunctionalityInGraalITCase extends PanacheFunctionalityTest { } diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/FastJarQuarkusIntegrationTestIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/FastJarQuarkusIntegrationTestIT.java new file mode 100644 index 00000000000000..2707eca2ba93a2 --- /dev/null +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/FastJarQuarkusIntegrationTestIT.java @@ -0,0 +1,15 @@ +package io.quarkus.maven.it; + +import java.io.IOException; + +import org.apache.maven.shared.invoker.MavenInvocationException; +import org.junit.jupiter.api.Test; + +@DisableForNative +public class FastJarQuarkusIntegrationTestIT extends QuarkusITBase { + + @Test + public void testFastJar() throws MavenInvocationException, IOException { + doTest("qit-fast-jar", "fastjar"); + } +} diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/LegacyJarQuarkusIntegrationTestIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/LegacyJarQuarkusIntegrationTestIT.java new file mode 100644 index 00000000000000..77c95ac6988d05 --- /dev/null +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/LegacyJarQuarkusIntegrationTestIT.java @@ -0,0 +1,15 @@ +package io.quarkus.maven.it; + +import java.io.IOException; + +import org.apache.maven.shared.invoker.MavenInvocationException; +import org.junit.jupiter.api.Test; + +@DisableForNative +public class LegacyJarQuarkusIntegrationTestIT extends QuarkusITBase { + + @Test + public void testFastJar() throws MavenInvocationException, IOException { + doTest("qit-legacy-jar", "legacyjar"); + } +} diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/QuarkusITBase.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/QuarkusITBase.java new file mode 100644 index 00000000000000..77db092b144102 --- /dev/null +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/QuarkusITBase.java @@ -0,0 +1,43 @@ +package io.quarkus.maven.it; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +import org.apache.maven.shared.invoker.MavenInvocationException; + +import io.quarkus.maven.it.verifier.MavenProcessInvocationResult; +import io.quarkus.maven.it.verifier.RunningInvoker; + +// meant to test that @QuarkusIntegrationTest can properly launch jars +abstract class QuarkusITBase extends MojoTestBase { + + void doTest(String projectName, String profile) throws MavenInvocationException, IOException { + File testDir = initProject("projects/reactive-routes", "projects/" + projectName); + RunningInvoker packageInvocation = new RunningInvoker(testDir, false); + + MavenProcessInvocationResult packageInvocationResult = packageInvocation + .execute(Arrays.asList("package", "-B", + "-D" + profile), Collections.emptyMap()); + + await().atMost(1, TimeUnit.MINUTES) + .until(() -> packageInvocationResult.getProcess() != null && !packageInvocationResult.getProcess().isAlive()); + assertThat(packageInvocation.log()).containsIgnoringCase("BUILD SUCCESS"); + + RunningInvoker integrationTestsInvocation = new RunningInvoker(testDir, false); + + MavenProcessInvocationResult integrationTestsInvocationResult = integrationTestsInvocation + .execute(Arrays.asList("failsafe:integration-test", "-B", + "-D" + profile), Collections.emptyMap()); + + await().atMost(1, TimeUnit.MINUTES) + .until(() -> integrationTestsInvocationResult.getProcess() != null + && !integrationTestsInvocationResult.getProcess().isAlive()); + assertThat(integrationTestsInvocation.log()).containsIgnoringCase("BUILD SUCCESS"); + } +} diff --git a/integration-tests/maven/src/test/java/io/quarkus/maven/it/UberJarQuarkusIntegrationTestIT.java b/integration-tests/maven/src/test/java/io/quarkus/maven/it/UberJarQuarkusIntegrationTestIT.java new file mode 100644 index 00000000000000..3e6fe6bef1ad10 --- /dev/null +++ b/integration-tests/maven/src/test/java/io/quarkus/maven/it/UberJarQuarkusIntegrationTestIT.java @@ -0,0 +1,15 @@ +package io.quarkus.maven.it; + +import java.io.IOException; + +import org.apache.maven.shared.invoker.MavenInvocationException; +import org.junit.jupiter.api.Test; + +@DisableForNative +public class UberJarQuarkusIntegrationTestIT extends QuarkusITBase { + + @Test + public void testFastJar() throws MavenInvocationException, IOException { + doTest("qit-uber-jar", "uberjar"); + } +} diff --git a/integration-tests/maven/src/test/resources/projects/reactive-routes/pom.xml b/integration-tests/maven/src/test/resources/projects/reactive-routes/pom.xml new file mode 100644 index 00000000000000..a71844a2343409 --- /dev/null +++ b/integration-tests/maven/src/test/resources/projects/reactive-routes/pom.xml @@ -0,0 +1,194 @@ + + + 4.0.0 + + io.quarkus.quickstarts + reactive-routes-quickstart + 1.0.0-SNAPSHOT + + + 3.0.0-M5 + 999-SNAPSHOT + quarkus-bom + io.quarkus + 999-SNAPSHOT + 1.8 + UTF-8 + 1.8 + 3.0.0 + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + io.quarkus + quarkus-vertx-web + + + io.quarkus + quarkus-container-image-jib + + + io.quarkus + quarkus-container-image-jib-deployment + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + ${awaitility.version} + test + + + + + + io.quarkus + quarkus-maven-plugin + ${quarkus-plugin.version} + + + + build + + + + + + maven-surefire-plugin + ${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + fast-jar + + + fastjar + + + + jar + + + + + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + org.jboss.logmanager.LogManager + + ${maven.home} + + + + + + + + + + legacy-jar + + + legacyjar + + + + legacy-jar + + + + + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + org.jboss.logmanager.LogManager + + ${maven.home} + + + + + + + + + + uber-jar + + + uberjar + + + + uber-jar + + + + + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + org.jboss.logmanager.LogManager + + ${maven.home} + + + + + + + + + + + diff --git a/integration-tests/maven/src/test/resources/projects/reactive-routes/src/main/java/org/acme/reactive/routes/MyDeclarativeRoutes.java b/integration-tests/maven/src/test/resources/projects/reactive-routes/src/main/java/org/acme/reactive/routes/MyDeclarativeRoutes.java new file mode 100644 index 00000000000000..42caa8d26028cd --- /dev/null +++ b/integration-tests/maven/src/test/resources/projects/reactive-routes/src/main/java/org/acme/reactive/routes/MyDeclarativeRoutes.java @@ -0,0 +1,25 @@ +package org.acme.reactive.routes; + +import javax.enterprise.context.ApplicationScoped; + +import io.quarkus.vertx.web.Route; +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.RoutingContext; + +@ApplicationScoped +public class MyDeclarativeRoutes { + + @Route(path = "/", methods = HttpMethod.GET) + public void handle(RoutingContext rc) { + rc.response().end("hello"); + } + + @Route(path = "/hello", methods = HttpMethod.GET) + public void greetings(RoutingContext rc) { + String name = rc.request().getParam("name"); + if (name == null) { + name = "world"; + } + rc.response().end("hello " + name); + } +} diff --git a/integration-tests/maven/src/test/resources/projects/reactive-routes/src/main/java/org/acme/reactive/routes/MyFilter.java b/integration-tests/maven/src/test/resources/projects/reactive-routes/src/main/java/org/acme/reactive/routes/MyFilter.java new file mode 100644 index 00000000000000..1e4c376525bbab --- /dev/null +++ b/integration-tests/maven/src/test/resources/projects/reactive-routes/src/main/java/org/acme/reactive/routes/MyFilter.java @@ -0,0 +1,17 @@ +package org.acme.reactive.routes; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; + +import io.quarkus.vertx.http.runtime.filters.Filters; + +@ApplicationScoped +public class MyFilter { + + public void registerMyFilter(@Observes Filters filters) { + filters.register(rc -> { + rc.response().putHeader("X-Header", "intercepting the request"); + rc.next(); + }, 100); + } +} diff --git a/integration-tests/maven/src/test/resources/projects/reactive-routes/src/test/java/org/acme/reactive/routes/RouteIT.java b/integration-tests/maven/src/test/resources/projects/reactive-routes/src/test/java/org/acme/reactive/routes/RouteIT.java new file mode 100644 index 00000000000000..c7c9ab9f63a20b --- /dev/null +++ b/integration-tests/maven/src/test/resources/projects/reactive-routes/src/test/java/org/acme/reactive/routes/RouteIT.java @@ -0,0 +1,8 @@ +package org.acme.reactive.routes; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class RouteIT extends RouteTest { + +} diff --git a/integration-tests/maven/src/test/resources/projects/reactive-routes/src/test/java/org/acme/reactive/routes/RouteTest.java b/integration-tests/maven/src/test/resources/projects/reactive-routes/src/test/java/org/acme/reactive/routes/RouteTest.java new file mode 100644 index 00000000000000..e1449dd769604c --- /dev/null +++ b/integration-tests/maven/src/test/resources/projects/reactive-routes/src/test/java/org/acme/reactive/routes/RouteTest.java @@ -0,0 +1,31 @@ +package org.acme.reactive.routes; + +import static org.hamcrest.core.Is.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; + +@QuarkusTest +public class RouteTest { + + @Test + public void testDeclarativeRoutes() { + RestAssured.get("/").then() + .header("X-Header", "intercepting the request") + .statusCode(200) + .body(is("hello")); + + RestAssured.get("/hello").then() + .header("X-Header", "intercepting the request") + .statusCode(200) + .body(is("hello world")); + + RestAssured.get("/hello?name=quarkus").then() + .header("X-Header", "intercepting the request") + .statusCode(200) + .body(is("hello quarkus")); + } + +} diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/ArtifactLauncher.java b/test-framework/common/src/main/java/io/quarkus/test/common/ArtifactLauncher.java new file mode 100644 index 00000000000000..e0e434140cda9a --- /dev/null +++ b/test-framework/common/src/main/java/io/quarkus/test/common/ArtifactLauncher.java @@ -0,0 +1,14 @@ +package io.quarkus.test.common; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Map; + +public interface ArtifactLauncher extends Closeable { + + void start() throws IOException; + + void addSystemProperties(Map systemProps); + + boolean isDefaultSsl(); +} diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/DockerContainerLauncher.java b/test-framework/common/src/main/java/io/quarkus/test/common/DockerContainerLauncher.java new file mode 100644 index 00000000000000..6e4d439ce45f83 --- /dev/null +++ b/test-framework/common/src/main/java/io/quarkus/test/common/DockerContainerLauncher.java @@ -0,0 +1,117 @@ +package io.quarkus.test.common; + +import static io.quarkus.test.common.LauncherUtil.installAndGetSomeConfig; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.OptionalInt; +import java.util.OptionalLong; + +import org.eclipse.microprofile.config.Config; + +import io.quarkus.test.common.http.TestHTTPResourceManager; + +public class DockerContainerLauncher implements ArtifactLauncher { + + private static final int DEFAULT_PORT = 8081; + private static final int DEFAULT_HTTPS_PORT = 8444; + private static final long DEFAULT_WAIT_TIME = 60; + + private final String containerImage; + private final String profile; + private Process quarkusProcess; + private int port; + private final int httpsPort; + private final long jarWaitTime; + private final Map systemProps = new HashMap<>(); + + private DockerContainerLauncher(String containerImage, Config config) { + this(containerImage, + config.getValue("quarkus.http.test-port", OptionalInt.class).orElse(DEFAULT_PORT), + config.getValue("quarkus.http.test-ssl-port", OptionalInt.class).orElse(DEFAULT_HTTPS_PORT), + config.getValue("quarkus.test.jar-wait-time", OptionalLong.class).orElse(DEFAULT_WAIT_TIME), + config.getOptionalValue("quarkus.test.native-image-profile", String.class) + .orElse(null)); + } + + public DockerContainerLauncher(String containerImage) { + this(containerImage, installAndGetSomeConfig()); + } + + public DockerContainerLauncher(String containerImage, int port, int httpsPort, long jarWaitTime, String profile) { + this.containerImage = containerImage; + this.port = port; + this.httpsPort = httpsPort; + this.jarWaitTime = jarWaitTime; + this.profile = profile; + } + + public void start() throws IOException { + + System.setProperty("test.url", TestHTTPResourceManager.getUri()); + + List args = new ArrayList<>(); + args.add("docker"); // TODO: determine this dynamically? + args.add("run"); + args.add("--rm"); + args.add("-p"); + args.add(port + ":" + port); + args.add("-p"); + args.add(httpsPort + ":" + httpsPort); + args.addAll(toEnvVar("quarkus.http.port", "" + port)); + args.addAll(toEnvVar("quarkus.http.ssl-port", "" + httpsPort)); + // this won't be correct when using the random port but it's really only used by us for the rest client tests + // in the main module, since those tests hit the application itself + args.addAll(toEnvVar("test.url", TestHTTPResourceManager.getUri())); + if (profile != null) { + args.addAll(toEnvVar("quarkus.profile", profile)); + } + for (Map.Entry e : systemProps.entrySet()) { + args.addAll(toEnvVar(e.getKey(), e.getValue())); + } + args.add(containerImage); + + System.out.println("Executing " + args); + + quarkusProcess = Runtime.getRuntime().exec(args.toArray(new String[0])); + port = LauncherUtil.doStart(quarkusProcess, port, httpsPort, jarWaitTime, null); + } + + public boolean isDefaultSsl() { + try (Socket s = new Socket()) { + s.connect(new InetSocketAddress("localhost", port)); + return false; + } catch (IOException e) { + return true; + } + } + + public void addSystemProperties(Map systemProps) { + this.systemProps.putAll(systemProps); + } + + private List toEnvVar(String property, String value) { + if ((property != null) && (!property.isEmpty())) { + List result = new ArrayList<>(2); + result.add("--env"); + result.add(String.format("%s=%s", convertPropertyToEnVar(property), value)); + return result; + } + return Collections.emptyList(); + } + + private String convertPropertyToEnVar(String property) { + return property.toUpperCase().replace('-', '_').replace('.', '_'); + } + + @Override + public void close() { + quarkusProcess.destroy(); + } +} diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/JarLauncher.java b/test-framework/common/src/main/java/io/quarkus/test/common/JarLauncher.java new file mode 100644 index 00000000000000..0149a84a507fe6 --- /dev/null +++ b/test-framework/common/src/main/java/io/quarkus/test/common/JarLauncher.java @@ -0,0 +1,99 @@ +package io.quarkus.test.common; + +import static io.quarkus.test.common.LauncherUtil.installAndGetSomeConfig; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.OptionalInt; +import java.util.OptionalLong; + +import org.eclipse.microprofile.config.Config; + +import io.quarkus.test.common.http.TestHTTPResourceManager; + +public class JarLauncher implements ArtifactLauncher { + + private static final int DEFAULT_PORT = 8081; + private static final int DEFAULT_HTTPS_PORT = 8444; + private static final long DEFAULT_JAR_WAIT_TIME = 60; + + private final Path jarPath; + private final String profile; + private Process quarkusProcess; + private int port; + private final int httpsPort; + private final long jarWaitTime; + private final Map systemProps = new HashMap<>(); + + private JarLauncher(Path jarPath, Config config) { + this(jarPath, + config.getValue("quarkus.http.test-port", OptionalInt.class).orElse(DEFAULT_PORT), + config.getValue("quarkus.http.test-ssl-port", OptionalInt.class).orElse(DEFAULT_HTTPS_PORT), + config.getValue("quarkus.test.jar-wait-time", OptionalLong.class).orElse(DEFAULT_JAR_WAIT_TIME), + config.getOptionalValue("quarkus.test.native-image-profile", String.class) + .orElse(null)); + } + + public JarLauncher(Path jarPath) { + this(jarPath, installAndGetSomeConfig()); + } + + public JarLauncher(Path jarPath, int port, int httpsPort, long jarWaitTime, String profile) { + this.jarPath = jarPath; + this.port = port; + this.httpsPort = httpsPort; + this.jarWaitTime = jarWaitTime; + this.profile = profile; + } + + public void start() throws IOException { + + System.setProperty("test.url", TestHTTPResourceManager.getUri()); + + List args = new ArrayList<>(); + args.add("java"); + args.add("-Dquarkus.http.port=" + port); + args.add("-Dquarkus.http.ssl-port=" + httpsPort); + // this won't be correct when using the random port but it's really only used by us for the rest client tests + // in the main module, since those tests hit the application itself + args.add("-Dtest.url=" + TestHTTPResourceManager.getUri()); + args.add("-Dquarkus.log.file.path=" + PropertyTestUtil.getLogFileLocation()); + if (profile != null) { + args.add("-Dquarkus.profile=" + profile); + } + for (Map.Entry e : systemProps.entrySet()) { + args.add("-D" + e.getKey() + "=" + e.getValue()); + } + args.add("-jar"); + args.add(jarPath.toAbsolutePath().toString()); + + System.out.println("Executing " + args); + + quarkusProcess = Runtime.getRuntime().exec(args.toArray(new String[0])); + port = LauncherUtil.doStart(quarkusProcess, port, httpsPort, jarWaitTime, null); + } + + public boolean isDefaultSsl() { + try (Socket s = new Socket()) { + s.connect(new InetSocketAddress("localhost", port)); + return false; + } catch (IOException e) { + return true; + } + } + + public void addSystemProperties(Map systemProps) { + this.systemProps.putAll(systemProps); + } + + @Override + public void close() { + quarkusProcess.destroy(); + } +} diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/LauncherUtil.java b/test-framework/common/src/main/java/io/quarkus/test/common/LauncherUtil.java new file mode 100644 index 00000000000000..5b8f0a657f9c0e --- /dev/null +++ b/test-framework/common/src/main/java/io/quarkus/test/common/LauncherUtil.java @@ -0,0 +1,105 @@ +package io.quarkus.test.common; + +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.function.Supplier; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; + +import io.quarkus.runtime.configuration.ConfigUtils; +import io.quarkus.runtime.configuration.QuarkusConfigFactory; +import io.quarkus.test.common.http.TestHTTPResourceManager; +import io.smallrye.config.SmallRyeConfig; + +final class LauncherUtil { + + private LauncherUtil() { + } + + static Config installAndGetSomeConfig() { + final SmallRyeConfig config = ConfigUtils.configBuilder(false).build(); + QuarkusConfigFactory.setConfig(config); + final ConfigProviderResolver cpr = ConfigProviderResolver.instance(); + try { + final Config installed = cpr.getConfig(); + if (installed != config) { + cpr.releaseConfig(installed); + } + } catch (IllegalStateException ignored) { + } + return config; + } + + static int doStart(Process quarkusProcess, int httpPort, int httpsPort, long waitTime, Supplier startedSupplier) { + PortCapturingProcessReader portCapturingProcessReader = null; + if (httpPort == 0) { + // when the port is 0, then the application starts on a random port and the only way for us to figure it out + // is to capture the output + portCapturingProcessReader = new PortCapturingProcessReader(quarkusProcess.getInputStream()); + } + new Thread(portCapturingProcessReader != null ? portCapturingProcessReader + : new ProcessReader(quarkusProcess.getInputStream())).start(); + new Thread(new ProcessReader(quarkusProcess.getErrorStream())).start(); + + if (portCapturingProcessReader != null) { + try { + portCapturingProcessReader.awaitForPort(); + } catch (InterruptedException ignored) { + + } + if (portCapturingProcessReader.getPort() == null) { + quarkusProcess.destroy(); + throw new RuntimeException("Unable to determine actual running port as dynamic port was used"); + } + + waitForQuarkus(quarkusProcess, portCapturingProcessReader.getPort(), httpsPort, waitTime, startedSupplier); + + System.setProperty("quarkus.http.port", portCapturingProcessReader.getPort().toString()); //set the port as a system property in order to have it applied to Config + System.setProperty("quarkus.http.test-port", portCapturingProcessReader.getPort().toString()); // needed for RestAssuredManager + int capturedPort = portCapturingProcessReader.getPort(); + installAndGetSomeConfig(); // reinitialize the configuration to make sure the actual port is used + System.setProperty("test.url", TestHTTPResourceManager.getUri()); + return capturedPort; + } else { + waitForQuarkus(quarkusProcess, httpPort, httpsPort, waitTime, startedSupplier); + return httpPort; + } + } + + private static void waitForQuarkus(Process quarkusProcess, int httpPort, int httpsPort, long waitTime, + Supplier startedSupplier) { + long bailout = System.currentTimeMillis() + waitTime * 1000; + + while (System.currentTimeMillis() < bailout) { + if (!quarkusProcess.isAlive()) { + throw new RuntimeException("Failed to start target quarkus application, process has exited"); + } + try { + Thread.sleep(100); + if (startedSupplier != null) { + if (startedSupplier.get()) { + return; + } + } + try { + try (Socket s = new Socket()) { + s.connect(new InetSocketAddress("localhost", httpPort)); + //SSL is bound after https + //we add a small delay to make sure SSL is available if installed + Thread.sleep(100); + return; + } + } catch (Exception expected) { + } + try (Socket s = new Socket()) { + s.connect(new InetSocketAddress("localhost", httpsPort)); + return; + } + } catch (Exception expected) { + } + } + quarkusProcess.destroyForcibly(); + throw new RuntimeException("Unable to start target quarkus application " + waitTime + "s"); + } +} diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/NativeImageLauncher.java b/test-framework/common/src/main/java/io/quarkus/test/common/NativeImageLauncher.java index 4f543f5216f416..495ae20a8a2a59 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/NativeImageLauncher.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/NativeImageLauncher.java @@ -1,14 +1,13 @@ package io.quarkus.test.common; -import java.io.Closeable; +import static io.quarkus.test.common.LauncherUtil.installAndGetSomeConfig; + import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.net.URL; import java.net.URLClassLoader; -import java.nio.charset.StandardCharsets; import java.security.CodeSource; import java.util.ArrayList; import java.util.HashMap; @@ -18,21 +17,13 @@ import java.util.OptionalInt; import java.util.OptionalLong; import java.util.ServiceLoader; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.Lock; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import java.util.function.Supplier; import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.spi.ConfigProviderResolver; -import org.wildfly.common.lock.Locks; -import io.quarkus.runtime.configuration.ConfigUtils; -import io.quarkus.runtime.configuration.QuarkusConfigFactory; import io.quarkus.test.common.http.TestHTTPResourceManager; -import io.smallrye.config.SmallRyeConfig; -public class NativeImageLauncher implements Closeable { +public class NativeImageLauncher implements ArtifactLauncher { private static final int DEFAULT_PORT = 8081; private static final int DEFAULT_HTTPS_PORT = 8444; @@ -47,7 +38,7 @@ public class NativeImageLauncher implements Closeable { private final int httpsPort; private final long imageWaitTime; private final Map systemProps = new HashMap<>(); - private List startedNotifiers; + private final Supplier startedSupplier; private NativeImageLauncher(Class testClass, Config config) { this(testClass, @@ -63,20 +54,6 @@ public NativeImageLauncher(Class testClass) { this(testClass, installAndGetSomeConfig()); } - private static Config installAndGetSomeConfig() { - final SmallRyeConfig config = ConfigUtils.configBuilder(false).build(); - QuarkusConfigFactory.setConfig(config); - final ConfigProviderResolver cpr = ConfigProviderResolver.instance(); - try { - final Config installed = cpr.getConfig(); - if (installed != config) { - cpr.releaseConfig(installed); - } - } catch (IllegalStateException ignored) { - } - return config; - } - public NativeImageLauncher(Class testClass, int port, int httpsPort, long imageWaitTime, String profile) { this.testClass = testClass; this.port = port; @@ -86,12 +63,18 @@ public NativeImageLauncher(Class testClass, int port, int httpsPort, long ima for (NativeImageStartedNotifier i : ServiceLoader.load(NativeImageStartedNotifier.class)) { startedNotifiers.add(i); } - this.startedNotifiers = startedNotifiers; this.profile = profile; + this.startedSupplier = () -> { + for (NativeImageStartedNotifier i : startedNotifiers) { + if (i.isNativeImageStarted()) { + return true; + } + } + return false; + }; } public void start() throws IOException { - System.setProperty("test.url", TestHTTPResourceManager.getUri()); String path = System.getProperty("native.image.path"); @@ -115,39 +98,8 @@ public void start() throws IOException { System.out.println("Executing " + args); - quarkusProcess = Runtime.getRuntime().exec(args.toArray(new String[args.size()])); - - PortCapturingProcessReader portCapturingProcessReader = null; - if (port == 0) { - // when the port is 0, then the application starts on a random port and the only way for us to figure it out - // is to capture the output - portCapturingProcessReader = new PortCapturingProcessReader(quarkusProcess.getInputStream()); - } - new Thread(portCapturingProcessReader != null ? portCapturingProcessReader - : new ProcessReader(quarkusProcess.getInputStream())).start(); - new Thread(new ProcessReader(quarkusProcess.getErrorStream())).start(); - - if (portCapturingProcessReader != null) { - try { - portCapturingProcessReader.awaitForPort(); - } catch (InterruptedException ignored) { - - } - if (portCapturingProcessReader.port == null) { - quarkusProcess.destroy(); - throw new RuntimeException("Unable to determine actual running port as dynamic port was used"); - } - - waitForQuarkus(portCapturingProcessReader.port); - - System.setProperty("quarkus.http.port", portCapturingProcessReader.port.toString()); //set the port as a system property in order to have it applied to Config - System.setProperty("quarkus.http.test-port", portCapturingProcessReader.port.toString()); // needed for RestAssuredManager - port = portCapturingProcessReader.port; - installAndGetSomeConfig(); // reinitialize the configuration to make sure the actual port is used - System.setProperty("test.url", TestHTTPResourceManager.getUri()); - } else { - waitForQuarkus(port); - } + quarkusProcess = Runtime.getRuntime().exec(args.toArray(new String[0])); + port = LauncherUtil.doStart(quarkusProcess, port, httpsPort, imageWaitTime, startedSupplier); } private static String guessPath(Class testClass) { @@ -233,41 +185,6 @@ private static void logGuessedPath(String guessedPath) { System.err.println("======================================================================================"); } - private void waitForQuarkus(int port) { - long bailout = System.currentTimeMillis() + imageWaitTime * 1000; - - while (System.currentTimeMillis() < bailout) { - if (!quarkusProcess.isAlive()) { - throw new RuntimeException("Failed to start native image, process has exited"); - } - try { - Thread.sleep(100); - for (NativeImageStartedNotifier i : startedNotifiers) { - if (i.isNativeImageStarted()) { - return; - } - } - try { - try (Socket s = new Socket()) { - s.connect(new InetSocketAddress("localhost", port)); - //SSL is bound after https - //we add a small delay to make sure SSL is available if installed - Thread.sleep(100); - return; - } - } catch (Exception expected) { - } - try (Socket s = new Socket()) { - s.connect(new InetSocketAddress("localhost", httpsPort)); - return; - } - } catch (Exception expected) { - } - } - quarkusProcess.destroyForcibly(); - throw new RuntimeException("Unable to start native image in " + imageWaitTime + "s"); - } - public boolean isDefaultSsl() { try (Socket s = new Socket()) { s.connect(new InetSocketAddress("localhost", port)); @@ -281,108 +198,6 @@ public void addSystemProperties(Map systemProps) { this.systemProps.putAll(systemProps); } - private static class ProcessReader implements Runnable { - - private final InputStream inputStream; - - private ProcessReader(InputStream inputStream) { - this.inputStream = inputStream; - } - - @Override - public void run() { - handleStart(); - byte[] b = new byte[100]; - int i; - try { - while ((i = inputStream.read(b)) > 0) { - String str = new String(b, 0, i, StandardCharsets.UTF_8); - System.out.print(str); - handleString(str); - } - } catch (IOException e) { - handleError(e); - } - } - - protected void handleStart() { - - } - - protected void handleString(String str) { - - } - - protected void handleError(IOException e) { - - } - } - - private static final class PortCapturingProcessReader extends ProcessReader { - private Integer port; - - private boolean portDetermined = false; - private StringBuilder sb = new StringBuilder(); - private final Lock lock = Locks.reentrantLock(); - private final Condition portDeterminedCondition = lock.newCondition(); - private final Pattern portRegex = Pattern.compile("Listening on:\\s+https?://.*:(\\d+)"); - - private PortCapturingProcessReader(InputStream inputStream) { - super(inputStream); - } - - @Override - protected void handleStart() { - lock.lock(); - } - - @Override - protected void handleString(String str) { - if (portDetermined) { // we are done with determining the port - return; - } - sb.append(str); - String currentOutput = sb.toString(); - Matcher regexMatcher = portRegex.matcher(currentOutput); - if (!regexMatcher.find()) { // haven't read enough data yet - if (currentOutput.contains("Exception")) { - portDetermined(null); - } - return; - } - portDetermined(Integer.valueOf(regexMatcher.group(1))); - } - - private void portDetermined(Integer portValue) { - this.port = portValue; - try { - portDetermined = true; - sb = null; - portDeterminedCondition.signal(); - } finally { - lock.unlock(); - } - } - - @Override - protected void handleError(IOException e) { - if (!portDetermined) { - portDetermined(null); - } - } - - public void awaitForPort() throws InterruptedException { - lock.lock(); - try { - while (!portDetermined) { - portDeterminedCondition.await(); - } - } finally { - lock.unlock(); - } - } - } - @Override public void close() { quarkusProcess.destroy(); diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/PortCapturingProcessReader.java b/test-framework/common/src/main/java/io/quarkus/test/common/PortCapturingProcessReader.java new file mode 100644 index 00000000000000..5921b56d90ec4a --- /dev/null +++ b/test-framework/common/src/main/java/io/quarkus/test/common/PortCapturingProcessReader.java @@ -0,0 +1,79 @@ +package io.quarkus.test.common; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.wildfly.common.lock.Locks; + +final class PortCapturingProcessReader extends ProcessReader { + private Integer port; + + private boolean portDetermined = false; + private StringBuilder sb = new StringBuilder(); + private final Lock lock = Locks.reentrantLock(); + private final Condition portDeterminedCondition = lock.newCondition(); + private final Pattern portRegex = Pattern.compile("Listening on:\\s+https?://.*:(\\d+)"); + + PortCapturingProcessReader(InputStream inputStream) { + super(inputStream); + } + + @Override + protected void handleStart() { + lock.lock(); + } + + @Override + protected void handleString(String str) { + if (portDetermined) { // we are done with determining the port + return; + } + sb.append(str); + String currentOutput = sb.toString(); + Matcher regexMatcher = portRegex.matcher(currentOutput); + if (!regexMatcher.find()) { // haven't read enough data yet + if (currentOutput.contains("Exception")) { + portDetermined(null); + } + return; + } + portDetermined(Integer.valueOf(regexMatcher.group(1))); + } + + private void portDetermined(Integer portValue) { + this.port = portValue; + try { + portDetermined = true; + sb = null; + portDeterminedCondition.signal(); + } finally { + lock.unlock(); + } + } + + @Override + protected void handleError(IOException e) { + if (!portDetermined) { + portDetermined(null); + } + } + + public void awaitForPort() throws InterruptedException { + lock.lock(); + try { + while (!portDetermined) { + portDeterminedCondition.await(); + } + } finally { + lock.unlock(); + } + } + + public Integer getPort() { + return port; + } +} diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/ProcessReader.java b/test-framework/common/src/main/java/io/quarkus/test/common/ProcessReader.java new file mode 100644 index 00000000000000..986897570e20be --- /dev/null +++ b/test-framework/common/src/main/java/io/quarkus/test/common/ProcessReader.java @@ -0,0 +1,42 @@ +package io.quarkus.test.common; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +class ProcessReader implements Runnable { + + private final InputStream inputStream; + + ProcessReader(InputStream inputStream) { + this.inputStream = inputStream; + } + + @Override + public void run() { + handleStart(); + byte[] b = new byte[100]; + int i; + try { + while ((i = inputStream.read(b)) > 0) { + String str = new String(b, 0, i, StandardCharsets.UTF_8); + System.out.print(str); + handleString(str); + } + } catch (IOException e) { + handleError(e); + } + } + + protected void handleStart() { + + } + + protected void handleString(String str) { + + } + + protected void handleError(IOException e) { + + } +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/DisabledOnIntegrationTest.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/DisabledOnIntegrationTest.java new file mode 100644 index 00000000000000..28077cf6f2b99c --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/DisabledOnIntegrationTest.java @@ -0,0 +1,39 @@ +package io.quarkus.test.junit; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * {@code @DisabledOnIntegrationTest} is used to signal that the annotated test class or + * test method should not be executed for as part of a {@link QuarkusIntegrationTest}. + * + *

+ * {@code @DisabledOnIntegrationTest} may optionally be declared with a {@linkplain #value + * reason} to document why the annotated test class or test method is disabled. + * + *

+ * When applied at the class level, all test methods within that class + * are automatically disabled. + * + *

+ * When applied at the method level, the presence of this annotation does not + * prevent the test class from being instantiated. Rather, it prevents the + * execution of the test method and method-level lifecycle callbacks such as + * {@code @BeforeEach} methods, {@code @AfterEach} methods, and corresponding + * extension APIs. + * + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface DisabledOnIntegrationTest { + /** + * Reason for disabling this test + */ + String value() default ""; +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/DisabledOnIntegrationTestCondition.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/DisabledOnIntegrationTestCondition.java new file mode 100644 index 00000000000000..824d643121e02d --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/DisabledOnIntegrationTestCondition.java @@ -0,0 +1,57 @@ +package io.quarkus.test.junit; + +import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.Optional; +import java.util.function.Function; + +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.platform.commons.util.StringUtils; + +public class DisabledOnIntegrationTestCondition implements ExecutionCondition { + + private static final ConditionEvaluationResult ENABLED = ConditionEvaluationResult + .enabled("@DisabledOnIntegrationTest is not present"); + + /** + * Containers/tests are disabled if {@code @DisabledOnIntegrationTest} is present on the test + * class or method and we're running on a native image. + */ + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + Optional element = context.getElement(); + ConditionEvaluationResult disabledOnIntegrationTestReason = check(context, element, DisabledOnIntegrationTest.class, + DisabledOnIntegrationTest::value); + if (disabledOnIntegrationTestReason != null) { + return disabledOnIntegrationTestReason; + } + // support DisabledOnNativeImage for backward compatibility + ConditionEvaluationResult disabledOnNativeImageReason = check(context, element, DisabledOnNativeImage.class, + DisabledOnNativeImage::value); + if (disabledOnNativeImageReason != null) { + return disabledOnNativeImageReason; + } + return ENABLED; + } + + private ConditionEvaluationResult check(ExtensionContext context, Optional element, + Class annotationClass, Function valueExtractor) { + Optional disabled = findAnnotation(element, annotationClass); + if (disabled.isPresent()) { + // Cannot use ExtensionState here because this condition needs to be evaluated before QuarkusTestExtension + boolean it = findAnnotation(context.getTestClass(), QuarkusIntegrationTest.class).isPresent(); + if (it) { + String reason = disabled.map(valueExtractor) + .filter(StringUtils::isNotBlank) + .orElseGet(() -> element.get() + " is @DisabledOnIntegrationTest"); + return ConditionEvaluationResult.disabled(reason); + } + } + return null; + } + +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestExtensionState.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestExtensionState.java new file mode 100644 index 00000000000000..4ddd1241f06777 --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestExtensionState.java @@ -0,0 +1,54 @@ +package io.quarkus.test.junit; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Map; + +import org.junit.jupiter.api.extension.ExtensionContext; + +import io.quarkus.test.common.TestResourceManager; + +public class IntegrationTestExtensionState implements ExtensionContext.Store.CloseableResource { + + private final TestResourceManager testResourceManager; + private final Closeable resource; + private final Map sysPropRestore; + private final Thread shutdownHook; + + IntegrationTestExtensionState(TestResourceManager testResourceManager, Closeable resource, + Map sysPropRestore) { + this.testResourceManager = testResourceManager; + this.resource = resource; + this.sysPropRestore = sysPropRestore; + this.shutdownHook = new Thread(new Runnable() { + @Override + public void run() { + try { + IntegrationTestExtensionState.this.close(); + } catch (IOException ignored) { + } + } + }, "Quarkus Test Cleanup Shutdown task"); + Runtime.getRuntime().addShutdownHook(shutdownHook); + + } + + @Override + public void close() throws IOException { + testResourceManager.close(); + resource.close(); + for (Map.Entry entry : sysPropRestore.entrySet()) { + String val = entry.getValue(); + if (val == null) { + System.clearProperty(entry.getKey()); + } else { + System.setProperty(entry.getKey(), val); + } + } + Runtime.getRuntime().removeShutdownHook(shutdownHook); + } + + public TestResourceManager getTestResourceManager() { + return testResourceManager; + } +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java new file mode 100644 index 00000000000000..65b39584cc8de1 --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java @@ -0,0 +1,108 @@ +package io.quarkus.test.junit; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.enterprise.inject.Alternative; +import javax.inject.Inject; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.platform.commons.JUnitException; + +import io.quarkus.runtime.configuration.ProfileManager; +import io.quarkus.test.common.ArtifactLauncher; +import io.quarkus.test.common.http.TestHTTPResourceManager; + +final class IntegrationTestUtil { + + private IntegrationTestUtil() { + } + + static void ensureNoInjectAnnotationIsUsed(Class testClass) { + Class current = testClass; + while (current.getSuperclass() != null) { + for (Field field : current.getDeclaredFields()) { + Inject injectAnnotation = field.getAnnotation(Inject.class); + if (injectAnnotation != null) { + throw new JUnitException( + "@Inject is not supported in @NativeImageTest and @QuarkusIntegrationTest tests. Offending field is " + + field.getDeclaringClass().getTypeName() + "." + + field.getName()); + } + } + current = current.getSuperclass(); + } + + } + + static void doProcessTestInstance(Object testInstance, ExtensionContext context) { + TestHTTPResourceManager.inject(testInstance); + ExtensionContext root = context.getRoot(); + ExtensionContext.Store store = root.getStore(ExtensionContext.Namespace.GLOBAL); + IntegrationTestExtensionState state = store.get(IntegrationTestExtensionState.class.getName(), + IntegrationTestExtensionState.class); + state.getTestResourceManager().inject(testInstance); + } + + static Map getSysPropsToRestore() { + Map sysPropRestore = new HashMap<>(); + sysPropRestore.put(ProfileManager.QUARKUS_TEST_PROFILE_PROP, + System.getProperty(ProfileManager.QUARKUS_TEST_PROFILE_PROP)); + return sysPropRestore; + } + + static Map determineAdditionalProperties(Class profile, + Map sysPropRestore) throws InstantiationException, IllegalAccessException { + final Map additional = new HashMap<>(); + if (profile != null) { + QuarkusTestProfile profileInstance = profile.newInstance(); + additional.putAll(profileInstance.getConfigOverrides()); + final Set> enabledAlternatives = profileInstance.getEnabledAlternatives(); + if (!enabledAlternatives.isEmpty()) { + additional.put("quarkus.arc.selected-alternatives", enabledAlternatives.stream() + .peek((c) -> { + if (!c.isAnnotationPresent(Alternative.class)) { + throw new RuntimeException( + "Enabled alternative " + c + " is not annotated with @Alternative"); + } + }) + .map(Class::getName).collect(Collectors.joining(","))); + } + final String configProfile = profileInstance.getConfigProfile(); + if (configProfile != null) { + additional.put(ProfileManager.QUARKUS_PROFILE_PROP, configProfile); + } + additional.put("quarkus.configuration.build-time-mismatch-at-runtime", "fail"); + for (Map.Entry i : additional.entrySet()) { + sysPropRestore.put(i.getKey(), System.getProperty(i.getKey())); + } + for (Map.Entry i : additional.entrySet()) { + System.setProperty(i.getKey(), i.getValue()); + } + } + return additional; + } + + static void startLauncher(ArtifactLauncher launcher, Map additionalProperties, Runnable sslSetter) + throws IOException { + launcher.addSystemProperties(additionalProperties); + try { + launcher.start(); + } catch (IOException e) { + try { + launcher.close(); + } catch (Throwable ignored) { + } + throw e; + } + if (launcher.isDefaultSsl()) { + if (sslSetter != null) { + sslSetter.run(); + } + } + } +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/NativeTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/NativeTestExtension.java index 06da57d3aa9c53..83576833fe6a1b 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/NativeTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/NativeTestExtension.java @@ -1,35 +1,26 @@ package io.quarkus.test.junit; -import java.io.Closeable; -import java.io.IOException; -import java.lang.reflect.Field; -import java.util.HashMap; +import static io.quarkus.test.junit.IntegrationTestUtil.*; +import static io.quarkus.test.junit.IntegrationTestUtil.ensureNoInjectAnnotationIsUsed; + import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.function.Function; -import java.util.stream.Collectors; - -import javax.enterprise.inject.Alternative; -import javax.inject.Inject; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestInstancePostProcessor; -import org.junit.platform.commons.JUnitException; import org.opentest4j.TestAbortedException; -import io.quarkus.runtime.configuration.ProfileManager; import io.quarkus.runtime.test.TestHttpEndpointProvider; import io.quarkus.test.common.NativeImageLauncher; import io.quarkus.test.common.PropertyTestUtil; import io.quarkus.test.common.RestAssuredURLManager; import io.quarkus.test.common.TestResourceManager; import io.quarkus.test.common.TestScopeManager; -import io.quarkus.test.common.http.TestHTTPResourceManager; public class NativeTestExtension implements BeforeEachCallback, AfterEachCallback, BeforeAllCallback, TestInstancePostProcessor { @@ -65,30 +56,14 @@ public void beforeAll(ExtensionContext extensionContext) throws Exception { ensureStarted(extensionContext); } - private void ensureNoInjectAnnotationIsUsed(Class testClass) { - Class current = testClass; - while (current.getSuperclass() != null) { - for (Field field : current.getDeclaredFields()) { - Inject injectAnnotation = field.getAnnotation(Inject.class); - if (injectAnnotation != null) { - throw new JUnitException( - "@Inject is not supported in NativeImageTest tests. Offending field is " - + field.getDeclaringClass().getTypeName() + "." - + field.getName()); - } - } - current = current.getSuperclass(); - } - - } - - private ExtensionState ensureStarted(ExtensionContext extensionContext) { + private IntegrationTestExtensionState ensureStarted(ExtensionContext extensionContext) { Class testClass = extensionContext.getRequiredTestClass(); ensureNoInjectAnnotationIsUsed(testClass); ExtensionContext root = extensionContext.getRoot(); ExtensionContext.Store store = root.getStore(ExtensionContext.Namespace.GLOBAL); - ExtensionState state = store.get(ExtensionState.class.getName(), ExtensionState.class); + IntegrationTestExtensionState state = store.get(IntegrationTestExtensionState.class.getName(), + IntegrationTestExtensionState.class); TestProfile annotation = testClass.getAnnotation(TestProfile.class); Class selectedProfile = null; if (annotation != null) { @@ -108,7 +83,7 @@ private ExtensionState ensureStarted(ExtensionContext extensionContext) { PropertyTestUtil.setLogFileProperty(); try { state = doNativeStart(extensionContext, selectedProfile); - store.put(ExtensionState.class.getName(), state); + store.put(IntegrationTestExtensionState.class.getName(), state); } catch (Throwable e) { failedBoot = true; @@ -118,66 +93,26 @@ private ExtensionState ensureStarted(ExtensionContext extensionContext) { return state; } - private ExtensionState doNativeStart(ExtensionContext context, Class profile) + private IntegrationTestExtensionState doNativeStart(ExtensionContext context, Class profile) throws Throwable { quarkusTestProfile = profile; TestResourceManager testResourceManager = null; try { Class requiredTestClass = context.getRequiredTestClass(); - Map sysPropRestore = new HashMap<>(); - sysPropRestore.put(ProfileManager.QUARKUS_TEST_PROFILE_PROP, - System.getProperty(ProfileManager.QUARKUS_TEST_PROFILE_PROP)); - - QuarkusTestProfile profileInstance = null; - final Map additional = new HashMap<>(); - if (profile != null) { - profileInstance = profile.newInstance(); - additional.putAll(profileInstance.getConfigOverrides()); - final Set> enabledAlternatives = profileInstance.getEnabledAlternatives(); - if (!enabledAlternatives.isEmpty()) { - additional.put("quarkus.arc.selected-alternatives", enabledAlternatives.stream() - .peek((c) -> { - if (!c.isAnnotationPresent(Alternative.class)) { - throw new RuntimeException( - "Enabled alternative " + c + " is not annotated with @Alternative"); - } - }) - .map(Class::getName).collect(Collectors.joining(","))); - } - final String configProfile = profileInstance.getConfigProfile(); - if (configProfile != null) { - additional.put(ProfileManager.QUARKUS_PROFILE_PROP, configProfile); - } - additional.put("quarkus.configuration.build-time-mismatch-at-runtime", "fail"); - for (Map.Entry i : additional.entrySet()) { - sysPropRestore.put(i.getKey(), System.getProperty(i.getKey())); - } - for (Map.Entry i : additional.entrySet()) { - System.setProperty(i.getKey(), i.getValue()); - } - } + Map sysPropRestore = getSysPropsToRestore(); + Map additionalProperties = determineAdditionalProperties(profile, + sysPropRestore); testResourceManager = new TestResourceManager(requiredTestClass); testResourceManager.init(); - additional.putAll(testResourceManager.start()); + additionalProperties.putAll(testResourceManager.start()); NativeImageLauncher launcher = new NativeImageLauncher(requiredTestClass); - launcher.addSystemProperties(additional); - try { - launcher.start(); - } catch (IOException e) { - try { - launcher.close(); - } catch (Throwable t) { - } - throw e; - } - if (launcher.isDefaultSsl()) { - ssl = true; - } + startLauncher(launcher, additionalProperties, () -> ssl = true); - final ExtensionState state = new ExtensionState(testResourceManager, launcher, sysPropRestore); + final IntegrationTestExtensionState state = new IntegrationTestExtensionState(testResourceManager, launcher, + sysPropRestore); testHttpEndpointProviders = TestHttpEndpointProvider.load(); @@ -196,17 +131,13 @@ private ExtensionState doNativeStart(ExtensionContext context, Class sysPropRestore; - private final Thread shutdownHook; - - ExtensionState(TestResourceManager testResourceManager, Closeable resource, Map sysPropRestore) { - this.testResourceManager = testResourceManager; - this.resource = resource; - this.sysPropRestore = sysPropRestore; - this.shutdownHook = new Thread(new Runnable() { - @Override - public void run() { - try { - ExtensionState.this.close(); - } catch (IOException ignored) { - } - } - }, "Quarkus Test Cleanup Shutdown task"); - Runtime.getRuntime().addShutdownHook(shutdownHook); - - } - - @Override - public void close() throws IOException { - testResourceManager.close(); - resource.close(); - for (Map.Entry entry : sysPropRestore.entrySet()) { - String val = entry.getValue(); - if (val == null) { - System.clearProperty(entry.getKey()); - } else { - System.setProperty(entry.getKey(), val); - } - } - Runtime.getRuntime().removeShutdownHook(shutdownHook); - } - } } diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTest.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTest.java new file mode 100644 index 00000000000000..25af85954ec700 --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTest.java @@ -0,0 +1,34 @@ +package io.quarkus.test.junit; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Annotation that indicates that this test should be run the result of the Quarkus build. + * That means that if a jar was created, that jar is launched using {@code java -jar ...} + * (and thus runs in a separate JVM than the test). + * If instead a native image was created, the that image is launched. + * Finally, if a container image was created during the build, then a new container is created and run. + * + * The standard usage pattern is expected to be a base test class that runs the + * tests using the JVM version of Quarkus, with a subclass that extends the base + * test and is annotated with this annotation to perform the same checks against + * the native image. + * + * Note that it is not possible to mix {@code @QuarkusTest} and {@code QuarkusIntegrationTest} in the same test + * run, it is expected that the {@code @QuarkusTest} tests will be standard unit tests that are + * executed by surefire, while the {@code QuarkusIntegrationTest} tests will be integration tests + * executed by failsafe. + * This also means that injection of beans into a test class using {@code @Inject} is not supported + * in {@code QuarkusIntegrationTest}. Such injection is only possible in tests injected with + * {@link @QuarkusTest} so the test class structure must take this into account. + */ +@Target(ElementType.TYPE) +@ExtendWith({ DisabledOnIntegrationTestCondition.class, QuarkusTestExtension.class, QuarkusIntegrationTestExtension.class }) +@Retention(RetentionPolicy.RUNTIME) +public @interface QuarkusIntegrationTest { +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java new file mode 100644 index 00000000000000..059088d3437834 --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java @@ -0,0 +1,271 @@ +package io.quarkus.test.junit; + +import static io.quarkus.test.junit.IntegrationTestUtil.*; +import static io.quarkus.test.junit.IntegrationTestUtil.ensureNoInjectAnnotationIsUsed; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.CodeSource; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.function.Function; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestInstancePostProcessor; +import org.opentest4j.TestAbortedException; + +import io.quarkus.runtime.test.TestHttpEndpointProvider; +import io.quarkus.test.common.ArtifactLauncher; +import io.quarkus.test.common.DockerContainerLauncher; +import io.quarkus.test.common.JarLauncher; +import io.quarkus.test.common.NativeImageLauncher; +import io.quarkus.test.common.PropertyTestUtil; +import io.quarkus.test.common.RestAssuredURLManager; +import io.quarkus.test.common.TestResourceManager; +import io.quarkus.test.common.TestScopeManager; + +public class QuarkusIntegrationTestExtension + implements BeforeEachCallback, AfterEachCallback, BeforeAllCallback, TestInstancePostProcessor { + + private static boolean failedBoot; + + private static List, String>> testHttpEndpointProviders; + private static boolean ssl; + + private static Class quarkusTestProfile; + private static Throwable firstException; //if this is set then it will be thrown from the very first test that is run, the rest are aborted + + @Override + public void afterEach(ExtensionContext context) throws Exception { + if (!failedBoot) { + RestAssuredURLManager.clearURL(); + TestScopeManager.tearDown(true); + } + } + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + if (failedBoot) { + throwBootFailureException(); + } else { + RestAssuredURLManager.setURL(ssl, QuarkusTestExtension.getEndpointPath(context, testHttpEndpointProviders)); + TestScopeManager.setup(true); + } + } + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + ensureStarted(extensionContext); + } + + private IntegrationTestExtensionState ensureStarted(ExtensionContext extensionContext) { + Class testClass = extensionContext.getRequiredTestClass(); + ensureNoInjectAnnotationIsUsed(testClass); + Properties quarkusArtifactProperties = readQuarkusArtifactProperties(extensionContext); + + ExtensionContext root = extensionContext.getRoot(); + ExtensionContext.Store store = root.getStore(ExtensionContext.Namespace.GLOBAL); + IntegrationTestExtensionState state = store.get(IntegrationTestExtensionState.class.getName(), + IntegrationTestExtensionState.class); + TestProfile annotation = testClass.getAnnotation(TestProfile.class); + Class selectedProfile = null; + if (annotation != null) { + selectedProfile = annotation.value(); + } + boolean wrongProfile = !Objects.equals(selectedProfile, quarkusTestProfile); + if ((state == null && !failedBoot) || wrongProfile) { + if (wrongProfile) { + if (state != null) { + try { + state.close(); + } catch (Throwable throwable) { + throwable.printStackTrace(); + } + } + } + PropertyTestUtil.setLogFileProperty(); + try { + state = doProcessStart(quarkusArtifactProperties, selectedProfile, extensionContext); + store.put(IntegrationTestExtensionState.class.getName(), state); + } catch (Throwable e) { + failedBoot = true; + firstException = e; + } + } + return state; + } + + private Properties readQuarkusArtifactProperties(ExtensionContext context) { + Path buildOutputDirectory = determineBuildOutputDirectory(context); + Path artifactProperties = buildOutputDirectory.resolve("quarkus-artifact.properties"); + if (!Files.exists(artifactProperties)) { + throw new IllegalStateException( + "Unable to locate the artifact metadata file created that must be created by Quarkus in order to run tests annotated with '@QuarkusIntegrationTest'."); + } + try { + Properties properties = new Properties(); + properties.load(new FileInputStream(artifactProperties.toFile())); + return properties; + } catch (IOException e) { + throw new UncheckedIOException( + "Unable to read artifact metadata file created that must be created by Quarkus in order to run tests annotated with '@QuarkusIntegrationTest'.", + e); + } + } + + private Path determineBuildOutputDirectory(ExtensionContext context) { + String buildOutputDirStr = System.getProperty("build.output.directory"); + Path result = null; + if (buildOutputDirStr != null) { + result = Paths.get(buildOutputDirStr); + } else { + // we need to guess where the artifact properties file is based on the location of the test class + Class testClass = context.getRequiredTestClass(); + final CodeSource codeSource = testClass.getProtectionDomain().getCodeSource(); + if (codeSource != null) { + URL codeSourceLocation = codeSource.getLocation(); + File artifactPropertiesDirectory = determineBuildOutputDirectory(codeSourceLocation); + if (artifactPropertiesDirectory == null) { + throw new IllegalStateException( + "Unable to determine the output of the Quarkus build. Consider setting the 'build.output.directory' system property."); + } + result = artifactPropertiesDirectory.toPath(); + } + } + if (result == null) { + throw new IllegalStateException( + "Unable to locate the artifact metadata file created that must be created by Quarkus in order to run tests annotated with '@QuarkusIntegrationTest'."); + } + if (!Files.isDirectory(result)) { + throw new IllegalStateException( + "The determined Quarkus build output '" + result.toAbsolutePath().toString() + "' is not a directory"); + } + return result; + } + + private static File determineBuildOutputDirectory(final URL url) { + if (url == null) { + return null; + } + if (url.getProtocol().equals("file") && url.getPath().endsWith("test-classes/")) { + //we have the maven test classes dir + File testClasses = new File(url.getPath()); + return testClasses.getParentFile(); + } else if (url.getProtocol().equals("file") && url.getPath().endsWith("test/")) { + //we have the gradle test classes dir, build/classes/java/test + File testClasses = new File(url.getPath()); + return testClasses.getParentFile().getParentFile().getParentFile(); + } else if (url.getProtocol().equals("file") && url.getPath().contains("/target/surefire/")) { + //this will make mvn failsafe:integration-test work + String path = url.getPath(); + int index = path.lastIndexOf("/target/"); + return new File(path.substring(0, index) + "/target/"); + } + return null; + } + + private IntegrationTestExtensionState doProcessStart(Properties quarkusArtifactProperties, + Class profile, ExtensionContext context) + throws Throwable { + quarkusTestProfile = profile; + TestResourceManager testResourceManager = null; + try { + Class requiredTestClass = context.getRequiredTestClass(); + + Map sysPropRestore = getSysPropsToRestore(); + Map additionalProperties = determineAdditionalProperties(profile, + sysPropRestore); + + testResourceManager = new TestResourceManager(requiredTestClass); + testResourceManager.init(); + additionalProperties.putAll(testResourceManager.start()); + + String artifactType = quarkusArtifactProperties.getProperty("type"); + if (artifactType == null) { + throw new IllegalStateException("Unable to determine the type of artifact created by the Quarkus build"); + } + ArtifactLauncher launcher; + switch (artifactType) { + case "native": { + String pathStr = quarkusArtifactProperties.getProperty("path"); + if ((pathStr != null) && !pathStr.isEmpty()) { + String previousNativeImagePathValue = System.setProperty("native.image.path", + determineBuildOutputDirectory(context).resolve(pathStr).toAbsolutePath().toString()); + sysPropRestore.put("native.image.path", previousNativeImagePathValue); + launcher = new NativeImageLauncher(requiredTestClass); + } else { + throw new IllegalStateException("The path of the native binary could not be determined"); + } + break; + } + case "jar": { + String pathStr = quarkusArtifactProperties.getProperty("path"); + if ((pathStr != null) && !pathStr.isEmpty()) { + launcher = new JarLauncher(determineBuildOutputDirectory(context).resolve(pathStr)); + } else { + throw new IllegalStateException("The path of the native binary could not be determined"); + } + break; + } + case "jar-container": + case "native-container": + String containerImage = quarkusArtifactProperties.getProperty("metadata.container-image"); + if ((containerImage != null) && !containerImage.isEmpty()) { + launcher = new DockerContainerLauncher(containerImage); + } else { + throw new IllegalStateException("The container image to be launched could not be determined"); + } + break; + default: + throw new IllegalStateException( + "Artifact type + '" + artifactType + "' is not supported by @QuarkusIntegrationTest"); + } + + startLauncher(launcher, additionalProperties, () -> ssl = true); + + IntegrationTestExtensionState state = new IntegrationTestExtensionState(testResourceManager, launcher, + sysPropRestore); + testHttpEndpointProviders = TestHttpEndpointProvider.load(); + + return state; + } catch (Throwable e) { + + try { + if (testResourceManager != null) { + testResourceManager.close(); + } + } catch (Exception ex) { + e.addSuppressed(ex); + } + throw e; + } + } + + @Override + public void postProcessTestInstance(Object testInstance, ExtensionContext context) { + if (!failedBoot) { + doProcessTestInstance(testInstance, context); + } + } + + private void throwBootFailureException() { + if (firstException != null) { + Throwable throwable = firstException; + firstException = null; + throw new RuntimeException(throwable); + } else { + throw new TestAbortedException("Boot failed"); + } + } +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java index f88af54a72e983..67a36867f1a909 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java @@ -461,7 +461,7 @@ private void populateCallbacks(ClassLoader classLoader) throws ClassNotFoundExce @Override public void beforeEach(ExtensionContext context) throws Exception { - if (isNativeTest()) { + if (isNativeTestOrIntegration()) { return; } resetHangTimeout(); @@ -530,7 +530,7 @@ public static String getEndpointPath(ExtensionContext context, List i : currentTestClassStack) { - if (i.isAnnotationPresent(NativeImageTest.class)) { + if (i.isAnnotationPresent(NativeImageTest.class) || i.isAnnotationPresent(QuarkusIntegrationTest.class)) { return true; } } @@ -663,7 +663,7 @@ public void beforeAll(ExtensionContext context) throws Exception { currentTestClassStack.push(context.getRequiredTestClass()); //set the right launch mode in the outer CL, used by the HTTP host config source ProfileManager.setLaunchMode(LaunchMode.TEST); - if (isNativeTest()) { + if (isNativeTestOrIntegration()) { return; } resetHangTimeout(); @@ -702,7 +702,7 @@ private void popMockContext() { @Override public void interceptBeforeAllMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { - if (isNativeTest()) { + if (isNativeTestOrIntegration()) { invocation.proceed(); return; } @@ -719,7 +719,7 @@ public void interceptBeforeAllMethod(Invocation invocation, ReflectiveInvo @Override public T interceptTestClassConstructor(Invocation invocation, ReflectiveInvocationContext> invocationContext, ExtensionContext extensionContext) throws Throwable { - if (isNativeTest()) { + if (isNativeTestOrIntegration()) { return invocation.proceed(); } resetHangTimeout(); @@ -813,7 +813,7 @@ private void initTestState(ExtensionContext extensionContext, ExtensionState sta @Override public void interceptBeforeEachMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { - if (isNativeTest()) { + if (isNativeTestOrIntegration()) { invocation.proceed(); return; } @@ -824,7 +824,7 @@ public void interceptBeforeEachMethod(Invocation invocation, ReflectiveInv @Override public void interceptTestMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { - if (isNativeTest()) { + if (isNativeTestOrIntegration()) { invocation.proceed(); return; } @@ -835,7 +835,7 @@ public void interceptTestMethod(Invocation invocation, ReflectiveInvocatio @Override public void interceptTestTemplateMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { - if (isNativeTest()) { + if (isNativeTestOrIntegration()) { invocation.proceed(); return; } @@ -847,7 +847,7 @@ public void interceptTestTemplateMethod(Invocation invocation, ReflectiveI @Override public T interceptTestFactoryMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { - if (isNativeTest()) { + if (isNativeTestOrIntegration()) { return invocation.proceed(); } T result = (T) runExtensionMethod(invocationContext, extensionContext); @@ -858,7 +858,7 @@ public T interceptTestFactoryMethod(Invocation invocation, @Override public void interceptAfterEachMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { - if (isNativeTest()) { + if (isNativeTestOrIntegration()) { invocation.proceed(); return; } @@ -869,7 +869,7 @@ public void interceptAfterEachMethod(Invocation invocation, ReflectiveInvo @Override public void interceptAfterAllMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { - if (isNativeTest()) { + if (isNativeTestOrIntegration()) { invocation.proceed(); return; } @@ -936,7 +936,7 @@ private Object runExtensionMethod(ReflectiveInvocationContext invocation public void afterAll(ExtensionContext context) throws Exception { resetHangTimeout(); try { - if (!isNativeTest() && (runningQuarkusApplication != null)) { + if (!isNativeTestOrIntegration() && (runningQuarkusApplication != null)) { popMockContext(); } if (originalCl != null) {