All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.quarkus.test.QuarkusUnitTest Maven / Gradle / Ivy

Go to download

A runner for unit tests, intended for testing Quarkus rather than for end user consumption.

There is a newer version: 3.17.5
Show newest version
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.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
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.ServiceLoader;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
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 org.jboss.logmanager.Level;
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.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.InvocationInterceptor;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.extension.TestInstantiationException;

import io.quarkus.bootstrap.app.AdditionalDependency;
import io.quarkus.bootstrap.app.CuratedApplication;
import io.quarkus.bootstrap.app.QuarkusBootstrap;
import io.quarkus.bootstrap.app.RunningQuarkusApplication;
import io.quarkus.bootstrap.classloading.ClassLoaderEventListener;
import io.quarkus.bootstrap.classloading.ClassPathElement;
import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
import io.quarkus.builder.BuildChainBuilder;
import io.quarkus.builder.BuildContext;
import io.quarkus.builder.BuildException;
import io.quarkus.builder.BuildStep;
import io.quarkus.builder.item.BuildItem;
import io.quarkus.deployment.builditem.ApplicationClassPredicateBuildItem;
import io.quarkus.deployment.util.FileUtil;
import io.quarkus.maven.dependency.Dependency;
import io.quarkus.runner.bootstrap.AugmentActionImpl;
import io.quarkus.runner.bootstrap.StartupActionImpl;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.logging.JBossVersion;
import io.quarkus.test.common.GroovyClassValue;
import io.quarkus.test.common.PathTestHelper;
import io.quarkus.test.common.PropertyTestUtil;
import io.quarkus.test.common.RestAssuredURLManager;
import io.quarkus.test.common.TestConfigUtil;
import io.quarkus.test.common.TestResourceManager;
import io.quarkus.test.common.http.TestHTTPResourceManager;

/**
 * A test extension for testing Quarkus internals, not intended for end user consumption
 */
public class QuarkusUnitTest
        implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback,
        InvocationInterceptor, ParameterResolver {

    public static final String THE_BUILD_WAS_EXPECTED_TO_FAIL = "The build was expected to fail";
    private static final String APP_ROOT = "app-root";

    private static final Logger rootLogger;
    private Handler[] originalHandlers;

    static {
        JBossVersion.disableVersionLogging();
        System.setProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager");
        rootLogger = (Logger) LogManager.getLogManager().getLogger("");
    }

    boolean started = false;

    private Path deploymentDir;
    private Consumer assertException;
    private Supplier archiveProducer;
    private List additionalDependencies = new ArrayList<>();

    private List> buildChainCustomizers = new ArrayList<>();
    private Runnable afterUndeployListener;

    private String logFileName;
    private InMemoryLogHandler inMemoryLogHandler = new InMemoryLogHandler((r) -> false);
    private Consumer> assertLogRecords;

    private Timer timeoutTimer;
    private volatile TimerTask timeoutTask;
    private Properties customApplicationProperties;
    private String configResourceName;
    private Map customRuntimeApplicationProperties;
    private Runnable beforeAllCustomizer;
    private Runnable afterAllCustomizer;
    private CuratedApplication curatedApplication;
    private RunningQuarkusApplication runningQuarkusApplication;
    private QuarkusClassLoader quarkusUnitTestClassLoader;
    private ClassLoader originalClassLoader;
    private List forcedDependencies = Collections.emptyList();

    private boolean useSecureConnection;

    private Class actualTestClass;
    private Object actualTestInstance;
    private String[] commandLineParameters = new String[0];

    private boolean allowTestClassOutsideDeployment;
    private boolean flatClassPath;
    private List classLoadListeners = new ArrayList<>();

    private List testMethodInvokers;

    private List> bootstrapCustomizers = new ArrayList<>();

    private boolean debugBytecode = false;
    private List traceCategories = new ArrayList<>();

    private Map systemPropertiesToRestore = new HashMap<>();
    private Map loggerLevelsToRestore = new HashMap<>();

    public QuarkusUnitTest setExpectedException(Class expectedException) {
        return setExpectedException(expectedException, false);
    }

    public QuarkusUnitTest setExpectedException(Class expectedException, boolean logMessage) {
        return assertException(t -> {
            Throwable i = t;
            boolean found = false;
            while (i != null) {
                if (i.getClass().getName().equals(expectedException.getName())) {
                    found = true;
                    break;
                }
                i = i.getCause();
            }
            if (found && logMessage) {
                System.out.println("Build failed with the expected exception:" + i);
            }
            assertTrue(found, "Build failed with a wrong exception, expected " + expectedException + " but got " + t);
        });
    }

    public QuarkusUnitTest() {
        this(false);
    }

    public static QuarkusUnitTest withSecuredConnection() {
        return new QuarkusUnitTest(true);
    }

    private QuarkusUnitTest(boolean useSecureConnection) {
        this.useSecureConnection = useSecureConnection;
    }

    public QuarkusUnitTest assertException(Consumer assertException) {
        if (this.assertException != null) {
            throw new IllegalStateException("Don't set the asserted or excepted exception twice"
                    + " to avoid shadowing out the first call.");
        }
        this.assertException = assertException;
        return this;
    }

    public Supplier getArchiveProducer() {
        return archiveProducer;
    }

    /**
     *
     * @param archiveProducer
     * @return self
     * @see #withApplicationRoot(Consumer)
     */
    public QuarkusUnitTest setArchiveProducer(Supplier archiveProducer) {
        this.archiveProducer = Objects.requireNonNull(archiveProducer);
        return this;
    }

    /**
     * Customize the application root.
     *
     * @param applicationRootConsumer
     * @return self
     */
    public QuarkusUnitTest 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 QuarkusUnitTest withEmptyApplication() {
        return withApplicationRoot(new Consumer() {
            @Override
            public void accept(JavaArchive javaArchive) {

            }
        });
    }

    /**
     * Add the java archive as an additional dependency. This dependency is always considered an application archive, even if it
     * would not otherwise be one.
     *
     * @param archive
     * @return self
     */
    public QuarkusUnitTest addAdditionalDependency(JavaArchive archive) {
        this.additionalDependencies.add(Objects.requireNonNull(archive));
        return this;
    }

    /**
     * Add the java archive as an additional dependency. This dependency is always considered an application archive, even if it
     * would not otherwise be one.
     *
     * @param dependencyConsumer
     * @return self
     */
    public QuarkusUnitTest withAdditionalDependency(Consumer dependencyConsumer) {
        JavaArchive dependency = ShrinkWrap.create(JavaArchive.class);
        Objects.requireNonNull(dependencyConsumer).accept(dependency);
        return addAdditionalDependency(dependency);
    }

    public QuarkusUnitTest addBuildChainCustomizer(Consumer customizer) {
        this.buildChainCustomizers.add(customizer);
        return this;
    }

    public QuarkusUnitTest addClassLoaderEventListener(ClassLoaderEventListener listener) {
        this.classLoadListeners.add(listener);
        return this;
    }

    public QuarkusUnitTest setLogFileName(String logFileName) {
        this.logFileName = logFileName;
        return this;
    }

    public QuarkusUnitTest setLogRecordPredicate(Predicate predicate) {
        this.inMemoryLogHandler = new InMemoryLogHandler(predicate);
        return this;
    }

    /**
     * If this test should use a single ClassLoader to load all the classes.
     *
     * This is sometimes necessary when testing Quarkus itself, and we want the test classes
     * and Quarkus classes to be in the same CL.
     *
     */
    public QuarkusUnitTest setFlatClassPath(boolean flatClassPath) {
        this.flatClassPath = flatClassPath;
        return this;
    }

    public QuarkusUnitTest assertLogRecords(Consumer> assertLogRecords) {
        if (this.assertLogRecords != null) {
            throw new IllegalStateException("Don't set the a log record assertion twice"
                    + " to avoid shadowing out the first call.");
        }
        this.assertLogRecords = assertLogRecords;
        return this;
    }

    // set a Runnable that will run before ANYTHING else is done
    public QuarkusUnitTest setBeforeAllCustomizer(Runnable beforeAllCustomizer) {
        this.beforeAllCustomizer = beforeAllCustomizer;
        return this;
    }

    // set a Runnable that will run after EVERYTHING else is done
    public QuarkusUnitTest setAfterAllCustomizer(Runnable afterAllCustomizer) {
        this.afterAllCustomizer = afterAllCustomizer;
        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 QuarkusUnitTest setForcedDependencies(List forcedDependencies) {
        this.forcedDependencies = forcedDependencies;
        return this;
    }

    public String[] getCommandLineParameters() {
        return commandLineParameters;
    }

    public QuarkusUnitTest setCommandLineParameters(String... commandLineParameters) {
        this.commandLineParameters = commandLineParameters;
        return this;
    }

    /**
     * Normally access to any test classes that are not packaged in the deployment will result
     * in a ClassNotFoundException. If this is true then access is allowed, which can be useful
     * when testing shutdown behaviour.
     */
    public QuarkusUnitTest setAllowTestClassOutsideDeployment(boolean allowTestClassOutsideDeployment) {
        this.allowTestClassOutsideDeployment = allowTestClassOutsideDeployment;
        return this;
    }

    /**
     * An advanced option that allows tests to customize the {@link QuarkusBootstrap.Builder} that will be used to create the
     * {@link CuratedApplication}
     */
    public QuarkusUnitTest addBootstrapCustomizer(Consumer consumer) {
        this.bootstrapCustomizers.add(consumer);
        return this;
    }

    private void exportArchives(Path deploymentDir, Class testClass) {
        try {
            JavaArchive archive = getArchiveProducerOrDefault();
            Class c = testClass;
            archive.addClasses(c.getClasses());
            while (c != Object.class) {
                archive.addClass(c);
                c = c.getSuperclass();
            }

            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);
            }
            if (customApplicationProperties != null) {
                ExportUtil.mergeCustomApplicationProperties(archive, customApplicationProperties);
            }

            archive.as(ExplodedExporter.class).exportExplodedInto(deploymentDir.resolve(APP_ROOT).toFile());

            for (JavaArchive dependency : additionalDependencies) {
                dependency.as(ExplodedExporter.class).exportExplodedInto(deploymentDir.resolve(dependency.getName()).toFile());
            }

            //debugging code
            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 interceptBeforeAllMethod(Invocation invocation, ReflectiveInvocationContext invocationContext,
            ExtensionContext extensionContext) throws Throwable {
        runExtensionMethod(invocationContext, extensionContext, false);
        invocation.skip();
    }

    @Override
    public void interceptBeforeEachMethod(Invocation invocation, ReflectiveInvocationContext invocationContext,
            ExtensionContext extensionContext) throws Throwable {
        runExtensionMethod(invocationContext, extensionContext, true);
        invocation.skip();
    }

    @Override
    public void interceptAfterEachMethod(Invocation invocation, ReflectiveInvocationContext invocationContext,
            ExtensionContext extensionContext) throws Throwable {
        if (assertException == null) {
            runExtensionMethod(invocationContext, extensionContext, true);
            invocation.skip();
        } else {
            invocation.proceed();
        }
    }

    @Override
    public void interceptAfterAllMethod(Invocation invocation, ReflectiveInvocationContext invocationContext,
            ExtensionContext extensionContext) throws Throwable {
        if (assertException == null) {
            runExtensionMethod(invocationContext, extensionContext, false);
        }
        invocation.skip();
    }

    @Override
    public void interceptTestMethod(Invocation invocation, ReflectiveInvocationContext invocationContext,
            ExtensionContext extensionContext) throws Throwable {
        if (assertException == null) {
            runExtensionMethod(invocationContext, extensionContext, true);
        }
        invocation.skip();
    }

    @Override
    public void interceptTestTemplateMethod(Invocation invocation, ReflectiveInvocationContext invocationContext,
            ExtensionContext extensionContext) throws Throwable {
        if (assertException == null) {
            runExtensionMethod(invocationContext, extensionContext, false);
        }
        invocation.skip();
    }

    private void runExtensionMethod(ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext,
            boolean testMethodInvokersAllowed) throws Throwable {
        Method newMethod = null;
        Class c = actualTestClass;
        while ((c != Object.class) && newMethod == null) {
            Method[] declaredMethods = c.getDeclaredMethods();
            for (Method declaredMethod : declaredMethods) {
                if (!declaredMethod.getName().equals(invocationContext.getExecutable().getName())) {
                    continue;
                }
                boolean parametersMatch = true;
                for (Class declaredMethodParameterType : declaredMethod.getParameterTypes()) {
                    boolean parameterTypeFound = false;
                    for (Class executionContextParameterType : invocationContext.getExecutable().getParameterTypes()) {
                        if (declaredMethodParameterType.getName().equals(executionContextParameterType.getName())) {
                            parameterTypeFound = true;
                            break;
                        }
                    }
                    if (!parameterTypeFound) {
                        parametersMatch = false;
                        break;
                    }
                }
                if (parametersMatch) {
                    newMethod = declaredMethod;
                    break;
                }
            }
            c = c.getSuperclass();
        }
        if (newMethod == null) {
            throw new RuntimeException("Could not find method " + invocationContext.getExecutable() + " on test class");
        }

        Object testMethodInvokerToUse = null;
        if (testMethodInvokersAllowed) {
            for (Object testMethodInvoker : testMethodInvokers) {
                boolean supportsMethod = (boolean) testMethodInvoker.getClass()
                        .getMethod("supportsMethod", Class.class, Method.class).invoke(testMethodInvoker,
                                extensionContext.getRequiredTestClass(), invocationContext.getExecutable());
                if (supportsMethod) {
                    testMethodInvokerToUse = testMethodInvoker;
                    break;
                }
            }
        }

        try {
            newMethod.setAccessible(true);
            if (testMethodInvokerToUse != null) {
                List effectiveArguments = new ArrayList<>(invocationContext.getArguments().size());
                Class[] parameterTypes = newMethod.getParameterTypes();
                if (parameterTypes.length != invocationContext.getArguments().size()) {
                    throw new IllegalStateException(
                            "Improper integration of '" + testMethodInvokerToUse.getClass() + "' detected");
                }
                for (int i = 0; i < invocationContext.getArguments().size(); i++) {
                    Object originalValue = invocationContext.getArguments().get(i);
                    if ((originalValue == null)
                            && (testMethodInvokerHandlesParamType(testMethodInvokerToUse, parameterTypes[i].getName()))) {
                        effectiveArguments
                                .add(testMethodInvokerParamInstance(testMethodInvokerToUse, parameterTypes[i].getName()));
                    } else {
                        effectiveArguments.add(originalValue);
                    }
                }
                testMethodInvokerToUse.getClass()
                        .getMethod("invoke", Object.class, Method.class, List.class, String.class)
                        .invoke(testMethodInvokerToUse, actualTestInstance, newMethod, effectiveArguments,
                                extensionContext.getRequiredTestClass().getName());
            } else {
                newMethod.invoke(actualTestInstance, invocationContext.getArguments().toArray());
            }
        } catch (InvocationTargetException e) {
            throw e.getCause();
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void beforeAll(ExtensionContext extensionContext) throws Exception {
        TestConfigUtil.cleanUp();
        GroovyClassValue.disable();
        //set the right launch mode in the outer CL, used by the HTTP host config source
        LaunchMode.set(LaunchMode.TEST);
        if (beforeAllCustomizer != null) {
            beforeAllCustomizer.run();
        }
        if (debugBytecode) {
            // Use a unique ID to avoid overriding dumps between test classes (and re-execution of flaky tests).
            var testRunId = extensionContext.getRequiredTestClass().getName() + "/" + UUID.randomUUID();
            System.out.println("[QuarkusUnitTest] Debug dumps enabled. Test run ID: " + testRunId);
            // This needs to be set as system properties; see BootstrapDebug.java.
            // Note these paths are considered standard and may be taken advantage of in Quarkus CI (to collect dumps).
            overrideSystemProperty("quarkus.debug.transformed-classes-dir",
                    "target/debug/" + testRunId + "/transformed-classes");
            overrideSystemProperty("quarkus.debug.generated-classes-dir", "target/debug/" + testRunId + "/generated-classes");
            overrideSystemProperty("quarkus.debug.generated-sources-dir", "target/debug/" + testRunId + "/generated-sources");
        }
        if (!traceCategories.isEmpty()) {
            // This needs to be set very early (e.g. as system properties) in order to affect the build;
            // needs to be set programmatically in order to not leak to other tests (for some reason?).
            // See https://quarkusio.zulipchat.com/#narrow/stream/187038-dev/topic/Build.20logs
            for (String category : traceCategories) {
                overrideLoggerLevel(category, Level.TRACE);
            }
        }
        originalClassLoader = Thread.currentThread().getContextClassLoader();
        originalHandlers = rootLogger.getHandlers();
        rootLogger.addHandler(inMemoryLogHandler);

        timeoutTask = new TimerTask() {
            @Override
            public void run() {
                System.err.println("Test has been running for more than 5 minutes, thread dump is:");
                for (Map.Entry i : Thread.getAllStackTraces().entrySet()) {
                    System.err.println("\n");
                    System.err.println(i.toString());
                    System.err.println("\n");
                    for (StackTraceElement j : i.getValue()) {
                        System.err.println(j);
                    }
                }
            }
        };
        timeoutTimer = new Timer("Test thread dump timer");
        timeoutTimer.schedule(timeoutTask, 1000 * 60 * 5);
        if (logFileName != null) {
            PropertyTestUtil.setLogFileProperty(logFileName);
        } else {
            PropertyTestUtil.setLogFileProperty();
        }
        ExtensionContext.Store store = extensionContext.getRoot().getStore(ExtensionContext.Namespace.GLOBAL);

        ExclusivityChecker.checkTestType(extensionContext, QuarkusUnitTest.class);

        TestResourceManager testResourceManager = (TestResourceManager) store.get(TestResourceManager.class.getName());
        if (testResourceManager == null) {
            testResourceManager = new TestResourceManager(extensionContext.getRequiredTestClass());
            testResourceManager.init(null);
            testResourceManager.start();
            TestResourceManager tm = testResourceManager;
            store.put(TestResourceManager.class.getName(), testResourceManager);
            store.put(TestResourceManager.CLOSEABLE_NAME, new ExtensionContext.Store.CloseableResource() {

                @Override
                public void close() throws Throwable {
                    tm.close();
                }
            });
        }

        Class testClass = extensionContext.getRequiredTestClass();

        try {
            deploymentDir = Files.createTempDirectory("quarkus-unit-test");

            exportArchives(deploymentDir, testClass);

            List> customizers = new ArrayList<>(buildChainCustomizers);

            try {
                //this is a bit of a hack to avoid requiring a dep on the arc extension,
                //as this would mean we cannot use this to test the extension
                Class buildItem = Class
                        .forName("io.quarkus.arc.deployment.AdditionalBeanBuildItem").asSubclass(BuildItem.class);
                customizers.add(new Consumer() {
                    @Override
                    public void accept(BuildChainBuilder buildChainBuilder) {
                        buildChainBuilder.addBuildStep(new BuildStep() {
                            @Override
                            public void execute(BuildContext context) {
                                try {
                                    Method factoryMethod = buildItem.getMethod("unremovableOf", Class.class);
                                    context.produce((BuildItem) factoryMethod.invoke(null, testClass));
                                } catch (Exception e) {
                                    throw new RuntimeException(e);
                                }
                            }
                        }).produces(buildItem)
                                .build();

                        buildChainBuilder.addBuildStep(new BuildStep() {
                            @Override
                            public void execute(BuildContext context) {
                                //we need to make sure all hot reloadable classes are application classes
                                context.produce(new ApplicationClassPredicateBuildItem(new Predicate() {
                                    @Override
                                    public boolean test(String className) {
                                        return QuarkusClassLoader.isApplicationClass(className);
                                    }
                                }));
                            }
                        }).produces(ApplicationClassPredicateBuildItem.class).build();
                    }
                });
            } catch (ClassNotFoundException e) {
                System.err.println("Couldn't make the test class " + testClass.getSimpleName() + " an unremovable bean"
                        + " (probably because a dependency on io.quarkus:quarkus-arc-deployment is missing);"
                        + " other beans may also be removed and injection may not work as expected");
            }

            try {
                final Path testLocation = PathTestHelper.getTestClassesLocation(testClass);
                final Path projectDir = Path.of("").normalize().toAbsolutePath();
                QuarkusBootstrap.Builder builder = QuarkusBootstrap.builder()
                        .setBaseName(extensionContext.getDisplayName() + " (QuarkusUnitTest)")
                        .setApplicationRoot(deploymentDir.resolve(APP_ROOT))
                        .setMode(QuarkusBootstrap.Mode.TEST)
                        .addExcludedPath(testLocation)
                        .setProjectRoot(projectDir)
                        .setTargetDirectory(PathTestHelper.getProjectBuildDir(projectDir, testLocation))
                        .setFlatClassPath(flatClassPath)
                        .setForcedDependencies(forcedDependencies);
                for (JavaArchive dependency : additionalDependencies) {
                    builder.addAdditionalApplicationArchive(
                            new AdditionalDependency(deploymentDir.resolve(dependency.getName()), false, true));
                }
                if (!forcedDependencies.isEmpty()) {
                    //if we have forced dependencies we can't use the cache
                    //as it can screw everything up
                    builder.setDisableClasspathCache(true);
                }
                if (!allowTestClassOutsideDeployment) {
                    quarkusUnitTestClassLoader = QuarkusClassLoader
                            .builder("QuarkusUnitTest ClassLoader for " + extensionContext.getDisplayName(),
                                    getClass().getClassLoader(), false)
                            .addClassLoaderEventListeners(this.classLoadListeners)
                            .addBannedElement(ClassPathElement.fromPath(testLocation, true)).build();
                    builder.setBaseClassLoader(quarkusUnitTestClassLoader);
                }
                builder.addClassLoaderEventListeners(this.classLoadListeners);

                for (Consumer bootstrapCustomizer : bootstrapCustomizers) {
                    bootstrapCustomizer.accept(builder);
                }
                curatedApplication = builder.build().bootstrap();

                StartupActionImpl startupAction = new AugmentActionImpl(curatedApplication, customizers, classLoadListeners)
                        .createInitialRuntimeApplication();
                Map overriddenConfig = new HashMap<>(testResourceManager.getConfigProperties());
                if (customRuntimeApplicationProperties != null) {
                    overriddenConfig.putAll(customRuntimeApplicationProperties);
                }
                startupAction.overrideConfig(overriddenConfig);
                runningQuarkusApplication = startupAction
                        .run(commandLineParameters);
                //we restore the CL at the end of the test
                Thread.currentThread().setContextClassLoader(runningQuarkusApplication.getClassLoader());
                if (assertException != null) {
                    fail(THE_BUILD_WAS_EXPECTED_TO_FAIL);
                }
                started = true;
                System.setProperty("test.url", TestHTTPResourceManager.getUri(runningQuarkusApplication));
                try {
                    actualTestClass = Class.forName(testClass.getName(), true,
                            Thread.currentThread().getContextClassLoader());
                    actualTestInstance = runningQuarkusApplication.instance(actualTestClass);
                    Class resM = runningQuarkusApplication.getClassLoader()
                            .loadClass(TestHTTPResourceManager.class.getName());
                    resM.getDeclaredMethod("inject", Object.class).invoke(null, actualTestInstance);

                    populateTestMethodInvokers(startupAction.getClassLoader());

                } catch (Exception e) {
                    throw new TestInstantiationException("Failed to create test instance", e);
                }

                extensionContext.getStore(ExtensionContext.Namespace.GLOBAL).put(testClass.getName(), actualTestInstance);
            } catch (Throwable e) {
                started = false;
                if (assertException != null) {
                    if (e instanceof AssertionError && e.getMessage().equals(THE_BUILD_WAS_EXPECTED_TO_FAIL)) {
                        //don't pass the 'no failure' assertion into the assert exception function
                        throw e;
                    }
                    if (e instanceof RuntimeException) {
                        Throwable cause = e.getCause();
                        if (cause != null && cause instanceof BuildException) {
                            assertException.accept(unwrapException(cause.getCause()));
                        } else if (cause != null) {
                            assertException.accept(unwrapException(cause));
                        } else {
                            assertException.accept(e);
                        }
                    } else {
                        assertException.accept(e);
                    }
                } else {
                    throw e;
                }
            }
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private Throwable unwrapException(Throwable cause) {
        //TODO: huge hack
        try {
            Class localVer = QuarkusUnitTest.class.getClassLoader().loadClass(cause.getClass().getName());
            if (localVer != cause.getClass()) {
                Constructor ctor = localVer.getConstructor(String.class, Throwable.class);
                return (Throwable) ctor.newInstance(cause.getMessage(), cause.getCause());
            }
        } catch (Exception e) {
            //failed to unwrap
        }
        return cause;
    }

    private void overrideSystemProperty(String key, String value) {
        // IMPORTANT: Not logging the value in case it's a secret.
        System.out.printf("[QuarkusUnitTest] Overriding system property '%s'%n", key);
        systemPropertiesToRestore.putIfAbsent(key, System.getProperty(key));
        System.setProperty(key, value);
    }

    private void overrideLoggerLevel(String category, Level level) {
        System.out.printf("[QuarkusUnitTest] Overriding logger category '%s'; setting level '%s'%n", category, level);
        var logger = LogManager.getLogManager().getLogger(category);
        loggerLevelsToRestore.putIfAbsent(category, logger.getLevel());
        logger.setLevel(level);
    }

    @Override
    public void afterAll(ExtensionContext extensionContext) throws Exception {
        actualTestClass = null;
        actualTestInstance = null;
        List records = null;
        if (assertLogRecords != null) {
            records = new ArrayList<>(inMemoryLogHandler.records);
        }
        rootLogger.setHandlers(originalHandlers);
        inMemoryLogHandler.clearRecords();
        inMemoryLogHandler.setFilter(null);
        if (testMethodInvokers != null) {
            testMethodInvokers.clear();
        }

        try {
            if (runningQuarkusApplication != null) {
                runningQuarkusApplication.close();
                runningQuarkusApplication = null;
            }
            if (afterUndeployListener != null) {
                afterUndeployListener.run();
            }
            if (curatedApplication != null) {
                curatedApplication.close();
                curatedApplication = null;
            }
        } finally {
            System.clearProperty("test.url");
            Thread.currentThread().setContextClassLoader(originalClassLoader);
            originalClassLoader = null;
            if (quarkusUnitTestClassLoader != null) {
                quarkusUnitTestClassLoader.close();
                quarkusUnitTestClassLoader = null;
            }
            timeoutTask.cancel();
            timeoutTask = null;
            timeoutTimer.cancel();
            timeoutTimer = null;
            if (deploymentDir != null) {
                FileUtil.deleteDirectory(deploymentDir);
            }

            if (afterAllCustomizer != null) {
                afterAllCustomizer.run();
            }
            systemPropertiesToRestore.forEach((key, previousValue) -> {
                if (previousValue == null) {
                    System.clearProperty(key);
                } else {
                    System.setProperty(key, previousValue);
                }
            });
            systemPropertiesToRestore.clear();
            loggerLevelsToRestore.forEach((category, previousLevel) -> Logger.getLogger(category).setLevel(previousLevel));
            loggerLevelsToRestore.clear();
            ClearCache.clearCaches();
            TestConfigUtil.cleanUp();
        }
        if (records != null) {
            assertLogRecords.accept(records);
        }
    }

    @Override
    public void afterEach(ExtensionContext context) throws Exception {
        if (runningQuarkusApplication != null) {
            //this kinda sucks, but everything is isolated, so we need to hook into everything via reflection
            runningQuarkusApplication.getClassLoader().loadClass(RestAssuredURLManager.class.getName())
                    .getDeclaredMethod("clearURL")
                    .invoke(null);
        }
    }

    @Override
    public void beforeEach(ExtensionContext context) throws Exception {
        if (assertException != null) {
            // Build failed as expected - test methods are not invoked
            return;
        }
        if (runningQuarkusApplication != null) {
            runningQuarkusApplication.getClassLoader().loadClass(RestAssuredURLManager.class.getName())
                    .getDeclaredMethod("setURL", boolean.class).invoke(null, useSecureConnection);
        } else {
            Optional> testClass = context.getTestClass();
            if (testClass.isPresent()) {
                Field extensionField = Arrays.stream(testClass.get().getDeclaredFields()).filter(
                        f -> f.isAnnotationPresent(RegisterExtension.class) && QuarkusUnitTest.class.equals(f.getType()))
                        .findAny().orElse(null);
                if (extensionField != null && !Modifier.isStatic(extensionField.getModifiers())) {
                    throw new IllegalStateException(
                            "Test application not started - QuarkusUnitTest must be used with a static field: "
                                    + extensionField);
                }
            }
            throw new IllegalStateException("Test application not started for an unknown reason");
        }
    }

    public Runnable getAfterUndeployListener() {
        return afterUndeployListener;
    }

    public QuarkusUnitTest setAfterUndeployListener(Runnable afterUndeployListener) {
        this.afterUndeployListener = afterUndeployListener;
        return this;
    }

    /**
     * Add an {@code application.properties} asset loaded from the specified resource file in the test {@link JavaArchive}.
     * 

* If an {@code application.properties} asset was already added explicitly to the archive (for instance through * {@link JavaArchive#addAsResource(String)}), this formet asset is removed and completely replaced by the one given here. *

* Configuration properties added with {@link #overrideConfigKey(String, String)} take precedence over the properties from * the specified resource file. * * @param resourceName * @return the test configuration */ public QuarkusUnitTest withConfigurationResource(String resourceName) { this.configResourceName = Objects.requireNonNull(resourceName); return this; } /** * Overriden configuration properties take precedence over an {@code application.properties} asset added in the test * {@link JavaArchive}. * * @param propertyKey * @param propertyValue * @return the test configuration */ public QuarkusUnitTest overrideConfigKey(final String propertyKey, final String propertyValue) { if (customApplicationProperties == null) { customApplicationProperties = new Properties(); } customApplicationProperties.put(propertyKey, propertyValue); return this; } public QuarkusUnitTest overrideRuntimeConfigKey(final String propertyKey, final String propertyValue) { if (customRuntimeApplicationProperties == null) { customRuntimeApplicationProperties = new HashMap<>(); } customRuntimeApplicationProperties.put(propertyKey, propertyValue); return this; } /** * Controls bytecode-related debug dumping. *

* When enabled, each Quarkus startup will have configuration properties * such as {@code quarkus.debug.generated-classes-dir} set * so that generated code gets dumped in {@code target/debug}, * within a unique subdirectory for each test execution. *

* Look at the logs of a particular test to identify the corresponding dump directory. * * @param debugBytecode {@code true} if debug should be enabled * @return {@code this}, for method chaining. */ public QuarkusUnitTest debugBytecode(boolean debugBytecode) { this.debugBytecode = debugBytecode; return this; } /** * Enables trace logs for the given categories, * during both build and runtime. * * @param categories The categories for which to enable trace logging. * @return {@code this}, for method chaining. */ public QuarkusUnitTest traceCategories(String... categories) { Collections.addAll(this.traceCategories, categories); return this; } @Override public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { boolean isConstructor = parameterContext.getDeclaringExecutable() instanceof Constructor; if (isConstructor) { return true; } if (!(parameterContext.getDeclaringExecutable() instanceof Method)) { return false; } if (testMethodInvokers == null) { return false; } for (Object testMethodInvoker : testMethodInvokers) { boolean handlesMethodParamType = testMethodInvokerHandlesParamType(testMethodInvoker, parameterContext.getParameter().getType().getName()); if (handlesMethodParamType) { return true; } } return false; } /** * We don't actually have to resolve the parameter (thus the default values in the implementation) * since the class instance that is passed to JUnit isn't really used. * The actual test instance that is used is the one that is pulled from Arc, which of course will already have its * constructor parameters properly resolved */ @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { if ((parameterContext.getDeclaringExecutable() instanceof Method) && (testMethodInvokers != null)) { for (Object testMethodInvoker : testMethodInvokers) { if (testMethodInvokerHandlesParamType(testMethodInvoker, parameterContext.getParameter().getType().getName())) { return null; // don't return the actual value since it leads to class loading issues } } } String className = parameterContext.getParameter().getType().getName(); switch (className) { case "boolean": return false; case "byte": case "short": case "int": return 0; case "long": return 0L; case "float": return 0.0f; case "double": return 0.0d; case "char": return '\u0000'; default: return null; } } // we need to use reflection because the instances of TestMethodInvoker are loaded from the QuarkusClassLoader private boolean testMethodInvokerHandlesParamType(Object testMethodInvoker, String parameterTypeName) { try { return (boolean) testMethodInvoker.getClass().getMethod("handlesMethodParamType", String.class) .invoke(testMethodInvoker, parameterTypeName); } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { throw new IllegalStateException("Unable to determine if TestMethodInvoker supports parameter", e); } } // we need to use reflection because the instances of TestMethodInvoker are loaded from the QuarkusClassLoader private Object testMethodInvokerParamInstance(Object testMethodInvoker, String parameterTypeName) { try { return testMethodInvoker.getClass().getMethod("methodParamInstance", String.class) .invoke(testMethodInvoker, parameterTypeName); } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { throw new IllegalStateException("Unable to obtain instance of parameter using TestMethodInvoker", e); } } private void populateTestMethodInvokers(ClassLoader quarkusClassLoader) { testMethodInvokers = new ArrayList<>(); try { ServiceLoader loader = ServiceLoader.load(quarkusClassLoader.loadClass(TestMethodInvoker.class.getName()), quarkusClassLoader); for (Object testMethodInvoker : loader) { testMethodInvokers.add(testMethodInvoker); } } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } }