Skip to content

Commit

Permalink
Introduce @⁠SentenceFragment for IndicativeSentences DisplayNameGener…
Browse files Browse the repository at this point in the history
…ator

Closes junit-team#4347
  • Loading branch information
sbrannen committed Mar 2, 2025
1 parent 34509e5 commit 800ecba
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 11 deletions.
20 changes: 20 additions & 0 deletions documentation/src/test/java/example/DisplayNameGeneratorDemo.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.DisplayNameGenerator.IndicativeSentences.SentenceFragment;
import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores;
import org.junit.jupiter.api.IndicativeSentencesGeneration;
import org.junit.jupiter.api.Nested;
Expand Down Expand Up @@ -55,5 +56,24 @@ void if_it_is_one_of_the_following_years(int year) {

}

@Nested
@IndicativeSentencesGeneration
@DisplayName("A year is a leap year")
class LeapYearTests {

@Nested
@SentenceFragment("(nested)")
class NestedTests {

@ParameterizedTest(name = "{0}")
@ValueSource(ints = { 2016, 2020, 2048 })
@SentenceFragment("if it is one of the following years")
void validLeapYear(int year) {
}

}

}

}
// end::user_guide[]
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,23 @@
import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation;
import static org.junit.platform.commons.support.ModifierSupport.isStatic;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;

import org.apiguardian.api.API;
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.util.ClassUtils;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.StringUtils;

/**
* {@code DisplayNameGenerator} defines the SPI for generating display names
Expand Down Expand Up @@ -303,13 +311,44 @@ private static String replaceUnderscores(String name) {
* via the {@link IndicativeSentencesGeneration @IndicativeSentencesGeneration}
* annotation.
*
* <p>If you do not want to rely on a display name generator for individual
* sentence fragments, you can supply custom text for individual fragments
* via the {@link SentenceFragment @SentenceFragment} annotation.
*
* @since 5.7
*/
@API(status = STABLE, since = "5.10")
class IndicativeSentences implements DisplayNameGenerator {

/**
* {@code @SentenceFragment} is used to configure a custom sentence fragment
* for a sentence generated by the {@link IndicativeSentences}
* {@code DisplayNameGenerator}.
*
* <p>Note that {@link DisplayName @DisplayName} always takes precedence
* over {@code @SentenceFragment}.
*
* @since 5.13
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@API(status = EXPERIMENTAL, since = "5.13")
public @interface SentenceFragment {

/**
* Custom sentence fragment for the annotated class or method.
*
* @return a custom sentence fragment; never blank or consisting solely
* of whitespace
*/
String value();

}

static final DisplayNameGenerator INSTANCE = new IndicativeSentences();

private static final Logger logger = LoggerFactory.getLogger(IndicativeSentences.class);

private static final Predicate<Class<?>> notIndicativeSentences = clazz -> clazz != IndicativeSentences.class;

public IndicativeSentences() {
Expand All @@ -328,22 +367,30 @@ public String generateDisplayNameForNestedClass(List<Class<?>> enclosingInstance
@Override
public String generateDisplayNameForMethod(List<Class<?>> enclosingInstanceTypes, Class<?> testClass,
Method testMethod) {
return getSentenceBeginning(testClass, enclosingInstanceTypes)
+ getFragmentSeparator(testClass, enclosingInstanceTypes)
+ getGeneratorFor(testClass, enclosingInstanceTypes).generateDisplayNameForMethod(
enclosingInstanceTypes, testClass, testMethod);

String displayName = getSentenceBeginning(testClass, enclosingInstanceTypes)
+ getFragmentSeparator(testClass, enclosingInstanceTypes);

String sentenceFragment = getSentenceFragment(testMethod);
displayName += (sentenceFragment != null ? sentenceFragment
: getGeneratorFor(testClass, enclosingInstanceTypes).generateDisplayNameForMethod(
enclosingInstanceTypes, testClass, testMethod));
return displayName;
}

private String getSentenceBeginning(Class<?> testClass, List<Class<?>> enclosingInstanceTypes) {
Class<?> enclosingClass = enclosingInstanceTypes.isEmpty() ? null
: enclosingInstanceTypes.get(enclosingInstanceTypes.size() - 1);
boolean topLevelTestClass = (enclosingClass == null || isStatic(testClass));
Optional<String> displayName = findAnnotation(testClass, DisplayName.class)//
.map(DisplayName::value).map(String::trim);

String sentenceFragment = findAnnotation(testClass, DisplayName.class)//
.map(DisplayName::value)//
.map(String::trim)//
.orElseGet(() -> getSentenceFragment(testClass));

if (topLevelTestClass) {
if (displayName.isPresent()) {
return displayName.get();
if (sentenceFragment != null) {
return sentenceFragment;
}
Class<? extends DisplayNameGenerator> generatorClass = findDisplayNameGeneration(testClass,
enclosingInstanceTypes)//
Expand Down Expand Up @@ -371,9 +418,9 @@ private String getSentenceBeginning(Class<?> testClass, List<Class<?>> enclosing
+ getFragmentSeparator(testClass, enclosingInstanceTypes)
: "");

return prefix + displayName.orElseGet(
() -> getGeneratorFor(testClass, enclosingInstanceTypes).generateDisplayNameForNestedClass(
remainingEnclosingInstanceTypes, testClass));
return prefix + (sentenceFragment != null ? sentenceFragment
: getGeneratorFor(testClass, enclosingInstanceTypes).generateDisplayNameForNestedClass(
remainingEnclosingInstanceTypes, testClass));
}

/**
Expand Down Expand Up @@ -449,6 +496,25 @@ private static Optional<IndicativeSentencesGeneration> findIndicativeSentencesGe
return findAnnotation(testClass, IndicativeSentencesGeneration.class, enclosingInstanceTypes);
}

private static String getSentenceFragment(AnnotatedElement element) {
Optional<SentenceFragment> annotation = findAnnotation(element, SentenceFragment.class);
if (annotation.isPresent()) {
String sentenceFragment = annotation.get().value().trim();

// TODO [#242] Replace logging with precondition check once we have a proper mechanism for
// handling validation exceptions during the TestEngine discovery phase.
if (StringUtils.isBlank(sentenceFragment)) {
logger.warn(() -> String.format(
"Configuration error: @SentenceFragment on [%s] must be declared with a non-blank value.",
element));
}
else {
return sentenceFragment;
}
}
return null;
}

}

/**
Expand Down

0 comments on commit 800ecba

Please sign in to comment.