From f5973554d6db01019f2420d87416711a33c99f24 Mon Sep 17 00:00:00 2001 From: Holly Cummins Date: Mon, 9 Dec 2024 20:05:41 +1000 Subject: [PATCH] Make picocli tests definitely work in JVM mode --- .../dev/testing/CurrentTestApplication.java | 19 ++ .../dev/testing/FacadeClassLoader.java | 2 +- .../classloading/QuarkusClassLoader.java | 8 +- .../AbstractJvmQuarkusTestExtension.java | 14 +- .../test/junit/QuarkusMainTestExtension.java | 225 ++++++++++++++++-- 5 files changed, 238 insertions(+), 30 deletions(-) create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/testing/CurrentTestApplication.java diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/CurrentTestApplication.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/CurrentTestApplication.java new file mode 100644 index 0000000000000..169d124879501 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/CurrentTestApplication.java @@ -0,0 +1,19 @@ +package io.quarkus.deployment.dev.testing; + +import java.util.function.Consumer; + +import io.quarkus.bootstrap.app.CuratedApplication; + +/** + * This class is a bit of a hack, it provides a way to pass in the current curratedApplication into the TestExtension + * TODO It is only needed for QuarkusMainTest, so we may be able to find a better way. + * For example, what about JUnit state? + */ +public class CurrentTestApplication implements Consumer { + public static volatile CuratedApplication curatedApplication; + + @Override + public void accept(CuratedApplication c) { + curatedApplication = c; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/FacadeClassLoader.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/FacadeClassLoader.java index d5fec578efb36..4794fb735b8cd 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/FacadeClassLoader.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/FacadeClassLoader.java @@ -295,7 +295,7 @@ public Class loadClass(String name) throws ClassNotFoundException { // Doing it just for the test loads too little, doing it for everything gives java.lang.ClassCircularityError: io/quarkus/runtime/configuration/QuarkusConfigFactory // Anything loaded by JUnit will come through this classloader - if ((isQuarkusTest && !isIntegrationTest) || isMainTest) { + if (isQuarkusTest && !isIntegrationTest && !isMainTest) { System.out.println("HOLLY attempting to load " + name); QuarkusClassLoader runtimeClassLoader = getQuarkusClassLoader(key, fromCanary, profile); Class thing = runtimeClassLoader.loadClass(name); diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java index 79034cddcb654..4e6dd123ba6b8 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java @@ -520,7 +520,7 @@ public Class loadClass(String name) throws ClassNotFoundException { protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { ensureOpen(); - if (name.contains("acme")) { + if (name.contains("acme") || name.contains("Pico") || name.contains("Pico")) { System.out.println("HOLLY loading " + name + " with " + this); } @@ -576,12 +576,12 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE ClassPathElement[] resource = state.loadableResources.get(resourceName); if (resource != null) { - if (name.contains("acme")) { + if (name.contains("acme") || name.contains("Pico")) { System.out.println("checking " + resource[0].getRoot() + " for " + resourceName); } ClassPathElement classPathElement = resource[0]; ClassPathResource classPathElementResource = classPathElement.getResource(resourceName); - if (name.contains("acme")) { + if (name.contains("acme") || name.contains("Pico")) { System.out.println("did find it in " + classPathElementResource); } if (classPathElementResource != null) { //can happen if the class loader was closed @@ -596,7 +596,7 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE } } - if (name.contains("acme")) { + if (name.contains("acme") || name.contains("Pico")) { System.out.println("HOLLY ok, problem " + this + " can't find " + name); } if (!parentFirst) { diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java index 7b67f606c2e63..1334d2e528b1e 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.extension.ExtensionContext; import io.quarkus.bootstrap.BootstrapConstants; +import io.quarkus.bootstrap.BootstrapException; import io.quarkus.bootstrap.app.AugmentAction; import io.quarkus.bootstrap.app.CuratedApplication; import io.quarkus.bootstrap.app.RunningQuarkusApplication; @@ -59,8 +60,7 @@ protected PrepareResult createAugmentor(ExtensionContext context, Class requiredTestClass, ExtensionContext context, + Collection shutdownTasks) throws BootstrapException, AppModelResolverException, IOException { + // TODO make this abstract, push this implementation down to QuarkusTestExtension, since that is the only place it will work + CuratedApplication curatedApplication = ((QuarkusClassLoader) requiredTestClass.getClassLoader()) + .getCuratedApplication(); + return curatedApplication; + } + protected static QuarkusTestProfile getQuarkusTestProfile(Class profile, Collection shutdownTasks, Map additional) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { @@ -126,7 +134,7 @@ protected static QuarkusTestProfile getQuarkusTestProfile(Class requiredTestClass, ExtensionContext context, + Collection shutdownTasks) throws BootstrapException, AppModelResolverException, IOException { + // TODO is any of this common to AppMakerHelper? Almost all of it? + // Also, a lot of of duplication with parent class + CuratedApplication curatedApplication; + if (CurrentTestApplication.curatedApplication != null) { + curatedApplication = CurrentTestApplication.curatedApplication; + } else { + final Path projectRoot = Paths.get("") + .normalize() + .toAbsolutePath(); + + final PathList.Builder rootBuilder = PathList.builder(); + final Path testClassLocation; + final Path appClassLocation; + + Consumer addToBuilderIfConditionMet = path -> { + if (path != null && Files.exists(path) && !rootBuilder.contains(path)) { + rootBuilder.add(path); + } + }; + + final ApplicationModel gradleAppModel = getGradleAppModelForIDE(projectRoot); + // If gradle project running directly with IDE + if (gradleAppModel != null && gradleAppModel.getApplicationModule() != null) { + final WorkspaceModule module = gradleAppModel.getApplicationModule(); + final String testClassFileName = fromClassNameToResourceName(requiredTestClass.getName()); + Path testClassesDir = null; + for (String classifier : module.getSourceClassifiers()) { + final ArtifactSources sources = module.getSources(classifier); + if (sources.isOutputAvailable() && sources.getOutputTree().contains(testClassFileName)) { + for (SourceDir src : sources.getSourceDirs()) { + addToBuilderIfConditionMet.accept(src.getOutputDir()); + if (Files.exists(src.getOutputDir().resolve(testClassFileName))) { + testClassesDir = src.getOutputDir(); + } + } + for (SourceDir src : sources.getResourceDirs()) { + addToBuilderIfConditionMet.accept(src.getOutputDir()); + } + for (SourceDir src : module.getMainSources().getSourceDirs()) { + addToBuilderIfConditionMet.accept(src.getOutputDir()); + } + for (SourceDir src : module.getMainSources().getResourceDirs()) { + addToBuilderIfConditionMet.accept(src.getOutputDir()); + } + break; + } + } + if (testClassesDir == null) { + final StringBuilder sb = new StringBuilder(); + sb.append("Failed to locate ").append(requiredTestClass.getName()).append(" in "); + for (String classifier : module.getSourceClassifiers()) { + final ArtifactSources sources = module.getSources(classifier); + if (sources.isOutputAvailable()) { + for (SourceDir d : sources.getSourceDirs()) { + if (Files.exists(d.getOutputDir())) { + sb.append(System.lineSeparator()).append(d.getOutputDir()); + } + } + } + } + throw new RuntimeException(sb.toString()); + } + testClassLocation = testClassesDir; + + } else { + if (System.getProperty(BootstrapConstants.OUTPUT_SOURCES_DIR) != null) { + final String[] sourceDirectories = System.getProperty(BootstrapConstants.OUTPUT_SOURCES_DIR).split(","); + for (String sourceDirectory : sourceDirectories) { + final Path directory = Paths.get(sourceDirectory); + addToBuilderIfConditionMet.accept(directory); + } + } + + testClassLocation = getTestClassesLocation(requiredTestClass); + appClassLocation = getAppClassLocationForTestLocation(testClassLocation); + if (!appClassLocation.equals(testClassLocation)) { + addToBuilderIfConditionMet.accept(testClassLocation); + // if test classes is a dir, we should also check whether test resources dir exists as a separate dir (gradle) + // TODO: this whole app/test path resolution logic is pretty dumb, it needs be re-worked using proper workspace discovery + final Path testResourcesLocation = PathTestHelper.getResourcesForClassesDirOrNull(testClassLocation, + "test"); + addToBuilderIfConditionMet.accept(testResourcesLocation); + } + + addToBuilderIfConditionMet.accept(appClassLocation); + final Path appResourcesLocation = PathTestHelper.getResourcesForClassesDirOrNull(appClassLocation, "main"); + addToBuilderIfConditionMet.accept(appResourcesLocation); + } + + // TODO do we need to set isContinuousTesting? + curatedApplication = QuarkusBootstrap.builder() + //.setExistingModel(gradleAppModel) unfortunately this model is not re-usable due to PathTree serialization by Gradle + .setBaseName(context.getDisplayName() + " (QuarkusTest)") + .setIsolateDeployment(true) + .setMode(QuarkusBootstrap.Mode.TEST) + .setTest(true) + .setTargetDirectory(PathTestHelper.getProjectBuildDir(projectRoot, testClassLocation)) + .setProjectRoot(projectRoot) + .setApplicationRoot(rootBuilder.build()) + .build() + .bootstrap(); + shutdownTasks.add(curatedApplication::close); + } + + if (curatedApplication.getApplicationModel() + .getRuntimeDependencies() + .isEmpty()) { + throw new RuntimeException( + "The tests were run against a directory that does not contain a Quarkus project. Please ensure that the test is configured to use the proper working directory."); + } + + return curatedApplication; + } + private LaunchResult doLaunch(ExtensionContext context, Class selectedProfile, String[] arguments) throws Exception { ensurePrepared(context, selectedProfile); @@ -98,9 +237,13 @@ private LaunchResult doLaunch(ExtensionContext context, Class out = Arrays - .asList(String.join("", filter.captureOutput()).replaceAll("\\u001B\\[(.*?)[a-zA-Z]", "").split("\n")); + .asList(String.join("", filter.captureOutput()) + .replaceAll("\\u001B\\[(.*?)[a-zA-Z]", "") + .split("\n")); List err = Arrays - .asList(String.join("", filter.captureErrorOutput()).replaceAll("\\u001B\\[(.*?)[a-zA-Z]", "").split("\n")); + .asList(String.join("", filter.captureErrorOutput()) + .replaceAll("\\u001B\\[(.*?)[a-zA-Z]", "") + .split("\n")); return new LaunchResult() { @Override public List getOutputStream() { @@ -119,7 +262,8 @@ public int exitCode() { }; } finally { QuarkusConsole.removeOutputFilter(filter); - Thread.currentThread().setContextClassLoader(originalCl); + Thread.currentThread() + .setContextClassLoader(originalCl); } } @@ -132,7 +276,8 @@ public void afterEach(ExtensionContext context) throws Exception { private static Handler REDIRECT_QUARKUS_CONSOLE_HANDLER = null; private static void installLoggerRedirect() throws Exception { - var rootLogger = LogContext.getLogContext().getLogger(""); + var rootLogger = LogContext.getLogContext() + .getLogger(""); ORIGINAL_QUARKUS_CONSOLE_HANDLER = null; REDIRECT_QUARKUS_CONSOLE_HANDLER = null; @@ -158,7 +303,8 @@ private static void installLoggerRedirect() throws Exception { } private static void uninstallLoggerRedirect() throws Exception { - var rootLogger = LogContext.getLogContext().getLogger(""); + var rootLogger = LogContext.getLogContext() + .getLogger(""); if (REDIRECT_QUARKUS_CONSOLE_HANDLER != null) { rootLogger.addHandler(ORIGINAL_QUARKUS_CONSOLE_HANDLER); rootLogger.removeHandler(REDIRECT_QUARKUS_CONSOLE_HANDLER); @@ -166,10 +312,12 @@ private static void uninstallLoggerRedirect() throws Exception { } private void flushAllLoggers() { - Enumeration loggerNames = org.jboss.logmanager.LogContext.getLogContext().getLoggerNames(); + Enumeration loggerNames = org.jboss.logmanager.LogContext.getLogContext() + .getLoggerNames(); while (loggerNames != null && loggerNames.hasMoreElements()) { String loggerName = loggerNames.nextElement(); - var logger = org.jboss.logmanager.LogContext.getLogContext().getLogger(loggerName); + var logger = org.jboss.logmanager.LogContext.getLogContext() + .getLogger(loggerName); for (Handler h : logger.getHandlers()) { h.flush(); } @@ -184,30 +332,57 @@ private int doJavaStart(ExtensionContext context, Class requiredTestClass = startupAction.getClassLoader().loadClass(context.getRequiredTestClass().getName()); + + List additionalTestResources = getAdditionalTestResources(profileInstance, startupAction.getClassLoader()); + testResourceManager = (Closeable) startupAction.getClassLoader() + .loadClass(TestResourceManager.class.getName()) .getConstructor(Class.class, Class.class, List.class, boolean.class, Map.class, Optional.class) - .newInstance(context.getRequiredTestClass(), + .newInstance(requiredTestClass, profile != null ? profile : null, - getAdditionalTestResources(profileInstance, startupAction.getClassLoader()), + additionalTestResources, profileInstance != null && profileInstance.disableGlobalTestResources(), startupAction.getDevServicesProperties(), Optional.empty()); - testResourceManager.getClass().getMethod("init", String.class).invoke(testResourceManager, - profile != null ? profile.getName() : null); - Map properties = (Map) testResourceManager.getClass().getMethod("start") + testResourceManager.getClass() + .getMethod("init", String.class) + .invoke(testResourceManager, + profile != null ? profile.getName() : null); + Map properties = (Map) testResourceManager.getClass() + .getMethod("start") .invoke(testResourceManager); + + // testResourceManager = new TestResourceManager(requiredTestClass, + // profile, + // getAdditionalTestResources(profileInstance, startupAction.getClassLoader()), + // profileInstance != null && profileInstance.disableGlobalTestResources(), + // startupAction.getDevServicesProperties(), Optional.empty()); + // System.out.println("UGH TRM IS " + testResourceManager.getClass() + // .getClassLoader()); + // + // // ((TestResourceManager) testResourceManager).init(profile != null ? profile.getName() : null); + // Map properties = ((TestResourceManager) testResourceManager).start(); + startupAction.overrideConfig(properties); - hasPerTestResources = (boolean) testResourceManager.getClass().getMethod("hasPerTestResources") + hasPerTestResources = (boolean) testResourceManager.getClass() + .getMethod("hasPerTestResources") .invoke(testResourceManager); - testResourceManager.getClass().getMethod("inject", Object.class) + testResourceManager.getClass() + .getMethod("inject", Object.class) .invoke(testResourceManager, context.getRequiredTestInstance()); var result = startupAction.runMainClassBlocking(arguments); @@ -230,7 +405,8 @@ private int doJavaStart(ExtensionContext context, Class type = parameterContext.getParameter().getType(); + Class type = parameterContext.getParameter() + .getType(); return type == LaunchResult.class || type == QuarkusMainLauncher.class; } @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { - Class type = parameterContext.getParameter().getType(); + Class type = parameterContext.getParameter() + .getType(); Class profile = getQuarkusTestProfile(extensionContext); if (type == LaunchResult.class) { - var launch = extensionContext.getRequiredTestMethod().getAnnotation(Launch.class); + var launch = extensionContext.getRequiredTestMethod() + .getAnnotation(Launch.class); if (launch != null) { doLaunchAndAssertExitCode(extensionContext, profile, launch); } else { @@ -297,8 +476,10 @@ private void doLaunchAndAssertExitCode(ExtensionContext extensionContext, Class< public void interceptTestMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { Class profile = getQuarkusTestProfile(extensionContext); - if (invocationContext.getArguments().isEmpty()) { - var launch = extensionContext.getRequiredTestMethod().getAnnotation(Launch.class); + if (invocationContext.getArguments() + .isEmpty()) { + var launch = extensionContext.getRequiredTestMethod() + .getAnnotation(Launch.class); if (launch != null) { // in this case, resolveParameter has not been called by JUnit, so we need to make sure the application is launched doLaunchAndAssertExitCode(extensionContext, profile, launch);