From 0cc4b1b9b9a83755e313ce7479568b3d456ed01b Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 27 Aug 2024 17:15:59 +0300 Subject: [PATCH] Refactor TestResource handling code This is a prerequisite of fixing the behavior of `@WithTestResource` --- .../test/common/TestResourceManager.java | 439 ++++++++++++------ .../test/common/TestResourceScope.java | 14 + .../AbstractJvmQuarkusTestExtension.java | 44 +- .../test/junit/IntegrationTestUtil.java | 37 -- .../QuarkusIntegrationTestExtension.java | 9 +- .../QuarkusMainIntegrationTestExtension.java | 4 +- .../test/junit/QuarkusMainTestExtension.java | 6 +- .../test/junit/QuarkusTestExtension.java | 34 +- .../junit/TestResourceManagerReflections.java | 94 ++++ 9 files changed, 442 insertions(+), 239 deletions(-) create mode 100644 test-framework/common/src/main/java/io/quarkus/test/common/TestResourceScope.java create mode 100644 test-framework/junit5/src/main/java/io/quarkus/test/junit/TestResourceManagerReflections.java 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..5e4a722517531a 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 @@ -1,5 +1,8 @@ package io.quarkus.test.common; +import static io.quarkus.test.common.TestResourceScope.GLOBAL; +import static io.quarkus.test.common.TestResourceScope.RESTRICTED_TO_CLASS; + import java.io.Closeable; import java.lang.annotation.Annotation; import java.lang.reflect.Field; @@ -23,6 +26,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -40,36 +44,47 @@ public class TestResourceManager implements Closeable { private static final DotName WITH_TEST_RESOURCE = DotName.createSimple(WithTestResource.class); public static final String CLOSEABLE_NAME = TestResourceManager.class.getName() + ".closeable"; - private final List sequentialTestResourceEntries; - private final List parallelTestResourceEntries; - private final List allTestResourceEntries; + private final List sequentialTestResources; + private final List parallelTestResources; + private final List allTestResources; private final Map configProperties = new ConcurrentHashMap<>(); + private final TestResourceScope narrowestScope; + private boolean started = false; - private boolean hasPerTestResources = false; + private TestStatus testStatus = new TestStatus(null); public TestResourceManager(Class testClass) { this(testClass, null, Collections.emptyList(), false); } - public TestResourceManager(Class testClass, Class profileClass, List additionalTestResources, + public TestResourceManager(Class testClass, + Class profileClass, + List additionalTestResources, boolean disableGlobalTestResources) { this(testClass, profileClass, additionalTestResources, disableGlobalTestResources, Collections.emptyMap(), Optional.empty()); } - public TestResourceManager(Class testClass, Class profileClass, List additionalTestResources, - boolean disableGlobalTestResources, Map devServicesProperties, + public TestResourceManager(Class testClass, + Class profileClass, + List additionalTestResources, + boolean disableGlobalTestResources, + Map devServicesProperties, Optional containerNetworkId) { this(testClass, profileClass, additionalTestResources, disableGlobalTestResources, devServicesProperties, containerNetworkId, PathTestHelper.getTestClassesLocation(testClass)); } - public TestResourceManager(Class testClass, Class profileClass, List additionalTestResources, - boolean disableGlobalTestResources, Map devServicesProperties, - Optional containerNetworkId, Path testClassLocation) { - this.parallelTestResourceEntries = new ArrayList<>(); - this.sequentialTestResourceEntries = new ArrayList<>(); + public TestResourceManager(Class testClass, + Class profileClass, + List additionalTestResources, + boolean disableGlobalTestResources, + Map devServicesProperties, + Optional containerNetworkId, + Path testClassLocation) { + this.parallelTestResources = new ArrayList<>(); + this.sequentialTestResources = new ArrayList<>(); // we need to keep track of duplicate entries to make sure we don't start the same resource // multiple times even if there are multiple same @WithTestResource annotations @@ -80,11 +95,14 @@ public TestResourceManager(Class testClass, Class profileClass, List remainingUniqueEntries = initParallelTestResources(uniqueEntries); initSequentialTestResources(remainingUniqueEntries); - this.allTestResourceEntries = new ArrayList<>(sequentialTestResourceEntries); - this.allTestResourceEntries.addAll(parallelTestResourceEntries); + this.allTestResources = new ArrayList<>(sequentialTestResources); + this.allTestResources.addAll(parallelTestResources); DevServicesContext context = new DevServicesContext() { @Override public Map devServicesProperties() { @@ -96,19 +114,31 @@ public Optional containerNetworkId() { return containerNetworkId; } }; - for (var i : allTestResourceEntries) { + for (var i : allTestResources) { if (i.getTestResource() instanceof DevServicesContext.ContextAware) { ((DevServicesContext.ContextAware) i.getTestResource()).setIntegrationTestContext(context); } } } + public static TestResourceScope narrowestScope(Set uniqueEntries) { + TestResourceScope res = null; + for (TestResourceClassEntry entry : uniqueEntries) { + if (res == null) { + res = entry.scope; + } else if (res.compareTo(entry.scope) > 0) { + res = entry.scope; + } + } + return res; + } + public void setTestErrorCause(Throwable testErrorCause) { this.testStatus = new TestStatus(testErrorCause); } public void init(String testProfileName) { - for (TestResourceEntry entry : allTestResourceEntries) { + for (TestResourceStartInfo entry : allTestResources) { try { QuarkusTestResourceLifecycleManager testResource = entry.getTestResource(); testResource.setContext(new QuarkusTestResourceLifecycleManager.Context() { @@ -138,13 +168,13 @@ public TestStatus getTestStatus() { public Map start() { started = true; Map allProps = new ConcurrentHashMap<>(); - int taskSize = parallelTestResourceEntries.size() + 1; + int taskSize = parallelTestResources.size() + 1; ExecutorService executor = Executors.newFixedThreadPool(taskSize); List tasks = new ArrayList<>(taskSize); - for (TestResourceEntry entry : parallelTestResourceEntries) { - tasks.add(new TestResourceEntryRunnable(entry, allProps)); + for (TestResourceStartInfo entry : parallelTestResources) { + tasks.add(new TestResourceRunnable(entry, allProps)); } - tasks.add(new TestResourceEntryRunnable(sequentialTestResourceEntries, allProps)); + tasks.add(new TestResourceRunnable(sequentialTestResources, allProps)); try { // convert the tasks into an array of CompletableFuture @@ -163,7 +193,7 @@ public Map start() { } public void inject(Object testInstance) { - for (TestResourceEntry entry : allTestResourceEntries) { + for (TestResourceStartInfo entry : allTestResources) { QuarkusTestResourceLifecycleManager quarkusTestResourceLifecycleManager = entry.getTestResource(); quarkusTestResourceLifecycleManager.inject(testInstance); quarkusTestResourceLifecycleManager.inject(new DefaultTestInjector(testInstance)); @@ -175,7 +205,7 @@ public void close() { return; } started = false; - for (TestResourceEntry entry : allTestResourceEntries) { + for (TestResourceStartInfo entry : allTestResources) { try { entry.getTestResource().stop(); } catch (Exception e) { @@ -205,17 +235,17 @@ private Set initParallelTestResources(Set remainingUniqueEntries = new LinkedHashSet<>(uniqueEntries); for (TestResourceClassEntry entry : uniqueEntries) { if (entry.isParallel()) { - TestResourceEntry testResourceEntry = buildTestResourceEntry(entry); - this.parallelTestResourceEntries.add(testResourceEntry); + TestResourceStartInfo testResourceStartInfo = buildTestResourceEntry(entry); + this.parallelTestResources.add(testResourceStartInfo); remainingUniqueEntries.remove(entry); } } - parallelTestResourceEntries.sort(new Comparator() { + parallelTestResources.sort(new Comparator() { private final QuarkusTestResourceLifecycleManagerComparator lifecycleManagerComparator = new QuarkusTestResourceLifecycleManagerComparator(); @Override - public int compare(TestResourceEntry o1, TestResourceEntry o2) { + public int compare(TestResourceStartInfo o1, TestResourceStartInfo o2) { return lifecycleManagerComparator.compare(o1.getTestResource(), o2.getTestResource()); } }); @@ -227,8 +257,8 @@ private Set initSequentialTestResources(Set remainingUniqueEntries = new LinkedHashSet<>(uniqueEntries); for (TestResourceClassEntry entry : uniqueEntries) { if (!entry.isParallel()) { - TestResourceEntry testResourceEntry = buildTestResourceEntry(entry); - this.sequentialTestResourceEntries.add(testResourceEntry); + TestResourceStartInfo testResourceStartInfo = buildTestResourceEntry(entry); + this.sequentialTestResources.add(testResourceStartInfo); remainingUniqueEntries.remove(entry); } } @@ -236,16 +266,16 @@ private Set initSequentialTestResources(Set() { + this.sequentialTestResources.sort(new Comparator<>() { private final QuarkusTestResourceLifecycleManagerComparator lifecycleManagerComparator = new QuarkusTestResourceLifecycleManagerComparator(); @Override - public int compare(TestResourceEntry o1, TestResourceEntry o2) { + public int compare(TestResourceStartInfo o1, TestResourceStartInfo o2) { return lifecycleManagerComparator.compare(o1.getTestResource(), o2.getTestResource()); } }); @@ -253,10 +283,11 @@ public int compare(TestResourceEntry o1, TestResourceEntry o2) { return remainingUniqueEntries; } - private TestResourceManager.TestResourceEntry buildTestResourceEntry(TestResourceClassEntry entry) { + private TestResourceStartInfo buildTestResourceEntry(TestResourceClassEntry entry) { Class testResourceClass = entry.clazz; try { - return new TestResourceEntry(testResourceClass.getConstructor().newInstance(), entry.args, entry.configAnnotation); + return new TestResourceStartInfo(testResourceClass.getConstructor().newInstance(), entry.args, + entry.configAnnotation); } catch (InstantiationException | IllegalAccessException | IllegalArgumentException @@ -270,79 +301,66 @@ private TestResourceManager.TestResourceEntry buildTestResourceEntry(TestResourc private Set getUniqueTestResourceClassEntries(Path testClassLocation, Class testClass, Class profileClass, List additionalTestResources) { - IndexView index = TestClassIndexer.readIndex(testClassLocation, testClass); - Set uniqueEntries = new LinkedHashSet<>(); - // reload the test and profile classes in the right CL - Class testClassFromTCCL; - Class profileClassFromTCCL; - try { - testClassFromTCCL = Class.forName(testClass.getName(), false, Thread.currentThread().getContextClassLoader()); - if (profileClass != null) { - profileClassFromTCCL = Class.forName(profileClass.getName(), false, - Thread.currentThread().getContextClassLoader()); - } else { - profileClassFromTCCL = null; - } - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } - // handle meta-annotations: in this case we must rely on reflection because meta-annotations are not indexed - // because they are not in the user's test folder but come from test extensions - collectMetaAnnotations(testClassFromTCCL, Class::getSuperclass, uniqueEntries); - collectMetaAnnotations(testClassFromTCCL, Class::getEnclosingClass, uniqueEntries); + Class profileClassFromTCCL = profileClass != null ? alwaysFromTccl(profileClass) : null; + Consumer> profileMetaAnnotationsConsumer = null; if (profileClassFromTCCL != null) { - collectMetaAnnotations(profileClassFromTCCL, Class::getSuperclass, uniqueEntries); + profileMetaAnnotationsConsumer = new ProfileMetaAnnotationsUniqueEntriesConsumer(profileClassFromTCCL); } - for (AnnotationInstance annotation : findQuarkusTestResourceInstances(testClass, index)) { - try { - Class testResourceClass = loadTestResourceClassFromTCCL( - annotation.value().asString()); - AnnotationValue argsAnnotationValue = annotation.value("initArgs"); - Map args; - if (argsAnnotationValue == null) { - args = Collections.emptyMap(); - } else { - args = new HashMap<>(); - AnnotationInstance[] resourceArgsInstances = argsAnnotationValue.asNestedArray(); - for (AnnotationInstance resourceArgsInstance : resourceArgsInstances) { - args.put(resourceArgsInstance.value("name").asString(), resourceArgsInstance.value().asString()); - } - } + Set uniqueEntries = getUniqueTestResourceClassEntries(testClass, testClassLocation, + profileMetaAnnotationsConsumer); + uniqueEntries.addAll(additionalTestResources); + return uniqueEntries; + } - boolean isParallel = false; - AnnotationValue parallelAnnotationValue = annotation.value("parallel"); - if (parallelAnnotationValue != null) { - isParallel = parallelAnnotationValue.asBoolean(); - } + public static Set getUniqueTestResourceClassEntries(Class testClass, + Path testClassLocation) { + return getUniqueTestResourceClassEntries(testClass, testClassLocation, null); + } - if (restrictToAnnotatedClass(annotation)) { - hasPerTestResources = true; - } + private static Set getUniqueTestResourceClassEntries(Class testClass, + Path testClassLocation, + Consumer> afterMetaAnnotationAction) { + Class testClassFromTCCL = alwaysFromTccl(testClass); - uniqueEntries.add(new TestResourceClassEntry(testResourceClass, args, null, isParallel)); - } catch (IllegalArgumentException | SecurityException e) { - throw new RuntimeException("Unable to instantiate the test resource " + annotation.value().asString(), e); - } - } + Set uniqueEntries = new LinkedHashSet<>(); - uniqueEntries.addAll(additionalTestResources); + // handle meta-annotations: in this case we must rely on reflection because meta-annotations are not indexed + // because they are not in the user's test folder but come from test extensions + collectMetaAnnotations(testClassFromTCCL, Class::getSuperclass, uniqueEntries); + collectMetaAnnotations(testClassFromTCCL, Class::getEnclosingClass, uniqueEntries); + if (afterMetaAnnotationAction != null) { + afterMetaAnnotationAction.accept(uniqueEntries); + } + for (AnnotationInstance annotation : findTestResourceInstancesOfClass(testClass, + TestClassIndexer.readIndex(testClassLocation, testClass))) { + uniqueEntries.add(TestResourceClassEntryHandler.produceEntry(annotation)); + } return uniqueEntries; } - private void collectMetaAnnotations(Class testClassFromTCCL, Function, Class> next, + private static Class alwaysFromTccl(Class testClass) { + if (testClass.getClassLoader().equals(Thread.currentThread().getContextClassLoader())) { + return testClass; + } + try { + return Class.forName(testClass.getName(), false, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + private static void collectMetaAnnotations(Class testClassFromTCCL, Function, Class> next, Set uniqueEntries) { while (testClassFromTCCL != null && !testClassFromTCCL.getName().equals("java.lang.Object")) { for (Annotation metaAnnotation : testClassFromTCCL.getAnnotations()) { for (Annotation ann : metaAnnotation.annotationType().getAnnotations()) { if (ann.annotationType() == WithTestResource.class) { addTestResourceEntry((WithTestResource) ann, metaAnnotation, uniqueEntries); - hasPerTestResources = true; break; } else if (ann.annotationType() == WithTestResource.List.class) { Arrays.stream(((WithTestResource.List) ann).value()) .forEach(res -> addTestResourceEntry(res, metaAnnotation, uniqueEntries)); - hasPerTestResources = true; break; } else if (ann.annotationType() == WithTestResourceRepeatable.class) { for (Annotation repeatableMetaAnn : testClassFromTCCL @@ -351,18 +369,15 @@ private void collectMetaAnnotations(Class testClassFromTCCL, Function testClassFromTCCL, Function testClassFromTCCL, Function testResourceClass, + private static void addTestResourceEntry(Class testResourceClass, ResourceArg[] argsAnnotationValue, Annotation originalAnnotation, - Set uniqueEntries, boolean parallel) { + boolean parallel, TestResourceScope scope, Set uniqueEntries) { // NOTE: we don't need to check restrictToAnnotatedClass because by design config-based annotations // are not discovered outside the test class, so they're restricted @@ -391,34 +405,26 @@ private void addTestResourceEntry(Class uniqueEntries) { - addTestResourceEntry(quarkusTestResource.value(), quarkusTestResource.initArgs(), originalAnnotation, uniqueEntries, - quarkusTestResource.parallel()); + addTestResourceEntry(quarkusTestResource.value(), quarkusTestResource.initArgs(), originalAnnotation, + quarkusTestResource.parallel(), quarkusTestResource.restrictToAnnotatedClass() ? RESTRICTED_TO_CLASS : GLOBAL, + uniqueEntries); } - private void addTestResourceEntry(QuarkusTestResource quarkusTestResource, Annotation originalAnnotation, + private static void addTestResourceEntry(QuarkusTestResource quarkusTestResource, Annotation originalAnnotation, Set uniqueEntries) { - addTestResourceEntry(quarkusTestResource.value(), quarkusTestResource.initArgs(), originalAnnotation, uniqueEntries, - quarkusTestResource.parallel()); - } - - @SuppressWarnings("unchecked") - private Class loadTestResourceClassFromTCCL(String className) { - try { - return (Class) Class.forName(className, true, - Thread.currentThread().getContextClassLoader()); - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } + addTestResourceEntry(quarkusTestResource.value(), quarkusTestResource.initArgs(), originalAnnotation, + quarkusTestResource.parallel(), quarkusTestResource.restrictToAnnotatedClass() ? RESTRICTED_TO_CLASS : GLOBAL, + uniqueEntries); } - private Collection findQuarkusTestResourceInstances(Class testClass, IndexView index) { + private static Collection findTestResourceInstancesOfClass(Class testClass, IndexView index) { // collect all test supertypes for matching per-test targets Set testClasses = new HashSet<>(); Class current = testClass; @@ -457,12 +463,13 @@ private Collection findQuarkusTestResourceInstances(Class return testResourceAnnotations; } - // NOTE: called by reflection in QuarkusTestExtension - public boolean hasPerTestResources() { - return hasPerTestResources; + // also called by reflection in QuarkusTestExtension + public TestResourceScope narrowestScope() { + return narrowestScope; } - private boolean keepTestResourceAnnotation(AnnotationInstance annotation, ClassInfo targetClass, Set testClasses) { + private static boolean keepTestResourceAnnotation(AnnotationInstance annotation, ClassInfo targetClass, + Set testClasses) { if (targetClass.isAnnotation()) { // meta-annotations have already been handled in collectMetaAnnotations return false; @@ -475,44 +482,51 @@ private boolean keepTestResourceAnnotation(AnnotationInstance annotation, ClassI return true; } - private boolean restrictToAnnotatedClass(AnnotationInstance annotation) { - AnnotationValue restrict = annotation.value("restrictToAnnotatedClass"); - - return (annotation.name().equals(WITH_TEST_RESOURCE) && ((restrict == null) || restrict.asBoolean())) - || - (annotation.name().equals(QUARKUS_TEST_RESOURCE) && ((restrict != null) && restrict.asBoolean())); + private static boolean restrictToAnnotatedClass(AnnotationInstance annotation) { + return TestResourceClassEntryHandler.determineScope(annotation) != GLOBAL; } + /** + * Contains all the metadata that is needed to handle the lifecycle and perform all the bookkeeping associated + * with a Test Resource. + * When this information is produced by {@link TestResourceManager}, nothing has yet been started, so interrogating + * it is perfectly fine. + */ public static class TestResourceClassEntry { - private Class clazz; - private Map args; - private boolean parallel; - private Annotation configAnnotation; + private final Class clazz; + private final Map args; + private final boolean parallel; + private final Annotation configAnnotation; + private final TestResourceScope scope; public TestResourceClassEntry(Class clazz, Map args, Annotation configAnnotation, - boolean parallel) { + boolean parallel, + TestResourceScope scope) { this.clazz = clazz; this.args = args; this.configAnnotation = configAnnotation; this.parallel = parallel; + this.scope = scope; } @Override - public boolean equals(Object o) { - if (this == o) + public boolean equals(Object object) { + if (this == object) { return true; - if (o == null || getClass() != o.getClass()) + } + if (object == null || getClass() != object.getClass()) { return false; - TestResourceClassEntry that = (TestResourceClassEntry) o; - return clazz.equals(that.clazz) && args.equals(that.args) && Objects.equals(configAnnotation, that.configAnnotation) - && parallel == that.parallel; + } + TestResourceClassEntry entry = (TestResourceClassEntry) object; + return parallel == entry.parallel && Objects.equals(clazz, entry.clazz) && Objects.equals(args, + entry.args) && Objects.equals(configAnnotation, entry.configAnnotation) && scope == entry.scope; } @Override public int hashCode() { - return Objects.hash(clazz, args, configAnnotation, parallel); + return Objects.hash(clazz, args, parallel, configAnnotation, scope); } public boolean isParallel() { @@ -520,16 +534,16 @@ public boolean isParallel() { } } - private static class TestResourceEntryRunnable implements Runnable { - private final List entries; + private static class TestResourceRunnable implements Runnable { + private final List entries; private final Map allProps; - public TestResourceEntryRunnable(TestResourceEntry entry, + public TestResourceRunnable(TestResourceStartInfo entry, Map allProps) { this(Collections.singletonList(entry), allProps); } - public TestResourceEntryRunnable(List entries, + public TestResourceRunnable(List entries, Map allProps) { this.entries = entries; this.allProps = allProps; @@ -537,7 +551,7 @@ public TestResourceEntryRunnable(List entries, @Override public void run() { - for (TestResourceEntry entry : entries) { + for (TestResourceStartInfo entry : entries) { try { Map start = entry.getTestResource().start(); if (start != null) { @@ -551,17 +565,20 @@ public void run() { } } - private static class TestResourceEntry { + /** + * Contains all the information necessary to start a {@link QuarkusTestResourceLifecycleManager} + */ + private static class TestResourceStartInfo { private final QuarkusTestResourceLifecycleManager testResource; private final Map args; private final Annotation configAnnotation; - public TestResourceEntry(QuarkusTestResourceLifecycleManager testResource) { + public TestResourceStartInfo(QuarkusTestResourceLifecycleManager testResource) { this(testResource, Collections.emptyMap(), null); } - public TestResourceEntry(QuarkusTestResourceLifecycleManager testResource, Map args, + public TestResourceStartInfo(QuarkusTestResourceLifecycleManager testResource, Map args, Annotation configAnnotation) { this.testResource = testResource; this.args = args; @@ -614,4 +631,144 @@ public void injectIntoFields(Object fieldValue, Predicate predicate) { } + private sealed interface TestResourceClassEntryHandler + permits QuarkusTestResourceTestResourceClassEntryHandler, WithTestResourceTestResourceClassEntryHandler { + + List HANDLERS = List + .of(new QuarkusTestResourceTestResourceClassEntryHandler(), + new WithTestResourceTestResourceClassEntryHandler()); + + static TestResourceScope determineScope(AnnotationInstance annotation) { + for (TestResourceClassEntryHandler handler : HANDLERS) { + if (handler.appliesTo(annotation)) { + return handler.scope(annotation); + } + } + throw new IllegalStateException("Annotation '" + annotation.name() + "' is not supported"); + } + + static TestResourceClassEntry produceEntry(AnnotationInstance annotation) { + for (TestResourceClassEntryHandler producer : TestResourceClassEntryHandler.HANDLERS) { + if (producer.appliesTo(annotation)) { + return producer.produce(annotation); + } + } + throw new IllegalStateException("Annotation '" + annotation.name() + "' is not supported"); + } + + boolean appliesTo(AnnotationInstance annotation); + + TestResourceScope scope(AnnotationInstance annotationInstance); + + TestResourceClassEntry produce(AnnotationInstance annotation); + } + + private static abstract class AbstractTestResourceClassEntryHandler { + + Class lifecycleManager(AnnotationInstance annotation) { + return loadTestResourceClassFromTCCL(annotation.value().asString()); + } + + @SuppressWarnings("unchecked") + Class loadTestResourceClassFromTCCL(String className) { + try { + return (Class) Class.forName(className, true, + Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + Map args(AnnotationInstance annotation) { + AnnotationValue argsAnnotationValue = annotation.value("initArgs"); + Map args; + if (argsAnnotationValue == null) { + return Collections.emptyMap(); + } else { + args = new HashMap<>(); + AnnotationInstance[] resourceArgsInstances = argsAnnotationValue.asNestedArray(); + for (AnnotationInstance resourceArgsInstance : resourceArgsInstances) { + args.put(resourceArgsInstance.value("name").asString(), resourceArgsInstance.value().asString()); + } + return args; + } + } + + boolean isParallel(AnnotationInstance annotation) { + AnnotationValue parallelAnnotationValue = annotation.value("parallel"); + if (parallelAnnotationValue != null) { + return parallelAnnotationValue.asBoolean(); + } + return false; + } + } + + private static final class QuarkusTestResourceTestResourceClassEntryHandler extends + AbstractTestResourceClassEntryHandler + implements TestResourceClassEntryHandler { + + @Override + public boolean appliesTo(AnnotationInstance annotation) { + return QUARKUS_TEST_RESOURCE.equals(annotation.name()); + } + + @Override + public TestResourceClassEntry produce(AnnotationInstance annotation) { + return new TestResourceClassEntry(lifecycleManager(annotation), args(annotation), null, isParallel(annotation), + scope(annotation)); + } + + @Override + public TestResourceScope scope(AnnotationInstance annotation) { + TestResourceScope scope = GLOBAL; + AnnotationValue restrict = annotation.value("restrictToAnnotatedClass"); + if (restrict != null) { + if (restrict.asBoolean()) { + scope = RESTRICTED_TO_CLASS; + } + } + return scope; + } + } + + private static final class WithTestResourceTestResourceClassEntryHandler extends + AbstractTestResourceClassEntryHandler + implements TestResourceClassEntryHandler { + + @Override + public boolean appliesTo(AnnotationInstance annotation) { + return WITH_TEST_RESOURCE.equals(annotation.name()); + } + + @Override + public TestResourceClassEntry produce(AnnotationInstance annotation) { + return new TestResourceClassEntry(lifecycleManager(annotation), args(annotation), null, isParallel(annotation), + scope(annotation)); + } + + @Override + public TestResourceScope scope(AnnotationInstance annotation) { + TestResourceScope scope = RESTRICTED_TO_CLASS; + AnnotationValue restrict = annotation.value("restrictToAnnotatedClass"); + if (restrict != null) { + if (!restrict.asBoolean()) { + scope = GLOBAL; + } + } + return scope; + } + } + + private static class ProfileMetaAnnotationsUniqueEntriesConsumer implements Consumer> { + private final Class profileClassFromTCCL; + + public ProfileMetaAnnotationsUniqueEntriesConsumer(Class profileClassFromTCCL) { + this.profileClassFromTCCL = profileClassFromTCCL; + } + + @Override + public void accept(Set entries) { + collectMetaAnnotations(profileClassFromTCCL, Class::getSuperclass, entries); + } + } } diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceScope.java b/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceScope.java new file mode 100644 index 00000000000000..33dfba611d66d8 --- /dev/null +++ b/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceScope.java @@ -0,0 +1,14 @@ +package io.quarkus.test.common; + +/** + * Defines how Quarkus behaves with regard to the application of the resource to this test and the testsuite in general + */ +public enum TestResourceScope { + + /** + * The declaration order must be from the narrowest scope to the widest + */ + RESTRICTED_TO_CLASS, // means that Quarkus will run the test in complete isolation, i.e. it will restart every time it finds such a resource + MATCHING_RESOURCE, // means that Quarkus will not restart when running consecutive tests that use the same resource + GLOBAL, // means the resource applies to all tests in the testsuite +} 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 ec8e3cb970ada5..c20675e1119766 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 @@ -6,7 +6,6 @@ import java.io.FileOutputStream; import java.io.IOException; -import java.lang.annotation.Annotation; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -41,9 +40,9 @@ 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.TestClassIndexer; -import io.quarkus.test.common.WithTestResource; +import io.quarkus.test.common.TestResourceManager; +import io.quarkus.test.common.TestResourceScope; public class AbstractJvmQuarkusTestExtension extends AbstractQuarkusTestWithContextExtension { @@ -307,41 +306,14 @@ private Class findTestProfileAnnotation(Class c return null; } - protected static boolean hasPerTestResources(ExtensionContext extensionContext) { - return hasPerTestResources(extensionContext.getRequiredTestClass()); + protected static boolean newTestClassHasPerTestResources(ExtensionContext extensionContext) { + return newTestClassHasPerTestResources(extensionContext.getRequiredTestClass()); } - public static boolean hasPerTestResources(Class requiredTestClass) { - while (requiredTestClass != Object.class) { - for (WithTestResource testResource : requiredTestClass.getAnnotationsByType(WithTestResource.class)) { - if (testResource.restrictToAnnotatedClass()) { - return true; - } - } - - for (QuarkusTestResource testResource : requiredTestClass.getAnnotationsByType(QuarkusTestResource.class)) { - if (testResource.restrictToAnnotatedClass()) { - return true; - } - } - // scan for meta-annotations - for (Annotation annotation : requiredTestClass.getAnnotations()) { - // skip TestResource annotations - var annotationType = annotation.annotationType(); - - if ((annotationType != WithTestResource.class) && (annotationType != QuarkusTestResource.class)) { - // look for a TestResource on the annotation itself - if ((annotationType.getAnnotationsByType(WithTestResource.class).length > 0) - || (annotationType.getAnnotationsByType(QuarkusTestResource.class).length > 0)) { - // meta-annotations are per-test scoped for now - return true; - } - } - } - // look up - requiredTestClass = requiredTestClass.getSuperclass(); - } - return false; + private static boolean newTestClassHasPerTestResources(Class requiredTestClass) { + var entries = TestResourceManager.getUniqueTestResourceClassEntries(requiredTestClass, + getTestClassesLocation(requiredTestClass)); + return TestResourceManager.narrowestScope(entries) != TestResourceScope.GLOBAL; } protected static class PrepareResult { 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..8243b66f547e1a 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 @@ -8,8 +8,6 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.UncheckedIOException; -import java.lang.annotation.Annotation; -import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.net.URI; @@ -19,10 +17,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.security.CodeSource; -import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Properties; @@ -155,38 +150,6 @@ static TestProfileAndProperties determineTestProfileAndProperties(Class List getAdditionalTestResources( - QuarkusTestProfile profileInstance, ClassLoader classLoader) { - if ((profileInstance == null) || profileInstance.testResources().isEmpty()) { - return Collections.emptyList(); - } - - try { - Constructor testResourceClassEntryConstructor = Class - .forName(TestResourceManager.TestResourceClassEntry.class.getName(), true, classLoader) - .getConstructor(Class.class, Map.class, Annotation.class, boolean.class); - - List testResources = profileInstance.testResources(); - List result = new ArrayList<>(testResources.size()); - for (QuarkusTestProfile.TestResourceEntry testResource : testResources) { - T instance = (T) testResourceClassEntryConstructor.newInstance( - Class.forName(testResource.getClazz().getName(), true, classLoader), testResource.getArgs(), - null, testResource.isParallel()); - result.add(instance); - } - - return result; - } catch (Exception e) { - throw new IllegalStateException("Unable to handle profile " + profileInstance.getClass(), e); - } - } - static void startLauncher(ArtifactLauncher launcher, Map additionalProperties, Runnable sslSetter) throws IOException { launcher.includeAsSysProps(additionalProperties); diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java index d63eff417389e3..f05e6507390e11 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusIntegrationTestExtension.java @@ -8,12 +8,12 @@ import static io.quarkus.test.junit.IntegrationTestUtil.doProcessTestInstance; import static io.quarkus.test.junit.IntegrationTestUtil.ensureNoInjectAnnotationIsUsed; import static io.quarkus.test.junit.IntegrationTestUtil.findProfile; -import static io.quarkus.test.junit.IntegrationTestUtil.getAdditionalTestResources; import static io.quarkus.test.junit.IntegrationTestUtil.getArtifactType; import static io.quarkus.test.junit.IntegrationTestUtil.getSysPropsToRestore; import static io.quarkus.test.junit.IntegrationTestUtil.handleDevServices; import static io.quarkus.test.junit.IntegrationTestUtil.readQuarkusArtifactProperties; import static io.quarkus.test.junit.IntegrationTestUtil.startLauncher; +import static io.quarkus.test.junit.TestResourceManagerReflections.copyEntriesFromProfile; import java.io.Closeable; import java.io.File; @@ -56,6 +56,7 @@ import io.quarkus.test.common.TestConfigUtil; import io.quarkus.test.common.TestHostLauncher; import io.quarkus.test.common.TestResourceManager; +import io.quarkus.test.common.TestResourceScope; import io.quarkus.test.common.TestScopeManager; import io.quarkus.test.junit.callback.QuarkusTestMethodContext; import io.quarkus.test.junit.launcher.ArtifactLauncherProvider; @@ -155,7 +156,7 @@ private QuarkusTestExtensionState ensureStarted(ExtensionContext extensionContex } // we reload the test resources if we changed test class and if we had or will have per-test test resources boolean reloadTestResources = isNewTestClass - && (hasPerTestResources || QuarkusTestExtension.hasPerTestResources(extensionContext)); + && (hasPerTestResources || QuarkusTestExtension.newTestClassHasPerTestResources(extensionContext)); if ((state == null && !failedBoot) || wrongProfile || reloadTestResources) { if (wrongProfile || reloadTestResources) { if (state != null) { @@ -217,7 +218,7 @@ private QuarkusTestExtensionState doProcessStart(Properties quarkusArtifactPrope TestProfileAndProperties testProfileAndProperties = determineTestProfileAndProperties(profile, sysPropRestore); testResourceManager = new TestResourceManager(requiredTestClass, quarkusTestProfile, - getAdditionalTestResources(testProfileAndProperties.testProfile, + copyEntriesFromProfile(testProfileAndProperties.testProfile, context.getRequiredTestClass().getClassLoader()), testProfileAndProperties.testProfile != null && testProfileAndProperties.testProfile.disableGlobalTestResources(), @@ -225,7 +226,7 @@ private QuarkusTestExtensionState doProcessStart(Properties quarkusArtifactPrope testResourceManager.init( testProfileAndProperties.testProfile != null ? testProfileAndProperties.testProfile.getClass().getName() : null); - hasPerTestResources = testResourceManager.hasPerTestResources(); + hasPerTestResources = testResourceManager.narrowestScope() != TestResourceScope.GLOBAL; if (isCallbacksEnabledForIntegrationTests()) { populateCallbacks(requiredTestClass.getClassLoader()); } diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainIntegrationTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainIntegrationTestExtension.java index deb98903ab1ad0..7d7290fc8ecfcb 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainIntegrationTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusMainIntegrationTestExtension.java @@ -3,10 +3,10 @@ import static io.quarkus.test.junit.IntegrationTestUtil.activateLogging; import static io.quarkus.test.junit.IntegrationTestUtil.determineBuildOutputDirectory; import static io.quarkus.test.junit.IntegrationTestUtil.determineTestProfileAndProperties; -import static io.quarkus.test.junit.IntegrationTestUtil.getAdditionalTestResources; import static io.quarkus.test.junit.IntegrationTestUtil.getSysPropsToRestore; import static io.quarkus.test.junit.IntegrationTestUtil.handleDevServices; import static io.quarkus.test.junit.IntegrationTestUtil.readQuarkusArtifactProperties; +import static io.quarkus.test.junit.TestResourceManagerReflections.copyEntriesFromProfile; import java.nio.charset.StandardCharsets; import java.nio.file.Path; @@ -122,7 +122,7 @@ private ArtifactLauncher.LaunchResult doProcessStart(ExtensionContext context, S TestProfileAndProperties testProfileAndProperties = determineTestProfileAndProperties(profile, sysPropRestore); testResourceManager = new TestResourceManager(requiredTestClass, profile, - getAdditionalTestResources(testProfileAndProperties.testProfile, + copyEntriesFromProfile(testProfileAndProperties.testProfile, context.getRequiredTestClass().getClassLoader()), testProfileAndProperties.testProfile != null && testProfileAndProperties.testProfile.disableGlobalTestResources()); 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 744e35bc67d0ad..8201b9615905c8 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 @@ -1,7 +1,7 @@ package io.quarkus.test.junit; import static io.quarkus.test.junit.IntegrationTestUtil.activateLogging; -import static io.quarkus.test.junit.IntegrationTestUtil.getAdditionalTestResources; +import static io.quarkus.test.junit.TestResourceManagerReflections.copyEntriesFromProfile; import java.io.Closeable; import java.lang.reflect.Method; @@ -65,7 +65,7 @@ private void ensurePrepared(ExtensionContext extensionContext, Class quarkusTestMethodContextClass; - private static boolean hasPerTestResources; + private static boolean runningHasPerTestResources; private static List, String>> testHttpEndpointProviders; private static List testMethodInvokers; @@ -221,21 +221,22 @@ public Thread newThread(Runnable r) { populateDeepCloneField(startupAction); //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) - .newInstance(requiredTestClass, - profile != null ? profile : null, - getAdditionalTestResources(profileInstance, startupAction.getClassLoader()), - profileInstance != null && profileInstance.disableGlobalTestResources(), - startupAction.getDevServicesProperties(), Optional.empty(), result.testClassLocation); - testResourceManager.getClass().getMethod("init", String.class).invoke(testResourceManager, - profile != null ? profile.getName() : null); - Map properties = (Map) testResourceManager.getClass().getMethod("start") - .invoke(testResourceManager); + Class testResourceManagerClass = startupAction.getClassLoader().loadClass(TestResourceManager.class.getName()); + testResourceManager = TestResourceManagerReflections.createReflectively(testResourceManagerClass, + requiredTestClass, + profile, + TestResourceManagerReflections.copyEntriesFromProfile(profileInstance, + startupAction.getClassLoader()), + profileInstance != null && profileInstance.disableGlobalTestResources(), + startupAction.getDevServicesProperties(), + Optional.empty(), + result.testClassLocation); + TestResourceManagerReflections.initReflectively(testResourceManager, profile); + Map properties = TestResourceManagerReflections.startReflectively(testResourceManager); startupAction.overrideConfig(properties); startupAction.addRuntimeCloseTask(testResourceManager); - hasPerTestResources = (boolean) testResourceManager.getClass().getMethod("hasPerTestResources") - .invoke(testResourceManager); + TestResourceScope narrowestScope = TestResourceManagerReflections.narrowestScope(testResourceManager); + runningHasPerTestResources = narrowestScope != TestResourceScope.GLOBAL; // make sure that we start over every time we populate the callbacks // otherwise previous runs of QuarkusTest (with different TestProfile values can leak into the new run) @@ -594,7 +595,8 @@ private QuarkusTestExtensionState ensureStarted(ExtensionContext extensionContex 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)); + boolean reloadTestResources = isNewTestClass && (runningHasPerTestResources + || newTestClassHasPerTestResources(extensionContext)); if ((state == null && !failedBoot) || wrongProfile || reloadTestResources) { if (wrongProfile || reloadTestResources) { if (state != null) { diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/TestResourceManagerReflections.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/TestResourceManagerReflections.java new file mode 100644 index 00000000000000..a8b3bb14dae03e --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/TestResourceManagerReflections.java @@ -0,0 +1,94 @@ +package io.quarkus.test.junit; + +import java.io.Closeable; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import io.quarkus.test.common.TestResourceManager; +import io.quarkus.test.common.TestResourceScope; + +/** + * Contains a bunch of utilities that are needed for handling {@link TestResourceManager} + * via reflection (due to different classloaders) + */ +@SuppressWarnings("unchecked") +final class TestResourceManagerReflections { + + private TestResourceManagerReflections() { + } + + /** + * Since {@link TestResourceManager} is loaded from the ClassLoader passed in as an argument, + * we need to convert the user input {@link QuarkusTestProfile.TestResourceEntry} into instances of + * {@link TestResourceManager.TestResourceClassEntry} that are loaded from that ClassLoader + */ + static List copyEntriesFromProfile( + QuarkusTestProfile profileInstance, ClassLoader classLoader) { + if ((profileInstance == null) || profileInstance.testResources().isEmpty()) { + return Collections.emptyList(); + } + + try { + Class testResourceScopeClass = classLoader.loadClass(TestResourceScope.class.getName()); + Constructor testResourceClassEntryConstructor = Class + .forName(TestResourceManager.TestResourceClassEntry.class.getName(), true, classLoader) + .getConstructor(Class.class, Map.class, Annotation.class, boolean.class, testResourceScopeClass); + + List testResources = profileInstance.testResources(); + List result = new ArrayList<>(testResources.size()); + for (QuarkusTestProfile.TestResourceEntry testResource : testResources) { + T instance = (T) testResourceClassEntryConstructor.newInstance( + Class.forName(testResource.getClazz().getName(), true, classLoader), testResource.getArgs(), + null, testResource.isParallel(), + Enum.valueOf(testResourceScopeClass, TestResourceScope.RESTRICTED_TO_CLASS.name())); + result.add(instance); + } + + return result; + } catch (Exception e) { + throw new IllegalStateException("Unable to handle profile " + profileInstance.getClass(), e); + } + } + + public static Closeable createReflectively(Class testResourceManagerClass, + Class testClass, + Class profileClass, + List additionalTestResources, + boolean disableGlobalTestResources, + Map devServicesProperties, + Optional containerNetworkId, + Path testClassLocation) + throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + return (Closeable) testResourceManagerClass + .getConstructor(Class.class, Class.class, List.class, boolean.class, Map.class, Optional.class, Path.class) + .newInstance(testClass, profileClass, additionalTestResources, disableGlobalTestResources, + devServicesProperties, containerNetworkId, testClassLocation); + } + + public static void initReflectively(Object testResourceManager, Class profileClassName) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + testResourceManager.getClass().getMethod("init", String.class).invoke(testResourceManager, + profileClassName != null ? profileClassName.getName() : null); + } + + public static Map startReflectively(Object testResourceManager) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + return (Map) testResourceManager.getClass().getMethod("start") + .invoke(testResourceManager); + } + + public static TestResourceScope narrowestScope(Object testResourceManager) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + var result = testResourceManager.getClass().getMethod("narrowestScope") + .invoke(testResourceManager); + // "copy" to the Classloader of the caller + return TestResourceScope.valueOf(result.toString()); + } +}