From c2bfe37e21c7b798c0177ae1f23b6f3d934daa79 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Sat, 25 Jan 2025 14:54:43 +0100 Subject: [PATCH 1/7] Deprecate `SearchOption` and related method and introduce replacement --- .../release-notes-5.12.0-M1.adoc | 5 +- .../commons/support/AnnotationSupport.java | 59 ++++++++++++++++++- .../commons/support/SearchOption.java | 10 +++- .../support/AnnotationSupportTests.java | 26 +++++++- 4 files changed, 92 insertions(+), 8 deletions(-) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc index 0abbde8c9af2..b91553e4424b 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc @@ -32,7 +32,8 @@ JUnit repository on GitHub. [[release-notes-5.12.0-M1-junit-platform-deprecations-and-breaking-changes]] ==== Deprecations and Breaking Changes -* ❓ +* `SearchOption` and `AnnotationSupport.findAnnotation(Class, Class, SearchOption)` from + `junit-platform-commons` have been deprecated. [[release-notes-5.12.0-M1-junit-platform-new-features-and-improvements]] ==== New Features and Improvements @@ -75,6 +76,8 @@ JUnit repository on GitHub. to the most recent test or container that was started or has written output. * New public interface `ClasspathScanner` allowing third parties to provide a custom implementation for scanning the classpath for classes and resources. +* New `AnnotationSupport.findAnnotation(Class, Class, List)` method to support searching + for an annotation in an inner class and its runtime enclosing instance types. [[release-notes-5.12.0-M1-junit-jupiter]] diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/AnnotationSupport.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/AnnotationSupport.java index 3066916f3503..d56da2ae9a90 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/AnnotationSupport.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/AnnotationSupport.java @@ -10,8 +10,9 @@ package org.junit.platform.commons.support; +import static org.apiguardian.api.API.Status.DEPRECATED; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.MAINTAINED; -import static org.apiguardian.api.API.Status.STABLE; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; @@ -163,8 +164,13 @@ public static Optional findAnnotation(AnnotatedElement * @since 1.8 * @see SearchOption * @see #findAnnotation(AnnotatedElement, Class) + * @deprecated Use {@link #findAnnotation(Class, Class, List)} + * (for {@code SearchOption.DEFAULT}) or + * {@link #findAnnotation(Class, Class, List)} (for + * {@code SearchOption.INCLUDE_ENCLOSING_CLASSES}) instead */ - @API(status = STABLE, since = "1.10") + @Deprecated + @API(status = DEPRECATED, since = "1.12") public static Optional findAnnotation(Class clazz, Class annotationType, SearchOption searchOption) { @@ -174,6 +180,55 @@ public static Optional findAnnotation(Class clazz, searchOption == SearchOption.INCLUDE_ENCLOSING_CLASSES); } + /** + * Find the first annotation of the specified type that is either + * directly present, meta-present, or indirectly + * present on the supplied class. + * + *

If the annotation is neither directly present nor meta-present + * on the class, this method will additionally search on interfaces implemented + * by the class before searching for an annotation that is indirectly present + * on the class (i.e., within the class inheritance hierarchy). + * + *

If the annotation still has not been found, this method will optionally + * search recursively through supplied the enclosing instance types, starting + * at the innermost enclosing class (the last one in the supplied list of + * {@code enclosingInstanceTypes}). + * + * @implNote The classes supplied as {@code enclosingInstanceTypes} may + * differ from the classes returned from invocations of + * {@link Class#getEnclosingClass()} — for example, when a nested test + * class is inherited from a superclass. + * + * @param the annotation type + * @param clazz the class on which to search for the annotation; may be {@code null} + * @param annotationType the annotation type to search for; never {@code null} + * @param enclosingInstanceTypes the runtime types of the enclosing + * instances of {@code clazz}, ordered from outermost to innermost, + * excluding {@code class}; never {@code null} + * @return an {@code Optional} containing the annotation; never {@code null} but + * potentially empty if {@code clazz} is not an inner class + * @since 1.12 + * @see #findAnnotation(AnnotatedElement, Class) + */ + @API(status = EXPERIMENTAL, since = "1.12") + public static Optional findAnnotation(Class clazz, Class annotationType, + List> enclosingInstanceTypes) { + + Preconditions.notNull(enclosingInstanceTypes, "enclosingInstanceTypes must not be null"); + + Optional annotation = findAnnotation(clazz, annotationType); + if (!annotation.isPresent()) { + for (int i = enclosingInstanceTypes.size() - 1; i >= 0; i--) { + annotation = findAnnotation(enclosingInstanceTypes.get(i), annotationType); + if (annotation.isPresent()) { + break; + } + } + } + return annotation; + } + /** * Find all repeatable {@linkplain Annotation annotations} of the * supplied {@code annotationType} that are either present, diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/SearchOption.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/SearchOption.java index a4d21d210871..2e6a85d76abb 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/SearchOption.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/SearchOption.java @@ -10,7 +10,7 @@ package org.junit.platform.commons.support; -import static org.apiguardian.api.API.Status.STABLE; +import static org.apiguardian.api.API.Status.DEPRECATED; import org.apiguardian.api.API; @@ -20,8 +20,10 @@ * @since 1.8 * @see #DEFAULT * @see #INCLUDE_ENCLOSING_CLASSES + * @deprecated because there's only a single non-deprecated search option left */ -@API(status = STABLE, since = "1.10") +@Deprecated +@API(status = DEPRECATED, since = "1.12") public enum SearchOption { /** @@ -37,7 +39,11 @@ public enum SearchOption { * Search the inheritance hierarchy as with the {@link #DEFAULT} search option * but also search the {@linkplain Class#getEnclosingClass() enclosing class} * hierarchy for inner classes (i.e., a non-static member classes). + * + * @deprecated because it's preferable to inspect the runtime enclosing + * types of a class rather than where they are declared. */ + @Deprecated @API(status = DEPRECATED, since = "1.12") INCLUDE_ENCLOSING_CLASSES } diff --git a/platform-tests/src/test/java/org/junit/platform/commons/support/AnnotationSupportTests.java b/platform-tests/src/test/java/org/junit/platform/commons/support/AnnotationSupportTests.java index 88cf94db32ed..b882977606a1 100644 --- a/platform-tests/src/test/java/org/junit/platform/commons/support/AnnotationSupportTests.java +++ b/platform-tests/src/test/java/org/junit/platform/commons/support/AnnotationSupportTests.java @@ -20,6 +20,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.List; import java.util.Optional; import org.junit.jupiter.api.Tag; @@ -80,16 +81,24 @@ void findAnnotationOnElementDelegates() { AnnotationSupport.findAnnotation(element, Override.class)); } + @SuppressWarnings("deprecation") @Test - void findAnnotationOnClassPreconditions() { + void findAnnotationOnClassWithSearchModePreconditions() { assertPreconditionViolationException("annotationType", () -> AnnotationSupport.findAnnotation(Probe.class, null, SearchOption.INCLUDE_ENCLOSING_CLASSES)); assertPreconditionViolationException("SearchOption", - () -> AnnotationSupport.findAnnotation(Probe.class, Override.class, null)); + () -> AnnotationSupport.findAnnotation(Probe.class, Override.class, (SearchOption) null)); } @Test - void findAnnotationOnClassDelegates() { + void findAnnotationOnClassWithEnclosingInstanceTypesPreconditions() { + assertPreconditionViolationException("enclosingInstanceTypes", + () -> AnnotationSupport.findAnnotation(Probe.class, Override.class, (List>) null)); + } + + @SuppressWarnings("deprecation") + @Test + void findAnnotationOnClassWithSearchModeDelegates() { Class clazz = Probe.class; assertEquals(AnnotationUtils.findAnnotation(clazz, Tag.class, false), AnnotationSupport.findAnnotation(clazz, Tag.class, SearchOption.DEFAULT)); @@ -111,6 +120,16 @@ void findAnnotationOnClassDelegates() { AnnotationSupport.findAnnotation(clazz, Override.class, SearchOption.INCLUDE_ENCLOSING_CLASSES)); } + @Test + void findAnnotationOnClassWithEnclosingInstanceTypes() { + assertThat(AnnotationSupport.findAnnotation(Probe.class, Tag.class, List.of())) // + .contains(Probe.class.getDeclaredAnnotation(Tag.class)); + assertThat(AnnotationSupport.findAnnotation(Probe.InnerClass.class, Tag.class, List.of())) // + .isEmpty(); + assertThat(AnnotationSupport.findAnnotation(Probe.InnerClass.class, Tag.class, List.of(Probe.class))) // + .contains(Probe.class.getDeclaredAnnotation(Tag.class)); + } + @Test void findPublicAnnotatedFieldsPreconditions() { assertPreconditionViolationException("Class", @@ -301,6 +320,7 @@ void aMethod() { void bMethod() { } + @SuppressWarnings("InnerClassMayBeStatic") class InnerClass { } From 42022f9fec725099f8f08205643b3974eb27ab1a Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Sat, 25 Jan 2025 18:05:06 +0100 Subject: [PATCH 2/7] Check for DisplayNameGeneration annotations on runtime enclosing types Resolves #4131. --- .../jupiter/api/DisplayNameGenerator.java | 76 +++++++++++-------- .../engine/descriptor/DisplayNameUtils.java | 48 +++++++----- .../api/DisplayNameGenerationTests.java | 10 +++ ...entencesOnSubClassScenarioOneTestCase.java | 18 +++++ ...IndicativeSentencesOnSubClassTestCase.java | 30 ++++++++ 5 files changed, 131 insertions(+), 51 deletions(-) create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/api/IndicativeSentencesOnSubClassScenarioOneTestCase.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/api/IndicativeSentencesOnSubClassTestCase.java diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java index 89f7784d8c57..fed2c6a96ea3 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java @@ -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; @@ -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; @@ -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> enclosingInstanceTypes, Class nestedClass) { - return getSentenceBeginning(enclosingInstanceTypes, nestedClass); + return getSentenceBeginning(nestedClass, enclosingInstanceTypes); } @Override public String generateDisplayNameForMethod(List> 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> enclosingInstanceTypes, Class testClass) { + private String getSentenceBeginning(Class testClass, List> enclosingInstanceTypes) { Class enclosingClass = enclosingInstanceTypes.isEmpty() ? null : enclosingInstanceTypes.get(enclosingInstanceTypes.size() - 1); boolean topLevelTestClass = (enclosingClass == null || isStatic(testClass)); @@ -342,33 +343,35 @@ private String getSentenceBeginning(List> enclosingInstanceTypes, Class if (displayName.isPresent()) { return displayName.get(); } - Class generatorClass = findDisplayNameGeneration(testClass)// - .map(DisplayNameGeneration::value)// - .filter(not(IndicativeSentences.class))// - .orElse(null); + Class 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> 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> 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)); } /** @@ -381,17 +384,18 @@ private String getSentenceBeginning(List> 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> enclosingInstanceTypes) { + return findIndicativeSentencesGeneration(testClass, enclosingInstanceTypes)// .map(IndicativeSentencesGeneration::separator)// .orElse(IndicativeSentencesGeneration.DEFAULT_SEPARATOR); } /** * Get the display name generator to use for the supplied test class. - * *

If {@link IndicativeSentencesGeneration @IndicativeSentencesGeneration} * is present (searching enclosing classes if not found locally), the * configured {@link IndicativeSentencesGeneration#generator() generator} @@ -399,10 +403,12 @@ private static String getFragmentSeparator(Class testClass) { * 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> enclosingInstanceTypes) { + return findIndicativeSentencesGeneration(testClass, enclosingInstanceTypes)// .map(IndicativeSentencesGeneration::generator)// .filter(not(IndicativeSentences.class))// .map(DisplayNameGenerator::getDisplayNameGenerator)// @@ -412,26 +418,32 @@ private static DisplayNameGenerator getGeneratorFor(Class testClass) { /** * Find the first {@code DisplayNameGeneration} annotation that is either * directly present, meta-present, or indirectly present - * 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 findDisplayNameGeneration(Class testClass) { - return findAnnotation(testClass, DisplayNameGeneration.class, SearchOption.INCLUDE_ENCLOSING_CLASSES); + @API(status = INTERNAL, since = "5.12") + static Optional findDisplayNameGeneration(Class testClass, + List> enclosingInstanceTypes) { + return findAnnotation(testClass, DisplayNameGeneration.class, enclosingInstanceTypes); } /** * Find the first {@code IndicativeSentencesGeneration} annotation that is either * directly present, meta-present, or indirectly present - * 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 findIndicativeSentencesGeneration(Class testClass) { - return findAnnotation(testClass, IndicativeSentencesGeneration.class, - SearchOption.INCLUDE_ENCLOSING_CLASSES); + private static Optional findIndicativeSentencesGeneration(Class testClass, + List> enclosingInstanceTypes) { + return findAnnotation(testClass, IndicativeSentencesGeneration.class, enclosingInstanceTypes); } private static Predicate> not(Class clazz) { diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DisplayNameUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DisplayNameUtils.java index 76b65ef5e49a..c2c0903d91a8 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DisplayNameUtils.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DisplayNameUtils.java @@ -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; @@ -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; @@ -97,33 +97,43 @@ static String determineDisplayNameForMethod(Supplier>> enclosingIn } static Supplier 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 createDisplayNameSupplierForNestedClass(Supplier>> enclosingInstanceTypes, - Class testClass, JupiterConfiguration configuration) { - return createDisplayNameSupplier(testClass, configuration, - generator -> generator.generateDisplayNameForNestedClass(enclosingInstanceTypes.get(), testClass)); + static Supplier createDisplayNameSupplierForNestedClass( + Supplier>> enclosingInstanceTypesSupplier, Class testClass, + JupiterConfiguration configuration) { + return createDisplayNameSupplier(enclosingInstanceTypesSupplier, testClass, configuration, + (generator, enclosingInstanceTypes) -> generator.generateDisplayNameForNestedClass(enclosingInstanceTypes, + testClass)); } - private static Supplier createDisplayNameSupplierForMethod(Supplier>> enclosingInstanceTypes, - Class testClass, Method testMethod, JupiterConfiguration configuration) { - return createDisplayNameSupplier(testClass, configuration, - generator -> generator.generateDisplayNameForMethod(enclosingInstanceTypes.get(), testClass, testMethod)); + private static Supplier createDisplayNameSupplierForMethod( + Supplier>> enclosingInstanceTypesSupplier, Class testClass, Method testMethod, + JupiterConfiguration configuration) { + return createDisplayNameSupplier(enclosingInstanceTypesSupplier, testClass, configuration, + (generator, enclosingInstanceTypes) -> generator.generateDisplayNameForMethod(enclosingInstanceTypes, + testClass, testMethod)); } - private static Supplier createDisplayNameSupplier(Class testClass, JupiterConfiguration configuration, - Function generatorFunction) { - return () -> findDisplayNameGenerator(testClass) // - .map(generatorFunction) // - .orElseGet(() -> generatorFunction.apply(configuration.getDefaultDisplayNameGenerator())); + private static Supplier createDisplayNameSupplier(Supplier>> enclosingInstanceTypesSupplier, + Class testClass, JupiterConfiguration configuration, + BiFunction>, String> generatorFunction) { + return () -> { + List> enclosingInstanceTypes = enclosingInstanceTypesSupplier.get(); + return findDisplayNameGenerator(enclosingInstanceTypes, testClass) // + .map(it -> generatorFunction.apply(it, enclosingInstanceTypes)) // + .orElseGet(() -> generatorFunction.apply(configuration.getDefaultDisplayNameGenerator(), + enclosingInstanceTypes)); + }; } - private static Optional findDisplayNameGenerator(Class testClass) { + private static Optional findDisplayNameGenerator(List> 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) { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/DisplayNameGenerationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/DisplayNameGenerationTests.java index c7a1fe90efc6..898dfc1ea9e8 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/api/DisplayNameGenerationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/DisplayNameGenerationTests.java @@ -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(); diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/IndicativeSentencesOnSubClassScenarioOneTestCase.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/IndicativeSentencesOnSubClassScenarioOneTestCase.java new file mode 100644 index 000000000000..5ad3e99cdcce --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/IndicativeSentencesOnSubClassScenarioOneTestCase.java @@ -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 { +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/IndicativeSentencesOnSubClassTestCase.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/IndicativeSentencesOnSubClassTestCase.java new file mode 100644 index 000000000000..1127970070d1 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/IndicativeSentencesOnSubClassTestCase.java @@ -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() { + } + } + } +} From 4d05f5364f9c9c6faa869131d05ffc3cbdc3cd2b Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Sun, 26 Jan 2025 18:00:27 +0100 Subject: [PATCH 3/7] Fix typo Co-authored-by: Sam Brannen <104798+sbrannen@users.noreply.github.com> --- .../docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc index b91553e4424b..0998b5f01194 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc @@ -77,7 +77,7 @@ JUnit repository on GitHub. * New public interface `ClasspathScanner` allowing third parties to provide a custom implementation for scanning the classpath for classes and resources. * New `AnnotationSupport.findAnnotation(Class, Class, List)` method to support searching - for an annotation in an inner class and its runtime enclosing instance types. + for an annotation on an inner class and its runtime enclosing instance types. [[release-notes-5.12.0-M1-junit-jupiter]] From 87ef69d68ed7839ea08758849be572a014fd547d Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Sun, 26 Jan 2025 18:05:47 +0100 Subject: [PATCH 4/7] Improve Javadoc Co-authored-by: Sam Brannen <104798+sbrannen@users.noreply.github.com> --- .../java/org/junit/jupiter/api/DisplayNameGenerator.java | 2 +- .../junit/platform/commons/support/AnnotationSupport.java | 6 +++--- .../org/junit/platform/commons/support/SearchOption.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java index fed2c6a96ea3..e433e59e21ef 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java @@ -436,7 +436,7 @@ static Optional findDisplayNameGeneration(Class testCl * directly present, meta-present, or indirectly present * 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 diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/AnnotationSupport.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/AnnotationSupport.java index d56da2ae9a90..d5265aea9d05 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/AnnotationSupport.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/AnnotationSupport.java @@ -191,7 +191,7 @@ public static Optional findAnnotation(Class clazz, * on the class (i.e., within the class inheritance hierarchy). * *

If the annotation still has not been found, this method will optionally - * search recursively through supplied the enclosing instance types, starting + * search recursively through the supplied enclosing instance types, starting * at the innermost enclosing class (the last one in the supplied list of * {@code enclosingInstanceTypes}). * @@ -204,8 +204,8 @@ public static Optional findAnnotation(Class clazz, * @param clazz the class on which to search for the annotation; may be {@code null} * @param annotationType the annotation type to search for; never {@code null} * @param enclosingInstanceTypes the runtime types of the enclosing - * instances of {@code clazz}, ordered from outermost to innermost, - * excluding {@code class}; never {@code null} + * instances for the class, ordered from outermost to innermost, + * excluding {@code clazz}; never {@code null} * @return an {@code Optional} containing the annotation; never {@code null} but * potentially empty if {@code clazz} is not an inner class * @since 1.12 diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/SearchOption.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/SearchOption.java index 2e6a85d76abb..4152d14e8220 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/SearchOption.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/SearchOption.java @@ -20,7 +20,7 @@ * @since 1.8 * @see #DEFAULT * @see #INCLUDE_ENCLOSING_CLASSES - * @deprecated because there's only a single non-deprecated search option left + * @deprecated because there is only a single non-deprecated search option left */ @Deprecated @API(status = DEPRECATED, since = "1.12") @@ -40,7 +40,7 @@ public enum SearchOption { * but also search the {@linkplain Class#getEnclosingClass() enclosing class} * hierarchy for inner classes (i.e., a non-static member classes). * - * @deprecated because it's preferable to inspect the runtime enclosing + * @deprecated because it is preferable to inspect the runtime enclosing * types of a class rather than where they are declared. */ @Deprecated @API(status = DEPRECATED, since = "1.12") From ee7be4654bd1c57f23c891eecd72af9a395910a3 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Sun, 26 Jan 2025 18:29:23 +0100 Subject: [PATCH 5/7] Restore accidentally removed blank line --- .../main/java/org/junit/jupiter/api/DisplayNameGenerator.java | 1 + 1 file changed, 1 insertion(+) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java index e433e59e21ef..f2cb801a4b69 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java @@ -396,6 +396,7 @@ private static String getFragmentSeparator(Class testClass, List> en /** * Get the display name generator to use for the supplied test class. + * *

If {@link IndicativeSentencesGeneration @IndicativeSentencesGeneration} * is present (searching enclosing classes if not found locally), the * configured {@link IndicativeSentencesGeneration#generator() generator} From da61c329c0ad34de9abe0718028e323f567f0ca4 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Sun, 26 Jan 2025 18:31:26 +0100 Subject: [PATCH 6/7] Make method private again --- .../main/java/org/junit/jupiter/api/DisplayNameGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java index f2cb801a4b69..9852c79bffd5 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java @@ -427,7 +427,7 @@ private static DisplayNameGenerator getGeneratorFor(Class testClass, List findDisplayNameGeneration(Class testClass, + private static Optional findDisplayNameGeneration(Class testClass, List> enclosingInstanceTypes) { return findAnnotation(testClass, DisplayNameGeneration.class, enclosingInstanceTypes); } From b7237a84bcbe3508af3868aeaf7a1697328f4461 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Sun, 26 Jan 2025 19:21:46 +0100 Subject: [PATCH 7/7] Add release note entry --- .../docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc index 0998b5f01194..ff501e489fcf 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc @@ -90,6 +90,9 @@ JUnit repository on GitHub. to `DisplayNameGenerator` implementations. Prior to this change, such generators were only able to access the enclosing class in which `@Nested` was declared, but they could not access the concrete runtime type of the enclosing instance. +* `@DisplayNameGeneration` annotations are now discovered on the _runtime_ enclosing types + of `@Nested` test classes instead of the compile-time enclosing class in which the + `@Nested` class was _declared_. [[release-notes-5.12.0-M1-junit-jupiter-deprecations-and-breaking-changes]] ==== Deprecations and Breaking Changes