diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2078c532b4c8..2313cecda3ff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,10 +28,10 @@ Issue: #999 ## Pull Requests Our [Definition of Done](https://github.com/junit-team/junit5/wiki/Definition-of-Done) -offers some guidelines on what we expect from a pull request. +(DoD) offers some guidelines on what we expect from a pull request. Feel free to open a pull request that does not fulfill all criteria, e.g. to discuss a certain change before polishing it, but please be aware that we will only merge it -in case the DoD is met. +once the DoD is met. Please add the following lines to your pull request description: @@ -95,16 +95,15 @@ code -- class names, method names, variable names, etc. ### Javadoc - Javadoc comments should be wrapped after 80 characters whenever possible. -- This first paragraph must be a single, concise sentence that ends with a period ("."). -- Place `

` on the same line as the first line in a new paragraph and precede `

` with a blank line. +- This first paragraph must be a single, concise sentence that ends with a period (`.`). +- Place `

` on the same line as the first line of a new paragraph and precede `

` with a blank line. - Insert a blank line before at-clauses/tags. - Favor `{@code foo}` over `foo`. - Favor literals (e.g., `{@literal @}`) over HTML entities. -- New classes and methods should have `@since ...` annotation. -- Use `@since 5.0` instead of `@since 5.0.0`. -- Do not use `@author` tags. Instead, contributors are listed on [GitHub](https://github.com/junit-team/junit5/graphs/contributors). -- Do not use verbs in third person form (e.g. use "Discover tests..." instead of "Discovers tests...") - in the first sentence describing a method. +- New classes and methods should declare a `@since ...` tag. +- Use `@since 5.10` instead of `@since 5.10.0`. +- Do not use `@author` tags. Instead, contributors are listed on the [GitHub](https://github.com/junit-team/junit5/graphs/contributors) page. +- Do not use verbs in third-person form in the first sentence of the Javadoc for a method -- for example, use "Discover tests..." instead of "Discovers tests...". #### Examples @@ -121,11 +120,11 @@ See [`ExtensionContext`](junit-jupiter-api/src/main/java/org/junit/jupiter/api/e #### Assertions -- Use `org.junit.jupiter.api.Assertions` wherever possible. +- Use `org.junit.jupiter.api.Assertions` for simple assertions. - Use AssertJ when richer assertions are needed. - Do not use `org.junit.Assert` or `junit.framework.Assert`. -#### Mocking +#### Mocking and Stubbing - Use either [Mockito](https://github.com/mockito/mockito) or hand-written test doubles. @@ -143,10 +142,11 @@ See [`ExtensionContext`](junit-jupiter-api/src/main/java/org/junit/jupiter/api/e ### Deprecation -Publicly available interfaces, classes and methods have a defined lifecycle +The JUnit 5 project uses the `@API` annotation from [API Guardian](https://github.com/apiguardian-team/apiguardian). +Publicly available interfaces, classes, and methods have a defined lifecycle which is described in detail in the [User Guide](https://junit.org/junit5/docs/current/user-guide/#api-evolution). -This process is using the `@API` annotation from [API Guardian](https://github.com/apiguardian-team/apiguardian). -It also describes the deprecation process followed for API items. + +That following describes the deprecation process followed for API items. To deprecate an item: - Update the `@API.status` to `DEPRECATED`. diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-RC1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-RC1.adoc index 53db2d882329..c3a9bdaea88f 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-RC1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-RC1.adoc @@ -28,8 +28,11 @@ JUnit repository on GitHub. ==== New Features and Improvements * New `@SelectMethod` selector support in the `@Suite` test engine. -* Classes may now be selected by fully-qualified name via new the `names` attribute in +* Classes may now be selected by fully-qualified name via the `names` attribute in `@SelectClasses`. +* New `selectMethod()` and `selectNestedMethod()` variants in `DiscoverySelectors` that + accept a `Class...` argument of parameter types as a type-safe alternative to + providing the names of parameter types as a comma-delimited string. [[release-notes-5.10.0-RC1-junit-jupiter]] @@ -52,6 +55,11 @@ JUnit repository on GitHub. * Lifecycle and thread-safety semantics are now documented for the `TempDirFactory` SPI. * New `reason` attribute in `@Execution` which can be used to document the reason for using the selected execution mode. +* The <<../user-guide/index.adoc#extensions-RandomNumberExtension, User Guide>> now + includes an example implementation of the `RandomNumberExtension` in order to improve + the documentation for extension registration via `@ExtendWith` on fields. +* The scope of applicability for `TestWatcher` implementations is now more extensively + documented in the User Guide and Javadoc. [[release-notes-5.10.0-RC1-junit-vintage]] diff --git a/documentation/src/docs/asciidoc/user-guide/extensions.adoc b/documentation/src/docs/asciidoc/user-guide/extensions.adoc index a1fee08b8303..09ca2ec5e88b 100644 --- a/documentation/src/docs/asciidoc/user-guide/extensions.adoc +++ b/documentation/src/docs/asciidoc/user-guide/extensions.adoc @@ -115,6 +115,25 @@ include::{testDir}/example/extensions/Random.java[tags=user_guide] include::{testDir}/example/extensions/RandomNumberDemo.java[tags=user_guide] ---- +[[extensions-RandomNumberExtension]] +The following code listing provides an example of how one might choose to implement such a +`RandomNumberExtension`. This implementation works for the use cases in +`RandomNumberDemo`; however, it may not prove robust enough to cover all use cases – for +example, the random number generation support is limited to integers, it uses +`java.util.Random` instead of `java.security.SecureRandom`, etc. In any case, it is +important to note which extension APIs are implemented and for what reasons. + +Specifically, `RandomNumberExtension` implements the following extension APIs: + +- `BeforeAllCallback`: to support static field injection +- `TestInstancePostProcessor`: to support non-static field injection +- `ParameterResolver`: to support constructor and method injection + +[source,java,indent=0] +---- +include::{testDir}/example/extensions/RandomNumberExtension.java[tags=user_guide] +---- + [TIP] .Extension Registration Order for `@ExtendWith` on Fields ==== @@ -386,11 +405,11 @@ test instance, invoking custom de-initialization methods on the test instance, e runtime. If a _test class_ constructor, _test method_, or _lifecycle method_ (see -<>) declares a parameter, the parameter must be -_resolved_ at runtime by a `ParameterResolver`. A `ParameterResolver` can either be -built-in (see `{TestInfoParameterResolver}`) or <>. Generally speaking, parameters may be resolved by _name_, _type_, -_annotation_, or any combination thereof. +<>) declares a parameter, the parameter must be _resolved_ at +runtime by a `ParameterResolver`. A `ParameterResolver` can either be built-in (see +`{TestInfoParameterResolver}`) or <>. +Generally speaking, parameters may be resolved by _name_, _type_, _annotation_, or any +combination thereof. If you wish to implement a custom `{ParameterResolver}` that resolves parameters based solely on the type of the parameter, you may find it convenient to extend the @@ -436,20 +455,44 @@ information for the following events. * `testFailed`: invoked after a _test method_ has failed NOTE: In contrast to the definition of "test method" presented in -<>, in this context _test method_ refers to any -`@Test` method or `@TestTemplate` method (for example, a `@RepeatedTest` or -`@ParameterizedTest`). +<>, in this context _test method_ refers to any `@Test` method +or `@TestTemplate` method (for example, a `@RepeatedTest` or `@ParameterizedTest`). + +Extensions implementing this interface can be registered at the class level, instance +level, or method level. When registered at the class level, a `TestWatcher` will be +invoked for any contained _test method_ including those in `@Nested` classes. When +registered at the method level, a `TestWatcher` will only be invoked for the _test method_ +for which it was registered. + +[WARNING] +==== +If a `TestWatcher` is registered via a non-static (instance) field – for example, using +`@RegisterExtension` – and the test class is configured with +`@TestInstance(Lifecycle.PER_METHOD)` semantics (which is the default lifecycle mode), the +`TestWatcher` will **not** be invoked with events for `@TestTemplate` methods (for +example, `@RepeatedTest` or `@ParameterizedTest`). + +To ensure that a `TestWatcher` is invoked for all _test methods_ in a given class, it is +therefore recommended that the `TestWatcher` be registered at the class level with +`@ExtendWith` or via a `static` field with `@RegisterExtension` or `@ExtendWith`. +==== + +If there is a failure at the class level — for example, an exception thrown by a +`@BeforeAll` method — no test results will be reported. Similarly, if the test class is +disabled via an `ExecutionCondition` — for example, `@Disabled` — no test results will be +reported. -Extensions implementing this interface can be registered at the method level or at the -class level. In the latter case they will be invoked for any contained _test method_ -including those in `@Nested` classes. +In contrast to other Extension APIs, a `TestWatcher` is not permitted to adversely +influence the execution of tests. Consequently, any exception thrown by a method in the +`TestWatcher` API will be logged at `WARNING` level and will not be allowed to propagate +or fail test execution. [WARNING] ==== Any instances of `ExtensionContext.Store.CloseableResource` stored in the `Store` of the -provided `{ExtensionContext}` will be closed _before_ methods in this API are invoked (see -<>). You can use the parent context's `Store` to work with such -resources. +provided `{ExtensionContext}` will be closed _before_ methods in the `TestWatcher` API are +invoked (see <>). You can use the parent context's `Store` to +work with such resources. ==== [[extensions-lifecycle-callbacks]] @@ -797,7 +840,7 @@ callbacks implemented by `Extension2`. `Extension1` is therefore said to _wrap_ `Extension2`. JUnit Jupiter also guarantees _wrapping_ behavior within class and interface hierarchies -for user-supplied _lifecycle methods_ (see <>). +for user-supplied _lifecycle methods_ (see <>). * `@BeforeAll` methods are inherited from superclasses as long as they are not _hidden_, _overridden_, or _superseded_ (i.e., replaced based on signature only, irrespective of @@ -945,7 +988,6 @@ image::extensions_BrokenLifecycleMethodConfigDemo.png[caption='',title='BrokenLi [TIP] ==== Due to the aforementioned behavior, the JUnit Team recommends that developers declare at -most one of each type of _lifecycle method_ (see <>) -per test class or test interface unless there are no dependencies between such lifecycle -methods. +most one of each type of _lifecycle method_ (see <>) per test +class or test interface unless there are no dependencies between such lifecycle methods. ==== diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index 142f752e47c3..97991db4fb86 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -592,8 +592,7 @@ deterministic but intentionally nonobvious. This ensures that subsequent runs of suite execute test classes and test methods in the same order, thereby allowing for repeatable builds. -NOTE: See <> for a definition of _test method_ and -_test class_. +NOTE: See <> for a definition of _test method_ and _test class_. [[writing-tests-test-execution-order-methods]] ==== Method Order @@ -665,8 +664,8 @@ dependencies between test classes, or you may wish to order test classes to opti time as outlined in the following scenarios. * Run previously failing tests and faster tests first: "fail fast" mode -* With parallel execution enabled, run longer tests first: "shortest test plan execution - duration" mode +* With parallel execution enabled, schedule longer tests first: "shortest test plan + execution duration" mode * Various other use cases To configure test class execution order _globally_ for the entire test suite, use the @@ -728,8 +727,8 @@ include::{testDir}/example/OrderedNestedTestClassesDemo.java[tags=user_guide] In order to allow individual test methods to be executed in isolation and to avoid unexpected side effects due to mutable test instance state, JUnit creates a new instance of each test class before executing each _test method_ (see -<>). This "per-method" test instance lifecycle is the -default behavior in JUnit Jupiter and is analogous to all previous versions of JUnit. +<>). This "per-method" test instance lifecycle is the default +behavior in JUnit Jupiter and is analogous to all previous versions of JUnit. NOTE: Please note that the test class will still be instantiated if a given _test method_ is _disabled_ via a <> (e.g., `@Disabled`, @@ -840,8 +839,8 @@ constructors and methods. `{ParameterResolver}` defines the API for test extensions that wish to _dynamically_ resolve parameters at runtime. If a _test class_ constructor, a _test method_, or a -_lifecycle method_ (see <>) accepts a parameter, the -parameter must be resolved at runtime by a registered `ParameterResolver`. +_lifecycle method_ (see <>) accepts a parameter, the parameter +must be resolved at runtime by a registered `ParameterResolver`. There are currently three built-in resolvers that are registered automatically. @@ -2303,6 +2302,12 @@ might conflict with the configured execution order. Thus, in both cases, test me such test classes are only executed concurrently if the `@Execution(CONCURRENT)` annotation is present on the test class or method. +When parallel execution is enabled and a default `{ClassOrderer}` (see +<> for details) is registered, top-level test +classes will initially be sorted accordingly and scheduled in that order. However, they +are not guaranteed to be started in exactly that order since the threads they are executed +on are not controlled directly by JUnit. + All nodes of the test tree that are configured with the `CONCURRENT` execution mode will be executed fully in parallel according to the provided <> while observing the diff --git a/documentation/src/test/java/example/extensions/RandomNumberDemo.java b/documentation/src/test/java/example/extensions/RandomNumberDemo.java index d52af0a65ef8..8dd274bda73c 100644 --- a/documentation/src/test/java/example/extensions/RandomNumberDemo.java +++ b/documentation/src/test/java/example/extensions/RandomNumberDemo.java @@ -11,31 +11,34 @@ package example.extensions; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -@Disabled("RandomNumberExtension has not been implemented") -//tag::user_guide[] +// tag::user_guide[] class RandomNumberDemo { - // use random number field in test methods and @BeforeEach - // or @AfterEach lifecycle methods + // Use static randomNumber0 field anywhere in the test class, + // including @BeforeAll or @AfterEach lifecycle methods. + @Random + private static Integer randomNumber0; + + // Use randomNumber1 field in test methods and @BeforeEach + // or @AfterEach lifecycle methods. @Random private int randomNumber1; RandomNumberDemo(@Random int randomNumber2) { - // use random number in constructor + // Use randomNumber2 in constructor } @BeforeEach void beforeEach(@Random int randomNumber3) { - // use random number in @BeforeEach method + // Use randomNumber3 in @BeforeEach method. } @Test void test(@Random int randomNumber4) { - // use random number in test method + // Use randomNumber4 in test method. } } -//end::user_guide[] +// end::user_guide[] diff --git a/documentation/src/test/java/example/extensions/RandomNumberExtension.java b/documentation/src/test/java/example/extensions/RandomNumberExtension.java index 98f1d1ace3f8..3bbece08e206 100644 --- a/documentation/src/test/java/example/extensions/RandomNumberExtension.java +++ b/documentation/src/test/java/example/extensions/RandomNumberExtension.java @@ -10,30 +10,85 @@ package example.extensions; +// tag::user_guide[] + +import static org.junit.platform.commons.support.AnnotationSupport.findAnnotatedFields; + +import java.lang.reflect.Field; +import java.util.function.Predicate; + import org.junit.jupiter.api.extension.BeforeAllCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.TestInstancePostProcessor; +import org.junit.platform.commons.support.ModifierSupport; -class RandomNumberExtension implements BeforeAllCallback, BeforeEachCallback, ParameterResolver { +// end::user_guide[] +// @formatter:off +// tag::user_guide[] +class RandomNumberExtension + implements BeforeAllCallback, TestInstancePostProcessor, ParameterResolver { + private final java.util.Random random = new java.util.Random(System.nanoTime()); + + /** + * Inject a random integer into static fields that are annotated with + * {@code @Random} and can be assigned an integer value. + */ @Override - public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { - return false; + public void beforeAll(ExtensionContext context) { + Class testClass = context.getRequiredTestClass(); + injectFields(testClass, null, ModifierSupport::isStatic); } + /** + * Inject a random integer into non-static fields that are annotated with + * {@code @Random} and can be assigned an integer value. + */ @Override - public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { - return null; + public void postProcessTestInstance(Object testInstance, ExtensionContext context) { + Class testClass = context.getRequiredTestClass(); + injectFields(testClass, testInstance, ModifierSupport::isNotStatic); } + /** + * Determine if the parameter is annotated with {@code @Random} and can be + * assigned an integer value. + */ @Override - public void beforeAll(ExtensionContext context) { + public boolean supportsParameter(ParameterContext pc, ExtensionContext ec) { + return pc.isAnnotated(Random.class) && isInteger(pc.getParameter().getType()); } + /** + * Resolve a random integer. + */ @Override - public void beforeEach(ExtensionContext context) { + public Integer resolveParameter(ParameterContext pc, ExtensionContext ec) { + return this.random.nextInt(); + } + + private void injectFields(Class testClass, Object testInstance, + Predicate predicate) { + + predicate = predicate.and(field -> isInteger(field.getType())); + findAnnotatedFields(testClass, Random.class, predicate) + .forEach(field -> { + try { + field.setAccessible(true); + field.set(testInstance, this.random.nextInt()); + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + }); + } + + private static boolean isInteger(Class type) { + return int.class.isAssignableFrom(type); } } +// end::user_guide[] +// @formatter:on diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c4e291f9fca5..6c9ad74561ca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,8 +5,8 @@ asciidoctor-pdf = "1.5.3" asciidoctor-plugins = "4.0.0-alpha.1" # Check if workaround in documentation.gradle.kts can be removed when upgrading assertj = "3.24.2" bnd = "6.4.0" -checkstyle = "10.12.0" -gradleVersionsPlugin = "0.46.0" +checkstyle = "10.12.1" +gradleVersionsPlugin = "0.47.0" jacoco = "0.8.7" jmh = "1.36" junit4 = "4.13.2" @@ -30,10 +30,10 @@ bartholdy = { module = "de.sormuras:bartholdy", version = "0.2.3" } bndlib = { module = "biz.aQute.bnd:biz.aQute.bndlib", version.ref = "bnd" } checkstyle = { module = "com.puppycrawl.tools:checkstyle", version.ref = "checkstyle" } classgraph = { module = "io.github.classgraph:classgraph", version = "4.8.160" } -commons-io = { module = "commons-io:commons-io", version = "2.12.0" } +commons-io = { module = "commons-io:commons-io", version = "2.13.0" } gradle-commonCustomUserData = { module = "com.gradle:common-custom-user-data-gradle-plugin", version = "1.11" } gradle-foojayResolver = { module = "org.gradle.toolchains:foojay-resolver", version = "0.5.0" } -gradle-enterprise = { module = "com.gradle:gradle-enterprise-gradle-plugin", version = "3.13.3" } +gradle-enterprise = { module = "com.gradle:gradle-enterprise-gradle-plugin", version = "3.13.4" } gradle-bnd = { module = "biz.aQute.bnd:biz.aQute.bnd.gradle", version.ref = "bnd" } gradle-shadow = { module = "gradle.plugin.com.github.johnrengelman:shadow", version = "8.0.0" } gradle-spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version = "6.19.0" } @@ -53,7 +53,7 @@ log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4 maven = { module = "org.apache.maven:apache-maven", version = "3.9.2" } mavenSurefirePlugin = { module = "org.apache.maven.plugins:maven-surefire-plugin", version.ref = "surefire" } memoryfilesystem = { module = "com.github.marschall:memoryfilesystem", version = "2.6.1" } -mockito = { module = "org.mockito:mockito-junit-jupiter", version = "5.3.1" } +mockito = { module = "org.mockito:mockito-junit-jupiter", version = "5.4.0" } opentest4j = { module = "org.opentest4j:opentest4j", version.ref = "opentest4j" } openTestReporting-events = { module = "org.opentest4j.reporting:open-test-reporting-events", version.ref = "openTestReporting" } openTestReporting-tooling = { module = "org.opentest4j.reporting:open-test-reporting-tooling", version.ref = "openTestReporting" } diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.osgi-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.osgi-conventions.gradle.kts index ce36b6127823..03de80c3edd8 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.osgi-conventions.gradle.kts +++ b/gradle/plugins/common/src/main/kotlin/junitbuild.osgi-conventions.gradle.kts @@ -14,7 +14,7 @@ val projectDescription = objects.property().convention(provider { projec // metadata into the jar tasks.withType().matching { task: Jar -> task.name == "jar" || task.name == "shadowJar" -}.configureEach { +}.all { // configure tasks eagerly as workaround for https://github.com/bndtools/bnd/issues/5695 extra["importAPIGuardian"] = importAPIGuardian extensions.create(BundleTaskExtension.NAME, this).apply { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c1962a79e29d..f03f475efe8c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2c3425d49ec4..264e083fcdce 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,8 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=e111cb9948407e26351227dabce49822fb88c37ee72f1d1582a69c68af2e702f -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionSha256Sum=0906569bf96e8ebefbc1aa56318c74aeafd9710455b2817c70d709c5d77785c4 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-rc-2-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index aeb74cbb43e3..fcb6fca147c0 100755 --- a/gradlew +++ b/gradlew @@ -130,10 +130,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java index fbde194825dc..4be1561689dc 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java @@ -2845,14 +2845,24 @@ public static void assertNotEquals(Object unexpected, Object actual, SupplierAssert that {@code expected} and {@code actual} refer to the same object. + * Assert that the {@code expected} object and the {@code actual} object + * are the same object. + *

This method should only be used to assert identity between objects. + * To assert equality between two objects or two primitive values, + * use one of the {@code assertEquals(...)} methods instead — for example, + * use {@code assertEquals(999, 999)} instead of {@code assertSame(999, 999)}. */ public static void assertSame(Object expected, Object actual) { AssertSame.assertSame(expected, actual); } /** - * Assert that {@code expected} and {@code actual} refer to the same object. + * Assert that the {@code expected} object and the {@code actual} object + * are the same object. + *

This method should only be used to assert identity between objects. + * To assert equality between two objects or two primitive values, + * use one of the {@code assertEquals(...)} methods instead — for example, + * use {@code assertEquals(999, 999)} instead of {@code assertSame(999, 999)}. *

Fails with the supplied failure {@code message}. */ public static void assertSame(Object expected, Object actual, String message) { @@ -2860,8 +2870,14 @@ public static void assertSame(Object expected, Object actual, String message) { } /** - * Assert that {@code expected} and {@code actual} refer to the same object. - *

If necessary, the failure message will be retrieved lazily from the supplied {@code messageSupplier}. + * Assert that the {@code expected} object and the {@code actual} object + * are the same object. + *

This method should only be used to assert identity between objects. + * To assert equality between two objects or two primitive values, + * use one of the {@code assertEquals(...)} methods instead — for example, + * use {@code assertEquals(999, 999)} instead of {@code assertSame(999, 999)}. + *

If necessary, the failure message will be retrieved lazily from the supplied + * {@code messageSupplier}. */ public static void assertSame(Object expected, Object actual, Supplier messageSupplier) { AssertSame.assertSame(expected, actual, messageSupplier); @@ -2870,14 +2886,22 @@ public static void assertSame(Object expected, Object actual, Supplier m // --- assertNotSame ------------------------------------------------------- /** - * Assert that {@code expected} and {@code actual} do not refer to the same object. + * Assert that the {@code unexpected} object and the {@code actual} + * object are not the same object. + *

This method should only be used to compare the identity of two + * objects. To assert that two objects or two primitive values are not + * equal, use one of the {@code assertNotEquals(...)} methods instead. */ public static void assertNotSame(Object unexpected, Object actual) { AssertNotSame.assertNotSame(unexpected, actual); } /** - * Assert that {@code expected} and {@code actual} do not refer to the same object. + * Assert that the {@code unexpected} object and the {@code actual} + * object are not the same object. + *

This method should only be used to compare the identity of two + * objects. To assert that two objects or two primitive values are not + * equal, use one of the {@code assertNotEquals(...)} methods instead. *

Fails with the supplied failure {@code message}. */ public static void assertNotSame(Object unexpected, Object actual, String message) { @@ -2885,8 +2909,13 @@ public static void assertNotSame(Object unexpected, Object actual, String messag } /** - * Assert that {@code expected} and {@code actual} do not refer to the same object. - *

If necessary, the failure message will be retrieved lazily from the supplied {@code messageSupplier}. + * Assert that the {@code unexpected} object and the {@code actual} + * object are not the same object. + *

This method should only be used to compare the identity of two + * objects. To assert that two objects or two primitive values are not + * equal, use one of the {@code assertNotEquals(...)} methods instead. + *

If necessary, the failure message will be retrieved lazily from the supplied + * {@code messageSupplier}. */ public static void assertNotSame(Object unexpected, Object actual, Supplier messageSupplier) { AssertNotSame.assertNotSame(unexpected, actual, messageSupplier); diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestWatcher.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestWatcher.java index a720af0e061e..a38ff8d088a9 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestWatcher.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestWatcher.java @@ -31,9 +31,29 @@ * {@link org.junit.jupiter.api.TestTemplate @TestTemplate} methods (e.g., * {@code @RepeatedTest} and {@code @ParameterizedTest}). Moreover, if there is a * failure at the class level — for example, an exception thrown by a - * {@code @BeforeAll} method — no test results will be reported. + * {@code @BeforeAll} method — no test results will be reported. Similarly, + * if the test class is disabled via an {@link ExecutionCondition} — for + * example, {@code @Disabled} — no test results will be reported. * - *

Extensions implementing this API can be registered at any level. + *

Extensions implementing this interface can be registered at the class level, + * instance level, or method level. When registered at the class level, a + * {@code TestWatcher} will be invoked for any contained test method including + * those in {@link org.junit.jupiter.api.Nested @Nested} classes. When registered + * at the method level, a {@code TestWatcher} will only be invoked for the test + * method for which it was registered. + * + *

WARNING: If a {@code TestWatcher} is registered via a + * non-static (instance) field — for example, using + * {@link RegisterExtension @RegisterExtension} — and the test class is + * configured with + * {@link org.junit.jupiter.api.TestInstance @TestInstance(Lifecycle.PER_METHOD)} + * semantics (which is the default lifecycle mode), the {@code TestWatcher} will + * not be invoked with events for {@code @TestTemplate} methods + * (such as {@code @RepeatedTest} and {@code @ParameterizedTest}). To ensure that + * a {@code TestWatcher} is invoked for all test methods in a given class, it is + * therefore recommended that the {@code TestWatcher} be registered at the class + * level with {@link ExtendWith @ExtendWith} or via a {@code static} field with + * {@code @RegisterExtension} or {@code @ExtendWith}. * *

Exception Handling

* diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertSameAssertionsTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertSameAssertionsTests.java index e9ab08de1a49..c1299d8f5d04 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertSameAssertionsTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/AssertSameAssertionsTests.java @@ -12,6 +12,7 @@ import static org.junit.jupiter.api.AssertionTestUtils.assertExpectedAndActualValues; import static org.junit.jupiter.api.AssertionTestUtils.assertMessageContains; +import static org.junit.jupiter.api.AssertionTestUtils.assertMessageMatches; import static org.junit.jupiter.api.AssertionTestUtils.assertMessageStartsWith; import static org.junit.jupiter.api.AssertionTestUtils.expectAssertionFailedError; import static org.junit.jupiter.api.Assertions.assertSame; @@ -82,6 +83,20 @@ void assertSameWithDifferentObjects() { } } + @Test + void assertSameWithEqualPrimitivesAutoboxedToDifferentWrappers() { + try { + int i = 999; + assertSame(i, i); + expectAssertionFailedError(); + } + catch (AssertionFailedError ex) { + assertMessageMatches(ex, + "expected: java\\.lang\\.Integer@.+?<999> but was: java\\.lang\\.Integer@.+?<999>"); + assertExpectedAndActualValues(ex, 999, 999); + } + } + @Test void assertSameWithEquivalentStringsAndMessageSupplier() { String expected = new String("foo"); diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TestWatcherTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TestWatcherTests.java index 4c446f6fa094..6ada22bf2527 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TestWatcherTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TestWatcherTests.java @@ -33,12 +33,18 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.TestWatcher; import org.junit.jupiter.api.fixtures.TrackLogRecords; import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; @@ -68,7 +74,7 @@ void clearResults() { void testWatcherIsInvokedForTestMethodsInTopLevelAndNestedTestClasses() { assertCommonStatistics(executeTestsForClass(TrackingTestWatcherTestMethodsTestCase.class)); assertThat(TrackingTestWatcher.results.keySet()).containsAll(testWatcherMethodNames); - TrackingTestWatcher.results.values().forEach(uidList -> assertEquals(2, uidList.size())); + TrackingTestWatcher.results.values().forEach(testMethodNames -> assertEquals(2, testMethodNames.size())); } @Test @@ -80,12 +86,11 @@ void testWatcherIsInvokedForRepeatedTestMethods() { results.testEvents().assertStatistics( stats -> stats.dynamicallyRegistered(6).skipped(0).started(6).succeeded(2).aborted(2).failed(2)); - ArrayList expectedMethods = new ArrayList<>(testWatcherMethodNames); // Since the @RepeatedTest container is disabled, the individual invocations never occur. - assertThat(TrackingTestWatcher.results.keySet()).containsAll(expectedMethods); + assertThat(TrackingTestWatcher.results.keySet()).containsAll(testWatcherMethodNames); // 2 => number of iterations declared in @RepeatedTest(2). - TrackingTestWatcher.results.forEach( - (methodName, uidList) -> assertEquals("testDisabled".endsWith(methodName) ? 1 : 2, uidList.size())); + TrackingTestWatcher.results.forEach((testWatcherMethod, testMethodNames) -> assertEquals( + "testDisabled".equals(testWatcherMethod) ? 1 : 2, testMethodNames.size())); } @Test @@ -109,8 +114,9 @@ void testWatcherExceptionsAreLoggedAndSwallowed(LogRecordListener logRecordListe // @formatter:off long exceptionCount = logRecordListener.stream(MethodBasedTestDescriptor.class, Level.WARNING) .map(LogRecord::getThrown) - .filter(throwable -> throwable instanceof JUnitException) - .filter(throwable -> testWatcherMethodNames.contains(throwable.getStackTrace()[0].getMethodName())) + .filter(JUnitException.class::isInstance) + .map(throwable -> throwable.getStackTrace()[0].getMethodName()) + .filter(testWatcherMethodNames::contains) .count(); // @formatter:on @@ -118,18 +124,67 @@ void testWatcherExceptionsAreLoggedAndSwallowed(LogRecordListener logRecordListe } @Test - void testWatcherInvokedForTestMethodsInTestCaseWithProblematicConstructor() { + void testWatcherIsInvokedForTestMethodsInTestCaseWithProblematicConstructor() { EngineExecutionResults results = executeTestsForClass(ProblematicConstructorTestCase.class); results.testEvents().assertStatistics(stats -> stats.skipped(0).started(8).succeeded(0).aborted(0).failed(8)); assertThat(TrackingTestWatcher.results.keySet()).containsExactly("testFailed"); assertThat(TrackingTestWatcher.results.get("testFailed")).hasSize(8); } + @Test + void testWatcherSemanticsWhenRegisteredAtClassLevel() { + Class testClass = ClassLevelTestWatcherTestCase.class; + assertStatsForAbstractDisabledMethodsTestCase(testClass); + + // We get "testDisabled" events for the @Test method and the @RepeatedTest container. + assertThat(TrackingTestWatcher.results.get("testDisabled")).containsExactly("test", "repeatedTest"); + } + + @Test + void testWatcherSemanticsWhenRegisteredAtInstanceLevelWithTestInstanceLifecyclePerClass() { + Class testClass = TestInstancePerClassInstanceLevelTestWatcherTestCase.class; + assertStatsForAbstractDisabledMethodsTestCase(testClass); + + // We get "testDisabled" events for the @Test method and the @RepeatedTest container. + assertThat(TrackingTestWatcher.results.get("testDisabled")).containsExactly("test", "repeatedTest"); + } + + @Test + void testWatcherSemanticsWhenRegisteredAtInstanceLevelWithTestInstanceLifecyclePerMethod() { + Class testClass = TestInstancePerMethodInstanceLevelTestWatcherTestCase.class; + assertStatsForAbstractDisabledMethodsTestCase(testClass); + + // Since the TestWatcher is registered at the instance level with test instance + // lifecycle per-method semantics, we get a "testDisabled" event only for the @Test + // method and NOT for the @RepeatedTest container. + assertThat(TrackingTestWatcher.results.get("testDisabled")).containsExactly("test"); + } + + @Test + void testWatcherSemanticsWhenRegisteredAtMethodLevel() { + Class testClass = MethodLevelTestWatcherTestCase.class; + assertStatsForAbstractDisabledMethodsTestCase(testClass); + + // We get "testDisabled" events for the @Test method and the @RepeatedTest container. + assertThat(TrackingTestWatcher.results.get("testDisabled")).containsExactly("test", "repeatedTest"); + } + private void assertCommonStatistics(EngineExecutionResults results) { results.containerEvents().assertStatistics(stats -> stats.started(3).succeeded(3).failed(0)); results.testEvents().assertStatistics(stats -> stats.skipped(2).started(6).succeeded(2).aborted(2).failed(2)); } + private void assertStatsForAbstractDisabledMethodsTestCase(Class testClass) { + EngineExecutionResults results = executeTestsForClass(testClass); + + results.containerEvents().assertStatistics(// + stats -> stats.skipped(1).started(2).succeeded(2).aborted(0).failed(0)); + results.testEvents().assertStatistics(// + stats -> stats.skipped(1).started(0).succeeded(0).aborted(0).failed(0)); + + assertThat(TrackingTestWatcher.results.keySet()).containsExactly("testDisabled"); + } + // ------------------------------------------------------------------------- private static abstract class AbstractTestCase { @@ -251,32 +306,88 @@ static class ProblematicConstructorTestCase extends AbstractTestCase { } } + @TestMethodOrder(OrderAnnotation.class) + private static abstract class AbstractDisabledMethodsTestCase { + + @Disabled + @Test + @Order(1) + void test() { + } + + @Disabled + @RepeatedTest(2) + @Order(2) + void repeatedTest() { + } + } + + static class ClassLevelTestWatcherTestCase extends AbstractDisabledMethodsTestCase { + + @RegisterExtension + static TestWatcher watcher = new TrackingTestWatcher(); + } + + @TestInstance(Lifecycle.PER_CLASS) + static class TestInstancePerClassInstanceLevelTestWatcherTestCase extends AbstractDisabledMethodsTestCase { + + @RegisterExtension + TestWatcher watcher = new TrackingTestWatcher(); + } + + @TestInstance(Lifecycle.PER_METHOD) + static class TestInstancePerMethodInstanceLevelTestWatcherTestCase extends AbstractDisabledMethodsTestCase { + + @RegisterExtension + TestWatcher watcher = new TrackingTestWatcher(); + } + + static class MethodLevelTestWatcherTestCase extends AbstractDisabledMethodsTestCase { + + @Override + @Disabled + @Test + @Order(1) + @ExtendWith(TrackingTestWatcher.class) + void test() { + } + + @Override + @Disabled + @RepeatedTest(1) + @Order(2) + @ExtendWith(TrackingTestWatcher.class) + void repeatedTest() { + } + } + private static class TrackingTestWatcher implements TestWatcher { private static final Map> results = new HashMap<>(); @Override public void testSuccessful(ExtensionContext context) { - trackResult("testSuccessful", context.getUniqueId()); + trackResult("testSuccessful", context); } @Override public void testAborted(ExtensionContext context, Throwable cause) { - trackResult("testAborted", context.getUniqueId()); + trackResult("testAborted", context); } @Override public void testFailed(ExtensionContext context, Throwable cause) { - trackResult("testFailed", context.getUniqueId()); + trackResult("testFailed", context); } @Override public void testDisabled(ExtensionContext context, Optional reason) { - trackResult("testDisabled", context.getUniqueId()); + trackResult("testDisabled", context); } - protected void trackResult(String status, String uid) { - results.computeIfAbsent(status, k -> new ArrayList<>()).add(uid); + protected void trackResult(String testWatcherMethod, ExtensionContext context) { + String testMethod = context.getRequiredTestMethod().getName(); + results.computeIfAbsent(testWatcherMethod, k -> new ArrayList<>()).add(testMethod); } } 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 f32dc57a2965..8764d2d70670 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 @@ -285,7 +285,36 @@ public static List findPublicAnnotatedFields(Class clazz, Class fie */ @API(status = MAINTAINED, since = "1.4") public static List findAnnotatedFields(Class clazz, Class annotationType) { - return AnnotationUtils.findAnnotatedFields(clazz, annotationType, field -> true); + return findAnnotatedFields(clazz, annotationType, field -> true); + } + + /** + * Find all {@linkplain Field fields} of the supplied class or interface + * that are annotated or meta-annotated with the specified + * {@code annotationType} and match the specified {@code predicate}, using + * top-down search semantics within the type hierarchy. + * + *

Fields declared in the same class or interface will be ordered using + * an algorithm that is deterministic but intentionally nonobvious. + * + *

The results will not contain fields that are hidden or + * {@linkplain Field#isSynthetic() synthetic}. + * + * @param clazz the class or interface in which to find the fields; never {@code null} + * @param annotationType the annotation type to search for; never {@code null} + * @param predicate the field filter; never {@code null} + * @return the list of all such fields found; neither {@code null} nor mutable + * @since 1.10 + * @see Class#getDeclaredFields() + * @see #findPublicAnnotatedFields(Class, Class, Class) + * @see #findAnnotatedFields(Class, Class, Predicate, HierarchyTraversalMode) + * @see ReflectionSupport#findFields(Class, Predicate, HierarchyTraversalMode) + * @see ReflectionSupport#tryToReadFieldValue(Field, Object) + */ + @API(status = MAINTAINED, since = "1.10") + public static List findAnnotatedFields(Class clazz, Class annotationType, + Predicate predicate) { + return AnnotationUtils.findAnnotatedFields(clazz, annotationType, predicate); } /** diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/AnnotationUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/AnnotationUtils.java index 9da0cb5650d6..2cd1bbcee225 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/AnnotationUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/AnnotationUtils.java @@ -455,11 +455,7 @@ public static List findPublicAnnotatedFields(Class clazz, Class fie } /** - * Find all {@linkplain Field fields} of the supplied class or interface - * that are annotated or meta-annotated with the specified - * {@code annotationType} and match the specified {@code predicate}, using - * top-down search semantics within the type hierarchy. - * + * @see org.junit.platform.commons.support.AnnotationSupport#findAnnotatedFields(Class, Class, Predicate) * @see #findAnnotatedFields(Class, Class, Predicate, HierarchyTraversalMode) */ public static List findAnnotatedFields(Class clazz, Class annotationType, diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java index 74be92f77a3a..816b7fadd104 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java @@ -1361,7 +1361,8 @@ public static Optional findMethod(Class clazz, String methodName, Str return findMethod(clazz, methodName, resolveParameterTypes(clazz, methodName, parameterTypeNames)); } - private static Class[] resolveParameterTypes(Class clazz, String methodName, String parameterTypeNames) { + @API(status = INTERNAL, since = "1.10") + public static Class[] resolveParameterTypes(Class clazz, String methodName, String parameterTypeNames) { if (StringUtils.isBlank(parameterTypeNames)) { return EMPTY_CLASS_ARRAY; } diff --git a/junit-platform-commons/src/testFixtures/java/org/junit/platform/commons/test/TestClassLoader.java b/junit-platform-commons/src/testFixtures/java/org/junit/platform/commons/test/TestClassLoader.java index 5ec1970b4dc8..e408068352b1 100644 --- a/junit-platform-commons/src/testFixtures/java/org/junit/platform/commons/test/TestClassLoader.java +++ b/junit-platform-commons/src/testFixtures/java/org/junit/platform/commons/test/TestClassLoader.java @@ -28,7 +28,7 @@ *

This class loader is only suitable for specific testing scenarios, where * you need to load particular classes from a different class loader. * - * @since 5.10 + * @since 1.10 */ public class TestClassLoader extends URLClassLoader { diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectors.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectors.java index 42553214d695..3439d20d45e3 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectors.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectors.java @@ -504,9 +504,7 @@ public static MethodSelector selectMethod(String className, String methodName) { */ @API(status = EXPERIMENTAL, since = "1.10") public static MethodSelector selectMethod(ClassLoader classLoader, String className, String methodName) { - Preconditions.notBlank(className, "Class name must not be null or blank"); - Preconditions.notBlank(methodName, "Method name must not be null or blank"); - return new MethodSelector(classLoader, className, methodName, ""); + return selectMethod(classLoader, className, methodName, ""); } /** @@ -592,6 +590,48 @@ public static MethodSelector selectMethod(Class javaClass, String methodName, return new MethodSelector(javaClass, methodName, parameterTypeNames.trim()); } + /** + * Create a {@code MethodSelector} for the supplied class name, method name, + * and parameter types. + * + * @param className the fully qualified name of the class in which the method + * is declared, or a subclass thereof; never {@code null} or blank + * @param methodName the name of the method to select; never {@code null} or blank + * @param parameterTypes the formal parameter types of the method; never + * {@code null} though potentially empty if the method does not declare parameters + * @since 1.10 + * @see MethodSelector + */ + @API(status = EXPERIMENTAL, since = "1.10") + public static MethodSelector selectMethod(String className, String methodName, Class... parameterTypes) { + Preconditions.notBlank(className, "Class name must not be null or blank"); + Preconditions.notBlank(methodName, "Method name must not be null or blank"); + Preconditions.notNull(parameterTypes, "Parameter types array must not be null"); + Preconditions.containsNoNullElements(parameterTypes, "Parameter types array must not contain null elements"); + return new MethodSelector((ClassLoader) null, className, methodName, parameterTypes); + } + + /** + * Create a {@code MethodSelector} for the supplied {@link Class}, method name, + * and parameter types. + * + * @param javaClass the class in which the method is declared, or a subclass thereof; + * never {@code null} + * @param methodName the name of the method to select; never {@code null} or blank + * @param parameterTypes the formal parameter types of the method; never + * {@code null} though potentially empty if the method does not declare parameters + * @since 1.10 + * @see MethodSelector + */ + @API(status = EXPERIMENTAL, since = "1.10") + public static MethodSelector selectMethod(Class javaClass, String methodName, Class... parameterTypes) { + Preconditions.notNull(javaClass, "Class must not be null"); + Preconditions.notBlank(methodName, "Method name must not be null or blank"); + Preconditions.notNull(parameterTypes, "Parameter types array must not be null"); + Preconditions.containsNoNullElements(parameterTypes, "Parameter types array must not contain null elements"); + return new MethodSelector(javaClass, methodName, parameterTypes); + } + /** * Create a {@code MethodSelector} for the supplied {@link Class} and {@link Method}. * @@ -744,6 +784,33 @@ public static NestedMethodSelector selectNestedMethod(ClassLoader classLoader, L parameterTypeNames.trim()); } + /** + * Create a {@code NestedMethodSelector} for the supplied enclosing class names, + * nested class name, method name, and parameter types. + * + * @param enclosingClassNames the names of the enclosing classes; never {@code null} + * or empty + * @param nestedClassName the name of the nested class to select; never {@code null} + * or blank + * @param methodName the name of the method to select; never {@code null} or blank + * @param parameterTypes the formal parameter types of the method; never {@code null} + * though potentially empty if the method does not declare parameters + * @since 1.10 + * @see NestedMethodSelector + */ + @API(status = EXPERIMENTAL, since = "1.10") + public static NestedMethodSelector selectNestedMethod(List enclosingClassNames, String nestedClassName, + String methodName, Class... parameterTypes) { + + Preconditions.notEmpty(enclosingClassNames, "Enclosing class names must not be null or empty"); + Preconditions.notBlank(nestedClassName, "Nested class name must not be null or blank"); + Preconditions.notBlank(methodName, "Method name must not be null or blank"); + Preconditions.notNull(parameterTypes, "Parameter types array must not be null"); + Preconditions.containsNoNullElements(parameterTypes, "Parameter types array must not contain null elements"); + return new NestedMethodSelector((ClassLoader) null, enclosingClassNames, nestedClassName, methodName, + parameterTypes); + } + /** * Create a {@code NestedMethodSelector} for the supplied nested {@link Class} and method name. * @@ -788,6 +855,31 @@ public static NestedMethodSelector selectNestedMethod(List> enclosingCl return new NestedMethodSelector(enclosingClasses, nestedClass, methodName, parameterTypeNames.trim()); } + /** + * Create a {@code NestedMethodSelector} for the supplied enclosing classes, + * nested class, method name, and parameter types. + * + * @param enclosingClasses the path to the nested class to select; never {@code null} + * or empty + * @param nestedClass the nested class to select; never {@code null} + * @param methodName the name of the method to select; never {@code null} or blank + * @param parameterTypes the formal parameter types of the method; never {@code null} + * though potentially empty if the method does not declare parameters + * @since 1.10 + * @see NestedMethodSelector + */ + @API(status = EXPERIMENTAL, since = "1.10") + public static NestedMethodSelector selectNestedMethod(List> enclosingClasses, Class nestedClass, + String methodName, Class... parameterTypes) { + + Preconditions.notEmpty(enclosingClasses, "Enclosing classes must not be null or empty"); + Preconditions.notNull(nestedClass, "Nested class must not be null"); + Preconditions.notBlank(methodName, "Method name must not be null or blank"); + Preconditions.notNull(parameterTypes, "Parameter types array must not be null"); + Preconditions.containsNoNullElements(parameterTypes, "Parameter types array must not contain null elements"); + return new NestedMethodSelector(enclosingClasses, nestedClass, methodName, parameterTypes); + } + /** * Create a {@code NestedMethodSelector} for the supplied nested {@link Class} and {@link Method}. * diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/MethodSelector.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/MethodSelector.java index c1d9ad28deb8..c1055f68b743 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/MethodSelector.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/MethodSelector.java @@ -18,11 +18,11 @@ import java.util.Objects; import org.apiguardian.api.API; +import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.function.Try; import org.junit.platform.commons.util.ClassUtils; import org.junit.platform.commons.util.ReflectionUtils; -import org.junit.platform.commons.util.StringUtils; import org.junit.platform.commons.util.ToStringBuilder; import org.junit.platform.engine.DiscoverySelector; @@ -36,9 +36,9 @@ * {@linkplain #getJavaMethod() method} and its method name, class name, and * parameter types accordingly. If a {@link Class} and method name, a class name * and method name, or a fully qualified method name is provided, - * this selector will only attempt to lazily load the {@link Class} or - * {@link Method} if {@link #getJavaClass()} or {@link #getJavaMethod()} is - * invoked. + * this selector will only attempt to lazily load the class, method, or parameter + * types if {@link #getJavaClass()}, {@link #getJavaMethod()}, or + * {@link #getParameterTypes()} is invoked. * *

In this context, a Java {@code Method} means anything that can be referenced * as a {@link Method} on the JVM — for example, methods from Java classes @@ -63,6 +63,7 @@ public class MethodSelector implements DiscoverySelector { private volatile Class javaClass; private volatile Method javaMethod; + private volatile Class[] parameterTypes; /** * @since 1.10 @@ -82,13 +83,37 @@ public class MethodSelector implements DiscoverySelector { this.parameterTypeNames = parameterTypeNames; } + /** + * @since 1.10 + */ + MethodSelector(ClassLoader classLoader, String className, String methodName, Class... parameterTypes) { + this.classLoader = classLoader; + this.className = className; + this.methodName = methodName; + this.parameterTypes = parameterTypes.clone(); + this.parameterTypeNames = ClassUtils.nullSafeToString(Class::getTypeName, this.parameterTypes); + } + + /** + * @since 1.10 + */ + MethodSelector(Class javaClass, String methodName, Class... parameterTypes) { + this.classLoader = javaClass.getClassLoader(); + this.javaClass = javaClass; + this.className = javaClass.getName(); + this.methodName = methodName; + this.parameterTypes = parameterTypes.clone(); + this.parameterTypeNames = ClassUtils.nullSafeToString(Class::getTypeName, this.parameterTypes); + } + MethodSelector(Class javaClass, Method method) { this.classLoader = javaClass.getClassLoader(); this.javaClass = javaClass; this.className = javaClass.getName(); this.javaMethod = method; this.methodName = method.getName(); - this.parameterTypeNames = ClassUtils.nullSafeToString(method.getParameterTypes()); + this.parameterTypes = method.getParameterTypes(); + this.parameterTypeNames = ClassUtils.nullSafeToString(Class::getTypeName, this.parameterTypes); } /** @@ -121,10 +146,10 @@ public String getMethodName() { * *

See {@link #getParameterTypeNames()} for details. * - * @return the names of parameter types supplied to this {@code MethodSelector} - * via a constructor or deduced from a {@code Method} or parameter types supplied - * via a constructor; never {@code null} but potentially an empty string + * @return the names of parameter types * @since 1.0 + * @see #getParameterTypeNames() + * @see #getParameterTypes() * @deprecated since 1.10 in favor of {@link #getParameterTypeNames()} */ @Deprecated @@ -144,9 +169,10 @@ public String getMethodParameterTypes() { * the caller of this method to determine how to parse the returned string. * * @return the names of parameter types supplied to this {@code MethodSelector} - * via a constructor or deduced from a {@code Method} supplied via a constructor; - * never {@code null} but potentially an empty string + * via a constructor or deduced from a {@code Method} or parameter types supplied + * via a constructor; never {@code null} but potentially an empty string * @since 1.10 + * @see #getParameterTypes() */ @API(status = STABLE, since = "1.10") public String getParameterTypeNames() { @@ -182,6 +208,27 @@ public Method getJavaMethod() { return this.javaMethod; } + /** + * Get the parameter types for the selected method. + * + *

If the parameter types were not provided as {@link Class} references + * (or could not be deduced as {@code Class} references in the constructor), + * this method attempts to lazily load the class reference for each parameter + * type based on its name and throws a {@link JUnitException} if the class + * cannot be loaded. + * + * @return the method's parameter types; never {@code null} but potentially + * an empty array if the selected method does not declare parameters + * @since 1.10 + * @see #getParameterTypeNames() + * @see Method#getParameterTypes() + */ + @API(status = EXPERIMENTAL, since = "1.10") + public Class[] getParameterTypes() { + lazyLoadParameterTypes(); + return this.parameterTypes.clone(); + } + private void lazyLoadJavaClass() { // @formatter:off if (this.javaClass == null) { @@ -197,9 +244,10 @@ private void lazyLoadJavaClass() { private void lazyLoadJavaMethod() { if (this.javaMethod == null) { lazyLoadJavaClass(); - if (StringUtils.isNotBlank(this.parameterTypeNames)) { + lazyLoadParameterTypes(); + if (this.parameterTypes.length > 0) { this.javaMethod = ReflectionUtils.findMethod(this.javaClass, this.methodName, - this.parameterTypeNames).orElseThrow( + this.parameterTypes).orElseThrow( () -> new PreconditionViolationException(String.format( "Could not find method with name [%s] and parameter types [%s] in class [%s].", this.methodName, this.parameterTypeNames, this.javaClass.getName()))); @@ -213,6 +261,14 @@ private void lazyLoadJavaMethod() { } } + private void lazyLoadParameterTypes() { + if (this.parameterTypes == null) { + lazyLoadJavaClass(); + this.parameterTypes = ReflectionUtils.resolveParameterTypes(this.javaClass, this.methodName, + this.parameterTypeNames); + } + } + /** * @since 1.3 */ diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/NestedMethodSelector.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/NestedMethodSelector.java index 0d4ebb51a6c4..22e1ffdef440 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/NestedMethodSelector.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/NestedMethodSelector.java @@ -33,9 +33,9 @@ *

If a Java {@link Method} is provided, the selector will return that * {@linkplain #getMethod() method} and its method name, class name, enclosing * classes names, and parameter types accordingly. If class names or method names - * are provided, this selector will only attempt to lazily load the {@link Class} - * or {@link Method} if {@link #getEnclosingClasses()}, - * {@link #getNestedClass()}, or {@link #getMethod()} is invoked. + * are provided, this selector will only attempt to lazily load a class or method + * if {@link #getEnclosingClasses()}, {@link #getNestedClass()}, + * {@link #getMethod()}, or {@link #getParameterTypes()} is invoked. * *

In this context, a Java {@code Method} means anything that can be referenced * as a {@link Method} on the JVM — for example, methods from Java classes @@ -63,12 +63,30 @@ public class NestedMethodSelector implements DiscoverySelector { this.methodSelector = new MethodSelector(classLoader, nestedClassName, methodName, parameterTypeNames); } + /** + * @since 1.10 + */ + NestedMethodSelector(ClassLoader classLoader, List enclosingClassNames, String nestedClassName, + String methodName, Class... parameterTypes) { + this.nestedClassSelector = new NestedClassSelector(classLoader, enclosingClassNames, nestedClassName); + this.methodSelector = new MethodSelector(classLoader, nestedClassName, methodName, parameterTypes); + } + NestedMethodSelector(List> enclosingClasses, Class nestedClass, String methodName, String parameterTypeNames) { this.nestedClassSelector = new NestedClassSelector(enclosingClasses, nestedClass); this.methodSelector = new MethodSelector(nestedClass, methodName, parameterTypeNames); } + /** + * @since 1.10 + */ + NestedMethodSelector(List> enclosingClasses, Class nestedClass, String methodName, + Class... parameterTypes) { + this.nestedClassSelector = new NestedClassSelector(enclosingClasses, nestedClass); + this.methodSelector = new MethodSelector(nestedClass, methodName, parameterTypes); + } + NestedMethodSelector(List> enclosingClasses, Class nestedClass, Method method) { this.nestedClassSelector = new NestedClassSelector(enclosingClasses, nestedClass); this.methodSelector = new MethodSelector(nestedClass, method); @@ -147,10 +165,10 @@ public Method getMethod() { * *

See {@link #getParameterTypeNames()} for details. * - * @return the names of parameter types supplied to this {@code NestedMethodSelector} - * via a constructor or deduced from a {@code Method} supplied via a constructor; - * never {@code null} but potentially an empty string + * @return the names of parameter types * @since 1.6 + * @see #getParameterTypeNames() + * @see #getParameterTypes() * @deprecated since 1.10 in favor or {@link #getParameterTypeNames()} */ @Deprecated @@ -160,25 +178,38 @@ public String getMethodParameterTypes() { } /** - * Get the names of parameter types for the selected method as a {@link String}, - * typically a comma-separated list of primitive types, fully qualified - * class names, or array types. + * Get the names of parameter types for the selected method as a {@link String}. * - *

Note: the names of parameter types are provided as a single string instead - * of a collection in order to allow this selector to be used in a generic - * fashion by various test engines. It is therefore the responsibility of - * the caller of this method to determine how to parse the returned string. + *

See {@link MethodSelector#getParameterTypeNames()} for details. * * @return the names of parameter types supplied to this {@code NestedMethodSelector} - * via a constructor or deduced from a {@code Method} supplied via a constructor; - * never {@code null} but potentially an empty string + * via a constructor or deduced from a {@code Method} or parameter types supplied + * via a constructor; never {@code null} but potentially an empty string * @since 1.10 + * @see MethodSelector#getParameterTypeNames() + * */ @API(status = STABLE, since = "1.10") public String getParameterTypeNames() { return this.methodSelector.getParameterTypeNames(); } + /** + * Get the parameter types for the selected method. + * + *

See {@link MethodSelector#getParameterTypes()} for details. + * + * @return the method's parameter types; never {@code null} but potentially + * an empty array if the selected method does not declare parameters + * @since 1.10 + * @see #getParameterTypeNames() + * @see MethodSelector#getParameterTypes() + */ + @API(status = EXPERIMENTAL, since = "1.10") + public Class[] getParameterTypes() { + return this.methodSelector.getParameterTypes(); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/SelectMethod.java b/junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/SelectMethod.java index b83fd420405a..a1313a343398 100644 --- a/junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/SelectMethod.java +++ b/junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/SelectMethod.java @@ -30,7 +30,11 @@ * @since 1.10 * @see Suite * @see org.junit.platform.runner.JUnitPlatform + * @see org.junit.platform.engine.discovery.DiscoverySelectors#selectMethod(String) + * @see org.junit.platform.engine.discovery.DiscoverySelectors#selectMethod(String, String, String) + * @see org.junit.platform.engine.discovery.DiscoverySelectors#selectMethod(String, String, Class...) * @see org.junit.platform.engine.discovery.DiscoverySelectors#selectMethod(Class, String, String) + * @see org.junit.platform.engine.discovery.DiscoverySelectors#selectMethod(Class, String, Class...) */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @@ -40,15 +44,74 @@ @Repeatable(SelectMethods.class) public @interface SelectMethod { + /** + * The fully qualified method name of the method to select. + * + *

The following formats are supported. + * + *

    + *
  • {@code [fully qualified class name]#[methodName]}
  • + *
  • {@code [fully qualified class name]#[methodName](parameter type list)} + *
+ * + *

The parameter type list is a comma-separated list of primitive + * names or fully qualified class names for the types of parameters accepted + * by the method. + * + *

Array parameter types may be specified using either the JVM's internal + * String representation (e.g., {@code [[I} for {@code int[][]}, + * {@code [Ljava.lang.String;} for {@code java.lang.String[]}, etc.) or + * source code syntax (e.g., {@code int[][]}, {@code java.lang.String[]}, + * etc.). + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Examples
MethodFully Qualified Method Name
{@code java.lang.String.chars()}{@code java.lang.String#chars}
{@code java.lang.String.chars()}{@code java.lang.String#chars()}
{@code java.lang.String.equalsIgnoreCase(String)}{@code java.lang.String#equalsIgnoreCase(java.lang.String)}
{@code java.lang.String.substring(int, int)}{@code java.lang.String#substring(int, int)}
{@code example.Calc.avg(int[])}{@code example.Calc#avg([I)}
{@code example.Calc.avg(int[])}{@code example.Calc#avg(int[])}
{@code example.Matrix.multiply(double[][])}{@code example.Matrix#multiply([[D)}
{@code example.Matrix.multiply(double[][])}{@code example.Matrix#multiply(double[][])}
{@code example.Service.process(String[])}{@code example.Service#process([Ljava.lang.String;)}
{@code example.Service.process(String[])}{@code example.Service#process(java.lang.String[])}
{@code example.Service.process(String[][])}{@code example.Service#process([[Ljava.lang.String;)}
{@code example.Service.process(String[][])}{@code example.Service#process(java.lang.String[][])}
+ * + *

Cannot be combined with any other attribute. + * + * @see org.junit.platform.engine.discovery.DiscoverySelectors#selectMethod(String) + */ + String value() default ""; + /** * The class in which the method is declared, or a subclass thereof. + * + *

Cannot be used in conjunction with {@link #typeName()}. + */ + Class type() default Class.class; + + /** + * The fully qualified class name in which the method is declared, or a subclass thereof. + * + *

Cannot be used in conjunction with {@link #type()}. */ - Class clazz(); + String typeName() default ""; /** - * The name of the method to select; never blank. + * The name of the method to select; never blank unless {@link #value()} is used. */ - String name(); + String name() default ""; + + /** + * The parameter types of the method to select. + * + *

Cannot be used in conjunction with {@link #parameterTypeNames()}. + */ + Class[] parameterTypes() default {}; /** * The parameter types of the method to select. @@ -81,7 +144,9 @@ * {@code example.Service.process(String[][])}{@code [[Ljava.lang.String;} * {@code example.Service.process(String[][])}{@code java.lang.String[][]} * + * + *

Cannot be used in conjunction with {@link #parameterTypes()}. */ - String parameters() default ""; + String parameterTypeNames() default ""; } diff --git a/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/AdditionalDiscoverySelectors.java b/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/AdditionalDiscoverySelectors.java index 188385080855..65e6c3504190 100644 --- a/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/AdditionalDiscoverySelectors.java +++ b/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/AdditionalDiscoverySelectors.java @@ -23,7 +23,6 @@ import org.junit.platform.engine.discovery.DiscoverySelectors; import org.junit.platform.engine.discovery.FilePosition; import org.junit.platform.engine.discovery.FileSelector; -import org.junit.platform.engine.discovery.MethodSelector; import org.junit.platform.engine.discovery.ModuleSelector; import org.junit.platform.engine.discovery.PackageSelector; import org.junit.platform.engine.discovery.UriSelector; @@ -82,12 +81,6 @@ static Stream selectClasses(String... classNames) { return uniqueStreamOf(classNames).map(DiscoverySelectors::selectClass); } - static MethodSelector selectMethod(Class clazz, String name, String parameterTypeNames) { - Preconditions.notBlank(name, "Method name must not be null or blank"); - Preconditions.notNull(parameterTypeNames, "parameter type names must not be null"); - return DiscoverySelectors.selectMethod(clazz, name, parameterTypeNames); - } - static List selectModules(String... moduleNames) { Preconditions.notNull(moduleNames, "Module names must not be null"); Preconditions.containsNoNullElements(moduleNames, "Individual module names must not be null"); diff --git a/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilder.java b/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilder.java index 08cb4aea20fa..c94b8009aaf8 100644 --- a/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilder.java +++ b/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilder.java @@ -38,6 +38,7 @@ import org.junit.platform.engine.Filter; import org.junit.platform.engine.discovery.ClassNameFilter; import org.junit.platform.engine.discovery.ClassSelector; +import org.junit.platform.engine.discovery.DiscoverySelectors; import org.junit.platform.engine.discovery.MethodSelector; import org.junit.platform.engine.discovery.PackageNameFilter; import org.junit.platform.launcher.EngineFilter; @@ -155,7 +156,7 @@ public SuiteLauncherDiscoveryRequestBuilder suite(Class suiteClass) { .ifPresent(this::selectors); findRepeatableAnnotations(suiteClass, SelectMethod.class) .stream() - .map(annotation -> selectMethod(annotation.clazz(), annotation.name(), annotation.parameters())) + .map(annotation -> selectMethod(suiteClass, annotation)) .forEach(this::selectors); findAnnotationValues(suiteClass, IncludeClassNamePatterns.class, IncludeClassNamePatterns::value) .flatMap(SuiteLauncherDiscoveryRequestBuilder::trimmed) @@ -226,9 +227,68 @@ private static Stream toClassSelectors(Class suiteClass, Selec ); } - private MethodSelector selectMethod(Class clazz, String name, String parameters) { - selectedClassNames.add(clazz.getName()); - return AdditionalDiscoverySelectors.selectMethod(clazz, name, parameters); + private MethodSelector selectMethod(Class suiteClass, SelectMethod annotation) { + MethodSelector methodSelector = toMethodSelector(suiteClass, annotation); + selectedClassNames.add(methodSelector.getClassName()); + return methodSelector; + } + + private MethodSelector toMethodSelector(Class suiteClass, SelectMethod annotation) { + if (!annotation.value().isEmpty()) { + Preconditions.condition(annotation.type() == Class.class, + () -> prefixErrorMessageForInvalidSelectMethodUsage(suiteClass, + "type must not be set in conjunction with fully qualified method name")); + Preconditions.condition(annotation.typeName().isEmpty(), + () -> prefixErrorMessageForInvalidSelectMethodUsage(suiteClass, + "type name must not be set in conjunction with fully qualified method name")); + Preconditions.condition(annotation.name().isEmpty(), + () -> prefixErrorMessageForInvalidSelectMethodUsage(suiteClass, + "method name must not be set in conjunction with fully qualified method name")); + Preconditions.condition(annotation.parameterTypes().length == 0, + () -> prefixErrorMessageForInvalidSelectMethodUsage(suiteClass, + "parameter types must not be set in conjunction with fully qualified method name")); + Preconditions.condition(annotation.parameterTypeNames().isEmpty(), + () -> prefixErrorMessageForInvalidSelectMethodUsage(suiteClass, + "parameter type names must not be set in conjunction with fully qualified method name")); + + return DiscoverySelectors.selectMethod(annotation.value()); + } + + Class type = annotation.type() == Class.class ? null : annotation.type(); + String typeName = annotation.typeName().isEmpty() ? null : annotation.typeName().trim(); + String methodName = Preconditions.notBlank(annotation.name(), + () -> prefixErrorMessageForInvalidSelectMethodUsage(suiteClass, "method name must not be blank")); + Class[] parameterTypes = annotation.parameterTypes().length == 0 ? null : annotation.parameterTypes(); + String parameterTypeNames = annotation.parameterTypeNames().trim(); + if (parameterTypes != null) { + Preconditions.condition(parameterTypeNames.isEmpty(), + () -> prefixErrorMessageForInvalidSelectMethodUsage(suiteClass, + "either parameter type names or parameter types must be set but not both")); + } + if (type == null) { + Preconditions.notBlank(typeName, () -> prefixErrorMessageForInvalidSelectMethodUsage(suiteClass, + "type must be set or type name must not be blank")); + if (parameterTypes == null) { + return DiscoverySelectors.selectMethod(typeName, methodName, parameterTypeNames); + } + else { + return DiscoverySelectors.selectMethod(typeName, methodName, parameterTypes); + } + } + else { + Preconditions.condition(typeName == null, () -> prefixErrorMessageForInvalidSelectMethodUsage(suiteClass, + "either type name or type must be set but not both")); + if (parameterTypes == null) { + return DiscoverySelectors.selectMethod(type, methodName, parameterTypeNames); + } + else { + return DiscoverySelectors.selectMethod(type, methodName, parameterTypes); + } + } + } + + private static String prefixErrorMessageForInvalidSelectMethodUsage(Class suiteClass, String detailMessage) { + return String.format("@SelectMethod on class [%s]: %s", suiteClass.getName(), detailMessage); } private ClassNameFilter createIncludeClassNameFilter(String... patterns) { diff --git a/platform-tests/src/test/java/org/junit/platform/commons/util/ExceptionUtilsTests.java b/platform-tests/src/test/java/org/junit/platform/commons/util/ExceptionUtilsTests.java index 4d04fa970301..1c96ca1b7e9b 100644 --- a/platform-tests/src/test/java/org/junit/platform/commons/util/ExceptionUtilsTests.java +++ b/platform-tests/src/test/java/org/junit/platform/commons/util/ExceptionUtilsTests.java @@ -11,7 +11,6 @@ package org.junit.platform.commons.util; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertLinesMatch; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.platform.commons.util.ExceptionUtils.findNestedThrowables; import static org.junit.platform.commons.util.ExceptionUtils.pruneStackTrace; @@ -19,9 +18,6 @@ import static org.junit.platform.commons.util.ExceptionUtils.throwAsUncheckedException; import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import org.junit.platform.commons.JUnitException; @@ -137,11 +133,4 @@ void avoidCyclesWhileSearchingForNestedThrowables() { assertThat(findNestedThrowables(t1)).hasSize(3); } - private static void assertStackTraceMatch(StackTraceElement[] stackTrace, String expectedLines) { - List stackStraceAsLines = Arrays.stream(stackTrace) // - .map(StackTraceElement::toString) // - .collect(Collectors.toList()); - assertLinesMatch(expectedLines.lines().toList(), stackStraceAsLines); - } - } diff --git a/platform-tests/src/test/java/org/junit/platform/engine/discovery/DiscoverySelectorsTests.java b/platform-tests/src/test/java/org/junit/platform/engine/discovery/DiscoverySelectorsTests.java index a2e4af2113f2..dfb967afd9fd 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/discovery/DiscoverySelectorsTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/discovery/DiscoverySelectorsTests.java @@ -314,7 +314,7 @@ void selectClassByName() { } @Test - void selectClassByNameAndClassLoader() throws Exception { + void selectClassByNameWithExplicitClassLoader() throws Exception { try (var testClassLoader = TestClassLoader.forClasses(getClass())) { var selector = selectClass(testClassLoader, getClass().getName()); @@ -353,6 +353,19 @@ void selectMethodByClassNameMethodNameAndParameterTypeNamesPreconditions() { assertViolatesPrecondition(() -> selectMethod("TestClass", "method", (String) null)); } + @Test + @DisplayName("Preconditions: selectMethod(className, methodName, parameterTypes)") + void selectMethodByClassNameMethodNameAndParameterTypesPreconditions() { + assertViolatesPrecondition(() -> selectMethod("TestClass", null, int.class)); + assertViolatesPrecondition(() -> selectMethod("TestClass", "", int.class)); + assertViolatesPrecondition(() -> selectMethod("TestClass", " ", int.class)); + assertViolatesPrecondition(() -> selectMethod((String) null, "method", int.class)); + assertViolatesPrecondition(() -> selectMethod("", "method", int.class)); + assertViolatesPrecondition(() -> selectMethod(" ", "method", int.class)); + assertViolatesPrecondition(() -> selectMethod("TestClass", "method", (Class) null)); + assertViolatesPrecondition(() -> selectMethod("TestClass", "method", new Class[] { int.class, null })); + } + @Test @DisplayName("Preconditions: selectMethod(class, methodName)") void selectMethodByClassAndMethodNamePreconditions() { @@ -413,7 +426,7 @@ void selectMethodByFullyQualifiedName() throws Exception { } @Test - void selectMethodByFullyQualifiedNameAndClassLoader() throws Exception { + void selectMethodByFullyQualifiedNameWithExplicitClassLoader() throws Exception { try (var testClassLoader = TestClassLoader.forClasses(testClass())) { var clazz = testClassLoader.loadClass(testClass().getName()); assertThat(clazz).isNotEqualTo(testClass()); @@ -690,6 +703,58 @@ void selectMethodByClassMethodNameAndParameterTypeNames() throws Exception { assertThat(selector.getMethodName()).isEqualTo("myTest"); assertThat(selector.getJavaMethod()).isEqualTo(method); assertThat(selector.getParameterTypeNames()).isEqualTo("java.lang.String, boolean[]"); + assertThat(selector.getParameterTypes()).containsExactly(String.class, boolean[].class); + } + + @Test + void selectMethodByClassNameMethodNameAndParameterTypes() throws Exception { + var testClass = testClass(); + var method = testClass.getDeclaredMethod("myTest", String.class, boolean[].class); + + var selector = selectMethod(testClass.getName(), "myTest", String.class, boolean[].class); + + assertThat(selector.getClassName()).isEqualTo(testClass.getName()); + assertThat(selector.getJavaClass()).isEqualTo(testClass); + assertThat(selector.getMethodName()).isEqualTo("myTest"); + assertThat(selector.getJavaMethod()).isEqualTo(method); + assertThat(selector.getParameterTypeNames()).isEqualTo("java.lang.String, boolean[]"); + assertThat(selector.getParameterTypes()).containsExactly(String.class, boolean[].class); + } + + @Test + void selectMethodByClassNameMethodNameAndParameterTypeNamesWithExplicitClassLoader() throws Exception { + var testClass = testClass(); + + try (var testClassLoader = TestClassLoader.forClasses(testClass)) { + var clazz = testClassLoader.loadClass(testClass.getName()); + assertThat(clazz).isNotEqualTo(testClass); + + var method = clazz.getDeclaredMethod("myTest", String.class, boolean[].class); + var selector = selectMethod(testClassLoader, testClass.getName(), "myTest", + "java.lang.String, boolean[]"); + + assertThat(selector.getClassName()).isEqualTo(clazz.getName()); + assertThat(selector.getJavaClass()).isEqualTo(clazz); + assertThat(selector.getMethodName()).isEqualTo(method.getName()); + assertThat(selector.getJavaMethod()).isEqualTo(method); + assertThat(selector.getParameterTypeNames()).isEqualTo("java.lang.String, boolean[]"); + assertThat(selector.getParameterTypes()).containsExactly(String.class, boolean[].class); + } + } + + @Test + void selectMethodByClassMethodNameAndParameterTypes() throws Exception { + var testClass = testClass(); + var method = testClass.getDeclaredMethod("myTest", String.class, boolean[].class); + + var selector = selectMethod(testClass, "myTest", String.class, boolean[].class); + + assertThat(selector.getClassName()).isEqualTo(testClass.getName()); + assertThat(selector.getJavaClass()).isEqualTo(testClass); + assertThat(selector.getMethodName()).isEqualTo("myTest"); + assertThat(selector.getJavaMethod()).isEqualTo(method); + assertThat(selector.getParameterTypeNames()).isEqualTo("java.lang.String, boolean[]"); + assertThat(selector.getParameterTypes()).containsExactly(String.class, boolean[].class); } @Test @@ -703,7 +768,8 @@ void selectMethodWithParametersByMethodReference() throws Exception { assertThat(selector.getJavaClass()).isEqualTo(testClass); assertThat(selector.getMethodName()).isEqualTo("myTest"); assertThat(selector.getJavaMethod()).isEqualTo(method); - assertThat(selector.getParameterTypeNames()).isEqualTo("java.lang.String, [Z"); + assertThat(selector.getParameterTypeNames()).isEqualTo("java.lang.String, boolean[]"); + assertThat(selector.getParameterTypes()).containsExactly(String.class, boolean[].class); } @Test @@ -734,7 +800,7 @@ private static Class testClass() { class SelectNestedClassAndSelectNestedMethodTests { private final String enclosingClassName = getClass().getName() + "$ClassWithNestedInnerClass"; - private final String nestedClassName = getClass().getName() + "$AbstractClassWithNestedInnerClass$NestedClass"; + private final String nestedClassName = getClass().getName() + "$ClassWithNestedInnerClass$NestedClass"; private final String doubleNestedClassName = nestedClassName + "$DoubleNestedClass"; private final String methodName = "nestedTest"; @@ -743,23 +809,22 @@ void selectNestedClassByClassNames() { var selector = selectNestedClass(List.of(enclosingClassName), nestedClassName); assertThat(selector.getEnclosingClasses()).containsOnly(ClassWithNestedInnerClass.class); - assertThat(selector.getNestedClass()).isEqualTo(AbstractClassWithNestedInnerClass.NestedClass.class); + assertThat(selector.getNestedClass()).isEqualTo(ClassWithNestedInnerClass.NestedClass.class); assertThat(selector.getEnclosingClassNames()).containsOnly(enclosingClassName); assertThat(selector.getNestedClassName()).isEqualTo(nestedClassName); } @Test - void selectNestedClassByClassNamesAndClassLoader() throws Exception { - var testClasses = List.of(AbstractClassWithNestedInnerClass.class, ClassWithNestedInnerClass.class, - AbstractClassWithNestedInnerClass.NestedClass.class); + void selectNestedClassByClassNamesWithExplicitClassLoader() throws Exception { + var testClasses = List.of(ClassWithNestedInnerClass.class, ClassWithNestedInnerClass.NestedClass.class); try (var testClassLoader = TestClassLoader.forClasses(testClasses)) { var selector = selectNestedClass(testClassLoader, List.of(enclosingClassName), nestedClassName); assertThat(selector.getEnclosingClasses()).doesNotContain(ClassWithNestedInnerClass.class); assertThat(selector.getEnclosingClasses()).extracting(Class::getName).containsOnly(enclosingClassName); - assertThat(selector.getNestedClass()).isNotEqualTo(AbstractClassWithNestedInnerClass.NestedClass.class); + assertThat(selector.getNestedClass()).isNotEqualTo(ClassWithNestedInnerClass.NestedClass.class); assertThat(selector.getNestedClass().getName()).isEqualTo(nestedClassName); assertThat(selector.getClassLoader()).isSameAs(testClassLoader); @@ -774,9 +839,9 @@ void selectDoubleNestedClassByClassNames() { var selector = selectNestedClass(List.of(enclosingClassName, nestedClassName), doubleNestedClassName); assertThat(selector.getEnclosingClasses()).containsExactly(ClassWithNestedInnerClass.class, - AbstractClassWithNestedInnerClass.NestedClass.class); + ClassWithNestedInnerClass.NestedClass.class); assertThat(selector.getNestedClass()).isEqualTo( - AbstractClassWithNestedInnerClass.NestedClass.DoubleNestedClass.class); + ClassWithNestedInnerClass.NestedClass.DoubleNestedClass.class); assertThat(selector.getEnclosingClassNames()).containsExactly(enclosingClassName, nestedClassName); assertThat(selector.getNestedClassName()).isEqualTo(doubleNestedClassName); @@ -796,7 +861,7 @@ void selectNestedMethodByEnclosingClassNamesAndMethodName() throws Exception { var selector = selectNestedMethod(List.of(enclosingClassName), nestedClassName, methodName); assertThat(selector.getEnclosingClasses()).containsOnly(ClassWithNestedInnerClass.class); - assertThat(selector.getNestedClass()).isEqualTo(AbstractClassWithNestedInnerClass.NestedClass.class); + assertThat(selector.getNestedClass()).isEqualTo(ClassWithNestedInnerClass.NestedClass.class); assertThat(selector.getMethod()).isEqualTo(selector.getNestedClass().getDeclaredMethod(methodName)); assertThat(selector.getEnclosingClassNames()).containsOnly(enclosingClassName); @@ -805,9 +870,8 @@ void selectNestedMethodByEnclosingClassNamesAndMethodName() throws Exception { } @Test - void selectNestedMethodByEnclosingClassNamesAndMethodNameAndClassLoader() throws Exception { - var testClasses = List.of(AbstractClassWithNestedInnerClass.class, ClassWithNestedInnerClass.class, - AbstractClassWithNestedInnerClass.NestedClass.class); + void selectNestedMethodByEnclosingClassNamesAndMethodNameWithExplicitClassLoader() throws Exception { + var testClasses = List.of(ClassWithNestedInnerClass.class, ClassWithNestedInnerClass.NestedClass.class); try (var testClassLoader = TestClassLoader.forClasses(testClasses)) { var selector = selectNestedMethod(testClassLoader, List.of(enclosingClassName), nestedClassName, @@ -815,7 +879,7 @@ void selectNestedMethodByEnclosingClassNamesAndMethodNameAndClassLoader() throws assertThat(selector.getEnclosingClasses()).doesNotContain(ClassWithNestedInnerClass.class); assertThat(selector.getEnclosingClasses()).extracting(Class::getName).containsOnly(enclosingClassName); - assertThat(selector.getNestedClass()).isNotEqualTo(AbstractClassWithNestedInnerClass.NestedClass.class); + assertThat(selector.getNestedClass()).isNotEqualTo(ClassWithNestedInnerClass.NestedClass.class); assertThat(selector.getNestedClass().getName()).isEqualTo(nestedClassName); assertThat(selector.getClassLoader()).isSameAs(testClassLoader); @@ -830,10 +894,10 @@ void selectNestedMethodByEnclosingClassNamesAndMethodNameAndClassLoader() throws @Test void selectNestedMethodByEnclosingClassesAndMethodName() throws Exception { var selector = selectNestedMethod(List.of(ClassWithNestedInnerClass.class), - AbstractClassWithNestedInnerClass.NestedClass.class, methodName); + ClassWithNestedInnerClass.NestedClass.class, methodName); assertThat(selector.getEnclosingClasses()).containsOnly(ClassWithNestedInnerClass.class); - assertThat(selector.getNestedClass()).isEqualTo(AbstractClassWithNestedInnerClass.NestedClass.class); + assertThat(selector.getNestedClass()).isEqualTo(ClassWithNestedInnerClass.NestedClass.class); assertThat(selector.getMethod()).isEqualTo(selector.getNestedClass().getDeclaredMethod(methodName)); assertThat(selector.getEnclosingClassNames()).containsOnly(enclosingClassName); @@ -842,12 +906,12 @@ void selectNestedMethodByEnclosingClassesAndMethodName() throws Exception { } @Test - void selectNestedMethodByEnclosingClassNamesAndMethodNameWithParameterTypes() throws Exception { + void selectNestedMethodByEnclosingClassNamesMethodNameAndParameterTypeNames() throws Exception { var selector = selectNestedMethod(List.of(enclosingClassName), nestedClassName, methodName, String.class.getName()); assertThat(selector.getEnclosingClasses()).containsOnly(ClassWithNestedInnerClass.class); - assertThat(selector.getNestedClass()).isEqualTo(AbstractClassWithNestedInnerClass.NestedClass.class); + assertThat(selector.getNestedClass()).isEqualTo(ClassWithNestedInnerClass.NestedClass.class); assertThat(selector.getMethod()).isEqualTo( selector.getNestedClass().getDeclaredMethod(methodName, String.class)); @@ -856,18 +920,42 @@ void selectNestedMethodByEnclosingClassNamesAndMethodNameWithParameterTypes() th assertThat(selector.getMethodName()).isEqualTo(methodName); } + /** + * @since 1.0 + */ @Test - void selectNestedMethodByEnclosingClassNamesAndMethodNameWithParameterTypesAndClassLoader() throws Exception { - var testClasses = List.of(AbstractClassWithNestedInnerClass.class, ClassWithNestedInnerClass.class, - AbstractClassWithNestedInnerClass.NestedClass.class); + void selectNestedMethodByEnclosingClassNamesMethodNameAndParameterTypes() throws Exception { + var selector = selectNestedMethod(List.of(enclosingClassName), nestedClassName, methodName, String.class); - try (var testClassLoader = TestClassLoader.forClasses(testClasses.toArray(Class[]::new))) { + assertThat(selector.getEnclosingClasses()).containsOnly(ClassWithNestedInnerClass.class); + assertThat(selector.getNestedClass()).isEqualTo(ClassWithNestedInnerClass.NestedClass.class); + assertThat(selector.getMethod()).isEqualTo( + selector.getNestedClass().getDeclaredMethod(methodName, String.class)); + assertThat(selector.getParameterTypes()).containsExactly(String.class); + + assertThat(selector.getEnclosingClassNames()).containsOnly(enclosingClassName); + assertThat(selector.getNestedClassName()).isEqualTo(nestedClassName); + assertThat(selector.getMethodName()).isEqualTo(methodName); + assertThat(selector.getParameterTypeNames()).isEqualTo("java.lang.String"); + } + + /** + * @since 1.10 + */ + @Test + void selectNestedMethodByEnclosingClassNamesMethodNameAndParameterTypeNamesWithExplicitClassLoader() + throws Exception { + + var enclosingClass = ClassWithNestedInnerClass.class; + var nestedClass = ClassWithNestedInnerClass.NestedClass.class; + + try (var testClassLoader = TestClassLoader.forClasses(enclosingClass, nestedClass)) { var selector = selectNestedMethod(testClassLoader, List.of(enclosingClassName), nestedClassName, methodName, String.class.getName()); - assertThat(selector.getEnclosingClasses()).doesNotContain(ClassWithNestedInnerClass.class); + assertThat(selector.getEnclosingClasses()).doesNotContain(enclosingClass); assertThat(selector.getEnclosingClasses()).extracting(Class::getName).containsOnly(enclosingClassName); - assertThat(selector.getNestedClass()).isNotEqualTo(AbstractClassWithNestedInnerClass.NestedClass.class); + assertThat(selector.getNestedClass()).isNotEqualTo(nestedClass); assertThat(selector.getNestedClass().getName()).isEqualTo(nestedClassName); assertThat(selector.getClassLoader()).isSameAs(testClassLoader); @@ -880,6 +968,26 @@ void selectNestedMethodByEnclosingClassNamesAndMethodNameWithParameterTypesAndCl } } + /** + * @since 1.10 + */ + @Test + void selectNestedMethodByEnclosingClassesMethodNameAndParameterTypes() throws Exception { + var selector = selectNestedMethod(List.of(ClassWithNestedInnerClass.class), + ClassWithNestedInnerClass.NestedClass.class, methodName, String.class); + + assertThat(selector.getEnclosingClasses()).containsOnly(ClassWithNestedInnerClass.class); + assertThat(selector.getNestedClass()).isEqualTo(ClassWithNestedInnerClass.NestedClass.class); + assertThat(selector.getMethod()).isEqualTo( + selector.getNestedClass().getDeclaredMethod(methodName, String.class)); + assertThat(selector.getParameterTypes()).containsExactly(String.class); + + assertThat(selector.getEnclosingClassNames()).containsOnly(enclosingClassName); + assertThat(selector.getNestedClassName()).isEqualTo(nestedClassName); + assertThat(selector.getMethodName()).isEqualTo(methodName); + assertThat(selector.getParameterTypeNames()).isEqualTo("java.lang.String"); + } + @Test void selectDoubleNestedMethodByEnclosingClassNamesAndMethodName() throws Exception { var doubleNestedMethodName = "doubleNestedTest"; @@ -887,9 +995,9 @@ void selectDoubleNestedMethodByEnclosingClassNamesAndMethodName() throws Excepti doubleNestedMethodName); assertThat(selector.getEnclosingClasses()).containsExactly(ClassWithNestedInnerClass.class, - AbstractClassWithNestedInnerClass.NestedClass.class); + ClassWithNestedInnerClass.NestedClass.class); assertThat(selector.getNestedClass()).isEqualTo( - AbstractClassWithNestedInnerClass.NestedClass.DoubleNestedClass.class); + ClassWithNestedInnerClass.NestedClass.DoubleNestedClass.class); assertThat(selector.getMethod()).isEqualTo( selector.getNestedClass().getDeclaredMethod(doubleNestedMethodName)); @@ -899,53 +1007,88 @@ void selectDoubleNestedMethodByEnclosingClassNamesAndMethodName() throws Excepti } @Test - void selectNestedMethodPreconditions() { + @DisplayName("Preconditions: selectNestedMethod(enclosingClassNames, nestedClassName, methodName)") + void selectNestedMethodByEnclosingClassNamesAndMethodNamePreconditions() { assertViolatesPrecondition(() -> selectNestedMethod(null, "ClassName", "methodName")); - assertViolatesPrecondition(() -> selectNestedMethod(null, "ClassName", "methodName", "int")); assertViolatesPrecondition(() -> selectNestedMethod(List.of(), "ClassName", "methodName")); - assertViolatesPrecondition(() -> selectNestedMethod(List.of(), "ClassName", "methodName", "int")); assertViolatesPrecondition(() -> selectNestedMethod(List.of("ClassName"), null, "methodName")); - assertViolatesPrecondition(() -> selectNestedMethod(List.of("ClassName"), null, "methodName", "int")); assertViolatesPrecondition(() -> selectNestedMethod(List.of("ClassName"), " ", "methodName")); - assertViolatesPrecondition(() -> selectNestedMethod(List.of("ClassName"), " ", "methodName", "int")); assertViolatesPrecondition(() -> selectNestedMethod(List.of("ClassName"), "ClassName", null)); - assertViolatesPrecondition(() -> selectNestedMethod(List.of("ClassName"), "ClassName", null, "int")); assertViolatesPrecondition(() -> selectNestedMethod(List.of("ClassName"), "ClassName", " ")); + } + + @Test + @DisplayName("Preconditions: selectNestedMethod(enclosingClassNames, nestedClassName, methodName, parameterTypeNames)") + void selectNestedMethodByEnclosingClassNamesMethodNameAndParameterTypeNamesPreconditions() { + assertViolatesPrecondition(() -> selectNestedMethod(null, "ClassName", "methodName", "int")); + assertViolatesPrecondition(() -> selectNestedMethod(List.of(), "ClassName", "methodName", "int")); + assertViolatesPrecondition(() -> selectNestedMethod(List.of("ClassName"), null, "methodName", "int")); + assertViolatesPrecondition(() -> selectNestedMethod(List.of("ClassName"), " ", "methodName", "int")); + assertViolatesPrecondition(() -> selectNestedMethod(List.of("ClassName"), "ClassName", null, "int")); assertViolatesPrecondition(() -> selectNestedMethod(List.of("ClassName"), "ClassName", " ", "int")); assertViolatesPrecondition( () -> selectNestedMethod(List.of("ClassName"), "ClassName", "methodName", (String) null)); } - abstract class AbstractClassWithNestedInnerClass { + /** + * @since 1.10 + */ + @Test + @DisplayName("Preconditions: selectNestedMethod(enclosingClassNames, nestedClassName, methodName, parameterTypes)") + void selectNestedMethodByEnclosingClassNamesMethodNameAndParameterTypesPreconditions() { + assertViolatesPrecondition(() -> selectNestedMethod(null, "ClassName", "methodName", int.class)); + assertViolatesPrecondition(() -> selectNestedMethod(List.of(), "ClassName", "methodName", int.class)); + assertViolatesPrecondition(() -> selectNestedMethod(List.of("ClassName"), null, "methodName", int.class)); + assertViolatesPrecondition(() -> selectNestedMethod(List.of("ClassName"), " ", "methodName", int.class)); + assertViolatesPrecondition(() -> selectNestedMethod(List.of("ClassName"), "ClassName", null, int.class)); + assertViolatesPrecondition(() -> selectNestedMethod(List.of("ClassName"), "ClassName", " ", int.class)); + assertViolatesPrecondition( + () -> selectNestedMethod(List.of("ClassName"), "ClassName", "methodName", (Class) null)); + assertViolatesPrecondition(() -> selectNestedMethod(List.of("ClassName"), "ClassName", "methodName", + new Class[] { int.class, null })); + } + + /** + * @since 1.10 + */ + @Test + @DisplayName("Preconditions: selectNestedMethod(enclosingClasses, nestedClass, methodName, parameterTypes)") + void selectNestedMethodByEnclosingClassesClassMethodNameAndParameterTypesPreconditions() { + List> enclosingClassList = List.of(ClassWithNestedInnerClass.class); + Class nestedClass = ClassWithNestedInnerClass.NestedClass.class; + + assertViolatesPrecondition(() -> selectNestedMethod(null, nestedClass, "methodName", int.class)); + assertViolatesPrecondition(() -> selectNestedMethod(List.of(), nestedClass, "methodName", int.class)); + assertViolatesPrecondition(() -> selectNestedMethod(enclosingClassList, null, "methodName", int.class)); + assertViolatesPrecondition(() -> selectNestedMethod(enclosingClassList, nestedClass, null, int.class)); + assertViolatesPrecondition(() -> selectNestedMethod(enclosingClassList, nestedClass, " ", int.class)); + assertViolatesPrecondition( + () -> selectNestedMethod(enclosingClassList, nestedClass, "methodName", (Class) null)); + assertViolatesPrecondition(() -> selectNestedMethod(enclosingClassList, nestedClass, "methodName", + new Class[] { int.class, null })); + } + + static class ClassWithNestedInnerClass { - @Nested + // @Nested class NestedClass { - @Test + // @Test void nestedTest() { } - @Test + // @Test void nestedTest(String parameter) { } - @Nested + // @Nested class DoubleNestedClass { - @Test + // @Test void doubleNestedTest() { } - } - } - - } - - class ClassWithNestedInnerClass extends AbstractClassWithNestedInnerClass { - } - - class OtherClassWithNestedInnerClass extends AbstractClassWithNestedInnerClass { } } diff --git a/platform-tests/src/test/java/org/junit/platform/engine/discovery/MethodSelectorTests.java b/platform-tests/src/test/java/org/junit/platform/engine/discovery/MethodSelectorTests.java index 9f314ef7d8ed..b6d890ba0861 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/discovery/MethodSelectorTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/discovery/MethodSelectorTests.java @@ -35,8 +35,9 @@ void equalsAndHashCode() { var selector1 = new MethodSelector(null, TEST_CASE_NAME, "method", "int, boolean"); var selector2 = new MethodSelector(null, TEST_CASE_NAME, "method", "int, boolean"); var selector3 = new MethodSelector(TestCase.class, "method", "int, boolean"); + var selector4 = new MethodSelector(TestCase.class, "method", int.class, boolean.class); - Stream.of(selector2, selector3).forEach(selector -> { + Stream.of(selector2, selector3, selector4).forEach(selector -> { assertEqualsAndHashCode(selector1, selector, new MethodSelector(null, TEST_CASE_NAME, "method", "int")); assertEqualsAndHashCode(selector1, selector, new MethodSelector((ClassLoader) null, TEST_CASE_NAME, "method", "")); diff --git a/platform-tests/src/test/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilderTests.java b/platform-tests/src/test/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilderTests.java index 1848163bc6ce..55aa8d6eaa04 100644 --- a/platform-tests/src/test/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilderTests.java +++ b/platform-tests/src/test/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilderTests.java @@ -10,11 +10,14 @@ package org.junit.platform.suite.commons; +import static java.util.Map.entry; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.junit.platform.suite.commons.SuiteLauncherDiscoveryRequestBuilder.request; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -26,8 +29,11 @@ import java.util.Optional; import java.util.Set; import java.util.regex.Pattern; +import java.util.stream.Stream; +import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.engine.JupiterTestEngine; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.util.CollectionUtils; @@ -72,7 +78,7 @@ class SuiteLauncherDiscoveryRequestBuilderTests { - SuiteLauncherDiscoveryRequestBuilder builder = SuiteLauncherDiscoveryRequestBuilder.request(); + SuiteLauncherDiscoveryRequestBuilder builder = request(); @Test void configurationParameter() { @@ -249,38 +255,69 @@ class Suite { static class NonLocalTestCase { } - @Test - void selectOneMethodWithNoParameters() { - class TestClass { - void testMethod() { - } + @TestFactory + Stream selectOneMethodWithNoParameters() { + @SelectMethod("org.junit.platform.suite.commons.SuiteLauncherDiscoveryRequestBuilderTests$NoParameterTestCase#testMethod") + class SuiteA { } - @SelectMethod(clazz = TestClass.class, name = "testMethod") - class Suite { + @SelectMethod(type = NoParameterTestCase.class, name = "testMethod") + class SuiteB { + } + @SelectMethod(typeName = "org.junit.platform.suite.commons.SuiteLauncherDiscoveryRequestBuilderTests$NoParameterTestCase", name = "testMethod") + class SuiteC { } - LauncherDiscoveryRequest request = builder.suite(Suite.class).build(); - List selectors = request.getSelectorsByType(MethodSelector.class); - assertEquals(DiscoverySelectors.selectMethod(TestClass.class, "testMethod"), exactlyOne(selectors)); + return Stream.of(SuiteA.class, SuiteB.class, SuiteC.class) // + .map(suiteClass -> dynamicTest(suiteClass.getSimpleName(), () -> { + LauncherDiscoveryRequest request = request().suite(suiteClass).build(); + List selectors = request.getSelectorsByType(MethodSelector.class); + assertEquals(DiscoverySelectors.selectMethod(NoParameterTestCase.class, "testMethod"), + exactlyOne(selectors)); + })); } - @Test - void selectOneMethodWithOneParameters() { - class TestClass { - void testMethod(int i) { - } + static class NoParameterTestCase { + @SuppressWarnings("unused") + void testMethod() { } - @SelectMethod(clazz = TestClass.class, name = "testMethod", parameters = "int") - class Suite { + } + + @TestFactory + Stream selectOneMethodWithOneParameter() { + @SelectMethod("org.junit.platform.suite.commons.SuiteLauncherDiscoveryRequestBuilderTests$OneParameterTestCase#testMethod(int)") + class SuiteA { + } + @SelectMethod(type = OneParameterTestCase.class, name = "testMethod", parameterTypeNames = "int") + class SuiteB { + } + @SelectMethod(type = OneParameterTestCase.class, name = "testMethod", parameterTypes = int.class) + class SuiteC { + } + @SelectMethod(typeName = "org.junit.platform.suite.commons.SuiteLauncherDiscoveryRequestBuilderTests$OneParameterTestCase", name = "testMethod", parameterTypeNames = "int") + class SuiteD { + } + @SelectMethod(typeName = "org.junit.platform.suite.commons.SuiteLauncherDiscoveryRequestBuilderTests$OneParameterTestCase", name = "testMethod", parameterTypes = int.class) + class SuiteE { } - LauncherDiscoveryRequest request = builder.suite(Suite.class).build(); - List selectors = request.getSelectorsByType(MethodSelector.class); - assertEquals(DiscoverySelectors.selectMethod(TestClass.class, "testMethod", "int"), exactlyOne(selectors)); + return Stream.of(SuiteA.class, SuiteB.class, SuiteC.class, SuiteD.class, SuiteE.class) // + .map(suiteClass -> dynamicTest(suiteClass.getSimpleName(), () -> { + LauncherDiscoveryRequest request = request().suite(suiteClass).build(); + List selectors = request.getSelectorsByType(MethodSelector.class); + assertEquals(DiscoverySelectors.selectMethod(OneParameterTestCase.class, "testMethod", "int"), + exactlyOne(selectors)); + })); + } + + static class OneParameterTestCase { + @SuppressWarnings("unused") + void testMethod(int i) { + } } @Test - void selectTwoMethodWithTwoParameters() { + void selectTwoMethodsWithTwoParameters() { + @SuppressWarnings("unused") class TestClass { void firstTestMethod(int i, String j) { } @@ -288,8 +325,9 @@ void firstTestMethod(int i, String j) { void secondTestMethod(boolean i, float j) { } } - @SelectMethod(clazz = TestClass.class, name = "firstTestMethod", parameters = "int, java.lang.String") - @SelectMethod(clazz = TestClass.class, name = "secondTestMethod", parameters = "boolean, float") + @SelectMethod(type = TestClass.class, name = "firstTestMethod", parameterTypeNames = "int, java.lang.String") + @SelectMethod(type = TestClass.class, name = "secondTestMethod", parameterTypes = { boolean.class, + float.class }) class Suite { } @@ -302,6 +340,61 @@ class Suite { selectors.get(1)); } + @TestFactory + Stream selectMethodCausesExceptionOnInvalidUsage() { + @SelectMethod(value = "irrelevant", type = NoParameterTestCase.class) + class ValueAndType { + } + @SelectMethod(value = "SomeClass#someMethod", typeName = "org.junit.platform.suite.commons.SuiteLauncherDiscoveryRequestBuilderTests$NoParameterTestCase") + class ValueAndTypeName { + } + @SelectMethod(value = "SomeClass#someMethod", name = "testMethod") + class ValueAndMethodName { + } + @SelectMethod(value = "SomeClass#someMethod", parameterTypes = int.class) + class ValueAndParameterTypes { + } + @SelectMethod(value = "SomeClass#someMethod", parameterTypeNames = "int") + class ValueAndParameterTypeNames { + } + @SelectMethod(type = NoParameterTestCase.class) + class MissingMethodName { + } + @SelectMethod(name = "testMethod", type = NoParameterTestCase.class, typeName = "org.junit.platform.suite.commons.SuiteLauncherDiscoveryRequestBuilderTests$NoParameterTestCase") + class TypeAndTypeName { + } + @SelectMethod(name = "testMethod", parameterTypes = int.class, parameterTypeNames = "int") + class ParameterTypesAndParameterTypeNames { + } + + var expectedFailureMessages = Map.ofEntries( // + entry(ValueAndType.class, "type must not be set in conjunction with fully qualified method name"), // + entry(ValueAndTypeName.class, "type name must not be set in conjunction with fully qualified method name"), // + entry(ValueAndMethodName.class, + "method name must not be set in conjunction with fully qualified method name"), // + entry(ValueAndParameterTypes.class, + "parameter types must not be set in conjunction with fully qualified method name"), // + entry(ValueAndParameterTypeNames.class, + "parameter type names must not be set in conjunction with fully qualified method name"), // + entry(MissingMethodName.class, "method name must not be blank"), // + entry(TypeAndTypeName.class, "either type name or type must be set but not both"), // + entry(ParameterTypesAndParameterTypeNames.class, + "either parameter type names or parameter types must be set but not both") // + ); + return expectedFailureMessages.entrySet().stream() // + .map(entry -> { + Class suiteClassName = entry.getKey(); + var expectedFailureMessage = entry.getValue(); + return dynamicTest(suiteClassName.getSimpleName(), () -> { + var ex = assertThrows(PreconditionViolationException.class, + () -> request().suite(suiteClassName)); + assertEquals( + "@SelectMethod on class [" + suiteClassName.getName() + "]: " + expectedFailureMessage, + ex.getMessage()); + }); + }); + } + @Test void selectClasspathResource() { @SelectClasspathResource("com.example.testcases") diff --git a/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/SelectMethodsSuite.java b/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/SelectMethodsSuite.java index 1e9712e5e156..49184c267e23 100644 --- a/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/SelectMethodsSuite.java +++ b/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/SelectMethodsSuite.java @@ -18,6 +18,6 @@ * @since 1.10 */ @Suite -@SelectMethod(clazz = MultipleTestsTestCase.class, name = "test") +@SelectMethod(type = MultipleTestsTestCase.class, name = "test") public class SelectMethodsSuite { }