Skip to content

Commit 64ed21a

Browse files
committed
Apply method predicate before searching type hierarchy
Prior to this commit, findMethods() and streamMethods() in ReflectionSupport as well as findAnnotatedMethods() in AnnotationSupport first searched for all methods in the type hierarchy and then applied the user-supplied predicate (or "is annotated?" predicate) afterwards. This resulted in methods in subclasses incorrectly "shadowing" package-private methods in superclasses (in a different package) even if the predicate would otherwise exclude the method in such a subclass. For example, given a superclass that declares a package-private static @⁠BeforeAll "before()" method and a subclass (in a different package) that declares a @⁠BeforeEach "before()" method, when JUnit Jupiter looked up @⁠BeforeAll methods for the subclass, the @⁠BeforeAll "before()" method in the superclass was not found because the @⁠BeforeEach "before()" method shadowed it based solely on the method signature, ignoring the type of annotation sought. To address that, this commit modifies the internal search algorithms in ReflectionUtils so that method predicates are applied while searching the hierarchy for methods. Closes #3498 Closes #3500
1 parent 8dd95af commit 64ed21a

File tree

6 files changed

+128
-16
lines changed

6 files changed

+128
-16
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.10.1.adoc

+9-1
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,22 @@ JUnit repository on GitHub.
1515

1616
==== Bug Fixes
1717

18-
* ❓
18+
* Method predicates are now applied while searching the type hierarchy. This fixes bugs
19+
in `findMethods(...)` and `streamMethods(...)` in `ReflectionSupport` as well as
20+
`findAnnotatedMethods(...)` in `AnnotationSupport`.
21+
- See link:https://github.com/junit-team/junit5/issues/3498[issue 3498] for details.
1922

2023

2124
[[release-notes-5.10.1-junit-jupiter]]
2225
=== JUnit Jupiter
2326

2427
==== Bug Fixes
2528

29+
* A package-private class-level lifecycle method annotated with `@BeforeAll` or
30+
`@AfterAll` is no longer _shadowed_ by a method-level lifecycle method annotated with
31+
`@BeforeEach` or `@AfterEach` when the method-level lifecycle method resides in a
32+
different package and has the same name as the class-level lifecycle method.
33+
- See link:https://github.com/junit-team/junit5/issues/3498[issue 3498] for details.
2634
* The `RandomNumberExtension` example in the
2735
<<../user-guide/index.adoc#extensions-RandomNumberExtension, User Guide>> has been
2836
updated to properly support `Integer` types as well as non-static field injection.

junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java

+17-15
Original file line numberDiff line numberDiff line change
@@ -1489,29 +1489,27 @@ public static Stream<Method> streamMethods(Class<?> clazz, Predicate<Method> pre
14891489
Preconditions.notNull(predicate, "Predicate must not be null");
14901490
Preconditions.notNull(traversalMode, "HierarchyTraversalMode must not be null");
14911491

1492-
// @formatter:off
1493-
return findAllMethodsInHierarchy(clazz, traversalMode).stream()
1494-
.filter(predicate)
1495-
.distinct();
1496-
// @formatter:on
1492+
return findAllMethodsInHierarchy(clazz, predicate, traversalMode).stream().distinct();
14971493
}
14981494

14991495
/**
15001496
* Find all non-synthetic methods in the superclass and interface hierarchy,
1501-
* excluding Object.
1497+
* excluding Object, that match the specified {@code predicate}.
15021498
*/
1503-
private static List<Method> findAllMethodsInHierarchy(Class<?> clazz, HierarchyTraversalMode traversalMode) {
1499+
private static List<Method> findAllMethodsInHierarchy(Class<?> clazz, Predicate<Method> predicate,
1500+
HierarchyTraversalMode traversalMode) {
1501+
15041502
Preconditions.notNull(clazz, "Class must not be null");
15051503
Preconditions.notNull(traversalMode, "HierarchyTraversalMode must not be null");
15061504

15071505
// @formatter:off
15081506
List<Method> localMethods = getDeclaredMethods(clazz, traversalMode).stream()
1509-
.filter(method -> !method.isSynthetic())
1507+
.filter(predicate.and(method -> !method.isSynthetic()))
15101508
.collect(toList());
1511-
List<Method> superclassMethods = getSuperclassMethods(clazz, traversalMode).stream()
1509+
List<Method> superclassMethods = getSuperclassMethods(clazz, predicate, traversalMode).stream()
15121510
.filter(method -> !isMethodShadowedByLocalMethods(method, localMethods))
15131511
.collect(toList());
1514-
List<Method> interfaceMethods = getInterfaceMethods(clazz, traversalMode).stream()
1512+
List<Method> interfaceMethods = getInterfaceMethods(clazz, predicate, traversalMode).stream()
15151513
.filter(method -> !isMethodShadowedByLocalMethods(method, localMethods))
15161514
.collect(toList());
15171515
// @formatter:on
@@ -1647,16 +1645,18 @@ private static int defaultMethodSorter(Method method1, Method method2) {
16471645
return comparison;
16481646
}
16491647

1650-
private static List<Method> getInterfaceMethods(Class<?> clazz, HierarchyTraversalMode traversalMode) {
1648+
private static List<Method> getInterfaceMethods(Class<?> clazz, Predicate<Method> predicate,
1649+
HierarchyTraversalMode traversalMode) {
1650+
16511651
List<Method> allInterfaceMethods = new ArrayList<>();
16521652
for (Class<?> ifc : clazz.getInterfaces()) {
16531653

16541654
// @formatter:off
16551655
List<Method> localInterfaceMethods = getMethods(ifc).stream()
1656-
.filter(m -> !isAbstract(m))
1656+
.filter(predicate.and(method -> !isAbstract(method)))
16571657
.collect(toList());
16581658

1659-
List<Method> superinterfaceMethods = getInterfaceMethods(ifc, traversalMode).stream()
1659+
List<Method> superinterfaceMethods = getInterfaceMethods(ifc, predicate, traversalMode).stream()
16601660
.filter(method -> !isMethodShadowedByLocalMethods(method, localInterfaceMethods))
16611661
.collect(toList());
16621662
// @formatter:on
@@ -1706,12 +1706,14 @@ private static boolean isFieldShadowedByLocalFields(Field field, List<Field> loc
17061706
return localFields.stream().anyMatch(local -> local.getName().equals(field.getName()));
17071707
}
17081708

1709-
private static List<Method> getSuperclassMethods(Class<?> clazz, HierarchyTraversalMode traversalMode) {
1709+
private static List<Method> getSuperclassMethods(Class<?> clazz, Predicate<Method> predicate,
1710+
HierarchyTraversalMode traversalMode) {
1711+
17101712
Class<?> superclass = clazz.getSuperclass();
17111713
if (!isSearchable(superclass)) {
17121714
return Collections.emptyList();
17131715
}
1714-
return findAllMethodsInHierarchy(superclass, traversalMode);
1716+
return findAllMethodsInHierarchy(superclass, predicate, traversalMode);
17151717
}
17161718

17171719
private static boolean isMethodShadowedByLocalMethods(Method method, List<Method> localMethods) {

platform-tests/src/test/java/org/junit/platform/commons/util/AnnotationUtilsTests.java

+27
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,18 @@
3636
import java.lang.annotation.Target;
3737
import java.lang.reflect.AnnotatedElement;
3838
import java.lang.reflect.Field;
39+
import java.lang.reflect.Method;
3940
import java.math.BigDecimal;
4041
import java.util.List;
4142
import java.util.Optional;
4243
import java.util.function.Predicate;
4344

45+
import org.junit.jupiter.api.BeforeAll;
46+
import org.junit.jupiter.api.BeforeEach;
4447
import org.junit.jupiter.api.Test;
4548
import org.junit.platform.commons.PreconditionViolationException;
49+
import org.junit.platform.commons.util.pkg1.SuperclassWithStaticPackagePrivateBeforeMethod;
50+
import org.junit.platform.commons.util.pkg1.subpkg.SubclassWithNonStaticPackagePrivateBeforeMethod;
4651

4752
/**
4853
* Unit tests for {@link AnnotationUtils}.
@@ -380,6 +385,28 @@ void findAnnotatedMethodsForAnnotationUsedInClassAndSuperclassHierarchyDown() th
380385
assertThat(methods.subList(1, 3)).containsOnly(method1, method3);
381386
}
382387

388+
/**
389+
* @see https://github.com/junit-team/junit5/issues/3498
390+
*/
391+
@Test
392+
void findAnnotatedMethodsAppliesPredicateBeforeSearchingTypeHierarchy() throws Exception {
393+
final String BEFORE = "before";
394+
Class<?> superclass = SuperclassWithStaticPackagePrivateBeforeMethod.class;
395+
Method beforeAllMethod = superclass.getDeclaredMethod(BEFORE);
396+
Class<?> subclass = SubclassWithNonStaticPackagePrivateBeforeMethod.class;
397+
Method beforeEachMethod = subclass.getDeclaredMethod(BEFORE);
398+
399+
// Prerequisite
400+
var methods = findAnnotatedMethods(superclass, BeforeAll.class, TOP_DOWN);
401+
assertThat(methods).containsExactly(beforeAllMethod);
402+
403+
// Actual use cases for this test
404+
methods = findAnnotatedMethods(subclass, BeforeAll.class, TOP_DOWN);
405+
assertThat(methods).containsExactly(beforeAllMethod);
406+
methods = findAnnotatedMethods(subclass, BeforeEach.class, TOP_DOWN);
407+
assertThat(methods).containsExactly(beforeEachMethod);
408+
}
409+
383410
@Test
384411
void findAnnotatedMethodsForAnnotationUsedInInterface() throws Exception {
385412
var interfaceMethod = InterfaceWithAnnotatedDefaultMethod.class.getDeclaredMethod("interfaceMethod");

platform-tests/src/test/java/org/junit/platform/commons/util/ReflectionUtilsTests.java

+24
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@
7272
import org.junit.platform.commons.util.ReflectionUtilsTests.OuterClass.StaticNestedSiblingClass;
7373
import org.junit.platform.commons.util.ReflectionUtilsTests.OuterClassImplementingInterface.InnerClassImplementingInterface;
7474
import org.junit.platform.commons.util.classes.CustomType;
75+
import org.junit.platform.commons.util.pkg1.SuperclassWithStaticPackagePrivateBeforeMethod;
76+
import org.junit.platform.commons.util.pkg1.subpkg.SubclassWithNonStaticPackagePrivateBeforeMethod;
7577

7678
/**
7779
* Unit tests for {@link ReflectionUtils}.
@@ -1344,6 +1346,28 @@ void findMethodsIgnoresBridgeMethods() throws Exception {
13441346
assertEquals(0, methods.stream().filter(Method::isBridge).count());
13451347
}
13461348

1349+
/**
1350+
* @see https://github.com/junit-team/junit5/issues/3498
1351+
*/
1352+
@Test
1353+
void findMethodsAppliesPredicateBeforeSearchingTypeHierarchy() throws Exception {
1354+
final String BEFORE = "before";
1355+
Class<?> superclass = SuperclassWithStaticPackagePrivateBeforeMethod.class;
1356+
Method staticMethod = superclass.getDeclaredMethod(BEFORE);
1357+
Class<?> subclass = SubclassWithNonStaticPackagePrivateBeforeMethod.class;
1358+
Method nonStaticMethod = subclass.getDeclaredMethod(BEFORE);
1359+
1360+
// Prerequisite
1361+
var methods = findMethods(superclass, ReflectionUtils::isStatic);
1362+
assertThat(methods).containsExactly(staticMethod);
1363+
1364+
// Actual use cases for this test
1365+
methods = findMethods(subclass, ReflectionUtils::isStatic);
1366+
assertThat(methods).containsExactly(staticMethod);
1367+
methods = findMethods(subclass, ReflectionUtils::isNotStatic);
1368+
assertThat(methods).containsExactly(nonStaticMethod);
1369+
}
1370+
13471371
@Test
13481372
void isGeneric() {
13491373
for (var method : Generic.class.getMethods()) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2015-2023 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.platform.commons.util.pkg1;
12+
13+
import org.junit.jupiter.api.BeforeAll;
14+
15+
/**
16+
* @see https://github.com/junit-team/junit5/issues/3498
17+
*/
18+
public class SuperclassWithStaticPackagePrivateBeforeMethod {
19+
20+
@BeforeAll
21+
static void before() {
22+
// no-op
23+
}
24+
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2015-2023 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.platform.commons.util.pkg1.subpkg;
12+
13+
import org.junit.jupiter.api.BeforeEach;
14+
import org.junit.platform.commons.util.pkg1.SuperclassWithStaticPackagePrivateBeforeMethod;
15+
16+
/**
17+
* @see https://github.com/junit-team/junit5/issues/3498
18+
*/
19+
public class SubclassWithNonStaticPackagePrivateBeforeMethod extends SuperclassWithStaticPackagePrivateBeforeMethod {
20+
21+
@BeforeEach
22+
void before() {
23+
// no-op
24+
}
25+
26+
}

0 commit comments

Comments
 (0)