Ensure Bean Overrides are discovered once in hierarchies

Prior to this commit, bean overrides (such as @⁠MockitoBean, etc.) were
discovered multiple times if they were declared:

- at the type-level on an interface that is implemented at more than
  one level in the type hierarchy, the enclosing class hierarchy, or a
  combination of the type and enclosing class hierarchies.

or

- on a field declared in a class which can be reached multiple times
  while traversing the type and enclosing class hierarchies in
  scenarios such as the following: the class (X) in which the field is
  declared is a supertype of an enclosing type of the test class, and X
  is also an enclosing type of a supertype of the test class.

Such scenarios resulted in an IllegalStateException stating that a
duplicate BeanOverrideHandler was discovered.

To address that, this commit revises the search algorithm in
BeanOverrideHandler so that all types (superclasses, enclosing classes,
and implemented interfaces) are only visited once while traversing the
type and enclosing class hierarchies in search of bean override
handlers.

See gh-33925
See gh-34324
Closes gh-34844
This commit is contained in:
Sam Brannen
2025-05-01 17:36:16 +02:00
parent b943817f3e
commit e8f873a349
5 changed files with 211 additions and 10 deletions
@@ -184,30 +184,32 @@ public abstract class BeanOverrideHandler {
* @param testClass the original test class
* @param handlers the list of handlers found
* @param localFieldsOnly whether to search only on local fields within the type hierarchy
* @param visitedEnclosingClasses the set of enclosing classes already visited
* @param visitedTypes the set of types already visited
* @since 6.2.2
*/
private static void findHandlers(Class<?> clazz, Class<?> testClass, List<BeanOverrideHandler> handlers,
boolean localFieldsOnly, Set<Class<?>> visitedEnclosingClasses) {
boolean localFieldsOnly, Set<Class<?>> visitedTypes) {
// 0) Ensure that we do not process the same class or interface multiple times.
if (!visitedTypes.add(clazz)) {
return;
}
// 1) Search enclosing class hierarchy.
if (!localFieldsOnly && TestContextAnnotationUtils.searchEnclosingClass(clazz)) {
Class<?> enclosingClass = clazz.getEnclosingClass();
if (visitedEnclosingClasses.add(enclosingClass)) {
findHandlers(enclosingClass, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
}
findHandlers(clazz.getEnclosingClass(), testClass, handlers, localFieldsOnly, visitedTypes);
}
// 2) Search class hierarchy.
Class<?> superclass = clazz.getSuperclass();
if (superclass != null && superclass != Object.class) {
findHandlers(superclass, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
findHandlers(superclass, testClass, handlers, localFieldsOnly, visitedTypes);
}
if (!localFieldsOnly) {
// 3) Search interfaces.
for (Class<?> ifc : clazz.getInterfaces()) {
findHandlers(ifc, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
findHandlers(ifc, testClass, handlers, localFieldsOnly, visitedTypes);
}
// 4) Process current class.