Skip to content

Commit

Permalink
Change default behavior of @WithTestResource
Browse files Browse the repository at this point in the history
The new default @WithTestResource behavior is
for Quarkus to NOT restart when consecutive tests
are using the same @WithTestResource annotations.
  • Loading branch information
geoand committed Sep 3, 2024
1 parent e7b2153 commit 12fe9e0
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -424,7 +425,7 @@ private static void addTestResourceEntry(WithTestResource quarkusTestResource, A
Set<TestResourceClassEntry> uniqueEntries) {

addTestResourceEntry(quarkusTestResource.value(), quarkusTestResource.initArgs(), originalAnnotation,
quarkusTestResource.parallel(), quarkusTestResource.restrictToAnnotatedClass() ? RESTRICTED_TO_CLASS : GLOBAL,
quarkusTestResource.parallel(), quarkusTestResource.scope(),
uniqueEntries);
}

Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -506,11 +507,40 @@ public Set<TestResourceComparisonInfo> testResourceComparisonInfo() {
* <ul>
* <li>at least one the existing test resources is restricted to the test class</li>
* <li>at least one the next test resources is restricted to the test class</li>
* <li>different {@code MATCHING_RESOURCE} scoped test resources are being used</li>
* </ul>
*/
public static boolean testResourcesRequireReload(Set<TestResourceComparisonInfo> existing,
Set<TestResourceComparisonInfo> 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<TestResourceComparisonInfo> inExistingAndNotNext = onlyMatchingResourceItems(existing);
inExistingAndNotNext.removeAll(next);
if (!inExistingAndNotNext.isEmpty()) {
return true;
}

Set<TestResourceComparisonInfo> 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<TestResourceComparisonInfo> onlyMatchingResourceItems(Set<TestResourceComparisonInfo> set) {
return set.stream().filter(i -> i.scope == MATCHING_RESOURCE).collect(
Collectors.toSet());
}

private static boolean hasRestrictedToClassScope(Set<TestResourceComparisonInfo> existing) {
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,20 @@
/**
* Used to define a test resource, which can affect various aspects of the application lifecycle.
* <p>
* 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 <strong>not</strong> force a restart
* of Quarkus.
* <p>
* 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)},
* <p>
* 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.
* <p>
* 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.
Expand All @@ -28,14 +37,6 @@
* started <b>before</b> <b>any</b> test is run.
* <p>
* Note that test resources are never restarted when running {@code @Nested} test classes.
* <p>
* The only difference with {@link QuarkusTestResource} is that the default value for
* {@link #restrictToAnnotatedClass()} {@code == true}.
* </p>
* <p>
* 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.
* </p>
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
Expand All @@ -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.
* <p>
* 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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))));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
}

Expand Down Expand Up @@ -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 {
}

Expand Down Expand Up @@ -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 {
}

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -205,7 +206,8 @@ private void quarkusWithTestResourceMock(ClassDescriptor mock,
Class<? extends QuarkusTestResourceLifecycleManager> 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));
}

Expand All @@ -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 {
}

Expand Down

0 comments on commit 12fe9e0

Please sign in to comment.