diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/AppMakerHelper.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/AppMakerHelper.java new file mode 100644 index 00000000000000..59d7420c92ff88 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/AppMakerHelper.java @@ -0,0 +1,715 @@ +package io.quarkus.deployment.dev.testing; + +import static io.quarkus.test.common.PathTestHelper.getAppClassLocationForTestLocation; +import static io.quarkus.test.common.PathTestHelper.getTestClassesLocation; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import jakarta.enterprise.inject.Alternative; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.Index; +import org.jboss.jandex.Type; +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.QuarkusBootstrap; +import io.quarkus.bootstrap.app.StartupAction; +import io.quarkus.bootstrap.classloading.ClassPathElement; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; +import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.bootstrap.resolver.AppModelResolverException; +import io.quarkus.bootstrap.runner.Timing; +import io.quarkus.bootstrap.utils.BuildToolHelper; +import io.quarkus.bootstrap.workspace.ArtifactSources; +import io.quarkus.bootstrap.workspace.SourceDir; +import io.quarkus.bootstrap.workspace.WorkspaceModule; +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.commons.classloading.ClassloadHelper; +import io.quarkus.deployment.builditem.ApplicationClassPredicateBuildItem; +import io.quarkus.deployment.builditem.TestAnnotationBuildItem; +import io.quarkus.deployment.builditem.TestClassBeanBuildItem; +import io.quarkus.deployment.builditem.TestClassPredicateBuildItem; +import io.quarkus.paths.PathList; +import io.quarkus.runtime.LaunchMode; +import io.quarkus.runtime.test.TestHttpEndpointProvider; +import io.quarkus.test.common.PathTestHelper; +import io.quarkus.test.common.RestorableSystemProperties; +import io.quarkus.test.junit.QuarkusTestProfile; + +public class AppMakerHelper { + + // Copied from superclass of thing we copied + protected static final String TEST_LOCATION = "test-location"; + protected static final String TEST_CLASS = "test-class"; + protected static final String TEST_PROFILE = "test-profile"; + /// end copied + + private static boolean failedBoot; + + private static Class quarkusTestMethodContextClass; + private static boolean hasPerTestResources; + private static List, String>> testHttpEndpointProviders; + + private static List testMethodInvokers; + + protected static class PrepareResult { + protected final AugmentAction augmentAction; + protected final QuarkusTestProfile profileInstance; + protected final CuratedApplication curatedApplication; + protected final Path testClassLocation; + + public PrepareResult(AugmentAction augmentAction, QuarkusTestProfile profileInstance, + CuratedApplication curatedApplication, Path testClassLocation) { + System.out.println("PrepareResult" + augmentAction + ": " + profileInstance + " test class " + testClassLocation); + + this.augmentAction = augmentAction; + this.profileInstance = profileInstance; + this.curatedApplication = curatedApplication; + this.testClassLocation = testClassLocation; + } + } + + // TODO is this used? + protected PrepareResult v2createAugmentor(CuratedApplication curatedApplication, Class requiredTestClass, + Class profile, + Collection shutdownTasks, boolean isContinuousTesting) throws Exception { + + Path testClassLocation = getTestClassesLocation(requiredTestClass); + // TODO this is probably wrong since we find the gradle path in the method but the other path elsewhere + return v2createAugmentor(curatedApplication, PathList.of(testClassLocation), requiredTestClass, profile, shutdownTasks, + isContinuousTesting); + } + + // TODO Re-used from AbstractJvmQuarkusTestExtension + protected ApplicationModel getGradleAppModelForIDE(Path projectRoot) throws IOException, AppModelResolverException { + return System.getProperty(BootstrapConstants.SERIALIZED_TEST_APP_MODEL) == null + ? BuildToolHelper.enableGradleAppModelForTest(projectRoot) + : null; + } + + // TODO Re-used from AbstractJvmQuarkusTestExtension, delete it there + private PrepareResult createAugmentor(ExtensionContext context, CuratedApplication curatedApplication, + Class profile, + Collection shutdownTasks) throws Exception { + return createAugmentor(context.getRequiredTestClass(), context.getDisplayName(), curatedApplication, profile, + shutdownTasks); + } + + private PrepareResult createAugmentor(final Class requiredTestClass, String displayName, + CuratedApplication curatedApplication, + Class profile, + Collection shutdownTasks) throws Exception { + + if (curatedApplication == null) { + curatedApplication = makeCuratedApplication(requiredTestClass, displayName, shutdownTasks); + } + Path testClassLocation = getTestClassLocationIncludingPossibilityOfGradleModel(requiredTestClass); + + // clear the test.url system property as the value leaks into the run when using different profiles + System.clearProperty("test.url"); + Map additional = new HashMap<>(); + + QuarkusTestProfile profileInstance = null; + if (profile != null) { + profileInstance = profile.getConstructor().newInstance(); + additional.putAll(profileInstance.getConfigOverrides()); + if (!profileInstance.getEnabledAlternatives().isEmpty()) { + additional.put("quarkus.arc.selected-alternatives", profileInstance.getEnabledAlternatives().stream() + .peek((c) -> { + if (!c.isAnnotationPresent(Alternative.class)) { + throw new RuntimeException( + "Enabled alternative " + c + " is not annotated with @Alternative"); + } + }) + .map(Class::getName).collect(Collectors.joining(","))); + } + if (profileInstance.disableApplicationLifecycleObservers()) { + additional.put("quarkus.arc.test.disable-application-lifecycle-observers", "true"); + } + if (profileInstance.getConfigProfile() != null) { + additional.put(LaunchMode.TEST.getProfileKey(), profileInstance.getConfigProfile()); + } + //we just use system properties for now + //it's a lot simpler + shutdownTasks.add(RestorableSystemProperties.setProperties(additional)::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."); + } + + // TODO should we do this here, or when we prepare the curated application? + // Or is it needed at all? + Index testClassesIndex = TestClassIndexer.indexTestClasses(testClassLocation); + // we need to write the Index to make it reusable from other parts of the testing infrastructure that run in different ClassLoaders + TestClassIndexer.writeIndex(testClassesIndex, testClassLocation, requiredTestClass); + + Timing.staticInitStarted(curatedApplication + .getOrCreateBaseRuntimeClassLoader(), + curatedApplication + .getQuarkusBootstrap().isAuxiliaryApplication()); + final Map props = new HashMap<>(); + props.put(TEST_LOCATION, testClassLocation); + props.put(TEST_CLASS, requiredTestClass); + if (profile != null) { + props.put(TEST_PROFILE, profile.getName()); + } + //TODO copied, not needed - but do need to pass it out quarkusTestProfile = profile; + return new PrepareResult(curatedApplication + .createAugmentor(TestBuildChainFunction.class.getName(), props), profileInstance, + curatedApplication, testClassLocation); + } + + CuratedApplication makeCuratedApplication(Class requiredTestClass, String displayName, + Collection shutdownTasks) throws IOException, AppModelResolverException, BootstrapException { + final PathList.Builder rootBuilder = PathList.builder(); + Consumer addToBuilderIfConditionMet = path -> { + if (path != null && Files.exists(path) && !rootBuilder.contains(path)) { + rootBuilder.add(path); + } + }; + + final Path testClassLocation; + final Path appClassLocation; + final Path projectRoot = Paths.get("").normalize().toAbsolutePath(); + + 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 = ClassloadHelper + .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; + } + } + validateTestDir(requiredTestClass, testClassesDir, module); + 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); + } + + CuratedApplication curatedApplication = QuarkusBootstrap.builder() + //.setExistingModel(gradleAppModel) unfortunately this model is not re-usable due to PathTree serialization by Gradle + .setBaseName(displayName + " (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); + + return curatedApplication; + } + + private Path getTestClassLocationIncludingPossibilityOfGradleModel(Class requiredTestClass) + throws IOException, AppModelResolverException, BootstrapException { + + final Path projectRoot = Paths.get("").normalize().toAbsolutePath(); + + final Path testClassLocation; + + 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 = ClassloadHelper + .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()) { + if (Files.exists(src.getOutputDir().resolve(testClassFileName))) { + testClassesDir = src.getOutputDir(); + } + } + + break; + } + } + validateTestDir(requiredTestClass, testClassesDir, module); + testClassLocation = testClassesDir; + + } else { + testClassLocation = getTestClassesLocation(requiredTestClass); + } + + return testClassLocation; + } + + private static void validateTestDir(Class requiredTestClass, Path testClassesDir, WorkspaceModule module) { + 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()); + } + } + + protected PrepareResult v2createAugmentor(CuratedApplication curatedApplication, PathList bothLocations, + Class requiredTestClass, + Class profile, + Collection shutdownTasks, boolean isContinuousTesting) throws Exception { + System.out.println("HOLLY creating augmentor with prarams" + bothLocations + " " + requiredTestClass + " and app" + + curatedApplication); + Path testClassLocation = bothLocations.stream().findFirst().get(); + + // I think the required test class is just an example, since the augmentor is only + // created once per test profile + + QuarkusTestProfile profileInstance = null; + if (profile != null) { + profileInstance = profile.getConstructor() + .newInstance(); + } + + if (curatedApplication == null) { + + // TODO this is only needed if curatedApplication is null? Extract common code with the interceptor + // TODO we bypass all this in the interceptor, terrible!! + final PathList.Builder rootBuilder = PathList.builder(); + Consumer addToBuilderIfConditionMet = path -> { + if (path != null && Files.exists(path) && !rootBuilder.contains(path)) { + System.out.println("HOLLY adding path to builder " + path); + rootBuilder.add(path); + } + }; + + final Path appClassLocation; + Path appClassLocation1; + final Path projectRoot = Paths.get("") + .normalize() + .toAbsolutePath(); + + final ApplicationModel gradleAppModel = getGradleAppModelForIDE(projectRoot); + //If gradle project running directly with IDE + if (gradleAppModel != null && gradleAppModel.getApplicationModule() != null) { + System.out.println("HOLLY going down IDE gradle path"); + final WorkspaceModule module = gradleAppModel.getApplicationModule(); + final String testClassFileName = requiredTestClass.getName().replace('.', + '/') + ".class"; + 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; + } + } + validateTestDir(requiredTestClass, testClassesDir, module); + testClassLocation = testClassesDir; + + } else { + System.out.println("HOLLY going down not-gradle path " + + System.getProperty(BootstrapConstants.ALL_OUTPUT_SOURCES_DIR) + " " + + System.getProperty(BootstrapConstants.OUTPUT_SOURCES_DIR)); + // TODO sort out duplication + if (System.getProperty(BootstrapConstants.ALL_OUTPUT_SOURCES_DIR) != null) { + System.out + .println("HOLLY ALLS processing " + System.getProperty(BootstrapConstants.ALL_OUTPUT_SOURCES_DIR)); + final String[] sourceDirectories = System.getProperty( + BootstrapConstants.ALL_OUTPUT_SOURCES_DIR) + .split(","); + for (String sourceDirectory : sourceDirectories) { + final Path directory = Paths.get(sourceDirectory); + addToBuilderIfConditionMet.accept(directory); + } + } else if (System.getProperty(BootstrapConstants.OUTPUT_SOURCES_DIR) != null) { + System.out.println("HOLLY processing " + System.getProperty(BootstrapConstants.OUTPUT_SOURCES_DIR)); + 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); + System.out.println("test class location is " + testClassLocation); + + appClassLocation = getAppClassLocationForTestLocation(testClassLocation); + + System.out.println("app class location is " + appClassLocation); + if (!appClassLocation.equals(testClassLocation)) { + System.out.println("Adding test class location explicitly"); + 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); + } + + System.out.println("Adding app class location explicitly"); + addToBuilderIfConditionMet.accept(appClassLocation); + final Path appResourcesLocation = PathTestHelper.getResourcesForClassesDirOrNull( + appClassLocation, "main"); + + addToBuilderIfConditionMet.accept(appResourcesLocation); + + ClassLoader originalCl = Thread.currentThread() + .getContextClassLoader(); + + // clear the test.url system property as the value leaks into the run when using + // different profiles + System.clearProperty("test.url"); + Map additional = new HashMap<>(); + + if (profile != null) { + + additional.putAll(profileInstance.getConfigOverrides()); + if (!profileInstance.getEnabledAlternatives() + .isEmpty()) { + additional.put("quarkus.arc.selected-alternatives", + profileInstance.getEnabledAlternatives() + .stream() + .peek((c) -> { + if (!c.isAnnotationPresent(Alternative.class)) { + throw new RuntimeException( + "Enabled alternative " + c + " is not " + + "annotated with @Alternative"); + } + }) + .map(Class::getName) + .collect(Collectors.joining(","))); + } + if (profileInstance.disableApplicationLifecycleObservers()) { + additional.put("quarkus.arc.test.disable-application-lifecycle-observers", "true"); + } + if (profileInstance.getConfigProfile() != null) { + additional.put(LaunchMode.TEST.getProfileKey(), + profileInstance.getConfigProfile()); + } + //we just use system properties for now + //it's a lot simpler + // TODO do we need this? shutdownTasks.add(RestorableSystemProperties.setProperties(additional)::close); + } + + curatedApplication = QuarkusBootstrap.builder() + //.setExistingModel(gradleAppModel) TODO is this needed? + // unfortunately this model is not re-usable due + // to PathTree serialization by Gradle + .setIsolateDeployment(true) + .setMode(QuarkusBootstrap.Mode.TEST) + .setTest(true) + .setTargetDirectory( + PathTestHelper.getProjectBuildDir( + projectRoot, testClassLocation)) + .setProjectRoot(projectRoot) + .setApplicationRoot(rootBuilder.build()) + .setAuxiliaryApplication(isContinuousTesting) // TODO should be conditional on the launch mode? do not set to true for normal holly addition are we sure this is safe to do here? it's not done in what we copied from? will this work with mvn verify? this guards instrumenting the classes with tracing to decide what to hot reload + + .build() + .bootstrap(); + shutdownTasks.add(curatedApplication::close); + } + + System.out.println("HOLLY made it to the other side of curated application creation "); + System.out.println("HOLLY app model is " + curatedApplication.getApplicationModel()); + + 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."); + } + + //TODO is this needed? Or could we take advantage of this more? + // Index testClassesIndex = TestClassIndexer.indexTestClasses(testClassLocation); + // // we need to write the Index to make it reusable from other parts of the testing + // // infrastructure that run in different ClassLoaders + // TestClassIndexer.writeIndex(testClassesIndex, testClassLocation, requiredTestClass); + + // TODO get used to be ok, now it needs to be get or create, what *isn't* happening earlier on the lifecycle? + Timing.staticInitStarted(curatedApplication.getOrCreateBaseRuntimeClassLoader(), + curatedApplication.getQuarkusBootstrap() + .isAuxiliaryApplication()); + System.out.println("HOLLY did timing init started"); + System.out.println("HOLLY test class location is " + testClassLocation); + final Map props = new HashMap<>(); + props.put(TEST_LOCATION, testClassLocation); + // TODO surely someone reads this? props.put(TEST_CLASS, requiredTestClass); + // TODO what's going on here with the profile? + Class quarkusTestProfile = profile; + PrepareResult result = new PrepareResult(curatedApplication + .createAugmentor(TestBuildChainFunction.class.getName(), + props), + profileInstance, + curatedApplication, testClassLocation); + // TODO this could be cleaner + // TODO do we need this? StartupAction.storeTestClassLocation(result.testClassLocation); + return result; + } + + // TODO surely there's a cleaner way to see if it's continuous testing? + // TODO should we be doing something with these unused arguments? + // Note that curated application cannot be re-used between restarts, so this application + // should have been freshly created + // TODO maybe don't even accept one? + public QuarkusClassLoader getStartupAction(Class testClass, CuratedApplication curatedApplication, + boolean isContinuousTesting, Class ignoredProfile) + throws Exception { + + Class profile = ignoredProfile; + // TODO do we want any of these? + Collection shutdownTasks = new HashSet(); + // TODO work out a good display name + PrepareResult result = createAugmentor(testClass, "(QuarkusTest)", curatedApplication, profile, shutdownTasks); + AugmentAction augmentAction = result.augmentAction; + QuarkusTestProfile profileInstance = result.profileInstance; + + testHttpEndpointProviders = TestHttpEndpointProvider.load(); + + System.out.println("HOLLY about to make app for " + testClass); + StartupAction startupAction = augmentAction.createInitialRuntimeApplication(); + + // TODO this seems to be safe to do because the classloaders are the same + // TODO not doing it startupAction.store(); + System.out.println("HOLLY did store " + startupAction); + return (QuarkusClassLoader) startupAction.getClassLoader(); + + } + + // public QuarkusClassLoader doJavaStart(PathList location, CuratedApplication curatedApplication, boolean isContinuousTesting) + // throws Exception { + // Class profile = null; + // // TODO do we want any of these? + // Collection shutdownTasks = new HashSet(); + // // TODO clearly passing null is not really ideal + // PrepareResult result = createAugmentor(curatedApplication, location, null, profile, shutdownTasks, + // isContinuousTesting); + // AugmentAction augmentAction = result.augmentAction; + // QuarkusTestProfile profileInstance = result.profileInstance; + // + // testHttpEndpointProviders = TestHttpEndpointProvider.load(); + // System.out.println( + // "CORE MAKER SEES CLASS OF STARTUP " + StartupAction.class.getClassLoader()); + // + // System.out.println("HOLLY about to make app for " + location); + // StartupAction startupAction = augmentAction.createInitialRuntimeApplication(); + // // TODO this seems to be safe to do because the classloaders are the same + // // TODO not doing it startupAction.store(); + // System.out.println("HOLLY did store " + startupAction); + // return (QuarkusClassLoader) startupAction.getClassLoader(); + // + // } + + // TODO can we defer this and move it back to the junit5 module? + public static class TestBuildChainFunction implements Function, List>> { + + @Override + public List> apply(Map stringObjectMap) { + System.out.println("HOLLY in apply"); + Path testLocation = (Path) stringObjectMap.get(TEST_LOCATION); + System.out.println("HOLLY path is " + testLocation); + // the index was written by the extension + Class testClass = (Class) stringObjectMap.get(TEST_CLASS); + // TODO is this at all safe? + + Index testClassesIndex; + if (testClass != null) { + testClassesIndex = TestClassIndexer.readIndex(testLocation, testClass); + } else { + testClassesIndex = TestClassIndexer.readIndex(testLocation); + } + List> allCustomizers = new ArrayList<>(1); + Consumer defaultCustomizer = new Consumer() { + + @Override + public void accept(BuildChainBuilder buildChainBuilder) { + buildChainBuilder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + context.produce(new TestClassPredicateBuildItem(new Predicate() { + @Override + public boolean test(String className) { + return PathTestHelper.isTestClass(className, + Thread.currentThread().getContextClassLoader(), testLocation); + } + })); + } + }).produces(TestClassPredicateBuildItem.class) + .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 s) { + QuarkusClassLoader cl = (QuarkusClassLoader) Thread.currentThread() + .getContextClassLoader(); + //if the class file is present in this (and not the parent) CL then it is an application class + List res = cl + .getElementsWithResource(s.replace(".", "/") + ".class", true); + return !res.isEmpty(); + } + })); + } + }).produces(ApplicationClassPredicateBuildItem.class).build(); + buildChainBuilder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + // TODO leaking of knowledge from junit5 to core + context.produce(new TestAnnotationBuildItem("io.quarkus.test.junit.QuarkusTest")); + } + }).produces(TestAnnotationBuildItem.class) + .build(); + + List testClassBeans = new ArrayList<>(); + + List extendWith = testClassesIndex + .getAnnotations(DotNames.EXTEND_WITH); + for (AnnotationInstance annotationInstance : extendWith) { + if (annotationInstance.target().kind() != AnnotationTarget.Kind.CLASS) { + continue; + } + ClassInfo classInfo = annotationInstance.target().asClass(); + if (classInfo.isAnnotation()) { + continue; + } + Type[] extendsWithTypes = annotationInstance.value().asClassArray(); + for (Type type : extendsWithTypes) { + if (DotNames.QUARKUS_TEST_EXTENSION.equals(type.name())) { + testClassBeans.add(classInfo.name().toString()); + } + } + } + + List registerExtension = testClassesIndex.getAnnotations(DotNames.REGISTER_EXTENSION); + for (AnnotationInstance annotationInstance : registerExtension) { + if (annotationInstance.target().kind() != AnnotationTarget.Kind.FIELD) { + continue; + } + FieldInfo fieldInfo = annotationInstance.target().asField(); + if (DotNames.QUARKUS_TEST_EXTENSION.equals(fieldInfo.type().name())) { + testClassBeans.add(fieldInfo.declaringClass().name().toString()); + } + } + + if (!testClassBeans.isEmpty()) { + buildChainBuilder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + for (String quarkusExtendWithTestClass : testClassBeans) { + context.produce(new TestClassBeanBuildItem(quarkusExtendWithTestClass)); + } + } + }).produces(TestClassBeanBuildItem.class) + .build(); + } + + } + }; + allCustomizers.add(defaultCustomizer); + + // TODO disabled, to avoid dependency issues + // give other extensions the ability to customize the build chain + // for (TestBuildChainCustomizerProducer testBuildChainCustomizerProducer : ServiceLoader + // .load(TestBuildChainCustomizerProducer.class, this.getClass().getClassLoader())) { + // allCustomizers.add(testBuildChainCustomizerProducer.produce(testClassesIndex)); + // } + + System.out.println("HOLLY done apply"); + return allCustomizers; + } + } + +} 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 deleted file mode 100644 index 67c405779b8f33..00000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/CurrentTestApplication.java +++ /dev/null @@ -1,17 +0,0 @@ -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 - */ -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/DotNames.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/DotNames.java new file mode 100644 index 00000000000000..9a09e0c5b65497 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/DotNames.java @@ -0,0 +1,16 @@ +package io.quarkus.deployment.dev.testing; + +import org.jboss.jandex.DotName; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; + +public final class DotNames { + + private DotNames() { + } + + public static final DotName EXTEND_WITH = DotName.createSimple(ExtendWith.class.getName()); + public static final DotName REGISTER_EXTENSION = DotName.createSimple(RegisterExtension.class.getName()); + // TODO this leaks knowledge of the junit5 module into this module + public static final DotName QUARKUS_TEST_EXTENSION = DotName.createSimple("io.quarkus.test.junit.QuarkusTextExtension"); +} 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 new file mode 100644 index 00000000000000..31e240d884726b --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/FacadeClassLoader.java @@ -0,0 +1,338 @@ +package io.quarkus.deployment.dev.testing; + +import java.io.Closeable; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; + +import org.jboss.logging.Logger; + +import io.quarkus.bootstrap.app.CuratedApplication; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; +import io.quarkus.runtime.LaunchMode; + +/** + * JUnit has many interceptors and listeners, but it does not allow us to intercept test discovery in a fine-grained way that + * would allow us to swap the thread context classloader. + * Since we can't intercept with a JUnit hook, we hijack from inside the classloader. + * + * We need to load all our test classes in one go, during the discovery phase, before we start the applications. + * We may need several applications and therefore, several classloaders, depending on what profiles are set. + * To solve that, we prepare the applications, to get classloaders, and file them here. + */ +public class FacadeClassLoader extends ClassLoader implements Closeable { + private static final Logger log = Logger.getLogger(io.quarkus.bootstrap.classloading.QuarkusClassLoader.class); + private static final Logger lifecycleLog = Logger + .getLogger(io.quarkus.bootstrap.classloading.QuarkusClassLoader.class.getName() + ".lifecycle"); + private static final boolean LOG_ACCESS_TO_CLOSED_CLASS_LOADERS = Boolean + .getBoolean("quarkus-log-access-to-closed-class-loaders"); + + private static final byte STATUS_OPEN = 1; + private static final byte STATUS_CLOSING = 0; + private static final byte STATUS_CLOSED = -1; + + protected static final String META_INF_SERVICES = "META-INF/services/"; + protected static final String JAVA = "java."; + + private String name = "FacadeLoader"; + // TODO it would be nice, and maybe theoretically possible, to re-use the curated application? + // TODO and if we don't, how do we get a re-usable deployment classloader? + + // TODO does this need to be a thread safe maps? + private final Map curatedApplications = new HashMap<>(); + private final Map runtimeClassLoaders = new HashMap<>(); + private final ClassLoader parent; + + /* + * It seems kind of wasteful to load every class twice; that's true, but it's been the case (by a different mechanism) + * ever since Quarkus 1.2 and the move to isolated classloaders, because the test extension would reload classes into the + * runtime classloader. + * In the future, https://openjdk.org/jeps/466 would allow us to avoid inspecting the classes to avoid a double load in the + * delegating + * classloader + * // TODO should we use the canary loader, or the parent loader? + * //If we use the parent loader, does that stop the quarkus classloaders getting a crack at some classes? + */ + private final ClassLoader canaryLoader; + // TODO better mechanism; every QuarkusMainTest gets its own application + private int mainC = 0; + + public FacadeClassLoader(ClassLoader parent) { + // TODO in dev mode, sometimes this is the deployment classloader, which doesn't seem right? + System.out.println("HOLLY facade parent is " + parent); + this.parent = parent; + String classPath = System.getProperty("java.class.path"); + // This manipulation is needed to work in IDEs + URL[] urls = Arrays.stream(classPath.split(":")).map(s -> { + try { + //TODO what if it's not a file? + String spec = "file://" + s; + + if (!spec.endsWith("jar") && !spec.endsWith("/")) { + spec = spec + "/"; + } + // System.out.println("HOLLY added " + new URL(spec) + new URL(spec).openStream().available()); + return new URL(spec); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + }).toArray(URL[]::new); + // System.out.println("HOLLY my classpath is " + Arrays.toString(urls)); + //System.out.println("HOLLY their classpath is " + Arrays.toString(urls)); + + canaryLoader = new URLClassLoader(urls, null); + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + System.out.println("HOLLY loading " + name); + boolean isQuarkusTest = false; + // TODO hack that didn't even work + // if (runtimeClassLoader != null && name.contains("QuarkusTestProfileAwareClass")) { + // return runtimeClassLoader.loadClass(name); + // } else if (name.contains("QuarkusTestProfileAwareClass")) { + // return this.getClass().getClassLoader().loadClass(name); + // + // } + try { + Class fromParent = canaryLoader.loadClass(name); + System.out.println("HOLLY canary gave " + fromParent.getClassLoader()); + + // TODO want to exclude quarkus component test, but include quarkusmaintest - what about quarkusunittest? and quarkusintegrationtest? + // TODO knowledge of test annotations leaking in to here - should we have a superclass that lives in this package that we check for? + // TODO be tighter with the names we check for + // TODO this would be way easier if this was in the same module as the profile, could just do clazz.getAnnotation(TestProfile.class) + isQuarkusTest = Arrays.stream(fromParent.getAnnotations()) + .anyMatch(annotation -> annotation.annotationType().getName().endsWith("QuarkusTest")); + boolean isMainTest = Arrays.stream(fromParent.getAnnotations()) + .anyMatch(annotation -> annotation.annotationType().getName().endsWith("QuarkusMainTest")); + Optional profileAnnotation = Arrays.stream(fromParent.getAnnotations()) + .filter(annotation -> annotation.annotationType().getName().endsWith("TestProfile")).findFirst(); + String profileName = "no-profile"; + Class profile = null; + if (profileAnnotation.isPresent()) { + + System.out.println("HOLLY got an annotation! " + profileAnnotation.get()); + // TODO could do getAnnotationsByType if we were in the same module + Method m = profileAnnotation.get().getClass().getMethod("value"); + profile = (Class) m.invoke(profileAnnotation.get()); // TODO extends quarkustestprofile + System.out.println("HOLLY profile is " + profile); + profileName = profile.getName(); + } + + // increment the key unconditionally, we just need uniqueness + mainC++; + + String key = isQuarkusTest ? "QuarkusTest" + "-" + profileName : isMainTest ? "MainTest" + mainC : "vanilla"; + // TODO do we need to do extra work to make sure all of the quarkus app is in the cp? We'll return versions from the parent otherwise + // TODO think we need to make a 'first' runtime cl, and then switch for each new test? + // TODO how do we decide what to load with our classloader - everything? + // 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 + + System.out.println("HOLLY key 2 " + key); + if (isQuarkusTest || isMainTest) { + System.out.println("HOLLY attempting to load " + name); + QuarkusClassLoader runtimeClassLoader = getQuarkusClassLoader(key, fromParent, profile); + System.out.println("the rc parent is " + runtimeClassLoader.getParent()); + System.out.println("the grand- parent is " + runtimeClassLoader.getParent().getParent()); + Class thing = runtimeClassLoader.loadClass(name); + System.out.println("HOLLY did load " + thing); + return thing; + } else { + System.out.println("HOLLY sending to " + super.getName()); + return super.loadClass(name); + } + } catch (ClassNotFoundException e) { + System.out.println("Could not load with the canary " + name); + return super.loadClass(name); + } catch (NoSuchMethodException e) { + System.out.println("Could not load with the canary " + e); + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + System.out.println("Could not load with the canary " + e); + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + System.out.println("Could not load with the canary " + e); + throw new RuntimeException(e); + } + } + + private QuarkusClassLoader getQuarkusClassLoader(String key, Class requiredTestClass, Class profile) { + //TODO need to check if we should make a new one + // TODO how do we know how to stop them?? - compare the classloader and see if it changed + // we reload the test resources if we changed test class and the new test class is not a nested class, and if we had or will have per-test test resources + // boolean reloadTestResources = isNewTestClass && (hasPerTestResources || hasPerTestResources(extensionContext)); + // if ((state == null && !failedBoot)) { // TODO never reload, as it will not work || wrongProfile || reloadTestResources) { + // TODO diagnostic + profile = null; + QuarkusClassLoader runtimeClassLoader = runtimeClassLoaders.get(key); + if (runtimeClassLoader == null) { + try { + runtimeClassLoader = makeClassLoader(key, requiredTestClass, profile); + runtimeClassLoaders.put(key, runtimeClassLoader); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return runtimeClassLoader; + } + + private QuarkusClassLoader makeClassLoader(String key, Class requiredTestClass, Class profile) throws Exception { + + // This interception is only actually needed in limited circumstances; when + // - running in normal mode + // - *and* there is a @QuarkusTest to run + + // This class sets a Thead Context Classloader, which JUnit uses to load classes. + // However, in continuous testing mode, setting a TCCL here isn't sufficient for the + // tests to come in with our desired classloader; + // downstream code sets the classloader to the deployment classloader, so we then need + // to come in *after* that code. + + // TODO sometimes this is called in dev mode and sometimes it isn't? Ah, it's only not + // called if we die early, before we get to this + + // In continuous testing mode, the runner code will have executed before this + // interceptor, so + // this interceptor doesn't need to do anything. + // TODO what if we removed the changes in the runner code? + + // Bypass all this in continuous testing mode, where the custom runner will have already initialised things before we hit this class; the startup action holder is our best way + // of detecting it + + // TODO alternate way of detecting it ? Needs the build item, though + // TODO could the extension pass this through to us? no, I think we're invoked before anything quarkusy, and junit5 isn't even an extension + // DevModeType devModeType = launchModeBuildItem.getDevModeType().orElse(null); + // if (devModeType == null || !devModeType.isContinuousTestingSupported()) { + // return; + // } + + // Some places do this, but that assumes we already have a classloader! boolean isContinuousTesting = testClassClassLoader instanceof QuarkusClassLoader; + + Thread currentThread = Thread.currentThread(); + ClassLoader originalClassLoader = currentThread.getContextClassLoader(); + + System.out.println("HOLLY before launch mode is " + LaunchMode.current()); + // System.out.println("HOLLY other way us " + ConfigProvider.getConfig() + // .unwrap(SmallRyeConfig.class) + // .getProfiles()); + + System.out.println("HOLLY interceipt original" + originalClassLoader); + AppMakerHelper appMakerHelper = new AppMakerHelper(); + + CuratedApplication curatedApplication = curatedApplications.get(key); + + if (curatedApplication == null) { + Collection shutdownTasks = new HashSet(); + + String displayName = "JUnit" + key; // TODO come up with a good display name + curatedApplication = appMakerHelper.makeCuratedApplication(requiredTestClass, displayName, shutdownTasks); + curatedApplications.put(key, curatedApplication); + } + + // System.out.println("MAKING Curated application with root " + applicationRoot); + // + // System.out.println("An alternate root we couuld do is " + projectRoot); + // + // curatedApplication = QuarkusBootstrap.builder() + // //.setExistingModel(gradleAppModel) + // // unfortunately this model is not + // // re-usable + // // due + // // to PathTree serialization by Gradle + // .setIsolateDeployment(true) + // .setMode( + // QuarkusBootstrap.Mode.TEST) // + // // Even in continuous testing, we set + // // the mode to test - here, if we go + // // down this path we know it's normal mode + // // is this always right? + // .setTest(true) + // .setApplicationRoot(applicationRoot) + // + // // .setTargetDirectory( + // // PathTestHelper + // // .getProjectBuildDir( + // // projectRoot, testClassLocation)) + // .setProjectRoot(projectRoot) + // // .setApplicationRoot(rootBuilder.build()) + // .build() + // .bootstrap(); + + // QuarkusClassLoader tcl = curatedApplication + // .createDeploymentClassLoader(); + // System.out.println("HOLLY interceptor just made a " + + // tcl); + + // TODO should we set the context classloader to the deployment classloader? + // If not, how will anyone retrieve it? + // TODO commenting this out doesn't change much? + // Consumer currentTestAppConsumer = (Consumer) tcl + // .loadClass(CurrentTestApplication.class.getName()) + // .getDeclaredConstructor().newInstance(); + // currentTestAppConsumer.accept(curatedApplication); + + // TODO move this to close shutdownTasks.add(curatedApplication::close); + + // var appModelFactory = curatedApplication.getQuarkusBootstrap() + // .newAppModelFactory(); + // appModelFactory.setBootstrapAppModelResolver(null); + // appModelFactory.setTest(true); + // appModelFactory.setLocalArtifacts(Set.of()); + // // TODO if (!mainModule) { + // // appModelFactory.setAppArtifact(null); + // appModelFactory.setProjectRoot(projectRoot); + // // } + + // To do this deserialization, we need to have an app root, so we can't use it to find the application model + + // final ApplicationModel testModel = appModelFactory.resolveAppModel() + // .getApplicationModel(); + // System.out.println("HOLLY test model is " + testModel); + // // System.out.println( + // // "module dir is " + Arrays.toString(testModel.getWorkspaceModules().toArray())); + // // System.out.println( + // // "module dir is " + ((WorkspaceModule) testModel.getWorkspaceModules().toArray()[0]).getModuleDir()); + // System.out.println( + // "app dir is " + testModel.getApplicationModule() + // .getModuleDir()); + // + // System.out.println("HOLLY after launch mode is " + LaunchMode.current()); + // final QuarkusBootstrap.Mode currentMode = curatedApplication.getQuarkusBootstrap() + // .getMode(); + // TODO are all these args used? + QuarkusClassLoader loader = appMakerHelper.getStartupAction(requiredTestClass, + curatedApplication, false, profile); + + // TODO is this a good idea? + // TODO without this, the parameter dev mode tests regress, but it feels kind of wrong - is there some use of TCCL in JUnitRunner we need to find + currentThread.setContextClassLoader(loader); + + System.out.println("HOLLY did make a " + currentThread.getContextClassLoader()); + return loader; + + } + + @Override + public String getName() { + return name; + } + + @Override + public void close() throws IOException { + + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java index d1b59bbb95935a..16f377261714d4 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java @@ -12,6 +12,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.Date; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; @@ -22,7 +23,6 @@ import java.util.Set; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -139,9 +139,7 @@ public Runnable prepare() { LogCapturingOutputFilter logHandler = new LogCapturingOutputFilter(testApplication, true, true, TestSupport.instance().get()::isDisplayTestOutput); Thread.currentThread().setContextClassLoader(tcl); - Consumer currentTestAppConsumer = (Consumer) tcl.loadClass(CurrentTestApplication.class.getName()) - .getDeclaredConstructor().newInstance(); - currentTestAppConsumer.accept(testApplication); + System.out.println("139 HOLLY junit runner set classloader to deployment" + tcl); Set allDiscoveredIds = new HashSet<>(); Set dynamicIds = new HashSet<>(); @@ -151,6 +149,10 @@ public Runnable prepare() { LauncherDiscoveryRequestBuilder launchBuilder = LauncherDiscoveryRequestBuilder.request() .selectors(quarkusTestClasses.testClasses.stream().map(DiscoverySelectors::selectClass) .collect(Collectors.toList())); + + System.out.println("HOLLY in prepare, launch is " + + quarkusTestClasses.testClasses.stream().map(DiscoverySelectors::selectClass) + .collect(Collectors.toList())); launchBuilder.filters(new PostDiscoveryFilter() { @Override public FilterResult apply(TestDescriptor testDescriptor) { @@ -159,6 +161,11 @@ public FilterResult apply(TestDescriptor testDescriptor) { } }); if (classScanResult != null) { + System.out.println("HOLLY class scan result is " + classScanResult); + System.out.println("HOLLY changed class names is " + + classScanResult.getChangedClassNames()); + System.out.println("HOLLY filtering is " + + testClassUsages.getTestsToRun(classScanResult.getChangedClassNames(), testState)); launchBuilder.filters(testClassUsages.getTestsToRun(classScanResult.getChangedClassNames(), testState)); } if (!includeTags.isEmpty()) { @@ -187,6 +194,7 @@ public FilterResult apply(TestDescriptor testDescriptor) { .build(); TestPlan testPlan = launcher.discover(request); long toRun = testPlan.countTestIdentifiers(TestIdentifier::isTest); + System.out.println("HOLLY to run is " + toRun); for (TestRunListener listener : listeners) { listener.runStarted(toRun); } @@ -223,6 +231,7 @@ public void quarkusStarting() { AtomicReference currentNonDynamicTest = new AtomicReference<>(); Thread.currentThread().setContextClassLoader(tcl); + System.out.println("224 HOLLY junit runner set classloader to " + tcl); launcher.execute(testPlan, new TestExecutionListener() { @Override @@ -243,6 +252,7 @@ public void executionStarted(TestIdentifier testIdentifier) { for (TestRunListener listener : listeners) { listener.testStarted(testIdentifier, testClassName); } + System.out.println("HOLLY runner pushing onto touched "); touchedClasses.push(Collections.synchronizedSet(new HashSet<>())); } @@ -254,6 +264,7 @@ public void executionSkipped(TestIdentifier testIdentifier, String reason) { touchedClasses.pop(); Class testClass = getTestClassFromSource(testIdentifier.getSource()); String displayName = getDisplayNameFromIdentifier(testIdentifier, testClass); + System.out.println("HOLLY skipping " + displayName); UniqueId id = UniqueId.parse(testIdentifier.getUniqueId()); if (testClass != null) { Map results = resultsByClass.computeIfAbsent(testClass.getName(), @@ -282,6 +293,10 @@ public void dynamicTestRegistered(TestIdentifier testIdentifier) { @Override public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { + System.out.println("execution finished, " + testExecutionResult); + if (testExecutionResult.getThrowable().isPresent()) { + testExecutionResult.getThrowable().get().printStackTrace(); + } if (aborted) { return; } @@ -289,6 +304,7 @@ public void executionFinished(TestIdentifier testIdentifier, Set touched = touchedClasses.pop(); Class testClass = getTestClassFromSource(testIdentifier.getSource()); String displayName = getDisplayNameFromIdentifier(testIdentifier, testClass); + System.out.println("execution finished display name was " + displayName); UniqueId id = UniqueId.parse(testIdentifier.getUniqueId()); if (testClass == null) { @@ -391,10 +407,10 @@ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry e } } finally { try { - currentTestAppConsumer.accept(null); TracingHandler.setTracingHandler(null); QuarkusConsole.removeOutputFilter(logHandler); Thread.currentThread().setContextClassLoader(old); + System.out.println("398 HOLLY junit runner set classloader to old " + old); tcl.close(); try { quarkusTestClasses.close(); @@ -403,6 +419,7 @@ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry e } } finally { Thread.currentThread().setContextClassLoader(origCl); + System.out.println("406 HOLLY junit runner set classloader to orig " + origCl); synchronized (JunitTestRunner.this) { testsRunning = false; if (aborted) { @@ -528,11 +545,14 @@ private Map toResultsMap( } private DiscoveryResult discoverTestClasses() { + System.out.println(new Date() + "533 HOLLY doing discovery"); //maven has a lot of rules around this and is configurable //for now this is out of scope, we are just going to do annotation based discovery //we will need to fix this sooner rather than later though //we also only run tests from the current module, which we can also revisit later + + // TODO consolidate logic here with facadeclassloader, which is trying to solve similar problems; maybe even share the canary loader class? Indexer indexer = new Indexer(); moduleInfo.getTest().ifPresent(test -> { try (Stream files = Files.walk(Paths.get(test.getClassesPath()))) { @@ -549,6 +569,8 @@ private DiscoveryResult discoverTestClasses() { }); Index index = indexer.complete(); + + System.out.println("HOLLY index found known classes " + Arrays.toString(index.getKnownClasses().toArray())); //we now have all the classes by name //these tests we never run Set integrationTestClasses = new HashSet<>(); @@ -598,6 +620,9 @@ private DiscoveryResult discoverTestClasses() { } } } + System.out.println("HOLLY all test classes is " + Arrays.toString(allTestClasses.toArray())); + System.out.println("HOLLY quarkus test classes is " + Arrays.toString(quarkusTestClasses.toArray())); + System.out.println("HOLLY integration classes is " + Arrays.toString(integrationTestClasses.toArray())); //now we have all the classes with @Test //figure out which ones we want to actually run Set unitTestClasses = new HashSet<>(); @@ -626,13 +651,42 @@ private DiscoveryResult discoverTestClasses() { List> itClasses = new ArrayList<>(); List> utClasses = new ArrayList<>(); + + // TODO batch by profile and start once for each profile + + // TODO guard to only do this once? is this guard sufficient? see "wrongprofile" in QuarkusTestExtension + + System.out.println( + "HOLLY after the re-add or whatever? quarkus test classes is " + Arrays.toString(quarkusTestClasses.toArray())); + ClassLoader rcl = null; + System.out.println("classload thread is " + Thread.currentThread()); + for (String i : quarkusTestClasses) { + ClassLoader old = Thread.currentThread().getContextClassLoader(); try { + if (rcl == null) { + System.out.println("HOLLY Making a java start with " + testApplication); + // Although it looks like we need to start once per class, the class is just indicative of where classes for this module live + + // CuratedApplications cannot (right now) be re-used between restarts. So even though the builder gave us a + // curated application, don't use it. + // TODO can we make the app re-usable, or otherwise leverage the app that we got passed in? + // TODO sort out profiles! + rcl = new AppMakerHelper() + .getStartupAction(Thread.currentThread().getContextClassLoader().loadClass(i), null, + true, null); + } + Thread.currentThread().setContextClassLoader(rcl); + + System.out.println("639 HOLLY loading quarkus test with " + Thread.currentThread().getContextClassLoader()); itClasses.add(Thread.currentThread().getContextClassLoader().loadClass(i)); - } catch (ClassNotFoundException e) { + } catch (Exception e) { + System.out.println("HOLLY BAD BAD" + e); log.warnf( "Failed to load test class %s (possibly as it was added after the test run started), it will not be executed this run.", i); + } finally { + Thread.currentThread().setContextClassLoader(old); } } itClasses.sort(Comparator.comparing(new Function, String>() { @@ -647,12 +701,15 @@ public String apply(Class aClass) { } })); QuarkusClassLoader cl = null; + System.out.println("HOLLY made unit test classes " + Arrays.toString(unitTestClasses.toArray())); if (!unitTestClasses.isEmpty()) { //we need to work the unit test magic //this is a lot more complex //we need to transform the classes to make the tracing magic work QuarkusClassLoader deploymentClassLoader = (QuarkusClassLoader) Thread.currentThread().getContextClassLoader(); + System.out.println("HOLLY asking classloader " + deploymentClassLoader); Set classesToTransform = new HashSet<>(deploymentClassLoader.getLocalClassNames()); + System.out.println("HOLLY to transform is " + Arrays.toString(classesToTransform.toArray())); Map transformedClasses = new HashMap<>(); for (String i : classesToTransform) { try { @@ -672,6 +729,7 @@ public String apply(Class aClass) { cl.reset(Collections.emptyMap(), transformedClasses); for (String i : unitTestClasses) { try { + System.out.println("678 HOLLY loaded " + i + " with loader " + cl); utClasses.add(cl.loadClass(i)); } catch (ClassNotFoundException exception) { log.warnf( @@ -681,6 +739,7 @@ public String apply(Class aClass) { } } + System.out.println("HOLLY test tyoe us " + testType); if (testType == TestType.ALL) { //run unit style tests first //before the quarkus tests have started @@ -688,6 +747,7 @@ public String apply(Class aClass) { List> ret = new ArrayList<>(utClasses.size() + itClasses.size()); ret.addAll(utClasses); ret.addAll(itClasses); + System.out.println("RETURNING " + Arrays.toString(ret.toArray())); return new DiscoveryResult(cl, ret); } else if (testType == TestType.UNIT) { return new DiscoveryResult(cl, utClasses); @@ -780,6 +840,7 @@ public Builder setTestType(TestType testType) { return this; } + // TODO we now ignore what gets set here and make our own, how to handle that? public Builder setTestApplication(CuratedApplication testApplication) { this.testApplication = testApplication; return this; diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/TestClassIndexer.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestClassIndexer.java similarity index 96% rename from test-framework/common/src/main/java/io/quarkus/test/common/TestClassIndexer.java rename to core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestClassIndexer.java index 59eaac4c893227..f8539c8af7c2a8 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/TestClassIndexer.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestClassIndexer.java @@ -1,4 +1,4 @@ -package io.quarkus.test.common; +package io.quarkus.deployment.dev.testing; import static io.quarkus.test.common.PathTestHelper.getTestClassesLocation; @@ -22,6 +22,7 @@ import org.jboss.jandex.UnsupportedVersion; import io.quarkus.fs.util.ZipUtils; +import io.quarkus.test.common.PathTestHelper; public final class TestClassIndexer { @@ -68,6 +69,10 @@ public static Index readIndex(Class testClass) { return readIndex(getTestClassesLocation(testClass), testClass); } + public static Index readIndex(Path testLocation) { + return indexTestClasses(testLocation); + } + public static Index readIndex(Path testClassLocation, Class testClass) { Path path = indexPath(testClassLocation, testClass); if (path.toFile().exists()) { diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/TestStatus.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestStatus.java similarity index 92% rename from test-framework/common/src/main/java/io/quarkus/test/common/TestStatus.java rename to core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestStatus.java index ebebe30eed0786..2c677bb6de63d2 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/TestStatus.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestStatus.java @@ -1,4 +1,4 @@ -package io.quarkus.test.common; +package io.quarkus.deployment.dev.testing; public class TestStatus { diff --git a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/RunningQuarkusApplicationImpl.java b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/RunningQuarkusApplicationImpl.java index 58d9740842aa2f..2e91be5942a650 100644 --- a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/RunningQuarkusApplicationImpl.java +++ b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/RunningQuarkusApplicationImpl.java @@ -41,21 +41,40 @@ public void close() throws Exception { @Override public Optional getConfigValue(String key, Class type) { - //the config is in an isolated CL - //we need to extract it via reflection - //this is pretty yuck, but I don't really see a solution - ClassLoader old = Thread.currentThread().getContextClassLoader(); - try { - Class configProviderClass = classLoader.loadClass(ConfigProvider.class.getName()); - Method getConfig = configProviderClass.getMethod("getConfig", ClassLoader.class); - Thread.currentThread().setContextClassLoader(classLoader); - Object config = getConfig.invoke(null, classLoader); - return (Optional) getConfig.getReturnType().getMethod("getOptionalValue", String.class, Class.class) - .invoke(config, key, type); - } catch (Exception e) { - throw new RuntimeException(e); - } finally { - Thread.currentThread().setContextClassLoader(old); + + if (false && new Exception().getStackTrace().length > 100) { + // TODO expensive, awkward + System.out.println("HOLLY averting infinite loop " + key); + new Exception().printStackTrace(); + return Optional.empty(); + } else { + ClassLoader old = Thread.currentThread() + .getContextClassLoader(); + try { + // TODO this infinite loops, check that assumption + // we are assuming here that the the classloader has been initialised with some kind of different provider that does not infinite loop. + Thread.currentThread() + .setContextClassLoader(classLoader); + if (classLoader == ConfigProvider.class.getClassLoader()) { + return ConfigProvider.getConfig(classLoader) + .getOptionalValue(key, type); + } else { + //the config is in an isolated CL + //we need to extract it via reflection + //this is pretty yuck, but I don't really see a solution + Class configProviderClass = classLoader.loadClass(ConfigProvider.class.getName()); + Method getConfig = configProviderClass.getMethod("getConfig", ClassLoader.class); + Object config = getConfig.invoke(null, classLoader); + return (Optional) getConfig.getReturnType() + .getMethod("getOptionalValue", String.class, Class.class) + .invoke(config, key, type); + } + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + Thread.currentThread() + .setContextClassLoader(old); + } } } diff --git a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/StartupActionImpl.java b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/StartupActionImpl.java index 4589a5d5407c79..b8d1a9cc886ea2 100644 --- a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/StartupActionImpl.java +++ b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/StartupActionImpl.java @@ -78,10 +78,14 @@ public StartupActionImpl(CuratedApplication curatedApplication, BuildResult buil } else { baseClassLoader.reset(extractGeneratedResources(buildResult, false), transformedClasses); - runtimeClassLoader = curatedApplication.createRuntimeClassLoader( + // TODO Need to do recreations in JUnitTestRunner for dev mode case + // TODO This is wrong, since it should be re-created for every startup + // TODO need to clear it on shutdown + runtimeClassLoader = curatedApplication.getOrCreateRuntimeClassLoader( resources, transformedClasses); } this.runtimeClassLoader = runtimeClassLoader; + runtimeClassLoader.setStartupAction(this); } /** @@ -183,6 +187,7 @@ public void addRuntimeCloseTask(Closeable closeTask) { } private void doClose() { + curatedApplication.tidy(); try { runtimeClassLoader.loadClass(Quarkus.class.getName()).getMethod("blockingExit").invoke(null); } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException @@ -281,6 +286,7 @@ public RunningQuarkusApplication run(String... args) throws Exception { //we have our class loaders ClassLoader old = Thread.currentThread().getContextClassLoader(); + System.out.println("HOLLY running about to trigger SC " + runtimeClassLoader); try { Thread.currentThread().setContextClassLoader(runtimeClassLoader); final String className = applicationClassName; diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/PathTestHelper.java b/core/deployment/src/main/java/io/quarkus/test/common/PathTestHelper.java similarity index 80% rename from test-framework/common/src/main/java/io/quarkus/test/common/PathTestHelper.java rename to core/deployment/src/main/java/io/quarkus/test/common/PathTestHelper.java index d28982f69a6537..9d786a1ed01d71 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/PathTestHelper.java +++ b/core/deployment/src/main/java/io/quarkus/test/common/PathTestHelper.java @@ -144,8 +144,22 @@ public static Path getTestClassesLocation(Class testClass) { } catch (MalformedURLException e) { throw new RuntimeException("Failed to resolve the location of the JAR containing " + testClass, e); } + } else if (resource.getProtocol().equals("quarkus")) { + // resources loaded in memory in the runtime classloader may have a quarkus: prefix + // TODO terrible hack, why was this not needed in earlier prototypes? maybe it only happens second time round? + Path projectRoot = Paths.get("") + .normalize() + .toAbsolutePath(); + Path applicationRoot = getTestClassLocationForRootLocation(projectRoot.toString()); + System.out.println("HOLLY dealinh with " + resource); + Path path = applicationRoot.resolve(classFileName); + System.out.println("HOLLY so made " + path); + path = path.getRoot().resolve(path.subpath(0, path.getNameCount() - Path.of(classFileName).getNameCount())); + // TODO should we check existence in the test dir-ness, like we do on the other path? + return path; } Path path = toPath(resource); + path = path.getRoot().resolve(path.subpath(0, path.getNameCount() - Path.of(classFileName).getNameCount())); if (!isInTestDir(resource) && !path.getParent().getFileName().toString().equals(TARGET)) { @@ -168,16 +182,17 @@ public static Path getTestClassesLocation(Class testClass) { * @return directory or JAR containing the application being tested by the test class */ public static Path getAppClassLocation(Class testClass) { - return getAppClassLocationForTestLocation(getTestClassesLocation(testClass).toString()); + return getAppClassLocationForTestLocation(getTestClassesLocation(testClass)); } /** * Resolves the directory or the JAR file containing the application being tested by a test from the given location. * - * @param testClassLocation the test class location + * @param testClassLocationPath the test class location * @return directory or JAR containing the application being tested by a test from the given location */ - public static Path getAppClassLocationForTestLocation(String testClassLocation) { + public static Path getAppClassLocationForTestLocation(Path testClassLocationPath) { + String testClassLocation = testClassLocationPath.toString(); if (testClassLocation.endsWith(".jar")) { if (testClassLocation.endsWith("-tests.jar")) { return Paths.get(new StringBuilder() @@ -300,4 +315,52 @@ public static Path getProjectBuildDir(Path projectRoot, Path testClassLocation) } return projectRoot.resolve(projectRoot.relativize(testClassLocation).getName(0)); } + + public static Path getTestClassLocationForRootLocation(String rootLocation) { + if (rootLocation.endsWith(".jar")) { + if (rootLocation.endsWith("-tests.jar")) { + return Paths.get(new StringBuilder() + .append(rootLocation, 0, rootLocation.length() - "-tests.jar".length()) + .append(".jar") + .toString()); + } + return Path.of(rootLocation); + } + Optional mainClassesDir = TEST_TO_MAIN_DIR_FRAGMENTS.keySet() + .stream() + .map(s -> Path.of( + (rootLocation + File.separator + s).replaceAll("//", "/")).normalize()) + .filter(path -> Files.exists(path)) + .findFirst(); + if (mainClassesDir.isPresent()) { + return mainClassesDir.get(); + } + + // TODO reduce duplicated code, check if we can get rid of some of the regexes + + mainClassesDir = TEST_TO_MAIN_DIR_FRAGMENTS.keySet() + .stream() + .map(s -> Path.of( + (rootLocation + File.separator + "target" + File.separator + s).replaceAll("//", "/")).normalize()) + .filter(path -> Files.exists(path)) + .findFirst(); + if (mainClassesDir.isPresent()) { + return mainClassesDir.get(); + } + + // Try the gradle build dir + mainClassesDir = TEST_TO_MAIN_DIR_FRAGMENTS.keySet() + .stream() + .map(s -> Path.of( + (rootLocation + File.separator + "build" + File.separator + s).replaceAll("//", "/")).normalize()) + .filter(path -> Files.exists(path)) + .findFirst(); + if (mainClassesDir.isPresent()) { + return mainClassesDir.get(); + } + + // TODO is it safe to throw or return null? are there other build systems we should be considering? + // throw new IllegalStateException("Unable to find any application content in " + rootLocation); + return null; + } } diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/RestorableSystemProperties.java b/core/deployment/src/main/java/io/quarkus/test/common/RestorableSystemProperties.java similarity index 100% rename from test-framework/common/src/main/java/io/quarkus/test/common/RestorableSystemProperties.java rename to core/deployment/src/main/java/io/quarkus/test/common/RestorableSystemProperties.java diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestProfile.java b/core/deployment/src/main/java/io/quarkus/test/junit/QuarkusTestProfile.java similarity index 76% rename from test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestProfile.java rename to core/deployment/src/main/java/io/quarkus/test/junit/QuarkusTestProfile.java index 46b11de971cf3d..db3ce6852af8c7 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestProfile.java +++ b/core/deployment/src/main/java/io/quarkus/test/junit/QuarkusTestProfile.java @@ -5,8 +5,6 @@ import java.util.Map; import java.util.Set; -import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; - /** * Defines a 'test profile'. Tests run under a test profile * will have different configuration options to other tests. @@ -47,11 +45,11 @@ default String getConfigProfile() { } /** - * Additional {@link QuarkusTestResourceLifecycleManager} classes (along with their init params) to be used from this + * Additional { QuarkusTestResourceLifecycleManager} classes (along with their init params) to be used from this * specific test profile. * - * If this method is not overridden, then only the {@link QuarkusTestResourceLifecycleManager} classes enabled via the - * {@link io.quarkus.test.common.WithTestResource} class + * If this method is not overridden, then only the { QuarkusTestResourceLifecycleManager} classes enabled via the + * { io.quarkus.test.common.WithTestResource} class * annotation will be used for the tests using this profile (which is the same behavior as tests that don't use a profile at * all). */ @@ -60,7 +58,7 @@ default List testResources() { } /** - * If this returns true then only the test resources returned from {@link #testResources()} will be started, + * If this returns true then only the test resources returned from { #testResources()} will be started, * global annotated test resources will be ignored. */ default boolean disableGlobalTestResources() { @@ -80,7 +78,7 @@ default Set tags() { /** * The command line parameters that are passed to the main method on startup. * - * This is ignored for {@link io.quarkus.test.junit.main.QuarkusMainTest}, which has its own way of passing parameters. + * This is ignored for { io.quarkus.test.junit.main.QuarkusMainTest}, which has its own way of passing parameters. */ default String[] commandLineParameters() { return new String[0]; @@ -89,7 +87,7 @@ default String[] commandLineParameters() { /** * If the main method should be run. * - * This is ignored for {@link io.quarkus.test.junit.main.QuarkusMainTest}, where the main method is always run. + * This is ignored for { io.quarkus.test.junit.main.QuarkusMainTest}, where the main method is always run. */ default boolean runMainMethod() { return false; @@ -104,26 +102,26 @@ default boolean disableApplicationLifecycleObservers() { } final class TestResourceEntry { - private final Class clazz; + private final Class clazz; //TODO was extends QuarkusTestResourceLifecycleManager but that class is inaccessible now private final Map args; private final boolean parallel; - public TestResourceEntry(Class clazz) { + public TestResourceEntry(Class clazz) { this(clazz, Collections.emptyMap()); } - public TestResourceEntry(Class clazz, Map args) { + public TestResourceEntry(Class clazz, Map args) { this(clazz, args, false); } - public TestResourceEntry(Class clazz, Map args, + public TestResourceEntry(Class clazz, Map args, boolean parallel) { this.clazz = clazz; this.args = args; this.parallel = parallel; } - public Class getClazz() { + public Class getClazz() { return clazz; } diff --git a/core/deployment/src/main/java/io/quarkus/test/junit/util/QuarkusTestProfileAwareClassOrderer.java b/core/deployment/src/main/java/io/quarkus/test/junit/util/QuarkusTestProfileAwareClassOrderer.java new file mode 100644 index 00000000000000..0631815699f368 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/test/junit/util/QuarkusTestProfileAwareClassOrderer.java @@ -0,0 +1,149 @@ +package io.quarkus.test.junit.util; + +import java.util.Comparator; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.junit.jupiter.api.ClassDescriptor; +import org.junit.jupiter.api.ClassOrderer; +import org.junit.jupiter.api.ClassOrdererContext; +import org.junit.jupiter.api.Nested; + +/** + * TODO copied code for experi,entatopn + */ +public class QuarkusTestProfileAwareClassOrderer implements ClassOrderer { + + protected static final String DEFAULT_ORDER_PREFIX_QUARKUS_TEST = "20_"; + protected static final String DEFAULT_ORDER_PREFIX_QUARKUS_TEST_WITH_PROFILE = "40_"; + protected static final String DEFAULT_ORDER_PREFIX_QUARKUS_TEST_WITH_RESTRICTED_RES = "45_"; + protected static final String DEFAULT_ORDER_PREFIX_NON_QUARKUS_TEST = "60_"; + + static final String CFGKEY_ORDER_PREFIX_QUARKUS_TEST = "junit.quarkus.orderer.prefix.quarkus-test"; + + static final String CFGKEY_ORDER_PREFIX_QUARKUS_TEST_WITH_PROFILE = "junit.quarkus.orderer.prefix.quarkus-test-with-profile"; + + static final String CFGKEY_ORDER_PREFIX_QUARKUS_TEST_WITH_RESTRICTED_RES = "junit.quarkus.orderer.prefix.quarkus-test-with-restricted-resource"; + + static final String CFGKEY_ORDER_PREFIX_NON_QUARKUS_TEST = "junit.quarkus.orderer.prefix.non-quarkus-test"; + + static final String CFGKEY_SECONDARY_ORDERER = "junit.quarkus.orderer.secondary-orderer"; + + @Override + public void orderClasses(ClassOrdererContext context) { + // don't do anything if there is just one test class or the current order request is for @Nested tests + if (context.getClassDescriptors().size() <= 1 || context.getClassDescriptors().get(0).isAnnotated(Nested.class)) { + return; + } + var prefixQuarkusTest = getConfigParam( + CFGKEY_ORDER_PREFIX_QUARKUS_TEST, + DEFAULT_ORDER_PREFIX_QUARKUS_TEST, + context); + var prefixQuarkusTestWithProfile = getConfigParam( + CFGKEY_ORDER_PREFIX_QUARKUS_TEST_WITH_PROFILE, + DEFAULT_ORDER_PREFIX_QUARKUS_TEST_WITH_PROFILE, + context); + var prefixQuarkusTestWithRestrictedResource = getConfigParam( + CFGKEY_ORDER_PREFIX_QUARKUS_TEST_WITH_RESTRICTED_RES, + DEFAULT_ORDER_PREFIX_QUARKUS_TEST_WITH_RESTRICTED_RES, + context); + var prefixNonQuarkusTest = getConfigParam( + CFGKEY_ORDER_PREFIX_NON_QUARKUS_TEST, + DEFAULT_ORDER_PREFIX_NON_QUARKUS_TEST, + context); + + // first pass: run secondary orderer first (!), which is easier than running it per "grouping" + buildSecondaryOrderer(context).orderClasses(context); + var classDecriptors = context.getClassDescriptors(); + var firstPassIndexMap = IntStream.range(0, classDecriptors.size()).boxed() + .collect(Collectors.toMap(classDecriptors::get, i -> String.format("%06d", i))); + + // second pass: apply the actual Quarkus aware ordering logic, using the first pass indices as order key suffixes + classDecriptors.sort(Comparator.comparing(classDescriptor -> { + var secondaryOrderSuffix = firstPassIndexMap.get(classDescriptor); + Optional customOrderKey = getCustomOrderKey(classDescriptor, context, secondaryOrderSuffix) + .or(() -> getCustomOrderKey(classDescriptor, context)); + if (customOrderKey.isPresent()) { + return customOrderKey.get(); + } + // if (classDescriptor.isAnnotated(QuarkusTest.class) + // || classDescriptor.isAnnotated(QuarkusIntegrationTest.class) + // || classDescriptor.isAnnotated(QuarkusMainTest.class)) { + // return classDescriptor.findAnnotation(TestProfile.class) + // .map(TestProfile::value) + // .map(profileClass -> prefixQuarkusTestWithProfile + profileClass.getName() + "@" + secondaryOrderSuffix) + // .orElseGet(() -> { + // var prefix = hasRestrictedResource(classDescriptor) + // ? prefixQuarkusTestWithRestrictedResource + // : prefixQuarkusTest; + // return prefix + secondaryOrderSuffix; + // }); + // } + return prefixNonQuarkusTest + secondaryOrderSuffix; + })); + } + + private String getConfigParam(String key, String fallbackValue, ClassOrdererContext context) { + return context.getConfigurationParameter(key).orElse(fallbackValue); + } + + private ClassOrderer buildSecondaryOrderer(ClassOrdererContext context) { + return Optional.ofNullable(getConfigParam(CFGKEY_SECONDARY_ORDERER, null, context)) + .map(fqcn -> { + try { + return (ClassOrderer) Class.forName(fqcn).getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + throw new IllegalArgumentException("Failed to instantiate " + fqcn, e); + } + }) + .orElseGet(ClassName::new); + } + + private boolean hasRestrictedResource(ClassDescriptor classDescriptor) { + return false; + // return classDescriptor.findRepeatableAnnotations(WithTestResource.class).stream() + // .anyMatch(res -> res.restrictToAnnotatedClass() || isMetaTestResource(res, classDescriptor)) || + // classDescriptor.findRepeatableAnnotations(QuarkusTestResource.class).stream() + // .anyMatch(res -> res.restrictToAnnotatedClass() || isMetaTestResource(res, classDescriptor)); + } + + // @Deprecated(forRemoval = true) + // private boolean isMetaTestResource(QuarkusTestResource resource, ClassDescriptor classDescriptor) { + //// return Arrays.stream(classDescriptor.getTestClass().getAnnotationsByType(QuarkusTestResource.class)) + //// .map(QuarkusTestResource::value) + //// .noneMatch(resource.value()::equals); + // } + // + // private boolean isMetaTestResource(WithTestResource resource, ClassDescriptor classDescriptor) { + // return Arrays.stream(classDescriptor.getTestClass().getAnnotationsByType(WithTestResource.class)) + // .map(WithTestResource::value) + // .noneMatch(resource.value()::equals); + // } + + /** + * Template method that provides an optional custom order key for the given {@code classDescriptor}. + * + * @param classDescriptor the respective test class + * @param context for config lookup + * @return optional custom order key for the given test class + * @deprecated use {@link #getCustomOrderKey(ClassDescriptor, ClassOrdererContext, String)} instead + */ + @Deprecated(forRemoval = true, since = "2.7.0.CR1") + protected Optional getCustomOrderKey(ClassDescriptor classDescriptor, ClassOrdererContext context) { + return Optional.empty(); + } + + /** + * Template method that provides an optional custom order key for the given {@code classDescriptor}. + * + * @param classDescriptor the respective test class + * @param context for config lookup + * @param secondaryOrderSuffix the secondary order suffix that was calculated by the secondary orderer + * @return optional custom order key for the given test class + */ + protected Optional getCustomOrderKey(ClassDescriptor classDescriptor, ClassOrdererContext context, + String secondaryOrderSuffix) { + return Optional.empty(); + } +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/StartupContext.java b/core/runtime/src/main/java/io/quarkus/runtime/StartupContext.java index a06f7f2063349a..f90ba43f382ba0 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/StartupContext.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/StartupContext.java @@ -26,6 +26,7 @@ public class StartupContext implements Closeable { private String currentBuildStepName; public StartupContext() { + System.out.println("HOLLY SC construcging startup context"); ShutdownContext shutdownContext = new ShutdownContext() { @Override public void addShutdownTask(Runnable runnable) { @@ -46,6 +47,7 @@ public void addLastShutdownTask(Runnable runnable) { } }; values.put(ShutdownContext.class.getName(), shutdownContext); + System.out.println("HOLLY SC put in cl " + ShutdownContext.class.getName() + " is + " + shutdownContext); values.put(RAW_COMMAND_LINE_ARGS, new Supplier() { @Override public String[] get() { @@ -67,15 +69,18 @@ public Object getValue(String name) { @Override public void close() { + System.out.println("HOLLY SC YO CLOSING"); runAllAndClear(shutdownTasks); runAllAndClear(lastShutdownTasks); values.clear(); } private void runAllAndClear(Deque tasks) { + System.out.println("HOLLY run all and clear"); while (!tasks.isEmpty()) { try { var runnable = tasks.remove(); + System.out.println("HOLLY wull run " + runnable); runnable.run(); } catch (Throwable ex) { LOG.error("Running a shutdown task failed", ex); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/test/TestHttpEndpointProvider.java b/core/runtime/src/main/java/io/quarkus/runtime/test/TestHttpEndpointProvider.java index e6c1bd6d07be95..214bfcc2c98171 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/test/TestHttpEndpointProvider.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/test/TestHttpEndpointProvider.java @@ -14,8 +14,13 @@ public interface TestHttpEndpointProvider { static List, String>> load() { List, String>> ret = new ArrayList<>(); + System.out.println("HOLLY wull load " + TestHttpEndpointProvider.class.getClassLoader() + " and tccl " + + Thread.currentThread().getContextClassLoader()); + + ClassLoader targetclassloader = TestHttpEndpointProvider.class + .getClassLoader(); // Thread.currentThread().getContextClassLoader(); for (TestHttpEndpointProvider i : ServiceLoader.load(TestHttpEndpointProvider.class, - Thread.currentThread().getContextClassLoader())) { + targetclassloader)) { ret.add(i.endpointProvider()); } return ret; diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/BootstrapConstants.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/BootstrapConstants.java index 6c2aa72680a476..5170ccfca7819b 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/BootstrapConstants.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/BootstrapConstants.java @@ -18,6 +18,9 @@ public interface BootstrapConstants { */ String TEST_TO_MAIN_MAPPINGS = "TEST_TO_MAIN_MAPPINGS"; + // Added because OUTPUT_SOURCES_DIR does not include additional sources and we sometimes (always?) need them + String ALL_OUTPUT_SOURCES_DIR = "ALL_OUTPUT_SOURCES_DIR"; + String OUTPUT_SOURCES_DIR = "OUTPUT_SOURCES_DIR"; @Deprecated diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java index 7b66d6c8564135..5af207ae4a42e2 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java @@ -61,6 +61,9 @@ public class CuratedApplication implements Serializable, AutoCloseable { */ private volatile QuarkusClassLoader baseRuntimeClassLoader; + // TODO this probably isn't the right place to store this + private volatile QuarkusClassLoader runtimeClassLoader; + private final QuarkusBootstrap quarkusBootstrap; private final CurationResult curationResult; private final ConfiguredClassLoading configuredClassLoading; @@ -114,13 +117,17 @@ public AugmentAction createAugmentor() { * which is used to generate a list of build chain customisers to control the build. */ public AugmentAction createAugmentor(String functionName, Map props) { + System.out.println("HOLLY creating augmentor: " + functionName); try { Class augmentor = getOrCreateAugmentClassLoader().loadClass(AUGMENTOR); + System.out.println("HOLLY got cl: "); Function> function = (Function>) getOrCreateAugmentClassLoader() .loadClass(functionName) .getDeclaredConstructor() .newInstance(); + System.out.println("HOLLY got function r " + function + props.values()); List res = function.apply(props); + System.out.println("HOLLY got res: " + res); return (AugmentAction) augmentor.getConstructor(CuratedApplication.class, List.class).newInstance(this, res); } catch (Exception e) { throw new RuntimeException(e); @@ -191,6 +198,7 @@ private void addCpElement(QuarkusClassLoader.Builder builder, ResolvedDependency public synchronized QuarkusClassLoader getOrCreateAugmentClassLoader() { if (augmentClassLoader == null) { + System.out.println("HOLLY making augment classloader " + augmentClassLoader); //first run, we need to build all the class loaders QuarkusClassLoader.Builder builder = QuarkusClassLoader.builder( "Augmentation Class Loader: " + quarkusBootstrap.getMode() + getClassLoaderNameSuffix(), @@ -244,6 +252,8 @@ public QuarkusClassLoader getAugmentClassLoader() { * */ public synchronized QuarkusClassLoader getOrCreateBaseRuntimeClassLoader() { + System.out.println("HOLLY will get or create base runtime " + baseRuntimeClassLoader); + System.out.println("HOLLY root is " + quarkusBootstrap.getApplicationRoot()); if (baseRuntimeClassLoader == null) { QuarkusClassLoader.Builder builder = QuarkusClassLoader.builder( "Quarkus Base Runtime ClassLoader: " + quarkusBootstrap.getMode() + getClassLoaderNameSuffix(), @@ -384,7 +394,9 @@ public QuarkusClassLoader createRuntimeClassLoader(ClassLoader base, Map resources, + Map transformedClasses) { + if (runtimeClassLoader == null) { + runtimeClassLoader = createRuntimeClassLoader(resources, transformedClasses); + } + return runtimeClassLoader; + } + + // TODO delete this? the model doesn't really work? + public void tidy() { + // this.runtimeClassLoader = null; + } + /** * TODO: Fix everything in the universe to do loading properly * 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 016dbdd191b44b..3cd4eb2f7ef28b 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 @@ -30,6 +30,9 @@ import org.jboss.logging.Logger; +import io.quarkus.bootstrap.app.CuratedApplication; +import io.quarkus.bootstrap.app.StartupAction; + /** * The ClassLoader used for non production Quarkus applications (i.e. dev and test mode). */ @@ -46,6 +49,9 @@ public class QuarkusClassLoader extends ClassLoader implements Closeable { protected static final String META_INF_SERVICES = "META-INF/services/"; protected static final String JAVA = "java."; + private final CuratedApplication curatedApplication; + private StartupAction startupAction; + static { registerAsParallelCapable(); } @@ -159,6 +165,7 @@ private QuarkusClassLoader(Builder builder) { this.aggregateParentResources = builder.aggregateParentResources; this.classLoaderEventListeners = builder.classLoaderEventListeners.isEmpty() ? Collections.emptyList() : builder.classLoaderEventListeners; + this.curatedApplication = builder.curatedApplication; setDefaultAssertionStatus(builder.assertionsEnabled); if (lifecycleLog.isDebugEnabled()) { @@ -543,6 +550,9 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE } ClassPathElement[] resource = state.loadableResources.get(resourceName); if (resource != null) { + if (name.contains("org.acme")) { + System.out.println("checking " + resource[0].getRoot() + " for " + resourceName); + } ClassPathElement classPathElement = resource[0]; ClassPathResource classPathElementResource = classPathElement.getResource(resourceName); if (classPathElementResource != null) { //can happen if the class loader was closed @@ -557,9 +567,13 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE } } + if (name.contains("org.acme")) { + System.out.println("HOLLY ok, problem " + this + " can't find " + name); + } if (!parentFirst) { return parent.loadClass(name); } + throw new ClassNotFoundException(name); } @@ -729,11 +743,23 @@ private void ensureOpen() { } } + public CuratedApplication getCuratedApplication() { + return curatedApplication; + } + @Override public String toString() { return "QuarkusClassLoader:" + name + "@" + Integer.toHexString(hashCode()); } + public StartupAction getStartupAction() { + return startupAction; + } + + public void setStartupAction(StartupAction startupAction) { + this.startupAction = startupAction; + } + public static class Builder { final String name; final ClassLoader parent; @@ -742,6 +768,7 @@ public static class Builder { final List parentFirstElements = new ArrayList<>(); final List lesserPriorityElements = new ArrayList<>(); final boolean parentFirst; + CuratedApplication curatedApplication; MemoryClassPathElement resettableElement; private Map transformedClasses = Collections.emptyMap(); boolean aggregateParentResources; @@ -867,6 +894,11 @@ public Builder addClassLoaderEventListeners(List class return this; } + public Builder setCuratedApplication(CuratedApplication curatedApplication) { + this.curatedApplication = curatedApplication; + return this; + } + /** * Builds the class loader * @@ -888,7 +920,7 @@ public ClassLoader parent() { return parent; } - static final class ClassLoaderState { + public static final class ClassLoaderState { final Map loadableResources; final Set bannedResources; diff --git a/integration-tests/gradle/src/main/resources/inject-bean-from-test-config/library/src/test/java/org/acme/LibraryTestResource.java b/integration-tests/gradle/src/main/resources/inject-bean-from-test-config/library/src/test/java/org/acme/LibraryTestResource.java index f03e1a6dabb242..27d706039f15f7 100644 --- a/integration-tests/gradle/src/main/resources/inject-bean-from-test-config/library/src/test/java/org/acme/LibraryTestResource.java +++ b/integration-tests/gradle/src/main/resources/inject-bean-from-test-config/library/src/test/java/org/acme/LibraryTestResource.java @@ -4,7 +4,7 @@ import java.util.Collections; import java.util.Map; -public class LibraryTestResource implements QuarkusTestResourceLifecycleManager { +public class LibraryTestResource implements QuarkusTestResourceLifecycleManager { @Override public Map start() { @@ -12,5 +12,6 @@ public Map start() { } @Override - public void stop() {} + public void stop() { + } } \ No newline at end of file diff --git a/integration-tests/hibernate-validator-resteasy-reactive/src/test/java/io/quarkus/it/hibernate/validator/ConfigMappingStartupValidatorTest.java b/integration-tests/hibernate-validator-resteasy-reactive/src/test/java/io/quarkus/it/hibernate/validator/ConfigMappingStartupValidatorTest.java index 16652a5a9c2131..6b55822f7a87bb 100644 --- a/integration-tests/hibernate-validator-resteasy-reactive/src/test/java/io/quarkus/it/hibernate/validator/ConfigMappingStartupValidatorTest.java +++ b/integration-tests/hibernate-validator-resteasy-reactive/src/test/java/io/quarkus/it/hibernate/validator/ConfigMappingStartupValidatorTest.java @@ -8,9 +8,6 @@ import java.util.Map; import java.util.Set; -import jakarta.inject.Inject; -import jakarta.validation.constraints.Pattern; - import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -29,6 +26,8 @@ import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigBuilder; import io.smallrye.config.WithDefault; +import jakarta.inject.Inject; +import jakarta.validation.constraints.Pattern; @QuarkusTest @TestProfile(ConfigMappingStartupValidatorTest.Profile.class) diff --git a/integration-tests/test-extension/extension-that-defines-junit-test-extensions/deployment/src/main/java/io/quarkiverse/acme/deployment/AnnotationAdjuster.java b/integration-tests/test-extension/extension-that-defines-junit-test-extensions/deployment/src/main/java/io/quarkiverse/acme/deployment/AnnotationAdjuster.java index 5d4e93c4a9d632..0f0f9786a8ad42 100644 --- a/integration-tests/test-extension/extension-that-defines-junit-test-extensions/deployment/src/main/java/io/quarkiverse/acme/deployment/AnnotationAdjuster.java +++ b/integration-tests/test-extension/extension-that-defines-junit-test-extensions/deployment/src/main/java/io/quarkiverse/acme/deployment/AnnotationAdjuster.java @@ -19,7 +19,9 @@ public AnnotationAdjuster(ClassVisitor visitor, String className) { @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + System.out.println("HOLLY AA " + name + signature); AnnotationVisitor av = visitAnnotation(SIMPLE_ANNOTATION_TYPENAME, true); + System.out.println("HOLLY AA av is " + av); Type value = Type.getType(AnnotationAddedByExtension.class); if (av != null) { av.visit("value", value); diff --git a/integration-tests/test-extension/extension-that-defines-junit-test-extensions/runtime/src/main/java/org/acme/MyContextProvider.java b/integration-tests/test-extension/extension-that-defines-junit-test-extensions/runtime/src/main/java/org/acme/MyContextProvider.java index fe06208c53496c..af5ff5db9eab3d 100644 --- a/integration-tests/test-extension/extension-that-defines-junit-test-extensions/runtime/src/main/java/org/acme/MyContextProvider.java +++ b/integration-tests/test-extension/extension-that-defines-junit-test-extensions/runtime/src/main/java/org/acme/MyContextProvider.java @@ -2,9 +2,12 @@ import static java.util.Arrays.asList; +import java.lang.annotation.Annotation; +import java.util.Arrays; import java.util.List; import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterContext; @@ -17,6 +20,12 @@ public class MyContextProvider implements TestTemplateInvocationContextProvider @Override public boolean supportsTestTemplate(ExtensionContext extensionContext) { + System.out.println("HOLLY checking test template"); + Annotation[] myAnnotations = extensionContext.getClass().getAnnotations(); + Assertions.assertTrue(Arrays.toString(myAnnotations).contains("AnnotationAddedByExtensionHAHAHANO"), + "The context provider does not see the annotation, only sees " + Arrays.toString(myAnnotations) + + ". The classloader is " + this.getClass().getClassLoader()); + return true; } diff --git a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/ClasspathTestCase.java b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/ClasspathTestCase.java index 3a8efbf1b22f1f..e33a6a1932c4e5 100644 --- a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/ClasspathTestCase.java +++ b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/ClasspathTestCase.java @@ -3,7 +3,6 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.is; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import io.quarkus.test.junit.DisabledOnIntegrationTest; @@ -54,8 +53,6 @@ public void testStaticInitMainResourceNoDuplicate() { } @Test - @Disabled("For some reason, class files are not accessible as resources through the runtime init classloader;" - + " that's beside the point of this PR though, so we'll ignore that.") public void testRuntimeInitMainClassNoDuplicate() { given().param("resourceName", CLASS_FILE) .param("phase", "runtime_init") @@ -65,6 +62,8 @@ public void testRuntimeInitMainClassNoDuplicate() { @Test public void testRuntimeInitMainResourceNoDuplicate() { + // Runtime classloader classes are stored in memory, as "quarkus:" resources, and we do not have a quarkus filesystem provider + // at the moment, the path helper works around that by hacking/reverse engineering a file-based location given().param("resourceName", RESOURCE_FILE) .param("phase", "runtime_init") .when().get("/core/classpath").then() diff --git a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-callback-from-extension/src/test/java/org/acme/QuarkusTestAccessingBeanInCallback.java b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-callback-from-extension/src/test/java/org/acme/QuarkusTestAccessingBeanInCallback.java index cba8e4ff906cd1..ba9a7f95c361ca 100644 --- a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-callback-from-extension/src/test/java/org/acme/QuarkusTestAccessingBeanInCallback.java +++ b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-callback-from-extension/src/test/java/org/acme/QuarkusTestAccessingBeanInCallback.java @@ -1,6 +1,5 @@ package org.acme; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import jakarta.inject.Inject; @@ -8,6 +7,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import io.quarkus.arc.InjectableBean; import io.quarkus.test.junit.QuarkusTest; @QuarkusTest @@ -22,6 +22,7 @@ public class QuarkusTestAccessingBeanInCallback { @Callback public void callback() { + callbackHappened = true; // Callbacks invoked by test frameworks should be able to see injected beans assertNotNull(bean, "A method invoked by a test interceptor should have access to CDI beans"); diff --git a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/src/test/java/org/acme/TemplatedQuarkusTest.java b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/src/test/java/org/acme/TemplatedQuarkusTest.java index 1858ba4bb78a9b..62280177162666 100644 --- a/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/src/test/java/org/acme/TemplatedQuarkusTest.java +++ b/integration-tests/test-extension/tests/src/test/resources-filtered/projects/project-using-test-template-from-extension-with-bytecode-changes/src/test/java/org/acme/TemplatedQuarkusTest.java @@ -39,6 +39,7 @@ void classloaderIntrospectionTestTemplate(ExtensionContext context) { @TestTemplate @ExtendWith(MyContextProvider.class) void contextAnnotationCheckingTestTemplate(ExtensionContext context) { + System.out.println("HOLLY cl is " + context.getRequiredTestClass().getClassLoader()); Annotation[] contextAnnotations = context.getRequiredTestClass().getAnnotations(); Assertions.assertTrue(Arrays.toString(contextAnnotations).contains("AnnotationAddedByExtension"), "The JUnit extension context does not see the annotation, only sees " + Arrays.toString(contextAnnotations)); @@ -49,6 +50,7 @@ void contextAnnotationCheckingTestTemplate(ExtensionContext context) { void executionAnnotationCheckingTestTemplate(ExtensionContext context) { Annotation[] myAnnotations = this.getClass().getAnnotations(); Assertions.assertTrue(Arrays.toString(myAnnotations).contains("AnnotationAddedByExtension"), - "The test execution does not see the annotation, only sees " + Arrays.toString(myAnnotations)); + "The test execution does not see the annotation, only sees " + Arrays.toString(myAnnotations) + + ". The classloader is " + this.getClass().getClassLoader()); } } diff --git a/test-framework/amazon-lambda/src/main/resources/META-INF/services/io.quarkus.test.common.QuarkusTestResourceLifecycleManager b/test-framework/amazon-lambda/src/main/resources/META-INF/services/io.quarkus.deployment.dev.testing.QuarkusTestResourceLifecycleManager similarity index 100% rename from test-framework/amazon-lambda/src/main/resources/META-INF/services/io.quarkus.test.common.QuarkusTestResourceLifecycleManager rename to test-framework/amazon-lambda/src/main/resources/META-INF/services/io.quarkus.deployment.dev.testing.QuarkusTestResourceLifecycleManager diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/QuarkusTestResourceLifecycleManager.java b/test-framework/common/src/main/java/io/quarkus/test/common/QuarkusTestResourceLifecycleManager.java index a5f9ed3ce3f5e3..6c152bcf952807 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/QuarkusTestResourceLifecycleManager.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/QuarkusTestResourceLifecycleManager.java @@ -5,6 +5,8 @@ import java.util.Map; import java.util.function.Predicate; +import io.quarkus.deployment.dev.testing.TestStatus; + /** * Manage the lifecycle of a test resource, for instance a H2 test server. *

diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java b/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java index bc73a6cd5883de..e57215cf6cf9bc 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java @@ -33,6 +33,8 @@ import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; +import io.quarkus.deployment.dev.testing.TestClassIndexer; +import io.quarkus.deployment.dev.testing.TestStatus; import io.smallrye.config.SmallRyeConfigProviderResolver; public class TestResourceManager implements Closeable { diff --git a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusProdModeTest.java b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusProdModeTest.java index f2f9de9fd5b8f1..82fe857f494aa2 100644 --- a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusProdModeTest.java +++ b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusProdModeTest.java @@ -422,7 +422,7 @@ public void close() throws Throwable { // 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()); + final Path projectClassesDir = PathTestHelper.getAppClassLocationForTestLocation(testLocation); if (!Files.exists(projectClassesDir)) { Files.createDirectories(projectClassesDir); } diff --git a/test-framework/junit5-properties/src/main/resources/junit-platform.properties b/test-framework/junit5-properties/src/main/resources/junit-platform.properties index cdac134076ffb3..d25c41defe02a7 100644 --- a/test-framework/junit5-properties/src/main/resources/junit-platform.properties +++ b/test-framework/junit5-properties/src/main/resources/junit-platform.properties @@ -1,2 +1,3 @@ junit.jupiter.extensions.autodetection.enabled=true junit.jupiter.testclass.order.default=io.quarkus.test.junit.util.QuarkusTestProfileAwareClassOrderer +junit.platform.launcher.interceptors.enabled=true 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 7ec294c594874b..8f3fee4ac9e6aa 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 @@ -1,47 +1,36 @@ package io.quarkus.test.junit; -import static io.quarkus.commons.classloading.ClassloadHelper.fromClassNameToResourceName; -import static io.quarkus.test.common.PathTestHelper.getAppClassLocationForTestLocation; import static io.quarkus.test.common.PathTestHelper.getTestClassesLocation; import java.io.IOException; import java.lang.annotation.Annotation; -import java.nio.file.Files; +import java.lang.reflect.InvocationTargetException; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayDeque; import java.util.Collection; import java.util.Deque; import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.function.Consumer; import java.util.stream.Collectors; import jakarta.enterprise.inject.Alternative; -import org.jboss.jandex.Index; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.extension.ExtensionContext; import io.quarkus.bootstrap.BootstrapConstants; import io.quarkus.bootstrap.app.AugmentAction; import io.quarkus.bootstrap.app.CuratedApplication; -import io.quarkus.bootstrap.app.QuarkusBootstrap; +import io.quarkus.bootstrap.app.RunningQuarkusApplication; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.bootstrap.resolver.AppModelResolverException; import io.quarkus.bootstrap.runner.Timing; import io.quarkus.bootstrap.utils.BuildToolHelper; -import io.quarkus.bootstrap.workspace.ArtifactSources; -import io.quarkus.bootstrap.workspace.SourceDir; -import io.quarkus.bootstrap.workspace.WorkspaceModule; -import io.quarkus.deployment.dev.testing.CurrentTestApplication; -import io.quarkus.paths.PathList; import io.quarkus.runtime.LaunchMode; -import io.quarkus.test.common.PathTestHelper; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.common.RestorableSystemProperties; -import io.quarkus.test.common.TestClassIndexer; import io.quarkus.test.common.WithTestResource; public class AbstractJvmQuarkusTestExtension extends AbstractQuarkusTestWithContextExtension { @@ -52,103 +41,62 @@ public class AbstractJvmQuarkusTestExtension extends AbstractQuarkusTestWithCont protected ClassLoader originalCl; + // Used to preserve state from the previous run, so we know if we should restart an application + protected static RunningQuarkusApplication runningQuarkusApplication; + protected static Class quarkusTestProfile; //needed for @Nested protected static final Deque> currentTestClassStack = new ArrayDeque<>(); protected static Class currentJUnitTestClass; + // TODO only used by QuarkusMainTest, fix that class and delete this protected PrepareResult createAugmentor(ExtensionContext context, Class profile, Collection shutdownTasks) throws Exception { - final PathList.Builder rootBuilder = PathList.builder(); - Consumer addToBuilderIfConditionMet = path -> { - if (path != null && Files.exists(path) && !rootBuilder.contains(path)) { - rootBuilder.add(path); - } - }; - + originalCl = Thread.currentThread().getContextClassLoader(); final Class requiredTestClass = context.getRequiredTestClass(); - currentJUnitTestClass = requiredTestClass; - - final Path testClassLocation; - final Path appClassLocation; - final Path projectRoot = Paths.get("").normalize().toAbsolutePath(); - - 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.toString()); - 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); - } + System.out.println( + "HOLLY about to cast " + requiredTestClass.getName() + " which has cl " + requiredTestClass.getClassLoader()); + CuratedApplication curatedApplication = ((QuarkusClassLoader) requiredTestClass.getClassLoader()) + .getCuratedApplication(); + System.out.println("HOLLY " + requiredTestClass.getClassLoader() + " gives " + curatedApplication); - addToBuilderIfConditionMet.accept(appClassLocation); - final Path appResourcesLocation = PathTestHelper.getResourcesForClassesDirOrNull(appClassLocation, "main"); - addToBuilderIfConditionMet.accept(appResourcesLocation); - } + // TODO need to handle the gradle case - can we put it in that method? + Path testClassLocation = getTestClassesLocation(requiredTestClass); - originalCl = Thread.currentThread().getContextClassLoader(); + // TODO is this needed? + // Index testClassesIndex = TestClassIndexer.indexTestClasses(testClassLocation); + // // we need to write the Index to make it reusable from other parts of the testing infrastructure that run in different ClassLoaders + // TestClassIndexer.writeIndex(testClassesIndex, testClassLocation, requiredTestClass); + + Timing.staticInitStarted(curatedApplication.getOrCreateBaseRuntimeClassLoader(), + curatedApplication.getQuarkusBootstrap() + .isAuxiliaryApplication()); + + final Map props = new HashMap<>(); + props.put(TEST_LOCATION, testClassLocation); + props.put(TEST_CLASS, requiredTestClass); // clear the test.url system property as the value leaks into the run when using different profiles System.clearProperty("test.url"); Map additional = new HashMap<>(); + QuarkusTestProfile profileInstance = getQuarkusTestProfile(profile, shutdownTasks, additional); + + if (profile != null) { + props.put(TEST_PROFILE, profile.getName()); + } + quarkusTestProfile = profile; + return new PrepareResult(curatedApplication + .createAugmentor(QuarkusTestExtension.TestBuildChainFunction.class.getName(), props), profileInstance, + curatedApplication, testClassLocation); + } + + protected static QuarkusTestProfile getQuarkusTestProfile(Class profile, + Collection shutdownTasks, Map additional) + throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { QuarkusTestProfile profileInstance = null; if (profile != null) { profileInstance = profile.getConstructor().newInstance(); @@ -173,46 +121,7 @@ protected PrepareResult createAugmentor(ExtensionContext context, Class props = new HashMap<>(); - props.put(TEST_LOCATION, testClassLocation); - props.put(TEST_CLASS, requiredTestClass); - if (profile != null) { - props.put(TEST_PROFILE, profile.getName()); - } - quarkusTestProfile = profile; - return new PrepareResult(curatedApplication - .createAugmentor(QuarkusTestExtension.TestBuildChainFunction.class.getName(), props), profileInstance, - curatedApplication, testClassLocation); + return profileInstance; } private ApplicationModel getGradleAppModelForIDE(Path projectRoot) throws IOException, AppModelResolverException { @@ -221,21 +130,31 @@ private ApplicationModel getGradleAppModelForIDE(Path projectRoot) throws IOExce : null; } - protected Class getQuarkusTestProfile(ExtensionContext extensionContext) { + public static Class getQuarkusTestProfile(Class testClass) { // If the current class or any enclosing class in its hierarchy is annotated with `@TestProfile`. - Class testProfile = findTestProfileAnnotation(extensionContext.getRequiredTestClass()); + Class testProfile = findTestProfileAnnotation(testClass); if (testProfile != null) { return testProfile; } // Otherwise, if the current class is annotated with `@Nested`: - if (extensionContext.getRequiredTestClass().isAnnotationPresent(Nested.class)) { + while (testClass.isAnnotationPresent(Nested.class)) { // let's try to find the `@TestProfile` from the enclosing classes: - testProfile = findTestProfileAnnotation(extensionContext.getRequiredTestClass().getEnclosingClass()); + testProfile = getQuarkusTestProfile(testClass.getEnclosingClass()); if (testProfile != null) { return testProfile; } + } + return null; + } + protected Class getQuarkusTestProfile(ExtensionContext extensionContext) { + Class testClass = extensionContext.getRequiredTestClass(); + Class testProfile = getQuarkusTestProfile(testClass); + + if (testProfile == null && (testClass.isAnnotationPresent(Nested.class))) { + + // This is unlikely to work since we recursed up the test class stack, but err on the side of double-checking? // if not found, let's try the parents Optional parentContext = extensionContext.getParent(); while (parentContext.isPresent()) { @@ -256,7 +175,7 @@ protected Class getQuarkusTestProfile(ExtensionCon return null; } - private Class findTestProfileAnnotation(Class clazz) { + private static Class findTestProfileAnnotation(Class clazz) { Class testClass = clazz; while (testClass != null) { TestProfile annotation = testClass.getAnnotation(TestProfile.class); @@ -307,6 +226,17 @@ public static boolean hasPerTestResources(Class requiredTestClass) { return false; } + protected boolean isNewApplication(QuarkusTestExtensionState state, Class currentJUnitTestClass) { + + // How do we know how to stop the current application - compare the classloader and see if it changed + // We could also look at the running application attached to the junit test and see if it's started + + // TODO + return (runningQuarkusApplication == null + || runningQuarkusApplication.getClassLoader() != currentJUnitTestClass.getClassLoader()); + + } + protected static class PrepareResult { protected final AugmentAction augmentAction; protected final QuarkusTestProfile profileInstance; diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/DotNames.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/DotNames.java deleted file mode 100644 index 271ef5b36516e0..00000000000000 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/DotNames.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.quarkus.test.junit; - -import org.jboss.jandex.DotName; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; - -final class DotNames { - - private DotNames() { - } - - static final DotName EXTEND_WITH = DotName.createSimple(ExtendWith.class.getName()); - static final DotName REGISTER_EXTENSION = DotName.createSimple(RegisterExtension.class.getName()); - static final DotName QUARKUS_TEST_EXTENSION = DotName.createSimple(QuarkusTestExtension.class.getName()); -} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java index a29edfc4a04883..69f9a4c08970ed 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java @@ -49,6 +49,7 @@ import io.quarkus.bootstrap.workspace.ArtifactSources; import io.quarkus.bootstrap.workspace.SourceDir; import io.quarkus.deployment.builditem.DevServicesLauncherConfigResultBuildItem; +import io.quarkus.deployment.dev.testing.TestClassIndexer; import io.quarkus.deployment.util.ContainerRuntimeUtil; import io.quarkus.paths.PathList; import io.quarkus.runtime.LaunchMode; @@ -56,7 +57,6 @@ import io.quarkus.test.common.ArtifactLauncher; import io.quarkus.test.common.LauncherUtil; import io.quarkus.test.common.PathTestHelper; -import io.quarkus.test.common.TestClassIndexer; import io.quarkus.test.common.TestResourceManager; import io.quarkus.test.common.http.TestHTTPResourceManager; @@ -210,7 +210,7 @@ static ArtifactLauncher.InitContext.DevServicesLaunchResult handleDevServices(Ex boolean isDockerAppLaunch) throws Exception { Class requiredTestClass = context.getRequiredTestClass(); Path testClassLocation = getTestClassesLocation(requiredTestClass); - final Path appClassLocation = getAppClassLocationForTestLocation(testClassLocation.toString()); + final Path appClassLocation = getAppClassLocationForTestLocation(testClassLocation); final PathList.Builder rootBuilder = PathList.builder(); diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java index d793d8e74b6c46..3e9300b3b50606 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainTestExtension.java @@ -5,7 +5,12 @@ import java.io.Closeable; import java.lang.reflect.Method; -import java.util.*; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.concurrent.LinkedBlockingDeque; import java.util.logging.Handler; @@ -24,6 +29,7 @@ import org.junit.jupiter.api.extension.ReflectiveInvocationContext; import io.quarkus.bootstrap.app.StartupAction; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.bootstrap.logging.InitialConfigurator; import io.quarkus.bootstrap.logging.QuarkusDelayedHandler; import io.quarkus.deployment.dev.testing.LogCapturingOutputFilter; @@ -66,7 +72,16 @@ private void ensurePrepared(ExtensionContext extensionContext, Class selectedProfile, String[] arguments) throws Exception { ensurePrepared(context, selectedProfile); + System.out.println("HOLLY doing launch " + context.getDisplayName() + Arrays.toString(arguments)); LogCapturingOutputFilter filter = new LogCapturingOutputFilter(prepareResult.curatedApplication, false, false, () -> true); QuarkusConsole.addOutputFilter(filter); @@ -171,14 +187,18 @@ private void flushAllLoggers() { } } + // TODO we could definitely still pull out more common code between this and quarkustestextension private int doJavaStart(ExtensionContext context, Class profile, String[] arguments) throws Exception { + System.out.println("HOLLY doing Java start " + context.getDisplayName() + context.getRequiredTestClass() + + context.getRequiredTestMethod()); JBossVersion.disableVersionLogging(); + Class requiredTestClass = context.getRequiredTestClass(); TracingHandler.quarkusStarting(); Closeable testResourceManager = null; try { - StartupAction startupAction = prepareResult.augmentAction.createInitialRuntimeApplication(); + StartupAction startupAction = ((QuarkusClassLoader) requiredTestClass.getClassLoader()).getStartupAction(); Thread.currentThread().setContextClassLoader(startupAction.getClassLoader()); QuarkusConsole.installRedirects(); flushAllLoggers(); diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java index 0f881f19022ce2..38da1ca9714db3 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java @@ -1,5 +1,7 @@ package io.quarkus.test.junit; +import static io.quarkus.commons.classloading.ClassloadHelper.fromClassNameToResourceName; +import static io.quarkus.test.common.PathTestHelper.getTestClassesLocation; import static io.quarkus.test.junit.IntegrationTestUtil.activateLogging; import static io.quarkus.test.junit.IntegrationTestUtil.getAdditionalTestResources; @@ -67,8 +69,6 @@ import org.junit.jupiter.api.extension.TestInstantiationException; import org.opentest4j.TestAbortedException; -import io.quarkus.bootstrap.app.AugmentAction; -import io.quarkus.bootstrap.app.RunningQuarkusApplication; import io.quarkus.bootstrap.app.StartupAction; import io.quarkus.bootstrap.classloading.ClassPathElement; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; @@ -81,6 +81,8 @@ import io.quarkus.deployment.builditem.TestClassBeanBuildItem; import io.quarkus.deployment.builditem.TestClassPredicateBuildItem; import io.quarkus.deployment.builditem.TestProfileBuildItem; +import io.quarkus.deployment.dev.testing.DotNames; +import io.quarkus.deployment.dev.testing.TestClassIndexer; import io.quarkus.dev.testing.ExceptionReporting; import io.quarkus.dev.testing.TracingHandler; import io.quarkus.runtime.ApplicationLifecycleManager; @@ -94,7 +96,6 @@ import io.quarkus.test.common.PropertyTestUtil; import io.quarkus.test.common.RestAssuredURLManager; import io.quarkus.test.common.RestorableSystemProperties; -import io.quarkus.test.common.TestClassIndexer; import io.quarkus.test.common.TestResourceManager; import io.quarkus.test.common.TestScopeManager; import io.quarkus.test.common.http.TestHTTPEndpoint; @@ -102,8 +103,6 @@ import io.quarkus.test.junit.buildchain.TestBuildChainCustomizerProducer; import io.quarkus.test.junit.callback.QuarkusTestContext; import io.quarkus.test.junit.callback.QuarkusTestMethodContext; -import io.quarkus.test.junit.internal.DeepClone; -import io.quarkus.test.junit.internal.NewSerializingDeepClone; public class QuarkusTestExtension extends AbstractJvmQuarkusTestExtension implements BeforeEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback, AfterEachCallback, @@ -120,7 +119,6 @@ public class QuarkusTestExtension extends AbstractJvmQuarkusTestExtension private static Object actualTestInstance; // needed for @Nested private static final Deque outerInstances = new ArrayDeque<>(1); - private static RunningQuarkusApplication runningQuarkusApplication; 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 private static Class quarkusTestMethodContextClass; @@ -129,7 +127,6 @@ public class QuarkusTestExtension extends AbstractJvmQuarkusTestExtension private static List testMethodInvokers; - private static DeepClone deepClone; private static volatile ScheduledExecutorService hangDetectionExecutor; private static volatile Duration hangTimeout; private static volatile ScheduledFuture hangTaskKey; @@ -189,6 +186,7 @@ public void run() { private ExtensionState doJavaStart(ExtensionContext context, Class profile) throws Throwable { JBossVersion.disableVersionLogging(); + // TODO we should do much less of this, because it's being done upfront by the interceptor TracingHandler.quarkusStarting(); hangDetectionExecutor = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { @Override @@ -211,15 +209,39 @@ public Thread newThread(Runnable r) { Closeable testResourceManager = null; try { final LinkedBlockingDeque shutdownTasks = new LinkedBlockingDeque<>(); - PrepareResult result = createAugmentor(context, profile, shutdownTasks); - AugmentAction augmentAction = result.augmentAction; - QuarkusTestProfile profileInstance = result.profileInstance; + // PrepareResult result = createAugmentor(context, profile, shutdownTasks); + // AugmentAction augmentAction = result.augmentAction; + // QuarkusTestProfile profileInstance = result.profileInstance; testHttpEndpointProviders = TestHttpEndpointProvider.load(); - StartupAction startupAction = augmentAction.createInitialRuntimeApplication(); - Thread.currentThread().setContextClassLoader(startupAction.getClassLoader()); - populateDeepCloneField(startupAction); + System.out.println("HOLLY during execution, TCCL is " + Thread.currentThread().getContextClassLoader()); + System.out.println("HOLLY the test was loaded with " + requiredTestClass); + + // StartupAction startupAction = augmentAction.createInitialRuntimeApplication(); + // clear the test.url system property as the value leaks into the run when using different profiles + System.clearProperty("test.url"); + Map additional = new HashMap<>(); + QuarkusTestProfile profileInstance = getQuarkusTestProfile(profile, shutdownTasks, additional); + StartupAction startupAction = ((QuarkusClassLoader) requiredTestClass.getClassLoader()).getStartupAction(); + System.out.println("HOLLY made initial app"); + Thread.currentThread().setContextClassLoader(startupAction.getClassLoader()); + // populateDeepCloneField(startupAction); + + System.out.println("HOLLY class has come in as " + requiredTestClass.getClassLoader()); + System.out.println("HOLLY will now get a locextsion for " + + requiredTestClass.getClassLoader().getResource(fromClassNameToResourceName(requiredTestClass.getName()))); + // TODO could store this in the startup action? + Path testClassLocation = getTestClassesLocation(requiredTestClass); + + // TODO this is a bit sloppy, but the quarkus classloader uses a quarkus: scheme for its in memory resources and then we get a failure that it's not installed + // Path projectRoot = Paths.get("") + // .normalize() + // .toAbsolutePath(); + // Path applicationRoot = getTestClassLocationForRootLocation(projectRoot.toString()); + // Path testClassLocation = applicationRoot; + + // Do we need the augmentation classloader as the TCCL? //must be done after the TCCL has been set testResourceManager = (Closeable) startupAction.getClassLoader().loadClass(TestResourceManager.class.getName()) .getConstructor(Class.class, Class.class, List.class, boolean.class, Map.class, Optional.class, Path.class) @@ -227,7 +249,7 @@ public Thread newThread(Runnable r) { profile != null ? profile : null, getAdditionalTestResources(profileInstance, startupAction.getClassLoader()), profileInstance != null && profileInstance.disableGlobalTestResources(), - startupAction.getDevServicesProperties(), Optional.empty(), result.testClassLocation); + startupAction.getDevServicesProperties(), Optional.empty(), testClassLocation); testResourceManager.getClass().getMethod("init", String.class).invoke(testResourceManager, profile != null ? profile.getName() : null); Map properties = (Map) testResourceManager.getClass().getMethod("start") @@ -259,17 +281,16 @@ public Thread newThread(Runnable r) { TracingHandler.quarkusStarted(); - deepClone.setRunningQuarkusApplication(runningQuarkusApplication); - //now we have full config reset the hang timer if (hangTaskKey != null) { hangTaskKey.cancel(false); hangTimeout = runningQuarkusApplication.getConfigValue(QUARKUS_TEST_HANG_DETECTION_TIMEOUT, Duration.class) .orElse(Duration.of(10, ChronoUnit.MINUTES)); + hangTaskKey = hangDetectionExecutor.schedule(hangDetectionTask, hangTimeout.toMillis(), TimeUnit.MILLISECONDS); } - ConfigProviderResolver.setInstance(new RunningAppConfigResolver(runningQuarkusApplication)); + // TODO causes infinite loop, what problem is this solving? ConfigProviderResolver.setInstance(new RunningAppConfigResolver(runningQuarkusApplication)); RestorableSystemProperties restorableSystemProperties = RestorableSystemProperties.setProperties( Collections.singletonMap("test.url", TestHTTPResourceManager.getUri(runningQuarkusApplication))); @@ -346,10 +367,6 @@ private void shutdownHangDetection() { } } - private void populateDeepCloneField(StartupAction startupAction) { - deepClone = new NewSerializingDeepClone(originalCl, startupAction.getClassLoader()); - } - private void populateTestMethodInvokers(ClassLoader quarkusClassLoader) { testMethodInvokers = new ArrayList<>(); try { @@ -576,6 +593,9 @@ private boolean isNativeOrIntegrationTest(Class clazz) { private QuarkusTestExtensionState ensureStarted(ExtensionContext extensionContext) { QuarkusTestExtensionState state = getState(extensionContext); Class selectedProfile = getQuarkusTestProfile(extensionContext); + + // TODO all this check should go to the facade classloader, and we just need to know if it's started or not, and close the previous one if not + // TODO we also need to hope the tests are in the right order, and re-order them so we don't rely on luck (will that be ok? def better doc it) boolean wrongProfile = !Objects.equals(selectedProfile, quarkusTestProfile); // we reset the failed state if we changed test class and the new test class is not a nested class boolean isNewTestClass = !Objects.equals(extensionContext.getRequiredTestClass(), currentJUnitTestClass) @@ -584,10 +604,10 @@ private QuarkusTestExtensionState ensureStarted(ExtensionContext extensionContex state.setTestFailed(null); currentJUnitTestClass = extensionContext.getRequiredTestClass(); } - // we reload the test resources if we changed test class and the new test class is not a nested class, and if we had or will have per-test test resources - boolean reloadTestResources = isNewTestClass && (hasPerTestResources || hasPerTestResources(extensionContext)); - if ((state == null && !failedBoot) || wrongProfile || reloadTestResources) { - if (wrongProfile || reloadTestResources) { + boolean isNewApplication = isNewApplication(state, currentJUnitTestClass); + + if ((state == null && !failedBoot) || isNewApplication) { + if (isNewApplication) { if (state != null) { try { state.close(); @@ -754,8 +774,11 @@ public T interceptTestClassConstructor(Invocation invocation, private void initTestState(ExtensionContext extensionContext, QuarkusTestExtensionState state) { try { - actualTestClass = Class.forName(extensionContext.getRequiredTestClass().getName(), true, - Thread.currentThread().getContextClassLoader()); + // actualTestClass = Class.forName(extensionContext.getRequiredTestClass().getName(), true, + // Thread.currentThread().getContextClassLoader()); + // Do not reload the test class + actualTestClass = extensionContext.getRequiredTestClass(); + if (extensionContext.getRequiredTestClass().isAnnotationPresent(Nested.class)) { Class outerClass = actualTestClass.getEnclosingClass(); Constructor declaredConstructor = actualTestClass.getDeclaredConstructor(outerClass); @@ -840,6 +863,7 @@ public void interceptTestMethod(Invocation invocation, ReflectiveInvocatio @Override public void interceptDynamicTest(Invocation invocation, ExtensionContext extensionContext) throws Throwable { + // TODO check if this is needed; the earlier interceptor may already have done it if (runningQuarkusApplication == null) { invocation.proceed(); return; @@ -910,8 +934,9 @@ private Object runExtensionMethod(ReflectiveInvocationContext invocation ClassLoader old = setCCL(runningQuarkusApplication.getClassLoader()); try { - Class testClassFromTCCL = Class.forName(extensionContext.getRequiredTestClass().getName(), true, - Thread.currentThread().getContextClassLoader()); + // Class testClassFromTCCL = Class.forName(extensionContext.getRequiredTestClass().getName(), true, + // Thread.currentThread().getContextClassLoader()); + Class testClassFromTCCL = extensionContext.getRequiredTestClass(); Map, Object> allTestsClasses = new HashMap<>(); // static loading allTestsClasses.put(testClassFromTCCL, actualTestInstance); @@ -960,7 +985,7 @@ private Object runExtensionMethod(ReflectiveInvocationContext invocation .invoke(testMethodInvokerToUse, argClass.getName())); } else { Object arg = originalArguments.get(i); - argumentsFromTccl.add(deepClone.clone(arg)); + argumentsFromTccl.add(arg); // No clone } } @@ -995,9 +1020,11 @@ private Method determineTCCLExtensionMethod(Method originalMethod, Class c) if (type.isPrimitive()) { parameterTypesFromTccl.add(type); } else { - parameterTypesFromTccl - .add(Class.forName(type.getName(), true, - Thread.currentThread().getContextClassLoader())); + // TODO surely this whole method can go away? + // parameterTypesFromTccl + // .add(Class.forName(type.getName(), true, + // Thread.currentThread().getContextClassLoader())); + parameterTypesFromTccl.add(type); } } return declaringClass.getDeclaredMethod(originalMethod.getName(), diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/RunningAppConfigResolver.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/RunningAppConfigResolver.java index e3ec91cd40b6a1..b0ba24c370efc9 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/RunningAppConfigResolver.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/RunningAppConfigResolver.java @@ -17,6 +17,8 @@ class RunningAppConfigResolver extends ConfigProviderResolver { RunningAppConfigResolver(RunningQuarkusApplication runningQuarkusApplication) { this.runningQuarkusApplication = runningQuarkusApplication; + System.out.println("HOLLY constructing with " + this.getClass().getClassLoader()); + System.out.println("HOLLY running app is " + runningQuarkusApplication); } @Override diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/callback/QuarkusTestContext.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/callback/QuarkusTestContext.java index c9baba32d904f9..0ed514e9e2d6be 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/callback/QuarkusTestContext.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/callback/QuarkusTestContext.java @@ -2,7 +2,7 @@ import java.util.List; -import io.quarkus.test.common.TestStatus; +import io.quarkus.deployment.dev.testing.TestStatus; /** * Context object passed to {@link QuarkusTestAfterAllCallback} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/CustomLauncherInterceptor.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/CustomLauncherInterceptor.java new file mode 100644 index 00000000000000..48bd13b5e37c36 --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/launcher/CustomLauncherInterceptor.java @@ -0,0 +1,73 @@ +package io.quarkus.test.junit.launcher; + +import org.junit.platform.launcher.LauncherInterceptor; + +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; +import io.quarkus.deployment.dev.testing.FacadeClassLoader; + +public class CustomLauncherInterceptor implements LauncherInterceptor { + + private final ClassLoader customClassLoader; + private static int count = 0; + private static int constructCount = 0; + + public CustomLauncherInterceptor() throws Exception { + System.out.println(constructCount++ + "HOLLY interceipt construct" + getClass().getClassLoader()); + ClassLoader parent = Thread.currentThread() + .getContextClassLoader(); + System.out.println("HOLLY CCL is " + parent); + + customClassLoader = parent; + System.out.println("HOLLY stored variable loader" + customClassLoader); + } + + @Override + public T intercept(Invocation invocation) { + System.out.println("HOLLY intercept"); + if (System.getProperty("prod.mode.tests") != null) { + return invocation.proceed(); + + } else { + try { + return nintercept(invocation); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + } + + private T nintercept(Invocation invocation) { + ClassLoader old = Thread.currentThread().getContextClassLoader(); + System.out.println("Interceipt, TCCL is " + old); + // Don't make a facade loader if the JUnitRunner got there ahead of us + // they set a runtime classloader so handle that too + if (!(old instanceof FacadeClassLoader) || old instanceof QuarkusClassLoader && old.getName().contains("Runtime")) { + System.out.println("HOLLY INTERCEPT RESTART ------------------------------"); + try { + // TODO should this be a static variable, so we don't make zillions and cause too many files exceptions? + // Although in principle we only go through a few times + FacadeClassLoader facadeLoader = new FacadeClassLoader(old); + Thread.currentThread() + .setContextClassLoader(facadeLoader); + return invocation.proceed(); + } finally { + Thread.currentThread() + .setContextClassLoader(old); + } + } else { + return invocation.proceed(); + } + } + + @Override + public void close() { + + // // try { + // // // TODO customClassLoader.close(); + // // } catch (Exception e) { + // // throw new UncheckedIOException("Failed to close custom class + // loader", e); + // // } + } +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/util/QuarkusTestProfileAwareClassOrderer.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/util/QuarkusTestProfileAwareClassOrderer.java index 810150bec98ce6..c09ddd38d2f563 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/util/QuarkusTestProfileAwareClassOrderer.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/util/QuarkusTestProfileAwareClassOrderer.java @@ -15,6 +15,7 @@ import io.quarkus.test.common.WithTestResource; import io.quarkus.test.junit.QuarkusIntegrationTest; import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; import io.quarkus.test.junit.TestProfile; import io.quarkus.test.junit.main.QuarkusMainTest; @@ -33,7 +34,7 @@ *

* Internally, ordering is based on prefixes that are prepended to a secondary order suffix (by default the fully qualified * name of the respective test class), with the fully qualified class name of the - * {@link io.quarkus.test.junit.QuarkusTestProfile QuarkusTestProfile} as an infix (if present). + * {@link QuarkusTestProfile QuarkusTestProfile} as an infix (if present). * The default prefixes are defined by {@code DEFAULT_ORDER_PREFIX_*} and can be overridden in {@code junit-platform.properties} * via {@code CFGKEY_ORDER_PREFIX_*}, e.g. non-Quarkus tests can be run first (not last) by setting * {@link #CFGKEY_ORDER_PREFIX_NON_QUARKUS_TEST} to {@code 10_}. diff --git a/test-framework/junit5/src/main/resources/META-INF/services/org.junit.platform.launcher.LauncherInterceptor b/test-framework/junit5/src/main/resources/META-INF/services/org.junit.platform.launcher.LauncherInterceptor new file mode 100644 index 00000000000000..94c0c6bbff4f92 --- /dev/null +++ b/test-framework/junit5/src/main/resources/META-INF/services/org.junit.platform.launcher.LauncherInterceptor @@ -0,0 +1 @@ +io.quarkus.test.junit.launcher.CustomLauncherInterceptor