diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java index 4441a6def5f561..4a17e455e53e5b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ConfigGenerationBuildStep.java @@ -29,9 +29,10 @@ import io.quarkus.deployment.configuration.definition.RootDefinition; import io.quarkus.deployment.logging.LoggingSetupBuildItem; import io.quarkus.gizmo.ClassOutput; -import io.quarkus.runtime.ConfigChangeRecorder; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.configuration.ConfigChangeRecorder; +import io.quarkus.runtime.configuration.ConfigurationRuntimeConfig; public class ConfigGenerationBuildStep { @@ -82,7 +83,8 @@ private List getAdditionalBootstrapConfigSourceProviders( @BuildStep @Record(ExecutionTime.RUNTIME_INIT) public void checkForBuildTimeConfigChange( - ConfigChangeRecorder recorder, ConfigurationBuildItem configItem, LoggingSetupBuildItem loggingSetupBuildItem) { + ConfigChangeRecorder recorder, ConfigurationBuildItem configItem, LoggingSetupBuildItem loggingSetupBuildItem, + ConfigurationRuntimeConfig configurationConfig) { BuildTimeConfigurationReader.ReadResult readResult = configItem.getReadResult(); Config config = ConfigProvider.getConfig(); @@ -96,7 +98,7 @@ public void checkForBuildTimeConfigChange( } } values.remove("quarkus.profile"); - recorder.handleConfigChange(values); + recorder.handleConfigChange(configurationConfig, values); } private void handleMembers(Config config, Map values, Iterable members, diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ConfigChangeRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/ConfigChangeRecorder.java deleted file mode 100644 index b2027bc20181ed..00000000000000 --- a/core/runtime/src/main/java/io/quarkus/runtime/ConfigChangeRecorder.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.quarkus.runtime; - -import java.util.Map; -import java.util.Optional; - -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.ConfigProvider; -import org.jboss.logging.Logger; - -import io.quarkus.runtime.annotations.Recorder; - -@Recorder -public class ConfigChangeRecorder { - - private static final Logger log = Logger.getLogger(ConfigChangeRecorder.class); - - public void handleConfigChange(Map buildTimeConfig) { - Config configProvider = ConfigProvider.getConfig(); - for (Map.Entry entry : buildTimeConfig.entrySet()) { - Optional val = configProvider.getOptionalValue(entry.getKey(), String.class); - if (val.isPresent()) { - if (!val.get().equals(entry.getValue())) { - log.warn("Build time property cannot be changed at runtime. " + entry.getKey() + " was " - + entry.getValue() + " at build time and is now " + val.get()); - } - } - } - } -} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigChangeRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigChangeRecorder.java new file mode 100644 index 00000000000000..eb7a8e09bb58f4 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigChangeRecorder.java @@ -0,0 +1,53 @@ +package io.quarkus.runtime.configuration; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.logging.Logger; + +import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.runtime.configuration.ConfigurationRuntimeConfig.BuildTimeMismatchAtRuntime; + +@Recorder +public class ConfigChangeRecorder { + + private static final Logger log = Logger.getLogger(ConfigChangeRecorder.class); + + public void handleConfigChange(ConfigurationRuntimeConfig configurationConfig, Map buildTimeConfig) { + Config configProvider = ConfigProvider.getConfig(); + List mismatches = null; + for (Map.Entry entry : buildTimeConfig.entrySet()) { + Optional val = configProvider.getOptionalValue(entry.getKey(), String.class); + if (val.isPresent()) { + if (!val.get().equals(entry.getValue())) { + if (mismatches == null) { + mismatches = new ArrayList<>(); + } + mismatches.add( + " - " + entry.getKey() + " was '" + entry.getValue() + "' at build time and is now '" + val.get() + + "'"); + } + } + } + if (mismatches != null && !mismatches.isEmpty()) { + final String msg = "Build time property cannot be changed at runtime:\n" + + mismatches.stream().collect(Collectors.joining("\n")); + switch (configurationConfig.buildTimeMismatchAtRuntime) { + case fail: + throw new IllegalStateException(msg); + case warn: + log.warn(msg); + break; + default: + throw new IllegalStateException("Unexpected " + BuildTimeMismatchAtRuntime.class.getName() + ": " + + configurationConfig.buildTimeMismatchAtRuntime); + } + + } + } +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigurationRuntimeConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigurationRuntimeConfig.java new file mode 100644 index 00000000000000..6f31791effa8c4 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/configuration/ConfigurationRuntimeConfig.java @@ -0,0 +1,29 @@ +package io.quarkus.runtime.configuration; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "configuration", phase = ConfigPhase.RUN_TIME) +public class ConfigurationRuntimeConfig { + + /** + * What should happen if the application is started with a different build time configuration than it was compiled + * against. This may be useful to prevent misconfiguration. + *

+ * If this is set to {@code warn} the application will warn at start up. + *

+ * If this is set to {@code fail} the application will fail at start up. + *

+ * Native tests leveraging@io.quarkus.test.junit.TestProfile are always run with + * {@code quarkus.configuration.build-time-mismatch-at-runtime = fail}. + */ + @ConfigItem(defaultValue = "warn") + public BuildTimeMismatchAtRuntime buildTimeMismatchAtRuntime; + + public enum BuildTimeMismatchAtRuntime { + warn, + fail + } + +} diff --git a/devtools/maven/src/main/java/io/quarkus/maven/BuildMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/BuildMojo.java index cd1f01190fd22b..44a1fd9328a981 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/BuildMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/BuildMojo.java @@ -2,10 +2,15 @@ import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map.Entry; +import java.util.Properties; import org.apache.maven.artifact.Artifact; import org.apache.maven.plugin.MojoExecutionException; @@ -22,6 +27,7 @@ import io.quarkus.bootstrap.app.AugmentResult; import io.quarkus.bootstrap.app.CuratedApplication; import io.quarkus.bootstrap.util.IoUtils; +import io.quarkus.runtime.configuration.ProfileManager; /** * Builds the Quarkus application. @@ -46,6 +52,21 @@ public class BuildMojo extends QuarkusBootstrapMojo { @Parameter(defaultValue = "${project.build.directory}/generated-sources") private File generatedSourcesDirectory; + /** + * If {@code true} and the {@code quarkus.package.type} property is set to {@code native}, the mojo will build + * (1) the default native image and (2) a separate native image for each configuration profile that has + * {@code quarkus.package.output-name} set; otherwise the mojo will build only the default native image. + */ + @Parameter(property = "quarkus.buildConfigurationProfiles", defaultValue = "true") + private boolean buildConfigurationProfiles = true; + + /** Set only via {@code quarkus.package.type} property */ + @Parameter(readonly = true, defaultValue = "${quarkus.package.type}") + private String packageType; + + @Parameter(readonly = true, defaultValue = "${project.build.outputDirectory}") + private File outputDirectory; + /** * Skips the execution of this mojo */ @@ -72,6 +93,43 @@ protected boolean beforeExecute() throws MojoExecutionException { @Override protected void doExecute() throws MojoExecutionException { + doExecute(null); // always run with the default profile + + for (String configProfile : findNativeConfigProfiles()) { + doExecute(configProfile); + } + } + + private List findNativeConfigProfiles() { + final Path propsPath = outputDirectory.toPath().resolve("application.properties"); + if (Files.exists(propsPath)) { + final Properties props = new Properties(); + try (InputStream in = Files.newInputStream(propsPath)) { + props.load(in); + } catch (IOException e) { + throw new RuntimeException("Could not read " + propsPath, e); + } + + if ((packageType != null && packageType.equals("native")) + || Boolean.parseBoolean(props.getProperty("quarkus.package.type", "false"))) { + final List result = new ArrayList(); + final String suffix = ".quarkus.package.output-name"; + for (Entry en : props.entrySet()) { + final String key = en.getKey().toString(); + if (key.startsWith("%") && key.endsWith(suffix)) { + final String profile = key.substring(1, key.length() - suffix.length()); + result.add(profile); + } + } + return result; + } + } + return Collections.emptyList(); + } + + protected void doExecute(String configProfile) throws MojoExecutionException { + + final String origConfigProfile = setSystemProperty(configProfile); boolean clear = false; try { @@ -107,7 +165,18 @@ protected void doExecute() throws MojoExecutionException { if (clear) { System.clearProperty(QUARKUS_PACKAGE_UBER_JAR); } + setSystemProperty(origConfigProfile); + } + } + + static String setSystemProperty(String configProfile) { + String result = System.getProperty(ProfileManager.QUARKUS_PROFILE_PROP); + if (configProfile == null) { + System.clearProperty(ProfileManager.QUARKUS_PROFILE_PROP); + } else { + System.setProperty(ProfileManager.QUARKUS_PROFILE_PROP, configProfile); } + return result; } @Override diff --git a/integration-tests/native-config-profile/pom.xml b/integration-tests/native-config-profile/pom.xml new file mode 100644 index 00000000000000..24e84408f1908c --- /dev/null +++ b/integration-tests/native-config-profile/pom.xml @@ -0,0 +1,149 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-integration-test-native-config-profile + Quarkus - Integration Tests - Native Configuration Profile + Native Configuration Profile + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-resteasy + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.assertj + assertj-core + test + + + + + io.quarkus + quarkus-arc-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + false + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + default-test + + + + + + + + + + native-image-it-main + + + native + + + + native + + + + + io.quarkus + quarkus-maven-plugin + + + + + alt-profile + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + + + + diff --git a/integration-tests/native-config-profile/src/main/java/io/quarkus/it/nat/test/profile/NativeConfigProfileResource.java b/integration-tests/native-config-profile/src/main/java/io/quarkus/it/nat/test/profile/NativeConfigProfileResource.java new file mode 100644 index 00000000000000..f8b478f5c67175 --- /dev/null +++ b/integration-tests/native-config-profile/src/main/java/io/quarkus/it/nat/test/profile/NativeConfigProfileResource.java @@ -0,0 +1,19 @@ +package io.quarkus.it.nat.test.profile; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +import io.quarkus.arc.Arc; + +@Path("/native-config-profile") +public class NativeConfigProfileResource { + + @Path("/unused-exists") + @Produces("text/plain") + @GET + public boolean unusedExists() { + return Arc.container().instance(UnusedRemovableBean.class).isAvailable(); + } + +} diff --git a/integration-tests/native-config-profile/src/main/java/io/quarkus/it/nat/test/profile/UnusedRemovableBean.java b/integration-tests/native-config-profile/src/main/java/io/quarkus/it/nat/test/profile/UnusedRemovableBean.java new file mode 100644 index 00000000000000..1f2e302b1af741 --- /dev/null +++ b/integration-tests/native-config-profile/src/main/java/io/quarkus/it/nat/test/profile/UnusedRemovableBean.java @@ -0,0 +1,7 @@ +package io.quarkus.it.nat.test.profile; + +import javax.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class UnusedRemovableBean { +} diff --git a/integration-tests/native-config-profile/src/main/resources/application.properties b/integration-tests/native-config-profile/src/main/resources/application.properties new file mode 100644 index 00000000000000..965924b3e8137d --- /dev/null +++ b/integration-tests/native-config-profile/src/main/resources/application.properties @@ -0,0 +1,3 @@ +quarkus.arc.remove-unused-beans = none +%alt-profile.quarkus.package.output-name=alt +%alt-profile.quarkus.arc.remove-unused-beans = all diff --git a/integration-tests/native-config-profile/src/test/java/io/quarkus/it/nat/test/profile/BuiltTimeConfigChangeITCase.java b/integration-tests/native-config-profile/src/test/java/io/quarkus/it/nat/test/profile/BuiltTimeConfigChangeITCase.java new file mode 100644 index 00000000000000..1760d9f1680b61 --- /dev/null +++ b/integration-tests/native-config-profile/src/test/java/io/quarkus/it/nat/test/profile/BuiltTimeConfigChangeITCase.java @@ -0,0 +1,39 @@ +package io.quarkus.it.nat.test.profile; + +import java.util.Collections; +import java.util.Map; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.NativeImageTest; +import io.quarkus.test.junit.NativeTestExtension; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; + +/** + * Run this manually to ensure that + * {@link NativeTestExtension#beforeEach(org.junit.jupiter.api.extension.ExtensionContext)} throws an exception caused + * by an application boot failure. The failure should happen because {@link NativeTestExtension} is setting + * {@code quarkus.configuration.build-time-mismatch-at-runtime = fail} and + * {@link MismatchTestProfile#getConfigOverrides()} changes {@code quarkus.arc.remove-unused-beans}. + */ +@NativeImageTest +@TestProfile(BuiltTimeConfigChangeITCase.MismatchTestProfile.class) +@Disabled("Manual testing only") +public class BuiltTimeConfigChangeITCase { + @Test + public void failInNativeTestExtension_beforeEach() { + Assertions.fail("Expected to fail in io.quarkus.test.junit.NativeTestExtension.beforeEach(ExtensionContext)"); + } + + public static class MismatchTestProfile implements QuarkusTestProfile { + + @Override + public Map getConfigOverrides() { + return Collections.singletonMap("quarkus.arc.remove-unused-beans", "all"); + } + + } +} diff --git a/integration-tests/native-config-profile/src/test/java/io/quarkus/it/nat/test/profile/NativeAltTestProfileITCase.java b/integration-tests/native-config-profile/src/test/java/io/quarkus/it/nat/test/profile/NativeAltTestProfileITCase.java new file mode 100644 index 00000000000000..325d184a3104ac --- /dev/null +++ b/integration-tests/native-config-profile/src/test/java/io/quarkus/it/nat/test/profile/NativeAltTestProfileITCase.java @@ -0,0 +1,27 @@ +package io.quarkus.it.nat.test.profile; + +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.NativeImageTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; +import io.restassured.RestAssured; + +@NativeImageTest +@TestProfile(NativeAltTestProfileITCase.AltTestProfile.class) +public class NativeAltTestProfileITCase { + @Test + public void unusedExists() { + RestAssured.when().get("/native-config-profile/unused-exists").then() + .body(is("false")); + } + + public static class AltTestProfile implements QuarkusTestProfile { + @Override + public String getConfigProfile() { + return "alt-profile"; + } + } +} diff --git a/integration-tests/native-config-profile/src/test/java/io/quarkus/it/nat/test/profile/NativeTestProfileITCase.java b/integration-tests/native-config-profile/src/test/java/io/quarkus/it/nat/test/profile/NativeTestProfileITCase.java new file mode 100644 index 00000000000000..6f92a090e76d1f --- /dev/null +++ b/integration-tests/native-config-profile/src/test/java/io/quarkus/it/nat/test/profile/NativeTestProfileITCase.java @@ -0,0 +1,18 @@ +package io.quarkus.it.nat.test.profile; + +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.NativeImageTest; +import io.restassured.RestAssured; + +@NativeImageTest +public class NativeTestProfileITCase { + @Test + public void unusedExists() { + RestAssured.when().get("/native-config-profile/unused-exists").then() + .body(is("true")); + } + +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index a607b420defa5d..bbcff120ad56e5 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -269,6 +269,7 @@ logging-gelf bootstrap-config mailer + native-config-profile 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 762ce41c733e79..6255d1b4385973 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 @@ -15,6 +15,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.OptionalInt; import java.util.OptionalLong; import java.util.ServiceLoader; @@ -48,14 +49,15 @@ public class NativeImageLauncher implements Closeable { private final long imageWaitTime; private final Map systemProps = new HashMap<>(); private List startedNotifiers; + private final String nativeImagePath; private NativeImageLauncher(Class testClass, Config config) { this(testClass, 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.native-image-wait-time", OptionalLong.class).orElse(DEFAULT_IMAGE_WAIT_TIME), - config.getOptionalValue("quarkus.test.native-image-profile", String.class) - .orElse(null)); + config.getOptionalValue("quarkus.test.native-image-profile", String.class).orElse(null), + findNativeImagePath(testClass, config)); } public NativeImageLauncher(Class testClass) { @@ -77,7 +79,31 @@ private static Config installAndGetSomeConfig() { return config; } - public NativeImageLauncher(Class testClass, int port, int httpsPort, long imageWaitTime, String profile) { + private static String findNativeImagePath(Class testClass, Config config) { + String runnerRelPath = config.getOptionalValue("quarkus.package.output-name", String.class) + .map(p -> config.getOptionalValue("quarkus.package.output-directory", String.class) + .map(dir -> dir + "/") + .orElse("") + + p + + config + .getOptionalValue("quarkus.package.runner-suffix", String.class) + .orElse("-runner")) + .orElse(null); + if (runnerRelPath != null) { + if (IS_WINDOWS) { + runnerRelPath += ".exe"; + } + return guessPath(testClass, runnerRelPath); + } + String path = System.getProperty("native.image.path"); + if (path == null) { + path = guessPath(testClass, null); + } + return path; + } + + public NativeImageLauncher(Class testClass, int port, int httpsPort, long imageWaitTime, String profile, + String nativeImagePath) { this.testClass = testClass; this.port = port; this.httpsPort = httpsPort; @@ -88,18 +114,16 @@ public NativeImageLauncher(Class testClass, int port, int httpsPort, long ima } this.startedNotifiers = startedNotifiers; this.profile = profile; + + this.nativeImagePath = Objects.requireNonNull(nativeImagePath, "nativeImagePath must be non-null"); } public void start() throws IOException { System.setProperty("test.url", TestHTTPResourceManager.getUri()); - String path = System.getProperty("native.image.path"); - if (path == null) { - path = guessPath(testClass); - } List args = new ArrayList<>(); - args.add(path); + args.add(nativeImagePath); 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 @@ -149,7 +173,7 @@ public void start() throws IOException { } } - private static String guessPath(Class testClass) { + private static String guessPath(Class testClass, String runnerRelPath) { //ok, lets make a guess //this is a horrible hack, but it is intended to make this work in IDE's @@ -158,7 +182,7 @@ private static String guessPath(Class testClass) { if (cl instanceof URLClassLoader) { URL[] urls = ((URLClassLoader) cl).getURLs(); for (URL url : urls) { - final String applicationNativeImagePath = guessPath(url); + final String applicationNativeImagePath = guessPath(url, runnerRelPath); if (applicationNativeImagePath != null) { return applicationNativeImagePath; } @@ -168,7 +192,7 @@ private static String guessPath(Class testClass) { final CodeSource codeSource = testClass.getProtectionDomain().getCodeSource(); if (codeSource != null) { final URL codeSourceLocation = codeSource.getLocation(); - final String applicationNativeImagePath = guessPath(codeSourceLocation); + final String applicationNativeImagePath = guessPath(codeSourceLocation, runnerRelPath); if (applicationNativeImagePath != null) { return applicationNativeImagePath; } @@ -179,40 +203,44 @@ private static String guessPath(Class testClass) { "Unable to automatically find native image, please set the native.image.path to the native executable you wish to test"); } - private static String guessPath(final URL url) { + private static String guessPath(final URL url, String runnerRelPath) { 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()); - for (File file : testClasses.getParentFile().listFiles()) { - if (isNativeExecutable(file)) { - logGuessedPath(file.getAbsolutePath()); - return file.getAbsolutePath(); - } + if (url.getProtocol().equals("file")) { + final String urlPath = url.getPath(); + if (urlPath.endsWith("test-classes/")) { + // we have the maven test classes dir + File testClasses = new File(urlPath); + return findExecutable(testClasses.getParentFile(), runnerRelPath); + } else if (urlPath.endsWith("test/")) { + // we have the gradle test classes dir, build/classes/java/test + File testClasses = new File(urlPath); + return findExecutable(testClasses.getParentFile().getParentFile().getParentFile(), runnerRelPath); + } else if (urlPath.contains("/target/surefire/")) { + // this will make mvn failsafe:integration-test work + int index = urlPath.lastIndexOf("/target/"); + File targetDir = new File(urlPath.substring(0, index) + "/target/"); + return findExecutable(targetDir, runnerRelPath); } - } 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()); - for (File file : testClasses.getParentFile().getParentFile().getParentFile().listFiles()) { - if (isNativeExecutable(file)) { - logGuessedPath(file.getAbsolutePath()); - return file.getAbsolutePath(); - } + } + return null; + } + + private static String findExecutable(File dir, String runnerRelPath) { + if (runnerRelPath != null) { + File result = new File(dir, runnerRelPath); + if (result.exists()) { + return result.getAbsolutePath(); + } else { + throw new IllegalStateException(result.getAbsolutePath() + " does not exist."); } - } 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/"); - File targetDir = new File(path.substring(0, index) + "/target/"); - for (File file : targetDir.listFiles()) { - if (isNativeExecutable(file)) { - logGuessedPath(file.getAbsolutePath()); - return file.getAbsolutePath(); - } + } + for (File file : dir.listFiles()) { + if (isNativeExecutable(file)) { + logGuessedPath(file.getAbsolutePath()); + return file.getAbsolutePath(); } - } return null; } 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 c6d7775c12302d..1855808ef84829 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 @@ -3,29 +3,30 @@ import java.io.Closeable; import java.io.IOException; import java.lang.reflect.Field; +import java.util.HashMap; 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.jboss.jandex.AnnotationInstance; -import org.jboss.jandex.AnnotationTarget; -import org.jboss.jandex.ClassInfo; -import org.jboss.jandex.DotName; -import org.jboss.jandex.Index; 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.TestClassIndexer; import io.quarkus.test.common.TestResourceManager; import io.quarkus.test.common.TestScopeManager; import io.quarkus.test.common.http.TestHTTPResourceManager; @@ -38,6 +39,9 @@ public class NativeTestExtension 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) { @@ -48,7 +52,9 @@ public void afterEach(ExtensionContext context) throws Exception { @Override public void beforeEach(ExtensionContext context) throws Exception { - if (!failedBoot) { + if (failedBoot) { + throwBootFailureException(); + } else { RestAssuredURLManager.setURL(ssl, QuarkusTestExtension.getEndpointPath(context, testHttpEndpointProviders)); TestScopeManager.setup(true); } @@ -56,43 +62,7 @@ public void beforeEach(ExtensionContext context) throws Exception { @Override public void beforeAll(ExtensionContext extensionContext) throws Exception { - 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); - PropertyTestUtil.setLogFileProperty(); - if (state == null) { - ensureNoTestProfile(testClass); - - TestResourceManager testResourceManager = new TestResourceManager(testClass); - try { - testResourceManager.init(); - Map systemProps = testResourceManager.start(); - NativeImageLauncher launcher = new NativeImageLauncher(testClass); - launcher.addSystemProperties(systemProps); - try { - launcher.start(); - } catch (IOException e) { - try { - launcher.close(); - } catch (Throwable t) { - } - throw e; - } - if (launcher.isDefaultSsl()) { - ssl = true; - } - state = new ExtensionState(testResourceManager, launcher, true); - store.put(ExtensionState.class.getName(), state); - - testHttpEndpointProviders = TestHttpEndpointProvider.load(); - } catch (Exception e) { - - failedBoot = true; - throw new JUnitException("Quarkus native image start failed, original cause: " + e, e); - } - } + ensureStarted(extensionContext); } private void ensureNoInjectAnnotationIsUsed(Class testClass) { @@ -112,26 +82,119 @@ private void ensureNoInjectAnnotationIsUsed(Class testClass) { } - /** - * We don't support {@link TestProfile} in native tests because we don't want to incur the native binary rebuild cost - * which is very high. - * - * This method looks up the annotations via Jandex in order to try and prevent the image generation if there are - * any cases of {@link NativeImageTest} being used with {@link TestProfile} - */ - private void ensureNoTestProfile(Class testClass) { - Index index = TestClassIndexer.readIndex(testClass); - List instances = index.getAnnotations(DotName.createSimple(NativeImageTest.class.getName())); - for (AnnotationInstance instance : instances) { - if (instance.target().kind() != AnnotationTarget.Kind.CLASS) { - continue; + private ExtensionState 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); + 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(); + } + } } - ClassInfo testClassInfo = instance.target().asClass(); - if (testClassInfo.classAnnotation(DotName.createSimple(TestProfile.class.getName())) != null) { - throw new JUnitException( - "@TestProfile is not supported in NativeImageTest tests. Offending class is " + testClassInfo.name()); + PropertyTestUtil.setLogFileProperty(); + try { + state = doNativeStart(extensionContext, selectedProfile); + store.put(ExtensionState.class.getName(), state); + + } catch (Throwable e) { + failedBoot = true; + firstException = e; } } + return state; + } + + private ExtensionState 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"); + //we just use system properties for now + //its a lot simpler + 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()); + } + } + + testResourceManager = new TestResourceManager(requiredTestClass); + testResourceManager.init(); + additional.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; + } + + final ExtensionState state = new ExtensionState(testResourceManager, launcher, true); + + testHttpEndpointProviders = TestHttpEndpointProvider.load(); + + return state; + } catch (Throwable e) { + + try { + if (testResourceManager != null) { + testResourceManager.close(); + } + } catch (Exception ex) { + e.addSuppressed(ex); + } + throw e; + } } @Override @@ -143,20 +206,43 @@ public void postProcessTestInstance(Object testInstance, ExtensionContext contex state.testResourceManager.inject(testInstance); } + private void throwBootFailureException() throws Exception { + if (firstException != null) { + Throwable throwable = firstException; + firstException = null; + throw new RuntimeException(throwable); + } else { + throw new TestAbortedException("Boot failed"); + } + } + public class ExtensionState implements ExtensionContext.Store.CloseableResource { private final TestResourceManager testResourceManager; private final Closeable resource; + private final Thread shutdownHook; ExtensionState(TestResourceManager testResourceManager, Closeable resource, boolean nativeImage) { this.testResourceManager = testResourceManager; this.resource = resource; + 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 Throwable { + public void close() throws IOException { testResourceManager.close(); resource.close(); + Runtime.getRuntime().removeShutdownHook(shutdownHook); } } }