diff --git a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java index fba52593747295..58ead54da42dfd 100644 --- a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java +++ b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java @@ -13,6 +13,7 @@ import java.util.function.Consumer; import java.util.stream.Collectors; +import org.eclipse.microprofile.config.spi.ConfigBuilder; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; import org.jboss.logging.Logger; @@ -46,6 +47,7 @@ import io.quarkus.deployment.pkg.builditem.NativeImageBuildItem; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.configuration.ProfileManager; +import io.smallrye.config.PropertiesConfigSource; /** * The augmentation task that produces the application. @@ -219,6 +221,15 @@ private BuildResult runAugment(boolean firstRun, Set changedResources, C if (quarkusBootstrap.getBaseName() != null) { builder.setBaseName(quarkusBootstrap.getBaseName()); } + if (!quarkusBootstrap.getConfigOverrides().isEmpty()) { + builder.setConfigCustomizer(new Consumer() { + @Override + public void accept(ConfigBuilder configBuilder) { + configBuilder.withSources( + new PropertiesConfigSource(quarkusBootstrap.getConfigOverrides(), "Config Overrides", 1000)); + } + }); + } builder.setLaunchMode(launchMode); builder.setRebuild(quarkusBootstrap.isRebuild()); diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java index 185c0f171c067d..df658c445fc5b7 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Properties; @@ -80,6 +81,7 @@ public class QuarkusBootstrap implements Serializable { private final boolean disableClasspathCache; private final AppModel existingModel; private final boolean rebuild; + private final Map configOverrides; private QuarkusBootstrap(Builder builder) { this.applicationRoot = builder.applicationRoot; @@ -107,6 +109,7 @@ private QuarkusBootstrap(Builder builder) { this.disableClasspathCache = builder.disableClasspathCache; this.existingModel = builder.existingModel; this.rebuild = builder.rebuild; + this.configOverrides = builder.configOverrides == null ? Collections.emptyMap() : builder.configOverrides; } public CuratedApplication bootstrap() throws BootstrapException { @@ -163,6 +166,10 @@ public List getAdditionalApplicationArchives() { return Collections.unmodifiableList(additionalApplicationArchives); } + public Map getConfigOverrides() { + return Collections.unmodifiableMap(configOverrides); + } + public List getAdditionalDeploymentArchives() { return Collections.unmodifiableList(additionalDeploymentArchives); } @@ -238,6 +245,7 @@ public static class Builder { List forcedDependencies = new ArrayList<>(); boolean disableClasspathCache; AppModel existingModel; + Map configOverrides; public Builder() { } @@ -270,6 +278,15 @@ public Builder addExcludedPath(Path path) { return this; } + public Map getConfigOverrides() { + return configOverrides; + } + + public Builder setConfigOverrides(Map configOverrides) { + this.configOverrides = configOverrides; + return this; + } + /** * The project root, used only for project dependency discovery. */ diff --git a/integration-tests/main/pom.xml b/integration-tests/main/pom.xml index c3027219a681d7..e70412a10fd5f0 100644 --- a/integration-tests/main/pom.xml +++ b/integration-tests/main/pom.xml @@ -199,6 +199,13 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + alphabetical + + diff --git a/integration-tests/main/src/main/java/io/quarkus/it/rest/GreetingEndpoint.java b/integration-tests/main/src/main/java/io/quarkus/it/rest/GreetingEndpoint.java new file mode 100644 index 00000000000000..c083c65ac55db1 --- /dev/null +++ b/integration-tests/main/src/main/java/io/quarkus/it/rest/GreetingEndpoint.java @@ -0,0 +1,23 @@ +package io.quarkus.it.rest; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.annotations.jaxrs.PathParam; + +@Path("/greeting") +public class GreetingEndpoint { + + @Inject + GreetingService greetingService; + + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("{name}") + public String greet(@PathParam String name) { + return greetingService.greet(name); + } +} diff --git a/integration-tests/main/src/main/java/io/quarkus/it/rest/GreetingService.java b/integration-tests/main/src/main/java/io/quarkus/it/rest/GreetingService.java new file mode 100644 index 00000000000000..0551a7d62a2e4c --- /dev/null +++ b/integration-tests/main/src/main/java/io/quarkus/it/rest/GreetingService.java @@ -0,0 +1,10 @@ +package io.quarkus.it.rest; + +import javax.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class GreetingService { + public String greet(String greeting) { + return "Hello " + greeting; + } +} diff --git a/integration-tests/main/src/test/java/io/quarkus/it/main/BonjourService.java b/integration-tests/main/src/test/java/io/quarkus/it/main/BonjourService.java new file mode 100644 index 00000000000000..1a7b9bfbcb80c0 --- /dev/null +++ b/integration-tests/main/src/test/java/io/quarkus/it/main/BonjourService.java @@ -0,0 +1,16 @@ +package io.quarkus.it.main; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Alternative; + +import io.quarkus.it.rest.GreetingService; + +@ApplicationScoped +@Alternative +public class BonjourService extends GreetingService { + + @Override + public String greet(String greeting) { + return "Bonjour " + greeting; + } +} diff --git a/integration-tests/main/src/test/java/io/quarkus/it/main/GreetingNormalTestCase.java b/integration-tests/main/src/test/java/io/quarkus/it/main/GreetingNormalTestCase.java new file mode 100644 index 00000000000000..71caa9a3cac60f --- /dev/null +++ b/integration-tests/main/src/test/java/io/quarkus/it/main/GreetingNormalTestCase.java @@ -0,0 +1,21 @@ +package io.quarkus.it.main; + +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; + +@QuarkusTest +public class GreetingNormalTestCase { + + @Test + public void included() { + RestAssured.when() + .get("/greeting/Stu") + .then() + .statusCode(200) + .body(is("Hello Stu")); + } +} diff --git a/integration-tests/main/src/test/java/io/quarkus/it/main/GreetingProfileTestCase.java b/integration-tests/main/src/test/java/io/quarkus/it/main/GreetingProfileTestCase.java new file mode 100644 index 00000000000000..bdd0d5ccac1294 --- /dev/null +++ b/integration-tests/main/src/test/java/io/quarkus/it/main/GreetingProfileTestCase.java @@ -0,0 +1,38 @@ +package io.quarkus.it.main; + +import static org.hamcrest.Matchers.is; + +import java.util.Collections; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.WithProfile; +import io.restassured.RestAssured; + +/** + * Tests that QuarkusTestProfile works as expected + */ +@QuarkusTest +@WithProfile(GreetingProfileTestCase.MyProfile.class) +public class GreetingProfileTestCase { + + @Test + public void included() { + RestAssured.when() + .get("/greeting/Stu") + .then() + .statusCode(200) + .body(is("Bonjour Stu")); + } + + public static class MyProfile implements QuarkusTestProfile { + + @Override + public Set> getEnabledAlternatives() { + return Collections.singleton(BonjourService.class); + } + } +} 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 49e5fb7986f517..1731d2babfea81 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 @@ -15,11 +15,15 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.ServiceLoader; import java.util.concurrent.LinkedBlockingDeque; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; + +import javax.enterprise.inject.Alternative; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; import org.jboss.jandex.AnnotationInstance; @@ -54,6 +58,7 @@ import io.quarkus.deployment.builditem.TestAnnotationBuildItem; import io.quarkus.deployment.builditem.TestClassBeanBuildItem; import io.quarkus.deployment.builditem.TestClassPredicateBuildItem; +import io.quarkus.runtime.configuration.ProfileManager; import io.quarkus.test.common.PathTestHelper; import io.quarkus.test.common.PropertyTestUtil; import io.quarkus.test.common.RestAssuredURLManager; @@ -90,10 +95,12 @@ public class QuarkusTestExtension private static List beforeEachCallbacks = new ArrayList<>(); private static List afterEachCallbacks = new ArrayList<>(); private static Class quarkusTestMethodContextClass; + private static Class quarkusTestProfile; private static DeepClone deepClone; - private ExtensionState doJavaStart(ExtensionContext context) throws Throwable { + private ExtensionState doJavaStart(ExtensionContext context, Class profile) throws Throwable { + quarkusTestProfile = profile; Closeable testResourceManager = null; try { final LinkedBlockingDeque shutdownTasks = new LinkedBlockingDeque<>(); @@ -114,10 +121,29 @@ private ExtensionState doJavaStart(ExtensionContext context) throws Throwable { } } originalCl = Thread.currentThread().getContextClassLoader(); + String oldTestProfile = System.getProperty(ProfileManager.QUARKUS_TEST_PROFILE_PROP); final QuarkusBootstrap.Builder runnerBuilder = QuarkusBootstrap.builder() .setIsolateDeployment(true) .setMode(QuarkusBootstrap.Mode.TEST); + if (profile != null) { + QuarkusTestProfile profileInstance = profile.newInstance(); + Map additional = new HashMap<>(profileInstance.getConfigOverrides()); + if (!profileInstance.getEnabledAlternatives().isEmpty()) { + additional.put("quarkus.arc.selected-alternatives", profileInstance.getEnabledAlternatives().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(","))); + } + if (profileInstance.getConfigProfile() != null) { + System.setProperty(ProfileManager.QUARKUS_TEST_PROFILE_PROP, profileInstance.getConfigProfile()); + } + runnerBuilder.setConfigOverrides(additional); + } runnerBuilder.setProjectRoot(Paths.get("").normalize().toAbsolutePath()); @@ -176,6 +202,11 @@ public void close() throws IOException { shutdownTasks.pop().run(); } } finally { + if (oldTestProfile == null) { + System.clearProperty(ProfileManager.QUARKUS_TEST_PROFILE_PROP); + } else { + System.setProperty(ProfileManager.QUARKUS_TEST_PROFILE_PROP, oldTestProfile); + } tm.close(); } } @@ -310,10 +341,25 @@ private ExtensionState ensureStarted(ExtensionContext extensionContext) { ExtensionContext root = extensionContext.getRoot(); ExtensionContext.Store store = root.getStore(ExtensionContext.Namespace.GLOBAL); ExtensionState state = store.get(ExtensionState.class.getName(), ExtensionState.class); - if (state == null && !failedBoot) { + WithProfile annotation = extensionContext.getRequiredTestClass().getAnnotation(WithProfile.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 = doJavaStart(extensionContext); + state = doJavaStart(extensionContext, selectedProfile); store.put(ExtensionState.class.getName(), state); } catch (Throwable e) { diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestProfile.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestProfile.java new file mode 100644 index 00000000000000..f370cc0f929f67 --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestProfile.java @@ -0,0 +1,42 @@ +package io.quarkus.test.junit; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** + * Defines a 'test profile'. Tests run under a test profile + * will have different configuration options to other tests. + * + */ +public interface QuarkusTestProfile { + + /** + * Returns additional config to be applied to the test. This + * will override any existing config (including in application.properties), + * however existing config will be merged with this (i.e. application.properties + * config will still take effect, unless a specific config key has been overridden). + */ + default Map getConfigOverrides() { + return Collections.emptyMap(); + } + + /** + * Returns enabled alternatives. + * + * This has the same effect as setting the 'quarkus.arc.selected-alternatives' config key, + * however it may be more convenient. + */ + default Set> getEnabledAlternatives() { + return Collections.emptySet(); + } + + /** + * Allows the default config profile to be overridden. This basically just sets the quarkus.test.profile system + * property before the test is run. + * + */ + default String getConfigProfile() { + return null; + } +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/WithProfile.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/WithProfile.java new file mode 100644 index 00000000000000..fe716eb1a764ea --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/WithProfile.java @@ -0,0 +1,31 @@ +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; + +/** + * Defines a 'test profile'. Tests run under a test profile + * will have different configuration options to other tests. + * + * Due to the global nature of Quarkus if a previous test was + * run under a different profile then Quarkus will need to be + * restarted when the profile changes. Unfortunately there + * is currently no way to order tests based on profile, however + * this can be done manually by running tests in alphabetical + * order and putting all tests with the same profile in the same + * package. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface WithProfile { + + /** + * The test profile to use. If subsequent tests use the same + * profile then Quarkus will not be restarted between tests, + * giving a faster execution. + */ + Class value(); + +}