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 648dcf7e61fc1..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,6 +1,7 @@ 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; @@ -424,7 +425,7 @@ private static void addTestResourceEntry(WithTestResource quarkusTestResource, A Set uniqueEntries) { addTestResourceEntry(quarkusTestResource.value(), quarkusTestResource.initArgs(), originalAnnotation, - quarkusTestResource.parallel(), quarkusTestResource.restrictToAnnotatedClass() ? RESTRICTED_TO_CLASS : GLOBAL, + quarkusTestResource.parallel(), quarkusTestResource.scope(), uniqueEntries); } @@ -490,7 +491,7 @@ private static boolean keepTestResourceAnnotation(AnnotationInstance annotation, } private static boolean restrictToAnnotatedClass(AnnotationInstance annotation) { - return TestResourceClassEntryHandler.determineScope(annotation) != GLOBAL; + return TestResourceClassEntryHandler.determineScope(annotation) == RESTRICTED_TO_CLASS; } /** @@ -506,11 +507,40 @@ public Set testResourceComparisonInfo() { * */ public static boolean testResourcesRequireReload(Set existing, Set next) { - return hasRestrictedToClassScope(existing) || hasRestrictedToClassScope(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()); } private static boolean hasRestrictedToClassScope(Set existing) { @@ -818,12 +848,10 @@ public TestResourceClassEntry produce(AnnotationInstance annotation) { @Override public TestResourceScope scope(AnnotationInstance annotation) { - TestResourceScope scope = RESTRICTED_TO_CLASS; - AnnotationValue restrict = annotation.value("restrictToAnnotatedClass"); + TestResourceScope scope = MATCHING_RESOURCE; + AnnotationValue restrict = annotation.value("scope"); if (restrict != null) { - if (!restrict.asBoolean()) { - scope = GLOBAL; - } + scope = TestResourceScope.valueOf(restrict.asEnum()); } return scope; } 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 index 33dfba611d66d..3ed851576a72f 100644 --- 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 @@ -5,10 +5,21 @@ */ 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 + + /** + * 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/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 { }