Skip to content

Commit dceac6e

Browse files
authored
Avoid multiple class-level annotation lookups (#4053)
Instead of looking up annotations on the declaring and all enclosing classes for each test method, each `ResourceLockAware` test descriptor now delegates to the `ExclusiveResourceCollector` of its ancestors which cache the class-level annotations and `ResourceLocksProvider` instances. Resolves #2677.
1 parent 8e9094d commit dceac6e

File tree

8 files changed

+232
-99
lines changed

8 files changed

+232
-99
lines changed

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@
7474
import org.junit.platform.engine.TestTag;
7575
import org.junit.platform.engine.UniqueId;
7676
import org.junit.platform.engine.support.descriptor.ClassSource;
77-
import org.junit.platform.engine.support.hierarchical.ExclusiveResource;
7877
import org.junit.platform.engine.support.hierarchical.ThrowableCollector;
7978

8079
/**
@@ -83,13 +82,14 @@
8382
* @since 5.5
8483
*/
8584
@API(status = INTERNAL, since = "5.5")
86-
public abstract class ClassBasedTestDescriptor extends JupiterTestDescriptor {
85+
public abstract class ClassBasedTestDescriptor extends JupiterTestDescriptor implements ResourceLockAware {
8786

8887
private static final InterceptingExecutableInvoker executableInvoker = new InterceptingExecutableInvoker();
8988

9089
private final Class<?> testClass;
9190
protected final Set<TestTag> tags;
9291
protected final Lifecycle lifecycle;
92+
private final ExclusiveResourceCollector exclusiveResourceCollector;
9393

9494
private ExecutionMode defaultChildExecutionMode;
9595
private TestInstanceFactory testInstanceFactory;
@@ -104,6 +104,7 @@ public abstract class ClassBasedTestDescriptor extends JupiterTestDescriptor {
104104
this.tags = getTags(testClass);
105105
this.lifecycle = getTestInstanceLifecycle(testClass, configuration);
106106
this.defaultChildExecutionMode = (this.lifecycle == Lifecycle.PER_CLASS ? ExecutionMode.SAME_THREAD : null);
107+
this.exclusiveResourceCollector = ExclusiveResourceCollector.from(testClass);
107108
}
108109

109110
// --- TestDescriptor ------------------------------------------------------
@@ -112,6 +113,8 @@ public final Class<?> getTestClass() {
112113
return this.testClass;
113114
}
114115

116+
public abstract List<Class<?>> getEnclosingTestClasses();
117+
115118
@Override
116119
public Type getType() {
117120
return Type.CONTAINER;
@@ -139,13 +142,8 @@ public void setDefaultChildExecutionMode(ExecutionMode defaultChildExecutionMode
139142
}
140143

141144
@Override
142-
public Set<ExclusiveResource> getExclusiveResources() {
143-
// @formatter:off
144-
return getExclusiveResourcesFromAnnotations(
145-
getTestClass(),
146-
provider -> provider.provideForClass(getTestClass())
147-
);
148-
// @formatter:on
145+
public final ExclusiveResourceCollector getExclusiveResourceCollector() {
146+
return exclusiveResourceCollector;
149147
}
150148

151149
@Override

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassTestDescriptor.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,19 @@
1010

1111
package org.junit.jupiter.engine.descriptor;
1212

13+
import static java.util.Collections.emptyList;
1314
import static org.apiguardian.api.API.Status.INTERNAL;
1415
import static org.junit.jupiter.engine.descriptor.DisplayNameUtils.createDisplayNameSupplierForClass;
1516

1617
import java.util.LinkedHashSet;
18+
import java.util.List;
1719
import java.util.Optional;
1820
import java.util.Set;
1921

2022
import org.apiguardian.api.API;
2123
import org.junit.jupiter.api.extension.ExtensionContext;
2224
import org.junit.jupiter.api.extension.TestInstances;
25+
import org.junit.jupiter.api.parallel.ResourceLocksProvider;
2326
import org.junit.jupiter.engine.config.JupiterConfiguration;
2427
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
2528
import org.junit.jupiter.engine.extension.ExtensionRegistrar;
@@ -57,6 +60,11 @@ public Set<TestTag> getTags() {
5760
return new LinkedHashSet<>(this.tags);
5861
}
5962

63+
@Override
64+
public List<Class<?>> getEnclosingTestClasses() {
65+
return emptyList();
66+
}
67+
6068
// --- Node ----------------------------------------------------------------
6169

6270
@Override
@@ -72,4 +80,9 @@ protected TestInstances instantiateTestClass(JupiterEngineExecutionContext paren
7280
return instantiateTestClass(Optional.empty(), registry, extensionContext);
7381
}
7482

83+
@Override
84+
public Set<ResourceLocksProvider.Lock> evaluateResourceLocksProvider(ResourceLocksProvider provider) {
85+
return provider.provideForClass(getTestClass());
86+
}
87+
7588
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright 2015-2024 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.jupiter.engine.descriptor;
12+
13+
import static org.junit.platform.commons.support.AnnotationSupport.findRepeatableAnnotations;
14+
import static org.junit.platform.commons.util.CollectionUtils.toUnmodifiableList;
15+
16+
import java.lang.reflect.AnnotatedElement;
17+
import java.util.Collection;
18+
import java.util.List;
19+
import java.util.Set;
20+
import java.util.function.Function;
21+
import java.util.stream.Stream;
22+
23+
import org.junit.jupiter.api.parallel.ResourceAccessMode;
24+
import org.junit.jupiter.api.parallel.ResourceLock;
25+
import org.junit.jupiter.api.parallel.ResourceLocksProvider;
26+
import org.junit.platform.commons.JUnitException;
27+
import org.junit.platform.commons.util.ReflectionUtils;
28+
import org.junit.platform.commons.util.StringUtils;
29+
import org.junit.platform.engine.support.hierarchical.ExclusiveResource;
30+
31+
/**
32+
* @since 5.12
33+
*/
34+
abstract class ExclusiveResourceCollector {
35+
36+
private static final ExclusiveResourceCollector NO_EXCLUSIVE_RESOURCES = new ExclusiveResourceCollector() {
37+
38+
@Override
39+
Stream<ExclusiveResource> getAllExclusiveResources(
40+
Function<ResourceLocksProvider, Set<ResourceLocksProvider.Lock>> providerToLocks) {
41+
return Stream.empty();
42+
}
43+
44+
@Override
45+
public Stream<ExclusiveResource> getStaticResources() {
46+
return Stream.empty();
47+
}
48+
49+
@Override
50+
Stream<ExclusiveResource> getDynamicResources(
51+
Function<ResourceLocksProvider, Set<ResourceLocksProvider.Lock>> providerToLocks) {
52+
return Stream.empty();
53+
}
54+
};
55+
56+
Stream<ExclusiveResource> getAllExclusiveResources(
57+
Function<ResourceLocksProvider, Set<ResourceLocksProvider.Lock>> providerToLocks) {
58+
return Stream.concat(getStaticResources(), getDynamicResources(providerToLocks));
59+
}
60+
61+
abstract Stream<ExclusiveResource> getStaticResources();
62+
63+
abstract Stream<ExclusiveResource> getDynamicResources(
64+
Function<ResourceLocksProvider, Set<ResourceLocksProvider.Lock>> providerToLocks);
65+
66+
static ExclusiveResourceCollector from(AnnotatedElement element) {
67+
List<ResourceLock> annotations = findRepeatableAnnotations(element, ResourceLock.class);
68+
return annotations.isEmpty() ? NO_EXCLUSIVE_RESOURCES : new DefaultExclusiveResourceCollector(annotations);
69+
}
70+
71+
private static class DefaultExclusiveResourceCollector extends ExclusiveResourceCollector {
72+
73+
private final List<ResourceLock> annotations;
74+
private List<ResourceLocksProvider> providers;
75+
76+
DefaultExclusiveResourceCollector(List<ResourceLock> annotations) {
77+
this.annotations = annotations;
78+
}
79+
80+
@Override
81+
public Stream<ExclusiveResource> getStaticResources() {
82+
return annotations.stream() //
83+
.filter(annotation -> StringUtils.isNotBlank(annotation.value())) //
84+
.map(annotation -> new ExclusiveResource(annotation.value(), toLockMode(annotation.mode())));
85+
}
86+
87+
@Override
88+
Stream<ExclusiveResource> getDynamicResources(
89+
Function<ResourceLocksProvider, Set<ResourceLocksProvider.Lock>> providerToLocks) {
90+
List<ResourceLocksProvider> providers = getProviders();
91+
if (providers.isEmpty()) {
92+
return Stream.empty();
93+
}
94+
return providers.stream() //
95+
.map(providerToLocks) //
96+
.flatMap(Collection::stream) //
97+
.map(lock -> new ExclusiveResource(lock.getKey(), toLockMode(lock.getAccessMode())));
98+
}
99+
100+
private List<ResourceLocksProvider> getProviders() {
101+
if (this.providers == null) {
102+
this.providers = annotations.stream() //
103+
.flatMap(annotation -> Stream.of(annotation.providers()).map(ReflectionUtils::newInstance)) //
104+
.collect(toUnmodifiableList());
105+
}
106+
return providers;
107+
}
108+
109+
private static ExclusiveResource.LockMode toLockMode(ResourceAccessMode mode) {
110+
switch (mode) {
111+
case READ:
112+
return ExclusiveResource.LockMode.READ;
113+
case READ_WRITE:
114+
return ExclusiveResource.LockMode.READ_WRITE;
115+
}
116+
throw new JUnitException("Unknown ResourceAccessMode: " + mode);
117+
}
118+
}
119+
}

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java

Lines changed: 6 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -10,36 +10,28 @@
1010

1111
package org.junit.jupiter.engine.descriptor;
1212

13-
import static java.util.Collections.emptyList;
13+
import static java.util.Collections.emptySet;
1414
import static java.util.stream.Collectors.collectingAndThen;
1515
import static java.util.stream.Collectors.toCollection;
16-
import static java.util.stream.Collectors.toList;
1716
import static java.util.stream.Collectors.toSet;
1817
import static org.apiguardian.api.API.Status.INTERNAL;
1918
import static org.junit.jupiter.engine.descriptor.DisplayNameUtils.determineDisplayName;
2019
import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation;
2120
import static org.junit.platform.commons.support.AnnotationSupport.findRepeatableAnnotations;
2221

2322
import java.lang.reflect.AnnotatedElement;
24-
import java.util.ArrayList;
25-
import java.util.Collection;
2623
import java.util.Collections;
2724
import java.util.LinkedHashSet;
2825
import java.util.List;
2926
import java.util.Optional;
3027
import java.util.Set;
31-
import java.util.function.Function;
3228
import java.util.function.Supplier;
33-
import java.util.stream.Stream;
3429

3530
import org.apiguardian.api.API;
3631
import org.junit.jupiter.api.Tag;
3732
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
3833
import org.junit.jupiter.api.extension.Extension;
3934
import org.junit.jupiter.api.parallel.Execution;
40-
import org.junit.jupiter.api.parallel.ResourceAccessMode;
41-
import org.junit.jupiter.api.parallel.ResourceLock;
42-
import org.junit.jupiter.api.parallel.ResourceLocksProvider;
4335
import org.junit.jupiter.engine.config.JupiterConfiguration;
4436
import org.junit.jupiter.engine.execution.ConditionEvaluator;
4537
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
@@ -48,16 +40,13 @@
4840
import org.junit.platform.commons.logging.Logger;
4941
import org.junit.platform.commons.logging.LoggerFactory;
5042
import org.junit.platform.commons.util.ExceptionUtils;
51-
import org.junit.platform.commons.util.ReflectionUtils;
52-
import org.junit.platform.commons.util.StringUtils;
5343
import org.junit.platform.commons.util.UnrecoverableExceptions;
5444
import org.junit.platform.engine.TestDescriptor;
5545
import org.junit.platform.engine.TestSource;
5646
import org.junit.platform.engine.TestTag;
5747
import org.junit.platform.engine.UniqueId;
5848
import org.junit.platform.engine.support.descriptor.AbstractTestDescriptor;
5949
import org.junit.platform.engine.support.hierarchical.ExclusiveResource;
60-
import org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockMode;
6150
import org.junit.platform.engine.support.hierarchical.Node;
6251

6352
/**
@@ -109,17 +98,6 @@ static Set<TestTag> getTags(AnnotatedElement element) {
10998
// @formatter:on
11099
}
111100

112-
public List<Class<?>> getEnclosingTestClasses() {
113-
TestDescriptor parent = getParent().orElse(null);
114-
if (parent instanceof ClassBasedTestDescriptor) {
115-
ClassBasedTestDescriptor parentClassDescriptor = (ClassBasedTestDescriptor) parent;
116-
List<Class<?>> result = new ArrayList<>(parentClassDescriptor.getEnclosingTestClasses());
117-
result.add(parentClassDescriptor.getTestClass());
118-
return result;
119-
}
120-
return emptyList();
121-
}
122-
123101
/**
124102
* Invoke exception handlers for the supplied {@code Throwable} one-by-one
125103
* until none are left or the throwable to handle has been swallowed.
@@ -200,57 +178,12 @@ public static ExecutionMode toExecutionMode(org.junit.jupiter.api.parallel.Execu
200178
throw new JUnitException("Unknown ExecutionMode: " + mode);
201179
}
202180

203-
Set<ExclusiveResource> getExclusiveResourcesFromAnnotations(AnnotatedElement element,
204-
Function<ResourceLocksProvider, Set<ResourceLocksProvider.Lock>> providerToLocks) {
205-
// @formatter:off
206-
List<ResourceLock> ownAnnotations = findRepeatableAnnotations(element, ResourceLock.class);
207-
List<ResourceLock> enclosingClassesAnnotations = getEnclosingTestClasses().stream()
208-
.map(clazz -> findRepeatableAnnotations(clazz, ResourceLock.class))
209-
.flatMap(Collection::stream)
210-
.collect(toList());
211-
212-
return Stream.of(
213-
getExclusiveResourcesFromValues(ownAnnotations),
214-
getExclusiveResourcesFromProviders(ownAnnotations, providerToLocks),
215-
getExclusiveResourcesFromProviders(enclosingClassesAnnotations, providerToLocks)
216-
).flatMap(s -> s)
217-
.collect(toSet());
218-
// @formatter:on
219-
}
220-
221-
private Stream<ExclusiveResource> getExclusiveResourcesFromValues(List<ResourceLock> annotations) {
222-
// @formatter:off
223-
return annotations.stream()
224-
.flatMap(annotation -> {
225-
if (StringUtils.isBlank(annotation.value())) {
226-
return Stream.empty();
227-
}
228-
return Stream.of(new ExclusiveResource(annotation.value(), toLockMode(annotation.mode())));
229-
});
230-
// @formatter:on
231-
}
232-
233-
private Stream<ExclusiveResource> getExclusiveResourcesFromProviders(List<ResourceLock> annotations,
234-
Function<ResourceLocksProvider, Set<ResourceLocksProvider.Lock>> providerToLocks) {
235-
// @formatter:off
236-
return annotations.stream()
237-
.flatMap(annotation -> Stream.of(annotation.providers())
238-
.map(ReflectionUtils::newInstance)
239-
.map(providerToLocks)
240-
.flatMap(Collection::stream)
241-
.map(lock -> new ExclusiveResource(lock.getKey(), toLockMode(lock.getAccessMode())))
242-
);
243-
// @formatter:on
244-
}
245-
246-
private static LockMode toLockMode(ResourceAccessMode mode) {
247-
switch (mode) {
248-
case READ:
249-
return LockMode.READ;
250-
case READ_WRITE:
251-
return LockMode.READ_WRITE;
181+
@Override
182+
public Set<ExclusiveResource> getExclusiveResources() {
183+
if (this instanceof ResourceLockAware) {
184+
return ((ResourceLockAware) this).determineExclusiveResources().collect(toSet());
252185
}
253-
throw new JUnitException("Unknown ResourceAccessMode: " + mode);
186+
return emptySet();
254187
}
255188

256189
@Override

0 commit comments

Comments
 (0)