Skip to content

Commit

Permalink
Check for DisplayNameGeneration annotations on runtime enclosing types
Browse files Browse the repository at this point in the history
Resolves #4131.
  • Loading branch information
marcphilipp committed Jan 26, 2025
1 parent 034eede commit 3265d58
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import static java.util.Collections.emptyList;
import static org.apiguardian.api.API.Status.DEPRECATED;
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
import static org.apiguardian.api.API.Status.INTERNAL;
import static org.apiguardian.api.API.Status.STABLE;
import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation;
import static org.junit.platform.commons.support.ModifierSupport.isStatic;
Expand All @@ -24,7 +25,6 @@

import org.apiguardian.api.API;
import org.junit.platform.commons.support.ReflectionSupport;
import org.junit.platform.commons.support.SearchOption;
import org.junit.platform.commons.util.ClassUtils;
import org.junit.platform.commons.util.Preconditions;

Expand Down Expand Up @@ -315,23 +315,24 @@ public IndicativeSentences() {

@Override
public String generateDisplayNameForClass(Class<?> testClass) {
return getGeneratorFor(testClass).generateDisplayNameForClass(testClass);
return getGeneratorFor(testClass, emptyList()).generateDisplayNameForClass(testClass);
}

@Override
public String generateDisplayNameForNestedClass(List<Class<?>> enclosingInstanceTypes, Class<?> nestedClass) {
return getSentenceBeginning(enclosingInstanceTypes, nestedClass);
return getSentenceBeginning(nestedClass, enclosingInstanceTypes);
}

@Override
public String generateDisplayNameForMethod(List<Class<?>> enclosingInstanceTypes, Class<?> testClass,
Method testMethod) {
return getSentenceBeginning(enclosingInstanceTypes, testClass) + getFragmentSeparator(testClass)
+ getGeneratorFor(testClass).generateDisplayNameForMethod(enclosingInstanceTypes, testClass,
testMethod);
return getSentenceBeginning(testClass, enclosingInstanceTypes)
+ getFragmentSeparator(testClass, enclosingInstanceTypes)
+ getGeneratorFor(testClass, enclosingInstanceTypes).generateDisplayNameForMethod(
enclosingInstanceTypes, testClass, testMethod);
}

private String getSentenceBeginning(List<Class<?>> enclosingInstanceTypes, Class<?> testClass) {
private String getSentenceBeginning(Class<?> testClass, List<Class<?>> enclosingInstanceTypes) {
Class<?> enclosingClass = enclosingInstanceTypes.isEmpty() ? null
: enclosingInstanceTypes.get(enclosingInstanceTypes.size() - 1);
boolean topLevelTestClass = (enclosingClass == null || isStatic(testClass));
Expand All @@ -342,33 +343,35 @@ private String getSentenceBeginning(List<Class<?>> enclosingInstanceTypes, Class
if (displayName.isPresent()) {
return displayName.get();
}
Class<? extends DisplayNameGenerator> generatorClass = findDisplayNameGeneration(testClass)//
.map(DisplayNameGeneration::value)//
.filter(not(IndicativeSentences.class))//
.orElse(null);
Class<? extends DisplayNameGenerator> generatorClass = findDisplayNameGeneration(testClass,
enclosingInstanceTypes)//
.map(DisplayNameGeneration::value)//
.filter(not(IndicativeSentences.class))//
.orElse(null);
if (generatorClass != null) {
return getDisplayNameGenerator(generatorClass).generateDisplayNameForClass(testClass);
}
return generateDisplayNameForClass(testClass);
}

List<Class<?>> remainingEnclosingInstanceTypes = enclosingInstanceTypes.isEmpty() ? emptyList()
: enclosingInstanceTypes.subList(0, enclosingInstanceTypes.size() - 1);

// Only build prefix based on the enclosing class if the enclosing
// class is also configured to use the IndicativeSentences generator.
boolean buildPrefix = findDisplayNameGeneration(enclosingClass)//
boolean buildPrefix = findDisplayNameGeneration(enclosingClass, remainingEnclosingInstanceTypes)//
.map(DisplayNameGeneration::value)//
.filter(IndicativeSentences.class::equals)//
.isPresent();

List<Class<?>> remainingEnclosingInstanceTypes = enclosingInstanceTypes.isEmpty() ? emptyList()
: enclosingInstanceTypes.subList(0, enclosingInstanceTypes.size() - 1);

String prefix = (buildPrefix
? getSentenceBeginning(remainingEnclosingInstanceTypes, enclosingClass)
+ getFragmentSeparator(testClass)
? getSentenceBeginning(enclosingClass, remainingEnclosingInstanceTypes)
+ getFragmentSeparator(testClass, enclosingInstanceTypes)
: "");

return prefix + displayName.orElseGet(() -> getGeneratorFor(testClass).generateDisplayNameForNestedClass(
remainingEnclosingInstanceTypes, testClass));
return prefix + displayName.orElseGet(
() -> getGeneratorFor(testClass, enclosingInstanceTypes).generateDisplayNameForNestedClass(
remainingEnclosingInstanceTypes, testClass));
}

/**
Expand All @@ -381,28 +384,31 @@ private String getSentenceBeginning(List<Class<?>> enclosingInstanceTypes, Class
* will be used.
*
* @param testClass the test class to search on for {@code @IndicativeSentencesGeneration}
* @param enclosingInstanceTypes the runtime types of the enclosing
* instances; never {@code null}
* @return the sentence fragment separator
*/
private static String getFragmentSeparator(Class<?> testClass) {
return findIndicativeSentencesGeneration(testClass)//
private static String getFragmentSeparator(Class<?> testClass, List<Class<?>> enclosingInstanceTypes) {
return findIndicativeSentencesGeneration(testClass, enclosingInstanceTypes)//
.map(IndicativeSentencesGeneration::separator)//
.orElse(IndicativeSentencesGeneration.DEFAULT_SEPARATOR);
}

/**
* Get the display name generator to use for the supplied test class.
*
* <p>If {@link IndicativeSentencesGeneration @IndicativeSentencesGeneration}
* is present (searching enclosing classes if not found locally), the
* configured {@link IndicativeSentencesGeneration#generator() generator}
* will be used. Otherwise, {@link IndicativeSentencesGeneration#DEFAULT_GENERATOR}
* will be used.
*
* @param testClass the test class to search on for {@code @IndicativeSentencesGeneration}
* @param enclosingInstanceTypes the runtime types of the enclosing
* instances; never {@code null}
* @return the {@code DisplayNameGenerator} instance to use
*/
private static DisplayNameGenerator getGeneratorFor(Class<?> testClass) {
return findIndicativeSentencesGeneration(testClass)//
private static DisplayNameGenerator getGeneratorFor(Class<?> testClass, List<Class<?>> enclosingInstanceTypes) {
return findIndicativeSentencesGeneration(testClass, enclosingInstanceTypes)//
.map(IndicativeSentencesGeneration::generator)//
.filter(not(IndicativeSentences.class))//
.map(DisplayNameGenerator::getDisplayNameGenerator)//
Expand All @@ -412,26 +418,32 @@ private static DisplayNameGenerator getGeneratorFor(Class<?> testClass) {
/**
* Find the first {@code DisplayNameGeneration} annotation that is either
* <em>directly present</em>, <em>meta-present</em>, or <em>indirectly present</em>
* on the supplied {@code testClass} or on an enclosing class.
* on the supplied {@code testClass} or on an enclosing instance type.
*
* @param testClass the test class on which to find the annotation; never {@code null}
* @param enclosingInstanceTypes the runtime types of the enclosing
* instances; never {@code null}
* @return an {@code Optional} containing the annotation, potentially empty if not found
*/
private static Optional<DisplayNameGeneration> findDisplayNameGeneration(Class<?> testClass) {
return findAnnotation(testClass, DisplayNameGeneration.class, SearchOption.INCLUDE_ENCLOSING_CLASSES);
@API(status = INTERNAL, since = "5.12")
static Optional<DisplayNameGeneration> findDisplayNameGeneration(Class<?> testClass,
List<Class<?>> enclosingInstanceTypes) {
return findAnnotation(testClass, DisplayNameGeneration.class, enclosingInstanceTypes);
}

/**
* Find the first {@code IndicativeSentencesGeneration} annotation that is either
* <em>directly present</em>, <em>meta-present</em>, or <em>indirectly present</em>
* on the supplied {@code testClass} or on an enclosing class.
* on the supplied {@code testClass} or on an enclosing instance type.
*
* @param testClass the test class on which to find the annotation; never {@code null}
* @param testClass the test class on which to find the annotation; never {@code null}
* @param enclosingInstanceTypes the runtime types of the enclosing
* instances; never {@code null}
* @return an {@code Optional} containing the annotation, potentially empty if not found
*/
private static Optional<IndicativeSentencesGeneration> findIndicativeSentencesGeneration(Class<?> testClass) {
return findAnnotation(testClass, IndicativeSentencesGeneration.class,
SearchOption.INCLUDE_ENCLOSING_CLASSES);
private static Optional<IndicativeSentencesGeneration> findIndicativeSentencesGeneration(Class<?> testClass,
List<Class<?>> enclosingInstanceTypes) {
return findAnnotation(testClass, IndicativeSentencesGeneration.class, enclosingInstanceTypes);
}

private static Predicate<Class<?>> not(Class<?> clazz) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.BiFunction;
import java.util.function.Supplier;

import org.junit.jupiter.api.DisplayName;
Expand All @@ -30,7 +31,6 @@
import org.junit.platform.commons.logging.Logger;
import org.junit.platform.commons.logging.LoggerFactory;
import org.junit.platform.commons.support.ReflectionSupport;
import org.junit.platform.commons.support.SearchOption;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.StringUtils;

Expand Down Expand Up @@ -97,33 +97,43 @@ static String determineDisplayNameForMethod(Supplier<List<Class<?>>> enclosingIn
}

static Supplier<String> createDisplayNameSupplierForClass(Class<?> testClass, JupiterConfiguration configuration) {
return createDisplayNameSupplier(testClass, configuration,
generator -> generator.generateDisplayNameForClass(testClass));
return createDisplayNameSupplier(Collections::emptyList, testClass, configuration,
(generator, __) -> generator.generateDisplayNameForClass(testClass));
}

static Supplier<String> createDisplayNameSupplierForNestedClass(Supplier<List<Class<?>>> enclosingInstanceTypes,
Class<?> testClass, JupiterConfiguration configuration) {
return createDisplayNameSupplier(testClass, configuration,
generator -> generator.generateDisplayNameForNestedClass(enclosingInstanceTypes.get(), testClass));
static Supplier<String> createDisplayNameSupplierForNestedClass(
Supplier<List<Class<?>>> enclosingInstanceTypesSupplier, Class<?> testClass,
JupiterConfiguration configuration) {
return createDisplayNameSupplier(enclosingInstanceTypesSupplier, testClass, configuration,
(generator, enclosingInstanceTypes) -> generator.generateDisplayNameForNestedClass(enclosingInstanceTypes,
testClass));
}

private static Supplier<String> createDisplayNameSupplierForMethod(Supplier<List<Class<?>>> enclosingInstanceTypes,
Class<?> testClass, Method testMethod, JupiterConfiguration configuration) {
return createDisplayNameSupplier(testClass, configuration,
generator -> generator.generateDisplayNameForMethod(enclosingInstanceTypes.get(), testClass, testMethod));
private static Supplier<String> createDisplayNameSupplierForMethod(
Supplier<List<Class<?>>> enclosingInstanceTypesSupplier, Class<?> testClass, Method testMethod,
JupiterConfiguration configuration) {
return createDisplayNameSupplier(enclosingInstanceTypesSupplier, testClass, configuration,
(generator, enclosingInstanceTypes) -> generator.generateDisplayNameForMethod(enclosingInstanceTypes,
testClass, testMethod));
}

private static Supplier<String> createDisplayNameSupplier(Class<?> testClass, JupiterConfiguration configuration,
Function<DisplayNameGenerator, String> generatorFunction) {
return () -> findDisplayNameGenerator(testClass) //
.map(generatorFunction) //
.orElseGet(() -> generatorFunction.apply(configuration.getDefaultDisplayNameGenerator()));
private static Supplier<String> createDisplayNameSupplier(Supplier<List<Class<?>>> enclosingInstanceTypesSupplier,
Class<?> testClass, JupiterConfiguration configuration,
BiFunction<DisplayNameGenerator, List<Class<?>>, String> generatorFunction) {
return () -> {
List<Class<?>> enclosingInstanceTypes = enclosingInstanceTypesSupplier.get();
return findDisplayNameGenerator(enclosingInstanceTypes, testClass) //
.map(it -> generatorFunction.apply(it, enclosingInstanceTypes)) //
.orElseGet(() -> generatorFunction.apply(configuration.getDefaultDisplayNameGenerator(),
enclosingInstanceTypes));
};
}

private static Optional<DisplayNameGenerator> findDisplayNameGenerator(Class<?> testClass) {
private static Optional<DisplayNameGenerator> findDisplayNameGenerator(List<Class<?>> enclosingInstanceTypes,
Class<?> testClass) {
Preconditions.notNull(testClass, "Test class must not be null");

return findAnnotation(testClass, DisplayNameGeneration.class, SearchOption.INCLUDE_ENCLOSING_CLASSES) //
return findAnnotation(testClass, DisplayNameGeneration.class, enclosingInstanceTypes) //
.map(DisplayNameGeneration::value) //
.map(displayNameGeneratorClass -> {
if (displayNameGeneratorClass == Standard.class) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,16 @@ void indicativeSentencesRuntimeEnclosingType() {
);
}

@Test
void indicativeSentencesOnSubClass() {
check(IndicativeSentencesOnSubClassScenarioOneTestCase.class, //
"CONTAINER: IndicativeSentencesOnSubClassScenarioOneTestCase", //
"CONTAINER: IndicativeSentencesOnSubClassScenarioOneTestCase -> Level 1", //
"CONTAINER: IndicativeSentencesOnSubClassScenarioOneTestCase -> Level 1 -> Level 2", //
"TEST: IndicativeSentencesOnSubClassScenarioOneTestCase -> Level 1 -> Level 2 -> this is a test"//
);
}

private void check(Class<?> testClass, String... expectedDisplayNames) {
var request = request().selectors(selectClass(testClass)).build();
var descriptors = discoverTests(request).getDescendants();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright 2015-2025 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.jupiter.api;

/**
* @since 5.12
*/
@IndicativeSentencesGeneration(separator = " -> ", generator = DisplayNameGenerator.ReplaceUnderscores.class)
class IndicativeSentencesOnSubClassScenarioOneTestCase extends IndicativeSentencesOnSubClassTestCase {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2015-2025 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.jupiter.api;

/**
* @since 5.12
*/
@DisplayName("Base Scenario")
abstract class IndicativeSentencesOnSubClassTestCase {

@Nested
class Level_1 {

@Nested
class Level_2 {

@Test
void this_is_a_test() {
}
}
}
}

0 comments on commit 3265d58

Please sign in to comment.