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 bc73a6cd5883d..d9bca8af65bb9 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,9 @@ package io.quarkus.test.common; +import static io.quarkus.test.common.TestResourceScope.GLOBAL; +import static io.quarkus.test.common.TestResourceScope.MATCHING_RESOURCE; +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 +27,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; @@ -35,41 +40,60 @@ import io.smallrye.config.SmallRyeConfigProviderResolver; +/** + * Manages {@link QuarkusTestResourceLifecycleManager} + */ public class TestResourceManager implements Closeable { private static final DotName QUARKUS_TEST_RESOURCE = DotName.createSimple(QuarkusTestResource.class); 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 Set testResourceComparisonInfo; + 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<>(); + /** + * This is safe to call as it doesn't do anything other than create state - no {@link QuarkusTestResourceLifecycleManager} + * is ever touched + * at this stage. + */ + 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 @@ -77,14 +101,22 @@ public TestResourceManager(Class testClass, Class profileClass, List(additionalTestResources); } else { - uniqueEntries = getUniqueTestResourceClassEntries(testClassLocation, testClass, profileClass, + uniqueEntries = uniqueTestResourceClassEntries(testClassLocation, testClass, profileClass, additionalTestResources); } + + this.testResourceComparisonInfo = new HashSet<>(); + for (TestResourceClassEntry uniqueEntry : uniqueEntries) { + testResourceComparisonInfo.add(new TestResourceComparisonInfo( + uniqueEntry.testResourceLifecycleManagerClass().getName(), uniqueEntry.getScope())); + } + Set 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,7 +128,7 @@ 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); } @@ -108,7 +140,7 @@ public void setTestErrorCause(Throwable 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 +170,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 +195,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 +207,7 @@ public void close() { return; } started = false; - for (TestResourceEntry entry : allTestResourceEntries) { + for (TestResourceStartInfo entry : allTestResources) { try { entry.getTestResource().stop(); } catch (Exception e) { @@ -205,17 +237,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 +259,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 +268,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 +285,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 @@ -267,82 +300,80 @@ private TestResourceManager.TestResourceEntry buildTestResourceEntry(TestResourc } } - private Set getUniqueTestResourceClassEntries(Path testClassLocation, Class testClass, + private Set uniqueTestResourceClassEntries(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(); - } + /** + * Allows Quarkus to extra basic information about which test resources a test class will require + */ + public static Set testResourceComparisonInfo(Class testClass, + Path testClassLocation) { + Set uniqueEntries = getUniqueTestResourceClassEntries(testClass, testClassLocation, null); + if (uniqueEntries.isEmpty()) { + return Collections.emptySet(); + } + Set result = new HashSet<>(uniqueEntries.size()); + for (TestResourceClassEntry entry : uniqueEntries) { + result.add(new TestResourceComparisonInfo(entry.testResourceLifecycleManagerClass().getName(), entry.getScope())); + } + return result; + } - 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 +382,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 +418,26 @@ private void addTestResourceEntry(Class uniqueEntries) { - addTestResourceEntry(quarkusTestResource.value(), quarkusTestResource.initArgs(), originalAnnotation, uniqueEntries, - quarkusTestResource.parallel()); + addTestResourceEntry(quarkusTestResource.value(), quarkusTestResource.initArgs(), originalAnnotation, + quarkusTestResource.parallel(), quarkusTestResource.scope(), + 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 +476,8 @@ private Collection findQuarkusTestResourceInstances(Class return testResourceAnnotations; } - // NOTE: called by reflection in QuarkusTestExtension - public boolean hasPerTestResources() { - return hasPerTestResources; - } - - 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,61 +490,138 @@ private boolean keepTestResourceAnnotation(AnnotationInstance annotation, ClassI return true; } - private boolean restrictToAnnotatedClass(AnnotationInstance annotation) { - AnnotationValue restrict = annotation.value("restrictToAnnotatedClass"); + private static boolean restrictToAnnotatedClass(AnnotationInstance annotation) { + return TestResourceClassEntryHandler.determineScope(annotation) == RESTRICTED_TO_CLASS; + } + + /** + * Provides the basic information needed for comparing the test resources currently in use + */ + @SuppressWarnings("unused") // called with reflection in TestResourceManagerReflections + public Set testResourceComparisonInfo() { + return testResourceComparisonInfo; + } + + /** + * Quarkus needs to restart if one of the following is true: + *
    + *
  • at least one the existing test resources is restricted to the test class
  • + *
  • at least one the next test resources is restricted to the test class
  • + *
  • different {@code MATCHING_RESOURCE} scoped test resources are being used
  • + *
+ */ + public static boolean testResourcesRequireReload(Set existing, + Set next) { + if (existing.isEmpty() && next.isEmpty()) { + return false; + } + + if (hasRestrictedToClassScope(existing) || hasRestrictedToClassScope(next)) { + return true; + } + + // now we need to check whether the sets contain the exact same MATCHING_RESOURCE test resources + + Set inExistingAndNotNext = onlyMatchingResourceItems(existing); + inExistingAndNotNext.removeAll(next); + if (!inExistingAndNotNext.isEmpty()) { + return true; + } + + Set inNextAndNotExisting = onlyMatchingResourceItems(next); + inNextAndNotExisting.removeAll(existing); + if (!inNextAndNotExisting.isEmpty()) { + return true; + } + + // the sets contain the same objects, so no need to reload + return false; + } + + private static Set onlyMatchingResourceItems(Set set) { + return set.stream().filter(i -> i.scope == MATCHING_RESOURCE).collect( + Collectors.toSet()); + } - return (annotation.name().equals(WITH_TEST_RESOURCE) && ((restrict == null) || restrict.asBoolean())) - || - (annotation.name().equals(QUARKUS_TEST_RESOURCE) && ((restrict != null) && restrict.asBoolean())); + private static boolean hasRestrictedToClassScope(Set existing) { + for (TestResourceComparisonInfo info : existing) { + if (info.scope == RESTRICTED_TO_CLASS) { + return true; + } + } + return false; } + /** + * 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() { return parallel; } + + public Class testResourceLifecycleManagerClass() { + return clazz; + } + + public TestResourceScope getScope() { + return scope; + } + } + + public record TestResourceComparisonInfo(String testResourceLifecycleManagerClass, TestResourceScope scope) { + } - 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 +629,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 +643,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 +709,164 @@ public void injectIntoFields(Object fieldValue, Predicate predicate) { } + /** + * The entry point to handling the differences between {@link QuarkusTestResource} and {@link WithTestResource} + * (and whatever else we potentially come up with in the future). + */ + private sealed interface TestResourceClassEntryHandler + permits QuarkusTestResourceTestResourceClassEntryHandler, WithTestResourceTestResourceClassEntryHandler { + + List HANDLERS = List + .of(new QuarkusTestResourceTestResourceClassEntryHandler(), + new WithTestResourceTestResourceClassEntryHandler()); + + /** + * Find the {@link TestResourceScope} of the provided annotation + */ + 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"); + } + + /** + * Extract all the metadata needed from the provided annotation + */ + 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"); + } + + /** + * Whether the strategy applies to the current annotation + */ + boolean appliesTo(AnnotationInstance annotation); + + TestResourceScope scope(AnnotationInstance annotationInstance); + + TestResourceClassEntry produce(AnnotationInstance annotation); + } + + /** + * Hold code that is common for handling both {@link QuarkusTestResource} and {@link WithTestResource} + */ + 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; + } + } + + /** + * Handles {@link QuarkusTestResource} + */ + 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; + } + } + + /** + * Handles {@link WithTestResource} + */ + 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 = MATCHING_RESOURCE; + AnnotationValue restrict = annotation.value("scope"); + if (restrict != null) { + scope = TestResourceScope.valueOf(restrict.asEnum()); + } + 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 0000000000000..3ed851576a72f --- /dev/null +++ b/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceScope.java @@ -0,0 +1,25 @@ +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 + */ + + /** + * Means that Quarkus will run the test in complete isolation, i.e. it will restart every time it finds such a resource + */ + RESTRICTED_TO_CLASS, + /** + * Means that Quarkus will not restart when running consecutive tests that use the same resource + */ + MATCHING_RESOURCE, + + /** + * Means the resource applies to all tests in the testsuite + */ + GLOBAL +} diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/WithTestResource.java b/test-framework/common/src/main/java/io/quarkus/test/common/WithTestResource.java index a891640e1ae3d..b6dd17854f9a8 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/WithTestResource.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/WithTestResource.java @@ -13,11 +13,20 @@ /** * Used to define a test resource, which can affect various aspects of the application lifecycle. *

+ * As of Quarkus 3.16, the default behavior of the annotation (meaning that {@code scope} has not been set) + * is that test classes annotated with the same {@code WithTestResource} will not force a restart + * of Quarkus. + *

+ * The equivalent behavior to {@code QuarkusTestResource(restrictToAnnotatedClass = false)} is to use + * {@code WithTestResource(scope = TestResourceScope.GLOBAL)}, + * while the equivalent behavior to {@code QuarkusTestResource(restrictToAnnotatedClass = true)} is to use + * {@code WithTestResource(scope = TestResourceScope.RESTRICTED_TO_CLASS)}, + *

* WARNING: this annotation, introduced in 3.13, caused some issues so it was decided to undeprecate * {@link QuarkusTestResource} and rework the behavior of this annotation. For now, we recommend not using it * until we improve its behavior. *

- * Note: When using the {@code restrictToAnnotatedClass=true} (which is the default), each test that is annotated + * Note: When using the {@code scope=TestResourceScope.RESTRICTED_TO_CLASS}, each test that is annotated * with {@code @WithTestResource} will result in the application being re-augmented and restarted (in a similar fashion * as happens in dev-mode when a change is detected) in order to incorporate the settings configured by the annotation. * If there are many instances of the annotation used throughout the testsuite, this could result in slow test execution. @@ -28,14 +37,6 @@ * started before any test is run. *

* Note that test resources are never restarted when running {@code @Nested} test classes. - *

- * The only difference with {@link QuarkusTestResource} is that the default value for - * {@link #restrictToAnnotatedClass()} {@code == true}. - *

- *

- * This means that any resources managed by {@link #value()} apply to an individual test class or test profile, unlike with - * {@link QuarkusTestResource} where a resource applies to all test classes. - *

*/ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @@ -61,15 +62,11 @@ boolean parallel() default false; /** - * Whether this annotation should only be enabled if it is placed on the currently running test class or test profile. - * Note that this defaults to true for meta-annotations since meta-annotations are only considered - * for the current test class or test profile. - *

- * Note: When this is set to {@code true} (which is the default), the annotation {@code @WithTestResource} will result - * in the application being re-augmented and restarted (in a similar fashion as happens in dev-mode when a change is - * detected) in order to incorporate the settings configured by the annotation. + * Defines how Quarkus behaves with regard to the application of the resource to this test and the test-suite in general. + * The default is {@link TestResourceScope#MATCHING_RESOURCE} which means that if two tests are annotated with the same + * {@link WithTestResource} annotation, no restart will take place between tests. */ - boolean restrictToAnnotatedClass() default true; + TestResourceScope scope() default TestResourceScope.MATCHING_RESOURCE; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) diff --git a/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerInjectorTest.java b/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerInjectorTest.java index ae65a769ce40a..5c2b26a38481d 100644 --- a/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerInjectorTest.java +++ b/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerInjectorTest.java @@ -29,7 +29,7 @@ void testTestInjector(Class clazz) { Assertions.assertEquals("dummy", foo.dummy.value); } - @WithTestResource(value = UsingTestInjectorLifecycleManager.class, restrictToAnnotatedClass = false) + @WithTestResource(value = UsingTestInjectorLifecycleManager.class, scope = TestResourceScope.GLOBAL) public static class UsingInjectorTest { } diff --git a/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerReloadTest.java b/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerReloadTest.java new file mode 100644 index 0000000000000..94808bf0b17fb --- /dev/null +++ b/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerReloadTest.java @@ -0,0 +1,75 @@ +package io.quarkus.test.common; + +import static io.quarkus.test.common.TestResourceManager.testResourcesRequireReload; +import static io.quarkus.test.common.TestResourceScope.*; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collections; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.TestResourceManager.TestResourceComparisonInfo; + +public class TestResourceManagerReloadTest { + + @Test + public void emptyResources() { + assertFalse(testResourcesRequireReload(Collections.emptySet(), Set.of())); + } + + @Test + public void differentCount() { + assertTrue(testResourcesRequireReload(Collections.emptySet(), + Set.of(new TestResourceComparisonInfo("test", RESTRICTED_TO_CLASS)))); + + assertTrue(testResourcesRequireReload(Set.of(new TestResourceComparisonInfo("test", RESTRICTED_TO_CLASS)), + Collections.emptySet())); + } + + @Test + public void sameSingleRestrictedToClassResource() { + assertTrue(testResourcesRequireReload( + Set.of(new TestResourceComparisonInfo("test", RESTRICTED_TO_CLASS)), + Set.of(new TestResourceComparisonInfo("test", RESTRICTED_TO_CLASS)))); + } + + @Test + public void sameSingleMatchingResource() { + assertFalse(testResourcesRequireReload( + Set.of(new TestResourceComparisonInfo("test", MATCHING_RESOURCE)), + Set.of(new TestResourceComparisonInfo("test", MATCHING_RESOURCE)))); + } + + @Test + public void differentSingleMatchingResource() { + assertTrue(testResourcesRequireReload( + Set.of(new TestResourceComparisonInfo("test", MATCHING_RESOURCE)), + Set.of(new TestResourceComparisonInfo("test2", MATCHING_RESOURCE)))); + } + + @Test + public void sameMultipleMatchingResource() { + assertFalse(testResourcesRequireReload( + Set.of( + new TestResourceComparisonInfo("test", MATCHING_RESOURCE), + new TestResourceComparisonInfo("test2", MATCHING_RESOURCE), + new TestResourceComparisonInfo("test3", GLOBAL)), + Set.of(new TestResourceComparisonInfo("test3", GLOBAL), + new TestResourceComparisonInfo("test2", MATCHING_RESOURCE), + new TestResourceComparisonInfo("test", MATCHING_RESOURCE)))); + } + + @Test + public void differentMultipleMatchingResource() { + assertTrue(testResourcesRequireReload( + Set.of( + new TestResourceComparisonInfo("test", MATCHING_RESOURCE), + new TestResourceComparisonInfo("test2", MATCHING_RESOURCE), + new TestResourceComparisonInfo("test3", GLOBAL)), + Set.of(new TestResourceComparisonInfo("test3", GLOBAL), + new TestResourceComparisonInfo("test2", MATCHING_RESOURCE), + new TestResourceComparisonInfo("TEST", MATCHING_RESOURCE)))); + } +} diff --git a/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerTest.java b/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerTest.java index 0d83478dfbb2f..27c331420fd6a 100644 --- a/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerTest.java +++ b/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerTest.java @@ -49,8 +49,8 @@ void testParallelResourcesRunInParallel(Class clazz) { Assertions.assertEquals("value2", props.get("key2")); } - @WithTestResource(value = FirstLifecycleManager.class, restrictToAnnotatedClass = false) - @WithTestResource(value = SecondLifecycleManager.class, restrictToAnnotatedClass = false) + @WithTestResource(value = FirstLifecycleManager.class, scope = TestResourceScope.GLOBAL) + @WithTestResource(value = SecondLifecycleManager.class, scope = TestResourceScope.GLOBAL) public static class MyTest { } @@ -99,8 +99,8 @@ public void stop() { } } - @WithTestResource(value = FirstSequentialQuarkusTestResource.class, restrictToAnnotatedClass = false) - @WithTestResource(value = SecondSequentialQuarkusTestResource.class, restrictToAnnotatedClass = false) + @WithTestResource(value = FirstSequentialQuarkusTestResource.class, scope = TestResourceScope.GLOBAL) + @WithTestResource(value = SecondSequentialQuarkusTestResource.class, scope = TestResourceScope.GLOBAL) public static class SequentialTestResourcesTest { } @@ -150,8 +150,8 @@ public int order() { } } - @WithTestResource(value = FirstParallelQuarkusTestResource.class, parallel = true, restrictToAnnotatedClass = false) - @WithTestResource(value = SecondParallelQuarkusTestResource.class, parallel = true, restrictToAnnotatedClass = false) + @WithTestResource(value = FirstParallelQuarkusTestResource.class, parallel = true, scope = TestResourceScope.GLOBAL) + @WithTestResource(value = SecondParallelQuarkusTestResource.class, parallel = true, scope = TestResourceScope.GLOBAL) public static class ParallelTestResourcesTest { } @@ -257,7 +257,7 @@ public void stop() { } } - @WithTestResource(value = AnnotationBasedQuarkusTestResource.class, restrictToAnnotatedClass = false) + @WithTestResource(value = AnnotationBasedQuarkusTestResource.class, scope = TestResourceScope.GLOBAL) @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Repeatable(WithAnnotationBasedTestResource.List.class) 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 a7bf610f10f44..36500263f4638 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 @@ -5,7 +5,6 @@ import static io.quarkus.test.common.PathTestHelper.getTestClassesLocation; import java.io.IOException; -import java.lang.annotation.Annotation; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -39,10 +38,8 @@ 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 { @@ -270,43 +267,6 @@ private Class findTestProfileAnnotation(Class c return null; } - protected static boolean hasPerTestResources(ExtensionContext extensionContext) { - return hasPerTestResources(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; - } - 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/IntegrationTestUtil.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/IntegrationTestUtil.java index b44f78c962cf5..26953e90026c6 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 d63eff417389e..c2c6d43c99ac2 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,13 @@ 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.TestResourceUtil.testResourcesRequireReload; +import static io.quarkus.test.junit.TestResourceUtil.TestResourceManagerReflections.copyEntriesFromProfile; import java.io.Closeable; import java.io.File; @@ -75,7 +76,6 @@ public class QuarkusIntegrationTestExtension extends AbstractQuarkusTestWithCont 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 currentJUnitTestClass; - private static boolean hasPerTestResources; private static Map devServicesProps; private static String containerNetworkId; @@ -154,9 +154,9 @@ private QuarkusTestExtensionState ensureStarted(ExtensionContext extensionContex currentJUnitTestClass = extensionContext.getRequiredTestClass(); } // 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)); - if ((state == null && !failedBoot) || wrongProfile || reloadTestResources) { + boolean reloadTestResources = false; + if ((state == null && !failedBoot) || wrongProfile || (reloadTestResources = isNewTestClass + && TestResourceUtil.testResourcesRequireReload(state, extensionContext.getRequiredTestClass()))) { if (wrongProfile || reloadTestResources) { if (state != null) { try { @@ -217,7 +217,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 +225,7 @@ private QuarkusTestExtensionState doProcessStart(Properties quarkusArtifactPrope testResourceManager.init( testProfileAndProperties.testProfile != null ? testProfileAndProperties.testProfile.getClass().getName() : null); - hasPerTestResources = testResourceManager.hasPerTestResources(); + 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 deb98903ab1ad..a7dea2f0c0fc1 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.TestResourceUtil.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 d793d8e74b6c4..f68b87436684b 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.TestResourceUtil.TestResourceManagerReflections.copyEntriesFromProfile; import java.io.Closeable; import java.lang.reflect.Method; @@ -41,7 +41,6 @@ public class QuarkusMainTestExtension extends AbstractJvmQuarkusTestExtension AfterAllCallback { PrepareResult prepareResult; - private static boolean hasPerTestResources; /** * The result from an {@link Launch} test @@ -64,9 +63,9 @@ private void ensurePrepared(ExtensionContext extensionContext, Class properties = (Map) testResourceManager.getClass().getMethod("start") - .invoke(testResourceManager); + Class testResourceManagerClass = startupAction.getClassLoader().loadClass(TestResourceManager.class.getName()); + testResourceManager = TestResourceUtil.TestResourceManagerReflections.createReflectively(testResourceManagerClass, + context.getRequiredTestClass(), + profile, + copyEntriesFromProfile(profileInstance, startupAction.getClassLoader()), + profileInstance != null && profileInstance.disableGlobalTestResources(), + startupAction.getDevServicesProperties(), + Optional.empty()); + TestResourceUtil.TestResourceManagerReflections.initReflectively(testResourceManager, profile); + Map properties = TestResourceUtil.TestResourceManagerReflections + .startReflectively(testResourceManager); startupAction.overrideConfig(properties); - hasPerTestResources = (boolean) testResourceManager.getClass().getMethod("hasPerTestResources") - .invoke(testResourceManager); testResourceManager.getClass().getMethod("inject", Object.class) .invoke(testResourceManager, context.getRequiredTestInstance()); 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 2558f437c6dab..33e6a6175196d 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,7 +1,6 @@ package io.quarkus.test.junit; import static io.quarkus.test.junit.IntegrationTestUtil.activateLogging; -import static io.quarkus.test.junit.IntegrationTestUtil.getAdditionalTestResources; import java.io.Closeable; import java.io.IOException; @@ -123,7 +122,6 @@ public class QuarkusTestExtension extends AbstractJvmQuarkusTestExtension 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; - private static boolean hasPerTestResources; private static List, String>> testHttpEndpointProviders; private static List testMethodInvokers; @@ -220,21 +218,21 @@ 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 = TestResourceUtil.TestResourceManagerReflections.createReflectively(testResourceManagerClass, + requiredTestClass, + profile, + TestResourceUtil.TestResourceManagerReflections.copyEntriesFromProfile(profileInstance, + startupAction.getClassLoader()), + profileInstance != null && profileInstance.disableGlobalTestResources(), + startupAction.getDevServicesProperties(), + Optional.empty(), + result.testClassLocation); + TestResourceUtil.TestResourceManagerReflections.initReflectively(testResourceManager, profile); + Map properties = TestResourceUtil.TestResourceManagerReflections + .startReflectively(testResourceManager); startupAction.overrideConfig(properties); startupAction.addRuntimeCloseTask(testResourceManager); - hasPerTestResources = (boolean) testResourceManager.getClass().getMethod("hasPerTestResources") - .invoke(testResourceManager); // 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) @@ -584,8 +582,9 @@ 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)); - if ((state == null && !failedBoot) || wrongProfile || reloadTestResources) { + boolean reloadTestResources = false; + if ((state == null && !failedBoot) || wrongProfile || (reloadTestResources = isNewTestClass + && TestResourceUtil.testResourcesRequireReload(state, extensionContext.getRequiredTestClass()))) { if (wrongProfile || reloadTestResources) { if (state != null) { try { @@ -1175,7 +1174,7 @@ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext con + "' disabled because 'quarkus.profile.test.tags' don't match the tags of '" + testProfile + "'"); } - public class ExtensionState extends QuarkusTestExtensionState { + public static class ExtensionState extends QuarkusTestExtensionState { public ExtensionState(Closeable testResourceManager, Closeable resource) { super(testResourceManager, resource); diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/TestResourceUtil.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/TestResourceUtil.java new file mode 100644 index 0000000000000..2e3dbfcab7e85 --- /dev/null +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/TestResourceUtil.java @@ -0,0 +1,202 @@ +package io.quarkus.test.junit; + +import static io.quarkus.test.common.PathTestHelper.getTestClassesLocation; + +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.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import io.quarkus.test.common.TestResourceManager; +import io.quarkus.test.common.TestResourceScope; + +/** + * Contains methods that are needed for determining how to deal with {@link io.quarkus.test.common.QuarkusTestResource} and + * {@link io.quarkus.test.common.WithTestResource} + */ +final class TestResourceUtil { + + private TestResourceUtil() { + } + + /** + * This is where we decide if the test resources of the current state vs the ones required by the next test class + * to be executed require a Quarkus restart. + */ + static boolean testResourcesRequireReload(QuarkusTestExtensionState state, Class nextTestClass) { + Set existingTestResources = existingTestResources(state); + Set nextTestResources = nextTestResources(nextTestClass); + + return TestResourceManager.testResourcesRequireReload(existingTestResources, nextTestResources); + } + + static Set existingTestResources(QuarkusTestExtensionState state) { + if (state == null) { + return Collections.emptySet(); + } + Closeable closeable = state.testResourceManager; + if (closeable instanceof TestResourceManager testResourceManager) { + return TestResourceManagerReflections + .testResourceComparisonInfo(testResourceManager); + } + return Collections.emptySet(); + } + + static Set nextTestResources(Class requiredTestClass) { + return TestResourceManager + .testResourceComparisonInfo(requiredTestClass, getTestClassesLocation(requiredTestClass)); + } + + /** + * Contains a bunch of utilities that are needed for handling {@link TestResourceManager} + * via reflection (due to different classloaders) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + static 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); + } + } + + /** + * Corresponds to {@link TestResourceManager#TestResourceManager(Class, Class, List, boolean, Map, Optional, Path)} + */ + static Closeable createReflectively(Class testResourceManagerClass, + Class testClass, + Class profileClass, + List additionalTestResources, + boolean disableGlobalTestResources, + Map devServicesProperties, + Optional containerNetworkId, + Path testClassLocation) { + try { + 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); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException + | NoSuchMethodException | SecurityException e) { + throw new RuntimeException(e); + } + } + + /** + * Corresponds to {@link TestResourceManager#TestResourceManager(Class, Class, List, boolean, Map, Optional)} + */ + static Closeable createReflectively(Class testResourceManagerClass, + Class testClass, + Class profileClass, + List additionalTestResources, + boolean disableGlobalTestResources, + Map devServicesProperties, + Optional containerNetworkId) { + try { + return (Closeable) testResourceManagerClass + .getConstructor(Class.class, Class.class, List.class, boolean.class, Map.class, Optional.class) + .newInstance(testClass, profileClass, additionalTestResources, disableGlobalTestResources, + devServicesProperties, containerNetworkId); + } catch (InstantiationException | IllegalArgumentException | IllegalAccessException | InvocationTargetException + | NoSuchMethodException | SecurityException e) { + throw new RuntimeException(e); + } + } + + /** + * Corresponds to {@link TestResourceManager#init(String)} + */ + static void initReflectively(Object testResourceManager, Class profileClassName) { + try { + testResourceManager.getClass().getMethod("init", String.class).invoke(testResourceManager, + profileClassName != null ? profileClassName.getName() : null); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException + | SecurityException e) { + throw new RuntimeException(e); + } + } + + /** + * Corresponds to {@link TestResourceManager#start()} + */ + public static Map startReflectively(Object testResourceManager) { + try { + return (Map) testResourceManager.getClass().getMethod("start") + .invoke(testResourceManager); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException + | SecurityException e) { + throw new RuntimeException(e); + } + } + + /** + * Corresponds to {@link TestResourceManager#testResourceComparisonInfo()} + */ + static Set testResourceComparisonInfo(Object testResourceManager) { + try { + Set originalSet = (Set) testResourceManager.getClass().getMethod("testResourceComparisonInfo") + .invoke(testResourceManager); + if (originalSet.isEmpty()) { + return Collections.emptySet(); + } + + Set result = new HashSet<>(originalSet.size()); + for (var entry : originalSet) { + String testResourceLifecycleManagerClass = (String) entry.getClass() + .getMethod("testResourceLifecycleManagerClass").invoke(entry); + Object originalTestResourceScope = entry.getClass().getMethod("scope").invoke(entry); + TestResourceScope testResourceScope = null; + if (originalTestResourceScope != null) { + testResourceScope = TestResourceScope.valueOf(originalTestResourceScope.toString()); + } + result.add(new TestResourceManager.TestResourceComparisonInfo(testResourceLifecycleManagerClass, + testResourceScope)); + } + + return result; + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException + | SecurityException e) { + throw new RuntimeException(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 810150bec98ce..9607d14b5f7d7 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 @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Nested; import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.TestResourceScope; import io.quarkus.test.common.WithTestResource; import io.quarkus.test.junit.QuarkusIntegrationTest; import io.quarkus.test.junit.QuarkusTest; @@ -140,7 +141,9 @@ private ClassOrderer buildSecondaryOrderer(ClassOrdererContext context) { private boolean hasRestrictedResource(ClassDescriptor classDescriptor) { return classDescriptor.findRepeatableAnnotations(WithTestResource.class).stream() - .anyMatch(res -> res.restrictToAnnotatedClass() || isMetaTestResource(res, classDescriptor)) || + .anyMatch( + res -> res.scope() == TestResourceScope.RESTRICTED_TO_CLASS || isMetaTestResource(res, classDescriptor)) + || classDescriptor.findRepeatableAnnotations(QuarkusTestResource.class).stream() .anyMatch(res -> res.restrictToAnnotatedClass() || isMetaTestResource(res, classDescriptor)); } diff --git a/test-framework/junit5/src/test/java/io/quarkus/test/junit/util/QuarkusTestProfileAwareClassOrdererTest.java b/test-framework/junit5/src/test/java/io/quarkus/test/junit/util/QuarkusTestProfileAwareClassOrdererTest.java index 259a5f1347778..31d4edc559f50 100644 --- a/test-framework/junit5/src/test/java/io/quarkus/test/junit/util/QuarkusTestProfileAwareClassOrdererTest.java +++ b/test-framework/junit5/src/test/java/io/quarkus/test/junit/util/QuarkusTestProfileAwareClassOrdererTest.java @@ -28,6 +28,7 @@ import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import io.quarkus.test.common.TestResourceScope; import io.quarkus.test.common.WithTestResource; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.QuarkusTestProfile; @@ -205,7 +206,8 @@ private void quarkusWithTestResourceMock(ClassDescriptor mock, Class managerClass, boolean restrictToAnnotatedClass) { WithTestResource withResourceMock = Mockito.mock(WithTestResource.class, withSettings().strictness(Strictness.LENIENT)); doReturn(managerClass).when(withResourceMock).value(); - when(withResourceMock.restrictToAnnotatedClass()).thenReturn(restrictToAnnotatedClass); + when(withResourceMock.scope()).thenReturn( + restrictToAnnotatedClass ? TestResourceScope.RESTRICTED_TO_CLASS : TestResourceScope.MATCHING_RESOURCE); when(mock.findRepeatableAnnotations(WithTestResource.class)).thenReturn(List.of(withResourceMock)); } @@ -223,7 +225,7 @@ private static class Test01 { // this single made-up test class needs an actual annotation since the orderer will have to do the meta-check directly // because ClassDescriptor does not offer any details whether an annotation is directly annotated or meta-annotated - @WithTestResource(value = Manager3.class, restrictToAnnotatedClass = false) + @WithTestResource(value = Manager3.class, scope = TestResourceScope.GLOBAL) private static class Test02 { }