io.quarkus.test.QuarkusProdModeTest Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of quarkus-junit5-internal Show documentation
Show all versions of quarkus-junit5-internal Show documentation
A runner for unit tests, intended for testing Quarkus rather than
for end user consumption.
package io.quarkus.test;
import static io.quarkus.test.ExportUtil.APPLICATION_PROPERTIES;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.logging.Handler;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.stream.Collectors;
import jakarta.inject.Inject;
import org.jboss.logmanager.Logger;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.exporter.ExplodedExporter;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.extension.AfterAllCallback;
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.InvocationInterceptor;
import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
import org.junit.jupiter.api.extension.TestWatcher;
import org.junit.platform.commons.JUnitException;
import io.quarkus.bootstrap.app.AugmentAction;
import io.quarkus.bootstrap.app.AugmentResult;
import io.quarkus.bootstrap.app.CuratedApplication;
import io.quarkus.bootstrap.app.QuarkusBootstrap;
import io.quarkus.builder.BuildStep;
import io.quarkus.builder.item.BuildItem;
import io.quarkus.deployment.util.FileUtil;
import io.quarkus.maven.dependency.Dependency;
import io.quarkus.test.common.PathTestHelper;
import io.quarkus.test.common.RestAssuredURLManager;
import io.quarkus.test.common.TestConfigUtil;
import io.quarkus.test.common.TestResourceManager;
import io.quarkus.utilities.JavaBinFinder;
/**
* A test extension for producing a prod-mode jar. This is meant to be used by extension authors, it's not intended for end user
* consumption
*/
public class QuarkusProdModeTest
implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, TestWatcher, InvocationInterceptor {
private static final String EXPECTED_OUTPUT_FROM_SUCCESSFULLY_STARTED = "Installed features";
private static final int DEFAULT_HTTP_PORT_INT = 8081;
private static final String DEFAULT_HTTP_PORT = "" + DEFAULT_HTTP_PORT_INT;
private static final String QUARKUS_HTTP_PORT_PROPERTY = "quarkus.http.port";
static final String BUILD_CONTEXT_BUILD_STEP_ENTRIES = "buildStepEntries";
static final String BUILD_CONTEXT_BUILD_STEP_ENTRY_CONSUMES = "buildStepEntryConsumes";
static final String BUILD_CONTEXT_BUILD_STEP_ENTRY_PRODUCES = "buildStepEntryProduces";
public static String BUILD_CONTEXT_CUSTOM_SOURCES_PATH_KEY = "customSourcesDir";
private static final Logger rootLogger;
private Handler[] originalHandlers;
static {
System.setProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager");
rootLogger = (Logger) LogManager.getLogManager().getLogger("");
}
private Path outputDir;
private Path buildDir;
private Supplier archiveProducer;
private String applicationName;
private String applicationVersion;
private boolean buildNative;
private static final Timer timeoutTimer = new Timer("Test thread dump timer");
private volatile TimerTask timeoutTask;
private Properties customApplicationProperties;
private boolean defaultConfigResource;
private String configResourceName;
private CuratedApplication curatedApplication;
private boolean run;
private boolean preventOutputDirCleanup;
private String logFileName;
private Map runtimeProperties;
// by default, we use these lower heap settings
private List jvmArgs = Collections.singletonList("-Xmx192m");
private Map testResourceProperties = new HashMap<>();
// these will be used to create a directory that can then be obtained by the buildChainCustomizersProducer function
// values are meant to be resources that exist on the test classpath
// which should be copied to the files represented by the keys
private Map customSourcesMap = new HashMap<>();
private List buildChainCustomizerEntries = new ArrayList<>();
private Process process;
private Path builtResultArtifact;
private ProdModeTestResults prodModeTestResults;
private Optional prodModeTestResultsField = Optional.empty();
private Path logfilePath;
private Optional logfileField = Optional.empty();
private List forcedDependencies = Collections.emptyList();
private InMemoryLogHandler inMemoryLogHandler = new InMemoryLogHandler(r -> false);
private boolean expectExit;
private String startupConsoleOutput;
private Integer exitCode;
private Consumer assertBuildException;
private String[] commandLineParameters = new String[0];
private boolean clearRestAssuredURL;
public QuarkusProdModeTest() {
// If there is an application.properties resource available then load the properties
// unless a custom config resource name is used or an application.properties asset was added to the test archive
this.defaultConfigResource = Thread.currentThread().getContextClassLoader().getResource(APPLICATION_PROPERTIES) != null;
}
public Supplier getArchiveProducer() {
return archiveProducer;
}
public QuarkusProdModeTest setArchiveProducer(Supplier archiveProducer) {
Objects.requireNonNull(archiveProducer);
this.archiveProducer = archiveProducer;
return this;
}
/**
* Customize the application root.
*
* @param applicationRootConsumer
* @return self
*/
public QuarkusProdModeTest withApplicationRoot(Consumer applicationRootConsumer) {
Objects.requireNonNull(applicationRootConsumer);
return setArchiveProducer(() -> {
JavaArchive jar = ShrinkWrap.create(JavaArchive.class);
applicationRootConsumer.accept(jar);
return jar;
});
}
/**
* Use an empty application for the test
*
* @return self
*/
public QuarkusProdModeTest withEmptyApplication() {
return withApplicationRoot(new Consumer() {
@Override
public void accept(JavaArchive javaArchive) {
}
});
}
public QuarkusProdModeTest addBuildChainCustomizerEntries(BuildChainCustomizerEntry entry) {
Objects.requireNonNull(entry);
this.buildChainCustomizerEntries.add(entry);
return this;
}
public QuarkusProdModeTest addCustomResourceEntry(Path outputPath, String classPathLocation) {
Objects.requireNonNull(outputPath);
Objects.requireNonNull(classPathLocation);
this.customSourcesMap.put(outputPath, classPathLocation);
return this;
}
/**
* Effectively sets the quarkus.application.name property.
* This value will override quarkus.application.name if that has been set in the configuration properties
*/
public QuarkusProdModeTest setApplicationName(String applicationName) {
this.applicationName = applicationName;
return this;
}
/**
* Effectively sets the quarkus.application.version property.
* This value will override quarkus.application.version if that has been set in the configuration properties
*/
public QuarkusProdModeTest setApplicationVersion(String applicationVersion) {
this.applicationVersion = applicationVersion;
return this;
}
/**
* Effectively sets the quarkus.packaging.type property.
* This value will override quarkus.packaging.type if that has been set in the configuration properties
*/
public QuarkusProdModeTest setBuildNative(boolean buildNative) {
this.buildNative = buildNative;
return this;
}
/**
* If set to true, the built artifact will be run before starting the tests
*/
public QuarkusProdModeTest setRun(boolean run) {
this.run = run;
return this;
}
/**
* File where the running application logs its output
* This property effectively sets the quarkus.log.file.path runtime configuration property
* and will override that value if it has been set in the configuration properties of the test
*/
public QuarkusProdModeTest setLogFileName(String logFileName) {
this.logFileName = logFileName;
return this;
}
/**
* The complete set of JVM args to be used if the built artifact is configured to be run
*/
public QuarkusProdModeTest setJVMArgs(final List jvmArgs) {
this.jvmArgs = jvmArgs;
return this;
}
/**
* The runtime configuration properties to be used if the built artifact is configured to be run
*/
public QuarkusProdModeTest setRuntimeProperties(Map runtimeProperties) {
this.runtimeProperties = runtimeProperties;
return this;
}
public QuarkusProdModeTest setLogRecordPredicate(Predicate predicate) {
this.inMemoryLogHandler = new InMemoryLogHandler(predicate);
return this;
}
/**
* Provides a convenient way to either add additional dependencies to the application (if it doesn't already contain a
* dependency), or override a version (if the dependency already exists)
*/
public QuarkusProdModeTest setForcedDependencies(List forcedDependencies) {
this.forcedDependencies = forcedDependencies;
return this;
}
/**
* If this is true then the Quarkus application is expected to exit immediately (i.e. is a command mode app)
*/
public QuarkusProdModeTest setExpectExit(boolean expectExit) {
this.expectExit = expectExit;
return this;
}
public QuarkusProdModeTest assertBuildException(Consumer assertException) {
if (this.assertBuildException != null) {
throw new IllegalStateException("Don't set the asserted or excepted exception twice"
+ " to avoid shadowing out the first call.");
}
this.assertBuildException = assertException;
return this;
}
public QuarkusProdModeTest setExpectedException(Class extends Throwable> expectedException) {
return assertBuildException(t -> {
Throwable i = t;
boolean found = false;
while (i != null) {
if (i.getClass().getName().equals(expectedException.getName())) {
found = true;
break;
}
i = i.getCause();
}
assertTrue(found, "Build failed with wrong exception, expected " + expectedException + " but got " + t);
});
}
/**
* Returns the console output from startup. If {@link #expectExit} is true then this will contain
* all the console output.
*/
public String getStartupConsoleOutput() {
return startupConsoleOutput;
}
/**
* Returns the process exit code, this can only be used if {@link #expectExit} is true.
* Null if the app is running.
*/
public Integer getExitCode() {
return exitCode;
}
private void exportArchive(Path deploymentDir, Class> testClass) {
try {
JavaArchive archive = getArchiveProducerOrDefault();
if (configResourceName != null) {
if (archive.get(APPLICATION_PROPERTIES) != null) {
// Asset added explicitly to the archive must be completely replaced with custom config resource
ExportUtil.deleteApplicationProperties(archive);
}
archive.addAsResource(configResourceName, APPLICATION_PROPERTIES);
} else if (defaultConfigResource && archive.get(APPLICATION_PROPERTIES) == null) {
// No custom config resource set and no application.properties asset added
archive.addAsResource(APPLICATION_PROPERTIES);
}
if (customApplicationProperties != null) {
ExportUtil.mergeCustomApplicationProperties(archive, customApplicationProperties);
}
archive.as(ExplodedExporter.class).exportExplodedInto(deploymentDir.toFile());
ExportUtil.exportToQuarkusDeploymentPath(archive);
} catch (Exception e) {
throw new RuntimeException("Unable to create the archive", e);
}
}
private JavaArchive getArchiveProducerOrDefault() {
if (archiveProducer == null) {
return ShrinkWrap.create(JavaArchive.class);
} else {
return archiveProducer.get();
}
}
@Override
public void beforeAll(ExtensionContext extensionContext) throws Exception {
TestConfigUtil.cleanUp();
ensureNoInjectAnnotationIsUsed(extensionContext.getRequiredTestClass());
ExclusivityChecker.checkTestType(extensionContext, QuarkusProdModeTest.class);
originalHandlers = rootLogger.getHandlers();
rootLogger.addHandler(inMemoryLogHandler);
timeoutTask = new PrintStackTraceTimerTask();
timeoutTimer.schedule(timeoutTask, 1000 * 60 * 5);
ExtensionContext.Store store = extensionContext.getRoot().getStore(ExtensionContext.Namespace.GLOBAL);
if (store.get(TestResourceManager.class.getName()) == null) {
TestResourceManager manager = new TestResourceManager(extensionContext.getRequiredTestClass());
manager.init(null);
testResourceProperties = manager.start();
store.put(TestResourceManager.class.getName(), manager);
store.put(TestResourceManager.CLOSEABLE_NAME, new ExtensionContext.Store.CloseableResource() {
@Override
public void close() throws Throwable {
manager.close();
}
});
}
Class> testClass = extensionContext.getRequiredTestClass();
try {
Optional projectBuildDir = Optional.ofNullable(System.getProperty("project.build.directory")) //maven
.or(() -> Optional.ofNullable(System.getProperty("buildDir"))) //gradle
.map(Path::of);
outputDir = projectBuildDir.isPresent() ? Files.createTempDirectory(projectBuildDir.get(), "quarkus-prod-mode-test")
: Files.createTempDirectory("quarkus-prod-mode-test");
Path deploymentDir = outputDir.resolve("deployment-result");
buildDir = outputDir.resolve("build-result");
if (applicationName != null) {
overrideConfigKey("quarkus.application.name", applicationName);
}
if (applicationVersion != null) {
overrideConfigKey("quarkus.application.version", applicationVersion);
}
if (buildNative) {
overrideConfigKey("quarkus.native.enabled", "true");
}
exportArchive(deploymentDir, testClass);
Path testLocation = PathTestHelper.getTestClassesLocation(testClass);
Path customSourcesDir = createCustomSources(testClass);
// This is a bit of a hack but if the current project does not contain any
// sources nor resources, we need to create an empty classes dir to satisfy the resolver
// as this project will appear as the root application artifact during the bootstrap
if (Files.isDirectory(testLocation)) {
final Path projectClassesDir = PathTestHelper.getAppClassLocationForTestLocation(testLocation.toString());
if (!Files.exists(projectClassesDir)) {
Files.createDirectories(projectClassesDir);
}
}
QuarkusBootstrap.Builder builder = QuarkusBootstrap.builder()
.setApplicationRoot(deploymentDir)
.setMode(QuarkusBootstrap.Mode.PROD)
.setLocalProjectDiscovery(true)
.setIsolateDeployment(true)
.addExcludedPath(testLocation)
.setProjectRoot(testLocation)
.setTargetDirectory(buildDir)
.setForcedDependencies(forcedDependencies);
builder.setBaseName(applicationName != null ? applicationName
: extensionContext.getDisplayName() + " (QuarkusProdModeTest)");
Map buildContext = new HashMap<>();
buildContext.put(BUILD_CONTEXT_CUSTOM_SOURCES_PATH_KEY, customSourcesDir);
if (!buildChainCustomizerEntries.isEmpty()) {
// we need to make sure all the classes needed to support the customizer flow are available at bootstrap time
// for that purpose we add them to a new archive that is then added to Quarkus bootstrap
Path additionalDeploymentDir = Files.createDirectories(outputDir.resolve("additional-deployment"));
JavaArchive additionalDeploymentArchive = ShrinkWrap.create(JavaArchive.class)
.addClasses(ProdModeTestBuildChainCustomizerProducer.class, ProdModeTestBuildChainBuilderConsumer.class,
ProdModeTestBuildStep.class);
// we push data from the test extension down to the customizers via JDK classes only because this data needs to be
// accessible by different classloaders
Map
© 2015 - 2024 Weber Informatics LLC | Privacy Policy