From ec7aa6e100b9601d6f14050f3757cf68610d713c Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 12 Feb 2025 13:02:15 +0100 Subject: [PATCH 001/167] Create initial 5.13.0-M1 release notes from template --- .../docs/asciidoc/release-notes/index.adoc | 2 + .../release-notes-5.13.0-M1.adoc | 67 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc diff --git a/documentation/src/docs/asciidoc/release-notes/index.adoc b/documentation/src/docs/asciidoc/release-notes/index.adoc index 097e417e7854..f6092781ea7a 100644 --- a/documentation/src/docs/asciidoc/release-notes/index.adoc +++ b/documentation/src/docs/asciidoc/release-notes/index.adoc @@ -17,6 +17,8 @@ authors as well as build tool and IDE vendors. include::{includedir}/link-attributes.adoc[] +include::{basedir}/release-notes-5.13.0-M1.adoc[] + include::{basedir}/release-notes-5.12.0.adoc[] include::{basedir}/release-notes-5.11.4.adoc[] diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc new file mode 100644 index 000000000000..e7614bc3a786 --- /dev/null +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc @@ -0,0 +1,67 @@ +[[release-notes-5.13.0-M1]] +== 5.13.0-M1 + +*Date of Release:* ❓ + +*Scope:* ❓ + +For a complete list of all _closed_ issues and pull requests for this release, consult the +link:{junit5-repo}+/milestone/85?closed=1+[5.13.0-M1] milestone page in the JUnit +repository on GitHub. + + +[[release-notes-5.13.0-M1-junit-platform]] +=== JUnit Platform + +[[release-notes-5.13.0-M1-junit-platform-bug-fixes]] +==== Bug Fixes + +* ❓ + +[[release-notes-5.13.0-M1-junit-platform-deprecations-and-breaking-changes]] +==== Deprecations and Breaking Changes + +* ❓ + +[[release-notes-5.13.0-M1-junit-platform-new-features-and-improvements]] +==== New Features and Improvements + +* ❓ + + +[[release-notes-5.13.0-M1-junit-jupiter]] +=== JUnit Jupiter + +[[release-notes-5.13.0-M1-junit-jupiter-bug-fixes]] +==== Bug Fixes + +* ❓ + +[[release-notes-5.13.0-M1-junit-jupiter-deprecations-and-breaking-changes]] +==== Deprecations and Breaking Changes + +* ❓ + +[[release-notes-5.13.0-M1-junit-jupiter-new-features-and-improvements]] +==== New Features and Improvements + +* ❓ + + +[[release-notes-5.13.0-M1-junit-vintage]] +=== JUnit Vintage + +[[release-notes-5.13.0-M1-junit-vintage-bug-fixes]] +==== Bug Fixes + +* ❓ + +[[release-notes-5.13.0-M1-junit-vintage-deprecations-and-breaking-changes]] +==== Deprecations and Breaking Changes + +* ❓ + +[[release-notes-5.13.0-M1-junit-vintage-new-features-and-improvements]] +==== New Features and Improvements + +* ❓ From ff01c904f04de462503199a9aa0ad045e339dff2 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 12 Feb 2025 13:05:16 +0100 Subject: [PATCH 002/167] Start 5.13 release cycle --- .../docs/asciidoc/release-notes/index.adoc | 10 - .../release-notes/release-notes-5.11.0.adoc | 19 -- .../release-notes/release-notes-5.11.1.adoc | 55 ------ .../release-notes/release-notes-5.11.2.adoc | 33 ---- .../release-notes/release-notes-5.11.3.adoc | 40 ---- .../release-notes/release-notes-5.11.4.adoc | 39 ---- .../release-notes/release-notes-5.12.0.adoc | 171 +----------------- gradle.properties | 6 +- 8 files changed, 5 insertions(+), 368 deletions(-) delete mode 100644 documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0.adoc delete mode 100644 documentation/src/docs/asciidoc/release-notes/release-notes-5.11.1.adoc delete mode 100644 documentation/src/docs/asciidoc/release-notes/release-notes-5.11.2.adoc delete mode 100644 documentation/src/docs/asciidoc/release-notes/release-notes-5.11.3.adoc delete mode 100644 documentation/src/docs/asciidoc/release-notes/release-notes-5.11.4.adoc diff --git a/documentation/src/docs/asciidoc/release-notes/index.adoc b/documentation/src/docs/asciidoc/release-notes/index.adoc index f6092781ea7a..19bd43c11a0c 100644 --- a/documentation/src/docs/asciidoc/release-notes/index.adoc +++ b/documentation/src/docs/asciidoc/release-notes/index.adoc @@ -20,13 +20,3 @@ include::{includedir}/link-attributes.adoc[] include::{basedir}/release-notes-5.13.0-M1.adoc[] include::{basedir}/release-notes-5.12.0.adoc[] - -include::{basedir}/release-notes-5.11.4.adoc[] - -include::{basedir}/release-notes-5.11.3.adoc[] - -include::{basedir}/release-notes-5.11.2.adoc[] - -include::{basedir}/release-notes-5.11.1.adoc[] - -include::{basedir}/release-notes-5.11.0.adoc[] diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0.adoc deleted file mode 100644 index 19fdbc72dfa0..000000000000 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0.adoc +++ /dev/null @@ -1,19 +0,0 @@ -[[release-notes-5.11.0]] -== 5.11.0 - -*Date of Release:* August 14, 2024 - -*Scope:* - -* `@FieldSource` annotation for use with `@ParameterizedTest` methods -* Repeatable `@..Source` annotations for parameterized tests -* Enhancements for authoring dynamic and parameterized tests -* `@AutoClose` annotation to automatically close field resources in tests -* `ConversionSupport` utility for converting from a string to a supported target type -* Extensible syntax for specifying discovery selectors -* `@BeforeSuite` and `@AfterSuite` annotations -* Classpath resource scanning support for engines -* Numerous bug fixes and enhancements regarding field and method search algorithms - -For complete details consult the -https://junit.org/junit5/docs/5.11.0/release-notes/index.html[5.11.0 Release Notes] online. diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.1.adoc deleted file mode 100644 index 80af4a1a53d3..000000000000 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.1.adoc +++ /dev/null @@ -1,55 +0,0 @@ -[[release-notes-5.11.1]] -== 5.11.1 - -*Date of Release:* September 25, 2024 - -*Scope:* Bug fixes and enhancements since 5.11.0 - -For a complete list of all _closed_ issues and pull requests for this release, consult the -link:{junit5-repo}+/milestone/80?closed=1+[5.11.1] milestone page in the JUnit repository -on GitHub. - - -[[release-notes-5.11.1-junit-platform]] -=== JUnit Platform - -[[release-notes-5.11.1-junit-platform-bug-fixes]] -==== Bug Fixes - -* Fix support for disabling ANSI colors on the console when the `NO_COLOR` environment - variable is available. -* `NamespacedHierarchicalStore` no longer throws an exception after it has been closed if - the store is queried via one of the `get(...)` or `getOrComputeIfAbsent(...)` methods; - however, if a `getOrComputeIfAbsent(...)` invocation results in the computation of a new - value, an exception will still be thrown. -* Fixed potential locking issue with `ExclusiveResource` in the - `HierarchicalTestExecutorService`, which could lead to deadlocks in certain scenarios. - -[[release-notes-5.11.1-junit-platform-new-features-and-improvements]] -==== New Features and Improvements - -* Improve parallelism and reduce number of blocked threads used by - `HierarchicalTestEngine` implementations when parallel execution is enabled and the - global read-write lock is used. - - -[[release-notes-5.11.1-junit-jupiter]] -=== JUnit Jupiter - -[[release-notes-5.11.1-junit-jupiter-bug-fixes]] -==== Bug Fixes - -* `TestWatcher` callback methods can once again access data in the - `ExtensionContext.Store`. - -[[release-notes-5.11.1-junit-jupiter-new-features-and-improvements]] -==== New Features and Improvements - -* Improve parallelism and reduce number of blocked threads in the presence of `@Isolated` - tests when parallel execution is enabled - - -[[release-notes-5.11.1-junit-vintage]] -=== JUnit Vintage - -No changes. diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.2.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.2.adoc deleted file mode 100644 index 0bb4b292e1d9..000000000000 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.2.adoc +++ /dev/null @@ -1,33 +0,0 @@ -[[release-notes-5.11.2]] -== 5.11.2 - -*Date of Release:* October 4, 2024 - -*Scope:* Bug fixes and enhancements since 5.11.1 - -For a complete list of all _closed_ issues and pull requests for this release, consult the -link:{junit5-repo}+/milestone/82?closed=1+[5.11.2] milestone page in the JUnit repository -on GitHub. - - -[[release-notes-5.11.2-junit-platform]] -=== JUnit Platform - -[[release-notes-5.11.2-junit-platform-bug-fixes]] -==== Bug Fixes - -* Fix regression in parallel execution that was introduced in 5.11.1 regarding global - read-write locks. When such a lock was declared on descendants of top-level nodes in the - test tree, such as Cucumber scenarios, test execution failed. - - -[[release-notes-5.11.2-junit-jupiter]] -=== JUnit Jupiter - -No changes. - - -[[release-notes-5.11.2-junit-vintage]] -=== JUnit Vintage - -No changes. diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.3.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.3.adoc deleted file mode 100644 index 0588af0f1cc7..000000000000 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.3.adoc +++ /dev/null @@ -1,40 +0,0 @@ -[[release-notes-5.11.3]] -== 5.11.3 - -*Date of Release:* October 21, 2024 - -*Scope:* Bug fixes and enhancements since 5.11.2 - -For a complete list of all _closed_ issues and pull requests for this release, consult the -link:{junit5-repo}+/milestone/84?closed=1+[5.11.3] milestone page in the JUnit repository -on GitHub. - - -[[release-notes-5.11.3-junit-platform]] -=== JUnit Platform - -[[release-notes-5.11.3-junit-platform-bug-fixes]] -==== Bug Fixes - -* Fixed a regression in method search algorithms introduced in 5.11.0 when classes reside - in the default package and using a Java 8 runtime. - - -[[release-notes-5.11.3-junit-jupiter]] -=== JUnit Jupiter - -[[release-notes-5.11.3-junit-jupiter-bug-fixes]] -==== Bug Fixes - -* Extensions can once again be registered via multiple `@ExtendWith` meta-annotations on - the same composed annotation on a field within a test class. -* `@ExtendWith` annotations can now also be repeated when used directly on fields and - parameters. -* All `@...Source` annotations of parameterized tests can now also be repeated when used - as meta annotations. - - -[[release-notes-5.11.3-junit-vintage]] -=== JUnit Vintage - -No changes. diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.4.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.4.adoc deleted file mode 100644 index e9ed32bc772a..000000000000 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.4.adoc +++ /dev/null @@ -1,39 +0,0 @@ -[[release-notes-5.11.4]] -== 5.11.4 - -*Date of Release:* December 16, 2024 - -*Scope:* Bug fixes and enhancements since 5.11.3 - -For a complete list of all _closed_ issues and pull requests for this release, consult the -link:{junit5-repo}+/milestone/86?closed=1+[5.11.4] milestone page in the -JUnit repository on GitHub. - - -[[release-notes-5.11.4-junit-platform]] -=== JUnit Platform - -[[release-notes-5.11.4-junit-platform-bug-fixes]] -==== Bug Fixes - -* Escape whitespace characters (such as line breaks) in XML attribute values (such as - exception messages) in the legacy XML report generated by the Console Launcher. This - change ensures the resulting XML files can be processed by downstream tools while - preserving whitespace characters. -* Enable auto-flushing of output in the `ConsoleLauncher` to fix issues with buffering, - in particular when using the `--details=testfeed` option. - - -[[release-notes-5.11.4-junit-jupiter]] -=== JUnit Jupiter - -[[release-notes-5.11.4-junit-jupiter-new-features-and-improvements]] -==== New Features and Improvements - -* `JAVA_25` has been added to the `JRE` enum for use with JRE-based execution conditions. - - -[[release-notes-5.11.4-junit-vintage]] -=== JUnit Vintage - -No changes. diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0.adoc index d6176f3bc165..75ff54748e5f 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0.adoc @@ -20,172 +20,5 @@ * Parallel execution support in JUnit Vintage engine * Numerous bug fixes and other enhancements -For a complete list of all _closed_ issues and pull requests for this release, consult the link:{junit5-repo}+/milestone/75?closed=1+[5.12.0-M1], -link:{junit5-repo}+/milestone/88?closed=1+[5.12.0-RC1], -link:{junit5-repo}+/milestone/90?closed=1+[5.12.0-RC2], and -link:{junit5-repo}+/milestone/89?closed=1+[5.12.0] milestone pages in the JUnit repository -on GitHub. - - -[[release-notes-5.12.0-overall-improvements]] -=== Overall Improvements - -[[release-notes-5.12.0-overall-new-features-and-improvements]] -==== New Features and Improvements - -* All affected JAR files now include `native-image.properties` files that contain the -`--initialize-at-build-time` option to avoid breakages in GraalVM projects when updating -to newer versions of JUnit. - - -[[release-notes-5.12.0-junit-platform]] -=== JUnit Platform - -[[release-notes-5.12.0-junit-platform-deprecations-and-breaking-changes]] -==== Deprecations and Breaking Changes - -* `SearchOption` and `AnnotationSupport.findAnnotation(Class, Class, SearchOption)` from - `junit-platform-commons` have been deprecated. - -[[release-notes-5.12.0-junit-platform-new-features-and-improvements]] -==== New Features and Improvements - -* `ConsoleLauncher` now accepts multiple values for all `--select` options. -* `ConsoleLauncher` now supports a `--select-unique-id` option to select containers and - tests by unique ID. -* `ConsoleLauncher` supports new `--exclude-methodname` and `--include-methodname` options - to include or exclude methods based on fully qualified method names without parameters. - For example, `--exclude-methodname=^org\.example\..+#methodname` will exclude all - methods called `methodName` under package `org.example`. -* The `--select-file` and `--select-resource` options for the `ConsoleLauncher` now - support line and column numbers. -* New `ReflectionSupport.makeAccessible(Field)` public utility method to be used by third - parties instead of calling the internal `ReflectionUtils.makeAccessible(Field)` method - directly. -* The `ReflectionSupport.tryToLoadClass(...)` utility methods now support lookups for the - `"void"` pseudo-type, which indirectly supports `String` to `Class` conversion for - `"void"` in parameterized tests in JUnit Jupiter. -* New `addResourceContainerSelectorResolver()` method in - `EngineDiscoveryRequestResolver.Builder` which supports the discovery of class path - resource based tests, analogous to the existing `addClassContainerSelectorResolver()` - method. -* New `getOutputDirectoryProvider()` method in `EngineDiscoveryRequest` and `TestPlan` to - allow test engines to publish/attach files to containers and tests by calling - `EngineExecutionListener.fileEntryPublished(...)`. Registered `TestExecutionListeners` - can then access these files by overriding the `fileEntryPublished(...)` method. -* The following improvements have been made to the - <<../user-guide/index.adoc#junit-platform-reporting-open-test-reporting, Open Test Reporting>> - XML output: - - Information about the Git repository, the current branch, the commit hash, and the - current worktree status are now included in the XML report, if applicable. - - A section containing JUnit-specific metadata about each test/container to the HTML - report is now written by open-test-reporting when added to the classpath/module path - - Information about published files is now included as attachments. - - If <<../user-guide/index.adoc#running-tests-capturing-output, output capturing>> is - enabled, the captured output written to `System.out` and `System.err` is now included - in the XML report. -* Output written to `System.out` and `System.err` from non-test threads is now attributed - to the most recent test or container that was started or has written output. -* New public interface `ClasspathScanner` allowing third parties to provide a custom - implementation for scanning the classpath for classes and resources. -* New `AnnotationSupport.findAnnotation(Class, Class, List)` method to support searching - for an annotation on an inner class and its runtime enclosing instance types. -* New `TestDescriptor.orderChildren(UnaryOperator> orderer)` - method to order children in place - - -[[release-notes-5.12.0-junit-jupiter]] -=== JUnit Jupiter - -[[release-notes-5.12.0-junit-jupiter-bug-fixes]] -==== Bug Fixes - -* Provide _runtime_ enclosing types of `@Nested` test classes and contained test methods - to `DisplayNameGenerator` implementations. Prior to this change, such generators were - only able to access the enclosing class in which `@Nested` was declared, but they could - not access the concrete runtime type of the enclosing instance. -* `@DisplayNameGeneration` annotations are now discovered on the _runtime_ enclosing types - of `@Nested` test classes instead of the compile-time enclosing class in which the - `@Nested` class was _declared_. -* Fix handling of "junctions" on Windows during `@TempDir` cleanup: junctions will no - longer be followed when deleting directories and broken junctions will be deleted. - -[[release-notes-5.12.0-junit-jupiter-deprecations-and-breaking-changes]] -==== Deprecations and Breaking Changes - -* When injecting `TestInfo` into test class constructors, the `TestInfo` now contains data - for the test method for which the test class instance is being created, unless the test - instance lifecycle is set to `PER_CLASS` (in which case it continues to contain the data - for the test class). If you require the `TestInfo` of the test class, you can implement - a `@BeforeAll` lifecycle method and inject `TestInfo` into that method. -* When injecting `TestReporter` into test class constructors the published report entries - are now associated with the test method rather than the test class, unless the test - instance lifecycle is set to `PER_CLASS` (in which case the published report entries - will continue to be associated with the test class). If you want to publish report - entries for the test class, you can implement a `@BeforeAll` lifecycle method and inject - `TestReporter` into that method. - -[[release-notes-5.12.0-junit-jupiter-new-features-and-improvements]] -==== New Features and Improvements - -* Kotlin contracts for Kotlin-specific assertion methods in `Assertions`. -* `@TempDir` is now supported on test class constructors. -* Shared resource locks may now be determined programmatically at runtime via the new - `@ResourceLock#providers` attribute that accepts implementations of - `ResourceLocksProvider`. -* Shared resource locks for _direct_ child nodes may now be configured via the new - `@ResourceLock(target = CHILDREN)` attribute. This may improve parallelization when - a test class declares a `READ` lock, but only a few methods hold a `READ_WRITE` lock. -* `@EnumSource` has new `from` and `to` attributes that support the selection of enum - constants within the specified range. -* In a `@ParameterizedTest` method, a `null` value can now be supplied for Java Date/Time - types such as `LocalDate` if the new `nullable` attribute in - `@JavaTimeConversionPattern` is set to `true`. -* The new `@ParameterizedTest(allowZeroInvocations = true)` attribute allows to specify that - the absence of invocations is expected in some cases and should not cause a test failure. -* Parameterized tests now support argument count validation. If the - `junit.jupiter.params.argumentCountValidation=strict` configuration parameter or the - `@ParameterizedTest(argumentCountValidation = STRICT)` attribute is set, any mismatch - between the declared number of arguments and the number of arguments provided by the - arguments source will result in an error. By default, it is still only an error if there - are fewer arguments provided than declared. -* `ArgumentsProvider` (declared via `@ArgumentsSource`), `ArgumentConverter` (declared via - `@ConvertWith`), and `ArgumentsAggregator` (declared via `@AggregateWith`) - implementations can now use constructor injection from registered `ParameterResolver` - extensions. -* `TestTemplateInvocationContextProvider` extensions can now signal that they may - potentially return zero invocation contexts by overriding the new - `mayReturnZeroTestTemplateInvocationContexts()` method. -* Extensions that implement `TestInstancePreConstructCallback`, `TestInstanceFactory`, - `TestInstancePostProcessor`, `ParameterResolver`, or `InvocationInterceptor` may - override the `getTestInstantiationExtensionContextScope()` method to enable receiving - a test-scoped `ExtensionContext` in `Extension` methods called during test class - instantiation. This behavior will become the default in future versions of JUnit. -* The new `PreInterruptCallback` interface defines the API for `Extensions` that wish to - be called prior to invocations of `Thread#interrupt()` by the `@Timeout` extension. -* When enabled via the `junit.jupiter.execution.timeout.threaddump.enabled` configuration - parameter, an implementation of `PreInterruptCallback` is registered that writes a - thread dump to `System.out` prior to interrupting a test thread due to a timeout. -* `TestReporter` now allows publishing files for a test method or test class which can be - used to include them in test reports, such as the Open Test Reporting format. -* Auto-registered extensions can now be - <<../user-guide/index.adoc#extensions-registration-automatic-filtering, filtered>> using - include and exclude patterns that can be specified as configuration parameters. -* `JRE`-based conditions such as `@EnabledOnJre` and `@DisabledForJreRange` now support - arbitrary Java versions. See the - <<../user-guide/index.adoc#writing-tests-conditional-execution-jre, User Guide>> for - details. -* The `@TempDir` extension now warns during cleanup when deleting symlinks that target - locations outside the temporary directory to signal that the target file or directory is - _not_ deleted, only the link to it. - - -[[release-notes-5.12.0-junit-vintage]] -=== JUnit Vintage - -[[release-notes-5.12.0-junit-vintage-new-features-and-improvements]] -==== New Features and Improvements - -* Added support for executing test classes and/or methods in parallel. Please refer to the - <<../user-guide/index.adoc#migrating-from-junit4-parallel-execution, User Guide>> for - more information. +For complete details consult the +https://junit.org/junit5/docs/5.12.0/release-notes/index.html[5.12.0 Release Notes] online. diff --git a/gradle.properties b/gradle.properties index 4f2ab7df9946..948d9bf326cc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,13 +1,13 @@ group = org.junit -version = 5.12.0-SNAPSHOT +version = 5.13.0-SNAPSHOT jupiterGroup = org.junit.jupiter platformGroup = org.junit.platform -platformVersion = 1.12.0-SNAPSHOT +platformVersion = 1.13.0-SNAPSHOT vintageGroup = org.junit.vintage -vintageVersion = 5.12.0-SNAPSHOT +vintageVersion = 5.13.0-SNAPSHOT # We need more metaspace due to apparent memory leak in Asciidoctor/JRuby # The exports are needed due to https://github.com/diffplug/spotless/issues/834 From 51365f338721a9abce978a49d8452a22389d4912 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Feb 2025 12:18:20 +0000 Subject: [PATCH 003/167] Update graalvm/setup-graalvm digest to dea0e3d (#4314) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8188d59943f1..b70b0a7a6c96 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,7 +25,7 @@ jobs: with: fetch-depth: 1 - name: Install GraalVM - uses: graalvm/setup-graalvm@aafbedb8d382ed0ca6167d3a051415f20c859274 # v1 + uses: graalvm/setup-graalvm@dea0e3da952f9bd6e18653af61b6b4bd9d3dafa5 # v1 with: distribution: graalvm-community version: 'latest' From 779a46fcf75aa85b9256ee5edd5e823455dd1995 Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Sun, 9 Feb 2025 17:30:50 +0100 Subject: [PATCH 004/167] Move string-based argument conversion tests to `platform-test` Prior to this commit, `platform-tests` had no dedicated tests for `ConversionSupport`. Instead, `DefaultArgumentConverterTests` from `jupiter-tests` provided the corresponding coverage. Now, string-based tests are moved to `ConversionSupportTests`, while `DefaultArgumentConverterTests` verify that the delegation to `ConversionSupport` happens correctly. --- .../converter/DefaultArgumentConverter.java | 6 +- .../DefaultArgumentConverterTests.java | 286 ++------------ .../conversion/ConversionSupportTests.java | 348 ++++++++++++++++++ 3 files changed, 388 insertions(+), 252 deletions(-) create mode 100644 platform-tests/src/test/java/org/junit/platform/commons/support/conversion/ConversionSupportTests.java diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java index df77d1f759ac..1de98dcd494d 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java @@ -78,7 +78,7 @@ public final Object convert(Object source, Class targetType, ParameterContext Class declaringClass = context.getDeclaringExecutable().getDeclaringClass(); ClassLoader classLoader = ClassLoaderUtils.getClassLoader(declaringClass); try { - return ConversionSupport.convert((String) source, targetType, classLoader); + return convert((String) source, targetType, classLoader); } catch (ConversionException ex) { throw new ArgumentConversionException(ex.getMessage(), ex); @@ -90,4 +90,8 @@ public final Object convert(Object source, Class targetType, ParameterContext source.getClass().getTypeName(), targetType.getTypeName())); } + Object convert(String source, Class targetType, ClassLoader classLoader) { + return ConversionSupport.convert(source, targetType, classLoader); + } + } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java index 609f83cd95d3..b7e70fc1141e 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/converter/DefaultArgumentConverterTests.java @@ -12,44 +12,24 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.platform.commons.util.ClassLoaderUtils.getClassLoader; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.io.File; -import java.lang.Thread.State; import java.lang.reflect.Method; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.net.URI; -import java.net.URL; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Duration; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.MonthDay; -import java.time.OffsetDateTime; -import java.time.OffsetTime; -import java.time.Period; -import java.time.Year; -import java.time.YearMonth; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.util.Currency; -import java.util.Locale; -import java.util.UUID; -import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.junit.platform.commons.support.ReflectionSupport; +import org.junit.platform.commons.support.conversion.ConversionException; import org.junit.platform.commons.test.TestClassLoader; /** @@ -59,6 +39,8 @@ */ class DefaultArgumentConverterTests { + private final DefaultArgumentConverter underTest = spy(DefaultArgumentConverter.INSTANCE); + @Test void isAwareOfNull() { assertConverts(null, Object.class, null); @@ -92,44 +74,6 @@ void isAwareOfWideningConversions() { assertConverts(1.0f, double.class, 1.0f); } - @Test - void convertsStringsToPrimitiveTypes() { - assertConverts("true", boolean.class, true); - assertConverts("false", boolean.class, false); - assertConverts("o", char.class, 'o'); - assertConverts("1", byte.class, (byte) 1); - assertConverts("1_0", byte.class, (byte) 10); - assertConverts("1", short.class, (short) 1); - assertConverts("1_2", short.class, (short) 12); - assertConverts("42", int.class, 42); - assertConverts("700_050_000", int.class, 700_050_000); - assertConverts("42", long.class, 42L); - assertConverts("4_2", long.class, 42L); - assertConverts("42.23", float.class, 42.23f); - assertConverts("42.2_3", float.class, 42.23f); - assertConverts("42.23", double.class, 42.23); - assertConverts("42.2_3", double.class, 42.23); - } - - @Test - void convertsStringsToPrimitiveWrapperTypes() { - assertConverts("true", Boolean.class, true); - assertConverts("false", Boolean.class, false); - assertConverts("o", Character.class, 'o'); - assertConverts("1", Byte.class, (byte) 1); - assertConverts("1_0", Byte.class, (byte) 10); - assertConverts("1", Short.class, (short) 1); - assertConverts("1_2", Short.class, (short) 12); - assertConverts("42", Integer.class, 42); - assertConverts("700_050_000", Integer.class, 700_050_000); - assertConverts("42", Long.class, 42L); - assertConverts("4_2", Long.class, 42L); - assertConverts("42.23", Float.class, 42.23f); - assertConverts("42.2_3", Float.class, 42.23f); - assertConverts("42.23", Double.class, 42.23); - assertConverts("42.2_3", Double.class, 42.23); - } - @ParameterizedTest(name = "[{index}] {0}") @ValueSource(classes = { char.class, boolean.class, short.class, byte.class, int.class, long.class, float.class, double.class, void.class }) @@ -137,142 +81,44 @@ void throwsExceptionForNullToPrimitiveTypeConversion(Class type) { assertThatExceptionOfType(ArgumentConversionException.class) // .isThrownBy(() -> convert(null, type)) // .withMessage("Cannot convert null to primitive value of type " + type.getCanonicalName()); - } - @ParameterizedTest(name = "[{index}] {0}") - @ValueSource(classes = { Boolean.class, Character.class, Short.class, Byte.class, Integer.class, Long.class, - Float.class, Double.class }) - void throwsExceptionWhenConvertingTheWordNullToPrimitiveWrapperType(Class type) { - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert("null", type)) // - .withMessage("Failed to convert String \"null\" to type " + type.getCanonicalName()); - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert("NULL", type)) // - .withMessage("Failed to convert String \"NULL\" to type " + type.getCanonicalName()); + verify(underTest, never()).convert(any(), any(), any(ClassLoader.class)); } @Test - void throwsExceptionOnInvalidStringForPrimitiveTypes() { + void throwsExceptionForNonStringsConversion() { assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert("ab", char.class)) // - .withMessage("Failed to convert String \"ab\" to type char") // - .havingCause() // - .havingCause() // - .withMessage("String must have length of 1: ab"); - - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert("tru", boolean.class)) // - .withMessage("Failed to convert String \"tru\" to type boolean") // - .havingCause() // - .havingCause() // - .withMessage("String must be 'true' or 'false' (ignoring case): tru"); - - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert("null", boolean.class)) // - .withMessage("Failed to convert String \"null\" to type boolean") // - .havingCause() // - .havingCause() // - .withMessage("String must be 'true' or 'false' (ignoring case): null"); - - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert("NULL", boolean.class)) // - .withMessage("Failed to convert String \"NULL\" to type boolean") // - .havingCause() // - .havingCause() // - .withMessage("String must be 'true' or 'false' (ignoring case): NULL"); - } - - @Test - void throwsExceptionWhenImplicitConverstionIsUnsupported() { - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert("foo", Enigma.class)) // - .withMessage("No built-in converter for source type java.lang.String and target type %s", + .isThrownBy(() -> convert(new Enigma(), String.class)) // + .withMessage("No built-in converter for source type %s and target type java.lang.String", Enigma.class.getName()); - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert(new Enigma(), int[].class)) // - .withMessage("No built-in converter for source type %s and target type int[]", Enigma.class.getName()); - - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert(new long[] {}, int[].class)) // - .withMessage("No built-in converter for source type long[] and target type int[]"); - - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert(new String[] {}, boolean.class)) // - .withMessage("No built-in converter for source type java.lang.String[] and target type boolean"); - - assertThatExceptionOfType(ArgumentConversionException.class) // - .isThrownBy(() -> convert(Class.class, int[].class)) // - .withMessage("No built-in converter for source type java.lang.Class and target type int[]"); - } - - /** - * @since 5.4 - */ - @Test - @SuppressWarnings("OctalInteger") // We test parsing octal integers here as well as hex. - void convertsEncodedStringsToIntegralTypes() { - assertConverts("0x1f", byte.class, (byte) 0x1F); - assertConverts("-0x1F", byte.class, (byte) -0x1F); - assertConverts("010", byte.class, (byte) 010); - - assertConverts("0x1f00", short.class, (short) 0x1F00); - assertConverts("-0x1F00", short.class, (short) -0x1F00); - assertConverts("01000", short.class, (short) 01000); - - assertConverts("0x1f000000", int.class, 0x1F000000); - assertConverts("-0x1F000000", int.class, -0x1F000000); - assertConverts("010000000", int.class, 010000000); - - assertConverts("0x1f000000000", long.class, 0x1F000000000L); - assertConverts("-0x1F000000000", long.class, -0x1F000000000L); - assertConverts("0100000000000", long.class, 0100000000000L); + verify(underTest, never()).convert(any(), any(), any(ClassLoader.class)); } @Test - void convertsStringsToEnumConstants() { - assertConverts("DAYS", TimeUnit.class, TimeUnit.DAYS); - } - - // --- java.io and java.nio ------------------------------------------------ + void delegatesStringsConversion() { + doReturn(null).when(underTest).convert(any(), any(), any(ClassLoader.class)); - @Test - void convertsStringToCharset() { - assertConverts("ISO-8859-1", Charset.class, StandardCharsets.ISO_8859_1); - assertConverts("UTF-8", Charset.class, StandardCharsets.UTF_8); - } + convert("value", int.class); - @Test - void convertsStringToFile() { - assertConverts("file", File.class, new File("file")); - assertConverts("/file", File.class, new File("/file")); - assertConverts("/some/file", File.class, new File("/some/file")); + verify(underTest).convert("value", int.class, getClassLoader(DefaultArgumentConverterTests.class)); } @Test - void convertsStringToPath() { - assertConverts("path", Path.class, Paths.get("path")); - assertConverts("/path", Path.class, Paths.get("/path")); - assertConverts("/some/path", Path.class, Paths.get("/some/path")); - } + void throwsExceptionForDelegatedConversionFailure() { + ConversionException exception = new ConversionException("fail"); + doThrow(exception).when(underTest).convert(any(), any(), any(ClassLoader.class)); - // --- java.lang ----------------------------------------------------------- + assertThatExceptionOfType(ArgumentConversionException.class) // + .isThrownBy(() -> convert("value", int.class)) // + .withCause(exception) // + .withMessage(exception.getMessage()); - @Test - void convertsStringToClass() { - assertConverts("java.lang.Integer", Class.class, Integer.class); - assertConverts("java.lang.Void", Class.class, Void.class); - assertConverts("java.lang.Thread$State", Class.class, State.class); - assertConverts("byte", Class.class, byte.class); - assertConverts("void", Class.class, void.class); - assertConverts("char[]", Class.class, char[].class); - assertConverts("java.lang.Long[][]", Class.class, Long[][].class); - assertConverts("[[[I", Class.class, int[][][].class); - assertConverts("[[Ljava.lang.String;", Class.class, String[][].class); + verify(underTest).convert("value", int.class, getClassLoader(DefaultArgumentConverterTests.class)); } @Test - void convertsStringToClassWithCustomTypeFromDifferentClassLoader() throws Exception { + void delegatesStringToClassWithCustomTypeFromDifferentClassLoaderConversion() throws Exception { String customTypeName = Enigma.class.getName(); try (var testClassLoader = TestClassLoader.forClasses(Enigma.class)) { var customType = testClassLoader.loadClass(customTypeName); @@ -281,79 +127,15 @@ void convertsStringToClassWithCustomTypeFromDifferentClassLoader() throws Except var declaringExecutable = ReflectionSupport.findMethod(customType, "foo").get(); assertThat(declaringExecutable.getDeclaringClass().getClassLoader()).isSameAs(testClassLoader); + doReturn(customType).when(underTest).convert(any(), any(), any(ClassLoader.class)); + var clazz = (Class) convert(customTypeName, Class.class, parameterContext(declaringExecutable)); assertThat(clazz).isNotEqualTo(Enigma.class); assertThat(clazz).isEqualTo(customType); assertThat(clazz.getClassLoader()).isSameAs(testClassLoader); - } - } - - // --- java.math ----------------------------------------------------------- - - @Test - void convertsStringToBigDecimal() { - assertConverts("123.456e789", BigDecimal.class, new BigDecimal("123.456e789")); - } - - @Test - void convertsStringToBigInteger() { - assertConverts("1234567890123456789", BigInteger.class, new BigInteger("1234567890123456789")); - } - - // --- java.net ------------------------------------------------------------ - - @Test - void convertsStringToURI() { - assertConverts("https://docs.oracle.com/en/java/javase/12/", URI.class, - URI.create("https://docs.oracle.com/en/java/javase/12/")); - } - @Test - void convertsStringToURL() throws Exception { - assertConverts("https://junit.org/junit5", URL.class, URI.create("https://junit.org/junit5").toURL()); - } - - // --- java.time ----------------------------------------------------------- - - @Test - void convertsStringsToJavaTimeInstances() { - assertConverts("PT1234.5678S", Duration.class, Duration.ofSeconds(1234, 567800000)); - assertConverts("1970-01-01T00:00:00Z", Instant.class, Instant.ofEpochMilli(0)); - assertConverts("2017-03-14", LocalDate.class, LocalDate.of(2017, 3, 14)); - assertConverts("2017-03-14T12:34:56.789", LocalDateTime.class, - LocalDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000)); - assertConverts("12:34:56.789", LocalTime.class, LocalTime.of(12, 34, 56, 789_000_000)); - assertConverts("--03-14", MonthDay.class, MonthDay.of(3, 14)); - assertConverts("2017-03-14T12:34:56.789Z", OffsetDateTime.class, - OffsetDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)); - assertConverts("12:34:56.789Z", OffsetTime.class, OffsetTime.of(12, 34, 56, 789_000_000, ZoneOffset.UTC)); - assertConverts("P2M6D", Period.class, Period.of(0, 2, 6)); - assertConverts("2017", Year.class, Year.of(2017)); - assertConverts("2017-03", YearMonth.class, YearMonth.of(2017, 3)); - assertConverts("2017-03-14T12:34:56.789Z", ZonedDateTime.class, - ZonedDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)); - assertConverts("Europe/Berlin", ZoneId.class, ZoneId.of("Europe/Berlin")); - assertConverts("+02:30", ZoneOffset.class, ZoneOffset.ofHoursMinutes(2, 30)); - } - - // --- java.util ----------------------------------------------------------- - - @Test - void convertsStringToCurrency() { - assertConverts("JPY", Currency.class, Currency.getInstance("JPY")); - } - - @Test - @SuppressWarnings("deprecation") - void convertsStringToLocale() { - assertConverts("en", Locale.class, Locale.ENGLISH); - assertConverts("en_us", Locale.class, new Locale(Locale.US.toString())); - } - - @Test - void convertsStringToUUID() { - var uuid = "d043e930-7b3b-48e3-bdbe-5a3ccfb833db"; - assertConverts(uuid, UUID.class, UUID.fromString(uuid)); + verify(underTest).convert(customTypeName, Class.class, testClassLoader); + } } // ------------------------------------------------------------------------- @@ -364,6 +146,8 @@ private void assertConverts(Object input, Class targetClass, Object expectedO assertThat(result) // .describedAs(input + " --(" + targetClass.getName() + ")--> " + expectedOutput) // .isEqualTo(expectedOutput); + + verify(underTest, never()).convert(any(), any(), any(ClassLoader.class)); } private Object convert(Object input, Class targetClass) { @@ -371,7 +155,7 @@ private Object convert(Object input, Class targetClass) { } private Object convert(Object input, Class targetClass, ParameterContext parameterContext) { - return DefaultArgumentConverter.INSTANCE.convert(input, targetClass, parameterContext); + return underTest.convert(input, targetClass, parameterContext); } private static ParameterContext parameterContext() { diff --git a/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/ConversionSupportTests.java b/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/ConversionSupportTests.java new file mode 100644 index 000000000000..3a57fbe3b5a3 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/commons/support/conversion/ConversionSupportTests.java @@ -0,0 +1,348 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.commons.support.conversion; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import java.io.File; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URI; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Currency; +import java.util.Locale; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.junit.platform.commons.support.ReflectionSupport; +import org.junit.platform.commons.test.TestClassLoader; +import org.junit.platform.commons.util.ClassLoaderUtils; + +/** + * Unit tests for {@link ConversionSupport}. + * + * @since 5.12 + */ +class ConversionSupportTests { + + @Test + void isAwareOfNull() { + assertConverts(null, Object.class, null); + assertConverts(null, String.class, null); + assertConverts(null, Boolean.class, null); + } + + @Test + void convertsStringsToPrimitiveTypes() { + assertConverts("true", boolean.class, true); + assertConverts("false", boolean.class, false); + assertConverts("o", char.class, 'o'); + assertConverts("1", byte.class, (byte) 1); + assertConverts("1_0", byte.class, (byte) 10); + assertConverts("1", short.class, (short) 1); + assertConverts("1_2", short.class, (short) 12); + assertConverts("42", int.class, 42); + assertConverts("700_050_000", int.class, 700_050_000); + assertConverts("42", long.class, 42L); + assertConverts("4_2", long.class, 42L); + assertConverts("42.23", float.class, 42.23f); + assertConverts("42.2_3", float.class, 42.23f); + assertConverts("42.23", double.class, 42.23); + assertConverts("42.2_3", double.class, 42.23); + } + + @Test + void convertsStringsToPrimitiveWrapperTypes() { + assertConverts("true", Boolean.class, true); + assertConverts("false", Boolean.class, false); + assertConverts("o", Character.class, 'o'); + assertConverts("1", Byte.class, (byte) 1); + assertConverts("1_0", Byte.class, (byte) 10); + assertConverts("1", Short.class, (short) 1); + assertConverts("1_2", Short.class, (short) 12); + assertConverts("42", Integer.class, 42); + assertConverts("700_050_000", Integer.class, 700_050_000); + assertConverts("42", Long.class, 42L); + assertConverts("4_2", Long.class, 42L); + assertConverts("42.23", Float.class, 42.23f); + assertConverts("42.2_3", Float.class, 42.23f); + assertConverts("42.23", Double.class, 42.23); + assertConverts("42.2_3", Double.class, 42.23); + } + + @ParameterizedTest(name = "[{index}] {0}") + @ValueSource(classes = { char.class, boolean.class, short.class, byte.class, int.class, long.class, float.class, + double.class, void.class }) + void throwsExceptionForNullToPrimitiveTypeConversion(Class type) { + assertThatExceptionOfType(ConversionException.class) // + .isThrownBy(() -> convert(null, type)) // + .withMessage("Cannot convert null to primitive value of type " + type.getCanonicalName()); + } + + @ParameterizedTest(name = "[{index}] {0}") + @ValueSource(classes = { Boolean.class, Character.class, Short.class, Byte.class, Integer.class, Long.class, + Float.class, Double.class }) + void throwsExceptionWhenConvertingTheWordNullToPrimitiveWrapperType(Class type) { + assertThatExceptionOfType(ConversionException.class) // + .isThrownBy(() -> convert("null", type)) // + .withMessage("Failed to convert String \"null\" to type " + type.getCanonicalName()); + assertThatExceptionOfType(ConversionException.class) // + .isThrownBy(() -> convert("NULL", type)) // + .withMessage("Failed to convert String \"NULL\" to type " + type.getCanonicalName()); + } + + @Test + void throwsExceptionOnInvalidStringForPrimitiveTypes() { + assertThatExceptionOfType(ConversionException.class) // + .isThrownBy(() -> convert("ab", char.class)) // + .withMessage("Failed to convert String \"ab\" to type char") // + .havingCause() // + .withMessage("String must have length of 1: ab"); + + assertThatExceptionOfType(ConversionException.class) // + .isThrownBy(() -> convert("tru", boolean.class)) // + .withMessage("Failed to convert String \"tru\" to type boolean") // + .havingCause() // + .withMessage("String must be 'true' or 'false' (ignoring case): tru"); + + assertThatExceptionOfType(ConversionException.class) // + .isThrownBy(() -> convert("null", boolean.class)) // + .withMessage("Failed to convert String \"null\" to type boolean") // + .havingCause() // + .withMessage("String must be 'true' or 'false' (ignoring case): null"); + + assertThatExceptionOfType(ConversionException.class) // + .isThrownBy(() -> convert("NULL", boolean.class)) // + .withMessage("Failed to convert String \"NULL\" to type boolean") // + .havingCause() // + .withMessage("String must be 'true' or 'false' (ignoring case): NULL"); + } + + @Test + void throwsExceptionWhenImplicitConversionIsUnsupported() { + assertThatExceptionOfType(ConversionException.class) // + .isThrownBy(() -> convert("foo", Enigma.class)) // + .withMessage("No built-in converter for source type java.lang.String and target type %s", + Enigma.class.getName()); + } + + /** + * @since 5.4 + */ + @Test + @SuppressWarnings("OctalInteger") // We test parsing octal integers here as well as hex. + void convertsEncodedStringsToIntegralTypes() { + assertConverts("0x1f", byte.class, (byte) 0x1F); + assertConverts("-0x1F", byte.class, (byte) -0x1F); + assertConverts("010", byte.class, (byte) 010); + + assertConverts("0x1f00", short.class, (short) 0x1F00); + assertConverts("-0x1F00", short.class, (short) -0x1F00); + assertConverts("01000", short.class, (short) 01000); + + assertConverts("0x1f000000", int.class, 0x1F000000); + assertConverts("-0x1F000000", int.class, -0x1F000000); + assertConverts("010000000", int.class, 010000000); + + assertConverts("0x1f000000000", long.class, 0x1F000000000L); + assertConverts("-0x1F000000000", long.class, -0x1F000000000L); + assertConverts("0100000000000", long.class, 0100000000000L); + } + + @Test + void convertsStringsToEnumConstants() { + assertConverts("DAYS", TimeUnit.class, TimeUnit.DAYS); + } + + // --- java.io and java.nio ------------------------------------------------ + + @Test + void convertsStringToCharset() { + assertConverts("ISO-8859-1", Charset.class, StandardCharsets.ISO_8859_1); + assertConverts("UTF-8", Charset.class, StandardCharsets.UTF_8); + } + + @Test + void convertsStringToFile() { + assertConverts("file", File.class, new File("file")); + assertConverts("/file", File.class, new File("/file")); + assertConverts("/some/file", File.class, new File("/some/file")); + } + + @Test + void convertsStringToPath() { + assertConverts("path", Path.class, Paths.get("path")); + assertConverts("/path", Path.class, Paths.get("/path")); + assertConverts("/some/path", Path.class, Paths.get("/some/path")); + } + + // --- java.lang ----------------------------------------------------------- + + @Test + void convertsStringToClass() { + assertConverts("java.lang.Integer", Class.class, Integer.class); + assertConverts("java.lang.Void", Class.class, Void.class); + assertConverts("java.lang.Thread$State", Class.class, Thread.State.class); + assertConverts("byte", Class.class, byte.class); + assertConverts("void", Class.class, void.class); + assertConverts("char[]", Class.class, char[].class); + assertConverts("java.lang.Long[][]", Class.class, Long[][].class); + assertConverts("[[[I", Class.class, int[][][].class); + assertConverts("[[Ljava.lang.String;", Class.class, String[][].class); + } + + @Test + void convertsStringToClassWithCustomTypeFromDifferentClassLoader() throws Exception { + String customTypeName = Enigma.class.getName(); + try (var testClassLoader = TestClassLoader.forClasses(Enigma.class)) { + var customType = testClassLoader.loadClass(customTypeName); + assertThat(customType.getClassLoader()).isSameAs(testClassLoader); + + var declaringExecutable = ReflectionSupport.findMethod(customType, "foo").get(); + assertThat(declaringExecutable.getDeclaringClass().getClassLoader()).isSameAs(testClassLoader); + + var clazz = (Class) convert(customTypeName, Class.class, classLoader(declaringExecutable)); + assertThat(clazz).isNotEqualTo(Enigma.class); + assertThat(clazz).isEqualTo(customType); + assertThat(clazz.getClassLoader()).isSameAs(testClassLoader); + } + } + + // --- java.math ----------------------------------------------------------- + + @Test + void convertsStringToBigDecimal() { + assertConverts("123.456e789", BigDecimal.class, new BigDecimal("123.456e789")); + } + + @Test + void convertsStringToBigInteger() { + assertConverts("1234567890123456789", BigInteger.class, new BigInteger("1234567890123456789")); + } + + // --- java.net ------------------------------------------------------------ + + @Test + void convertsStringToURI() { + assertConverts("https://docs.oracle.com/en/java/javase/12/", URI.class, + URI.create("https://docs.oracle.com/en/java/javase/12/")); + } + + @Test + void convertsStringToURL() throws Exception { + assertConverts("https://junit.org/junit5", URL.class, URI.create("https://junit.org/junit5").toURL()); + } + + // --- java.time ----------------------------------------------------------- + + @Test + void convertsStringsToJavaTimeInstances() { + assertConverts("PT1234.5678S", Duration.class, Duration.ofSeconds(1234, 567800000)); + assertConverts("1970-01-01T00:00:00Z", Instant.class, Instant.ofEpochMilli(0)); + assertConverts("2017-03-14", LocalDate.class, LocalDate.of(2017, 3, 14)); + assertConverts("2017-03-14T12:34:56.789", LocalDateTime.class, + LocalDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000)); + assertConverts("12:34:56.789", LocalTime.class, LocalTime.of(12, 34, 56, 789_000_000)); + assertConverts("--03-14", MonthDay.class, MonthDay.of(3, 14)); + assertConverts("2017-03-14T12:34:56.789Z", OffsetDateTime.class, + OffsetDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)); + assertConverts("12:34:56.789Z", OffsetTime.class, OffsetTime.of(12, 34, 56, 789_000_000, ZoneOffset.UTC)); + assertConverts("P2M6D", Period.class, Period.of(0, 2, 6)); + assertConverts("2017", Year.class, Year.of(2017)); + assertConverts("2017-03", YearMonth.class, YearMonth.of(2017, 3)); + assertConverts("2017-03-14T12:34:56.789Z", ZonedDateTime.class, + ZonedDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)); + assertConverts("Europe/Berlin", ZoneId.class, ZoneId.of("Europe/Berlin")); + assertConverts("+02:30", ZoneOffset.class, ZoneOffset.ofHoursMinutes(2, 30)); + } + + // --- java.util ----------------------------------------------------------- + + @Test + void convertsStringToCurrency() { + assertConverts("JPY", Currency.class, Currency.getInstance("JPY")); + } + + @Test + @SuppressWarnings("deprecation") + void convertsStringToLocale() { + assertConverts("en", Locale.class, Locale.ENGLISH); + assertConverts("en_us", Locale.class, new Locale(Locale.US.toString())); + } + + @Test + void convertsStringToUUID() { + var uuid = "d043e930-7b3b-48e3-bdbe-5a3ccfb833db"; + assertConverts(uuid, UUID.class, UUID.fromString(uuid)); + } + + // ------------------------------------------------------------------------- + + private void assertConverts(String input, Class targetClass, Object expectedOutput) { + var result = convert(input, targetClass); + + assertThat(result) // + .describedAs(input + " --(" + targetClass.getName() + ")--> " + expectedOutput) // + .isEqualTo(expectedOutput); + } + + private Object convert(String input, Class targetClass) { + return convert(input, targetClass, classLoader()); + } + + private Object convert(String input, Class targetClass, ClassLoader classLoader) { + return ConversionSupport.convert(input, targetClass, classLoader); + } + + private static ClassLoader classLoader() { + Method declaringExecutable = ReflectionSupport.findMethod(ConversionSupportTests.class, "foo").get(); + return classLoader(declaringExecutable); + } + + private static ClassLoader classLoader(Method declaringExecutable) { + return ClassLoaderUtils.getClassLoader(declaringExecutable.getDeclaringClass()); + } + + @SuppressWarnings("unused") + private static void foo() { + } + + private static class Enigma { + + @SuppressWarnings("unused") + void foo() { + } + } + +} From 5596b9bb64d80baf1f9a1ffe56f88ffcd99186fb Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 12 Feb 2025 16:05:56 +0100 Subject: [PATCH 005/167] Reintroduce snapshot dependency on open-test-reporting --- .github/actions/run-gradle/action.yml | 1 + .github/scripts/checkBuildReproducibility.sh | 1 + gradle/libs.versions.toml | 2 +- settings.gradle.kts | 6 ++++++ 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/actions/run-gradle/action.yml b/.github/actions/run-gradle/action.yml index 86436e44afb3..5159a188fb5c 100644 --- a/.github/actions/run-gradle/action.yml +++ b/.github/actions/run-gradle/action.yml @@ -29,5 +29,6 @@ runs: -Pjunit.develocity.predictiveTestSelection.enabled=true \ -Pjunit.develocity.predictiveTestSelection.selectRemainingTests=${{ github.event_name != 'pull_request' }} \ "-Dscan.value.GitHub job=${{ github.job }}" \ + --refresh-dependencies \ javaToolchains \ ${{ inputs.arguments }} diff --git a/.github/scripts/checkBuildReproducibility.sh b/.github/scripts/checkBuildReproducibility.sh index 60f2bd165022..9c6bd410b383 100755 --- a/.github/scripts/checkBuildReproducibility.sh +++ b/.github/scripts/checkBuildReproducibility.sh @@ -13,6 +13,7 @@ function calculate_checksums() { --no-build-cache \ -Porg.gradle.java.installations.auto-download=false \ -Dscan.tag.Reproducibility \ + --refresh-dependencies \ clean \ assemble diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e8ee505d0b6d..e359324089fa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ log4j = "2.24.3" logback = "1.5.16" mockito = "5.15.2" opentest4j = "1.3.0" -openTestReporting = "0.2.0-M2" +openTestReporting = "0.2.0-SNAPSHOT" snapshotTests = "1.11.0" surefire = "3.5.2" xmlunit = "2.10.0" diff --git a/settings.gradle.kts b/settings.gradle.kts index ba5ae9fd677f..6af29a55f440 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,6 +15,12 @@ plugins { dependencyResolutionManagement { repositories { mavenCentral() + // TODO Remove --refresh-dependencies from CI builds when no longer consuming snapshots + maven(url = "https://oss.sonatype.org/content/repositories/snapshots") { + mavenContent { + snapshotsOnly() + } + } } } From 16674fb02199332e36a3fd3371424cadd1a2ad72 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 12 Feb 2025 19:09:30 +0100 Subject: [PATCH 006/167] Update open-test-reporting to 0.2.0-M3 Fixes #4313. --- .github/actions/run-gradle/action.yml | 1 - .github/scripts/checkBuildReproducibility.sh | 1 - documentation/documentation.gradle.kts | 2 +- gradle/libs.versions.toml | 2 +- settings.gradle.kts | 6 ------ 5 files changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/actions/run-gradle/action.yml b/.github/actions/run-gradle/action.yml index 5159a188fb5c..86436e44afb3 100644 --- a/.github/actions/run-gradle/action.yml +++ b/.github/actions/run-gradle/action.yml @@ -29,6 +29,5 @@ runs: -Pjunit.develocity.predictiveTestSelection.enabled=true \ -Pjunit.develocity.predictiveTestSelection.selectRemainingTests=${{ github.event_name != 'pull_request' }} \ "-Dscan.value.GitHub job=${{ github.job }}" \ - --refresh-dependencies \ javaToolchains \ ${{ inputs.arguments }} diff --git a/.github/scripts/checkBuildReproducibility.sh b/.github/scripts/checkBuildReproducibility.sh index 9c6bd410b383..60f2bd165022 100755 --- a/.github/scripts/checkBuildReproducibility.sh +++ b/.github/scripts/checkBuildReproducibility.sh @@ -13,7 +13,6 @@ function calculate_checksums() { --no-build-cache \ -Porg.gradle.java.installations.auto-download=false \ -Dscan.tag.Reproducibility \ - --refresh-dependencies \ clean \ assemble diff --git a/documentation/documentation.gradle.kts b/documentation/documentation.gradle.kts index 02c96f3035fc..45095cdef845 100644 --- a/documentation/documentation.gradle.kts +++ b/documentation/documentation.gradle.kts @@ -453,7 +453,7 @@ tasks { project.sourceSets.named { it.startsWith("main") }.map { it.allJava.srcDirs } ).asPath })) - addStringOption("-add-modules", "info.picocli") + addStringOption("-add-modules", "info.picocli,org.opentest4j.reporting.events") addOption(ModuleSpecificJavadocFileOption("-add-reads", mapOf( "org.junit.platform.console" to "info.picocli", "org.junit.platform.reporting" to "org.opentest4j.reporting.events", diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e359324089fa..1aac1c6a62dc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ log4j = "2.24.3" logback = "1.5.16" mockito = "5.15.2" opentest4j = "1.3.0" -openTestReporting = "0.2.0-SNAPSHOT" +openTestReporting = "0.2.0-M3" snapshotTests = "1.11.0" surefire = "3.5.2" xmlunit = "2.10.0" diff --git a/settings.gradle.kts b/settings.gradle.kts index 6af29a55f440..ba5ae9fd677f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,12 +15,6 @@ plugins { dependencyResolutionManagement { repositories { mavenCentral() - // TODO Remove --refresh-dependencies from CI builds when no longer consuming snapshots - maven(url = "https://oss.sonatype.org/content/repositories/snapshots") { - mavenContent { - snapshotsOnly() - } - } } } From 00130759a43c77c69c9654c9656ac9b242760fe1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 02:46:45 +0000 Subject: [PATCH 007/167] Update graalvm/setup-graalvm digest to b0cb26a (#4316) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b70b0a7a6c96..03341e634fc5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,7 +25,7 @@ jobs: with: fetch-depth: 1 - name: Install GraalVM - uses: graalvm/setup-graalvm@dea0e3da952f9bd6e18653af61b6b4bd9d3dafa5 # v1 + uses: graalvm/setup-graalvm@b0cb26a8da53cb3e97cdc0c827d8e3071240e730 # v1 with: distribution: graalvm-community version: 'latest' From c9d8cb1a6bf6174626b28977b84aa389fd9a1d5b Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Fri, 14 Feb 2025 15:47:21 +0100 Subject: [PATCH 008/167] Fix version references to be 1.x in Platform and 5.x in Jupiter --- .../junit/jupiter/api/EqualsAndHashCodeAssertions.java | 2 +- .../jupiter/engine/extension/DefaultTestReporter.java | 2 +- .../platform/commons/util/ServiceLoaderUtils.java | 4 ++-- .../platform/commons/util/ServiceLoaderUtils.java | 4 ++-- .../java/org/junit/platform/engine/TestDescriptor.java | 2 +- .../junit/platform/engine/reporting/ReportEntry.java | 2 +- .../org/junit/platform/launcher/TestIdentifier.java | 10 +++++----- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/junit-jupiter-api/src/testFixtures/java/org/junit/jupiter/api/EqualsAndHashCodeAssertions.java b/junit-jupiter-api/src/testFixtures/java/org/junit/jupiter/api/EqualsAndHashCodeAssertions.java index ed33d21ebf3d..7ca4175b0f58 100644 --- a/junit-jupiter-api/src/testFixtures/java/org/junit/jupiter/api/EqualsAndHashCodeAssertions.java +++ b/junit-jupiter-api/src/testFixtures/java/org/junit/jupiter/api/EqualsAndHashCodeAssertions.java @@ -16,7 +16,7 @@ * Assertions for unit tests that wish to test * {@link Object#equals(Object)} and {@link Object#hashCode()}. * - * @since 1.3 + * @since 5.3 */ public class EqualsAndHashCodeAssertions { diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultTestReporter.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultTestReporter.java index 966bb7ef6744..eb1564cda500 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultTestReporter.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultTestReporter.java @@ -20,7 +20,7 @@ import org.junit.platform.commons.util.Preconditions; /** - * @since 1.12 + * @since 5.12 */ class DefaultTestReporter implements TestReporter { diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ServiceLoaderUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ServiceLoaderUtils.java index ce006d770099..0a060bee0999 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ServiceLoaderUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ServiceLoaderUtils.java @@ -26,9 +26,9 @@ * itself. Any usage by external parties is not supported. * Use at your own risk! * - * @since 5.11 + * @since 1.11 */ -@API(status = API.Status.INTERNAL, since = "5.11") +@API(status = API.Status.INTERNAL, since = "1.11") public class ServiceLoaderUtils { private ServiceLoaderUtils() { diff --git a/junit-platform-commons/src/main/java9/org/junit/platform/commons/util/ServiceLoaderUtils.java b/junit-platform-commons/src/main/java9/org/junit/platform/commons/util/ServiceLoaderUtils.java index 878c62a2c7e7..db209116b615 100644 --- a/junit-platform-commons/src/main/java9/org/junit/platform/commons/util/ServiceLoaderUtils.java +++ b/junit-platform-commons/src/main/java9/org/junit/platform/commons/util/ServiceLoaderUtils.java @@ -26,9 +26,9 @@ * itself. Any usage by external parties is not supported. * Use at your own risk! * - * @since 5.11 + * @since 1.11 */ -@API(status = Status.INTERNAL, since = "5.11") +@API(status = Status.INTERNAL, since = "1.11") public class ServiceLoaderUtils { private ServiceLoaderUtils() { diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/TestDescriptor.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/TestDescriptor.java index 6dfd169ab9f4..ba423fd07452 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/TestDescriptor.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/TestDescriptor.java @@ -188,7 +188,7 @@ default Set getDescendants() { * @param orderer a unary operator to order the children of this test * descriptor. */ - @API(since = "5.12", status = EXPERIMENTAL) + @API(since = "1.12", status = EXPERIMENTAL) default void orderChildren(UnaryOperator> orderer) { Preconditions.notNull(orderer, "orderer must not be null"); Set originalChildren = getChildren(); diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/reporting/ReportEntry.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/reporting/ReportEntry.java index bdea0b7a9568..0af74b590121 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/reporting/ReportEntry.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/reporting/ReportEntry.java @@ -39,7 +39,7 @@ public final class ReportEntry { /** * @deprecated Use {@link #from(String, String)} or {@link #from(Map)} */ - @API(status = DEPRECATED, since = "5.8") + @API(status = DEPRECATED, since = "1.8") @Deprecated public ReportEntry() { } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestIdentifier.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestIdentifier.java index f397eb0b9bc0..80e7cd3b76ec 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestIdentifier.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestIdentifier.java @@ -122,9 +122,9 @@ public String getUniqueId() { * behind the scenes. * * @return the unique ID for this identifier; never {@code null} - * @since 5.8 + * @since 1.8 */ - @API(status = STABLE, since = "5.8") + @API(status = STABLE, since = "1.8") public UniqueId getUniqueIdObject() { return this.uniqueId; } @@ -150,9 +150,9 @@ public Optional getParentId() { * * @return a container for the unique ID for this identifier's parent; * never {@code null} though potentially empty - * @since 5.8 + * @since 1.8 */ - @API(status = STABLE, since = "5.8") + @API(status = STABLE, since = "1.8") public Optional getParentIdObject() { return Optional.ofNullable(this.parentId); } @@ -291,7 +291,7 @@ private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOEx /** * Represents the serialized output of {@code TestIdentifier}. The fields on this - * class match the fields that {@code TestIdentifier} had prior to 5.8. + * class match the fields that {@code TestIdentifier} had prior to 1.8. */ private static class SerializedForm implements Serializable { From 484320ef34d12eb93f6b976466e1a93f776f1731 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Mon, 17 Feb 2025 19:05:29 +0100 Subject: [PATCH 009/167] Update open-test-reporting to 0.2.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1aac1c6a62dc..050a6fa520e8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ log4j = "2.24.3" logback = "1.5.16" mockito = "5.15.2" opentest4j = "1.3.0" -openTestReporting = "0.2.0-M3" +openTestReporting = "0.2.0" snapshotTests = "1.11.0" surefire = "3.5.2" xmlunit = "2.10.0" From 67071ee8759353d3e816299aa4816cefb34ca0fd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Feb 2025 07:46:18 +0000 Subject: [PATCH 010/167] Update plugin develocity to v3.19.2 (#4324) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 050a6fa520e8..a842479988ce 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -94,7 +94,7 @@ asciidoctorPdf = { id = "org.asciidoctor.jvm.pdf", version.ref = "asciidoctor-pl bnd = { id = "biz.aQute.bnd", version.ref = "bnd" } buildParameters = { id = "org.gradlex.build-parameters", version = "1.4.4" } commonCustomUserData = { id = "com.gradle.common-custom-user-data-gradle-plugin", version = "2.1" } -develocity = { id = "com.gradle.develocity", version = "3.19.1" } +develocity = { id = "com.gradle.develocity", version = "3.19.2" } foojayResolver = { id = "org.gradle.toolchains.foojay-resolver", version = "0.9.0" } gitPublish = { id = "org.ajoberstar.git-publish", version = "5.1.0" } jmh = { id = "me.champeau.jmh", version = "0.7.3" } From 202d939953a2fa61f54a1b0c70e74e122740a427 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 19 Feb 2025 08:55:52 +0100 Subject: [PATCH 011/167] Introduce extension API for container templates (#4315) Analogous to `@TestTemplate` on the method level, this commit introduces a class-level `@ContainerTemplate` annotation with an accompanying `ContainerTemplateInvocationContextProvider` extension API. Resolves #871. --- .../src/docs/asciidoc/link-attributes.adoc | 3 + .../release-notes-5.13.0-M1.adoc | 6 +- .../docs/asciidoc/user-guide/extensions.adoc | 40 + .../asciidoc/user-guide/writing-tests.adoc | 17 +- .../java/example/ContainerTemplateDemo.java | 99 ++ .../java/org/junit/jupiter/api/AfterAll.java | 8 +- .../java/org/junit/jupiter/api/BeforeAll.java | 8 +- .../junit/jupiter/api/ContainerTemplate.java | 65 + .../java/org/junit/jupiter/api/Nested.java | 4 + .../org/junit/jupiter/api/TestInstance.java | 7 +- .../org/junit/jupiter/api/TestTemplate.java | 1 + .../ContainerTemplateInvocationContext.java | 70 + ...inerTemplateInvocationContextProvider.java | 121 ++ .../jupiter/api/parallel/ResourceLock.java | 6 + .../descriptor/ClassBasedTestDescriptor.java | 39 +- .../descriptor/ClassExtensionContext.java | 14 - .../descriptor/ClassTestDescriptor.java | 20 + ...nerTemplateInvocationExtensionContext.java | 77 + ...ainerTemplateInvocationTestDescriptor.java | 141 ++ .../ContainerTemplateTestDescriptor.java | 263 ++++ .../DynamicContainerTestDescriptor.java | 7 + .../descriptor/DynamicDescendantFilter.java | 24 + .../descriptor/DynamicNodeTestDescriptor.java | 5 +- .../descriptor/DynamicTestTestDescriptor.java | 8 + .../descriptor/JupiterTestDescriptor.java | 26 +- .../descriptor/MethodBasedTestDescriptor.java | 6 +- .../descriptor/NestedClassTestDescriptor.java | 24 +- .../engine/descriptor/TemplateExecutor.java | 100 ++ .../engine/descriptor/TestClassAware.java | 29 + .../descriptor/TestFactoryTestDescriptor.java | 19 +- .../descriptor/TestMethodTestDescriptor.java | 16 + .../TestTemplateInvocationTestDescriptor.java | 11 + .../TestTemplateTestDescriptor.java | 113 +- .../descriptor/UniqueIdPrefixTransformer.java | 46 + .../discovery/ClassSelectorResolver.java | 204 ++- .../discovery/DiscoverySelectorResolver.java | 27 +- .../discovery/MethodSelectorResolver.java | 14 +- .../engine/execution/ConditionEvaluator.java | 2 +- .../discovery/JupiterUniqueIdBuilder.java | 35 +- .../testkit/engine/EventConditions.java | 60 +- .../api/DisplayNameGenerationTests.java | 82 ++ .../api/extension/KitchenSinkExtension.java | 21 +- .../parallel/ResourceLockAnnotationTests.java | 144 ++ .../AbstractJupiterTestEngineTests.java | 19 +- .../ContainerTemplateInvocationTests.java | 1257 +++++++++++++++++ .../engine/TestInstanceLifecycleTests.java | 113 +- .../descriptor/ExtensionContextTests.java | 31 +- .../TestFactoryTestDescriptorTests.java | 25 + .../TestTemplateTestDescriptorTests.java | 69 +- .../DiscoverySelectorResolverTests.java | 58 +- .../engine/extension/OrderedClassTests.java | 111 ++ .../engine/extension/OrderedMethodTests.java | 12 +- .../CalculatorContainerTemplateTests.java | 59 + .../test/resources/junit-platform.properties | 2 + .../support/tests/AntStarterTests.java | 4 +- .../support/tests/GradleStarterTests.java | 68 +- .../support/tests/MavenStarterTests.java | 54 +- .../tests/UnalignedClasspathTests.java | 3 +- .../open-test-report.xml.snapshot | 197 ++- .../open-test-report.xml.snapshot | 199 ++- .../open-test-report.xml.snapshot | 197 ++- 61 files changed, 4228 insertions(+), 282 deletions(-) create mode 100644 documentation/src/test/java/example/ContainerTemplateDemo.java create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/ContainerTemplate.java create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ContainerTemplateInvocationContext.java create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ContainerTemplateInvocationContextProvider.java create mode 100644 junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateInvocationExtensionContext.java create mode 100644 junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateInvocationTestDescriptor.java create mode 100644 junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateTestDescriptor.java create mode 100644 junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TemplateExecutor.java create mode 100644 junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestClassAware.java create mode 100644 junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/UniqueIdPrefixTransformer.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/engine/ContainerTemplateInvocationTests.java create mode 100644 platform-tooling-support-tests/projects/jupiter-starter/src/test/java/com/example/project/CalculatorContainerTemplateTests.java create mode 100644 platform-tooling-support-tests/projects/jupiter-starter/src/test/resources/junit-platform.properties diff --git a/documentation/src/docs/asciidoc/link-attributes.adoc b/documentation/src/docs/asciidoc/link-attributes.adoc index 4010e54b7085..9862695d6d76 100644 --- a/documentation/src/docs/asciidoc/link-attributes.adoc +++ b/documentation/src/docs/asciidoc/link-attributes.adoc @@ -111,6 +111,7 @@ endif::[] :ClassOrderer_OrderAnnotation: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/ClassOrderer.OrderAnnotation.html[ClassOrderer.OrderAnnotation] :ClassOrderer_Random: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/ClassOrderer.Random.html[ClassOrderer.Random] :ClassOrderer: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/ClassOrderer.html[ClassOrderer] +:ContainerTemplate: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/ContainerTemplate.html[@ContainerTemplate] :Disabled: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/Disabled.html[@Disabled] :MethodOrderer_Alphanumeric: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/MethodOrderer.Alphanumeric.html[MethodOrderer.Alphanumeric] :MethodOrderer_DisplayName: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/MethodOrderer.DisplayName.html[MethodOrderer.DisplayName] @@ -142,6 +143,8 @@ endif::[] :BeforeAllCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeAllCallback.html[BeforeAllCallback] :BeforeEachCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeEachCallback.html[BeforeEachCallback] :BeforeTestExecutionCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeTestExecutionCallback.html[BeforeTestExecutionCallback] +:ContainerTemplateInvocationContext: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ContainerTemplateInvocationContext.html[ContainerTemplateInvocationContext] +:ContainerTemplateInvocationContextProvider: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ContainerTemplateInvocationContextProvider.html[ContainerTemplateInvocationContextProvider] :ExecutableInvoker: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ExecutableInvoker.html[ExecutableInvoker] :ExecutionCondition: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ExecutionCondition.html[ExecutionCondition] :ExtendWith: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ExtendWith.html[@ExtendWith] diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc index e7614bc3a786..15c3a64fbb5f 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc @@ -45,7 +45,11 @@ repository on GitHub. [[release-notes-5.13.0-M1-junit-jupiter-new-features-and-improvements]] ==== New Features and Improvements -* ❓ +* Introduce `@ContainerTemplate` and `ContainerTemplateInvocationContextProvider` that + allow declaring a top-level or `@Nested` test class as a template to be invoked multiple + times. This may be used, for example, to inject different parameters to be used by all + tests in the container template class or to set up each invocation of the container + template differently. [[release-notes-5.13.0-M1-junit-vintage]] diff --git a/documentation/src/docs/asciidoc/user-guide/extensions.adoc b/documentation/src/docs/asciidoc/user-guide/extensions.adoc index 11185fe05019..47bbd8947661 100644 --- a/documentation/src/docs/asciidoc/user-guide/extensions.adoc +++ b/documentation/src/docs/asciidoc/user-guide/extensions.adoc @@ -765,6 +765,46 @@ You may override the `getTestInstantiationExtensionContextScope(...)` method to on the test method level. ==== +[[extensions-container-templates]] +=== Providing Invocation Contexts for Container Templates + +A `{ContainerTemplate}` class can only be executed when at least one +`{ContainerTemplateInvocationContextProvider}` is registered. Each such provider is +responsible for providing a `Stream` of `{ContainerTemplateInvocationContext}` instances. +Each context may specify a custom display name and a list of additional extensions that +will only be used for the next invocation of the `{ContainerTemplate}` class. + +The following example shows how to write a container template as well as how to register +and implement a `{ContainerTemplateInvocationContextProvider}`. + +[source,java,indent=0] +.A container template with accompanying extension +---- +include::{testDir}/example/ContainerTemplateDemo.java[tags=user_guide] +---- + +In this example, the container template will be invoked twice, meaning all test methods in +the container template class will be executed twice. The display names of the container +invocations will be `apple` and `banana` as specified by the invocation context. Each +invocation registers a custom `{TestInstancePostProcessor}` which is used to inject a +value into a field. The output when using the `ConsoleLauncher` is as follows. + +.... +└─ ContainerTemplateDemo ✔ + ├─ apple ✔ + │ ├─ notNull() ✔ + │ └─ wellKnown() ✔ + └─ banana ✔ + ├─ notNull() ✔ + └─ wellKnown() ✔ +.... + +The `{ContainerTemplateInvocationContextProvider}` extension API is primarily intended for +implementing different kinds of tests that rely on repetitive invocation of _all_ test +methods in a test class albeit in different contexts — for example, with different +parameters, by preparing the test class instance differently, or multiple times without +modifying the context. + [[extensions-test-templates]] === Providing Invocation Contexts for Test Templates diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index 8442461ae2e1..7ea683a04df6 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -32,7 +32,7 @@ in the `junit-jupiter-api` module. | `@ParameterizedTest` | Denotes that a method is a <>. Such methods are inherited unless they are overridden. | `@RepeatedTest` | Denotes that a method is a test template for a <>. Such methods are inherited unless they are overridden. | `@TestFactory` | Denotes that a method is a test factory for <>. Such methods are inherited unless they are overridden. -| `@TestTemplate` | Denotes that a method is a <> designed to be invoked multiple times depending on the number of invocation contexts returned by the registered <>. Such methods are inherited unless they are overridden. +| `@TestTemplate` | Denotes that a method is a <> designed to be invoked multiple times depending on the number of invocation contexts returned by the registered <>. Such methods are inherited unless they are overridden. | `@TestClassOrder` | Used to configure the <> for `@Nested` test classes in the annotated test class. Such annotations are inherited. | `@TestMethodOrder` | Used to configure the <> for the annotated test class; similar to JUnit 4's `@FixMethodOrder`. Such annotations are inherited. | `@TestInstance` | Used to configure the <> for the annotated test class. Such annotations are inherited. @@ -42,6 +42,7 @@ in the `junit-jupiter-api` module. | `@AfterEach` | Denotes that the annotated method should be executed _after_ *each* `@Test`, `@RepeatedTest`, `@ParameterizedTest`, or `@TestFactory` method in the current class; analogous to JUnit 4's `@After`. Such methods are inherited unless they are overridden. | `@BeforeAll` | Denotes that the annotated method should be executed _before_ *all* `@Test`, `@RepeatedTest`, `@ParameterizedTest`, and `@TestFactory` methods in the current class; analogous to JUnit 4's `@BeforeClass`. Such methods are inherited unless they are overridden and must be `static` unless the "per-class" <> is used. | `@AfterAll` | Denotes that the annotated method should be executed _after_ *all* `@Test`, `@RepeatedTest`, `@ParameterizedTest`, and `@TestFactory` methods in the current class; analogous to JUnit 4's `@AfterClass`. Such methods are inherited unless they are overridden and must be `static` unless the "per-class" <> is used. +| `@ContainerTemplate` | Denotes that the annotated class is a <> designed to be executed multiple times depending on the number of invocation contexts returned by the registered <>. | `@Nested` | Denotes that the annotated class is a non-static <>. On Java 8 through Java 15, `@BeforeAll` and `@AfterAll` methods cannot be used directly in a `@Nested` test class unless the "per-class" <> is used. Beginning with Java 16, `@BeforeAll` and `@AfterAll` methods can be declared as `static` in a `@Nested` test class with either test instance lifecycle mode. Such annotations are not inherited. | `@Tag` | Used to declare <>, either at the class or method level; analogous to test groups in TestNG or Categories in JUnit 4. Such annotations are inherited at the class level but not at the method level. | `@Disabled` | Used to <> a test class or test method; analogous to JUnit 4's `@Ignore`. Such annotations are not inherited. @@ -2448,12 +2449,22 @@ lifecycle methods (e.g. `@BeforeEach`) and test class constructors. include::{testDir}/example/ParameterizedTestDemo.java[tags=ParameterResolver_example] ---- +[[writing-tests-container-templates]] +=== Container Templates + +A `{ContainerTemplate}` class is not a regular test class but rather a template for the +contained test cases. As such, it is designed to be invoked multiple times depending on +invocation contexts returned by the registered providers. Thus, it must be used in +conjunction with a registered `{ContainerTemplateInvocationContextProvider}` extension. +Each invocation of a container template class behaves like the execution of a regular test +class with full support for the same lifecycle callbacks and extensions. Please refer to +<> for usage examples. [[writing-tests-test-templates]] === Test Templates -A `{TestTemplate}` method is not a regular test case but rather a template for test -cases. As such, it is designed to be invoked multiple times depending on the number of +A `{TestTemplate}` method is not a regular test case but rather a template for a test +case. As such, it is designed to be invoked multiple times depending on the number of invocation contexts returned by the registered providers. Thus, it must be used in conjunction with a registered `{TestTemplateInvocationContextProvider}` extension. Each invocation of a test template method behaves like the execution of a regular `@Test` diff --git a/documentation/src/test/java/example/ContainerTemplateDemo.java b/documentation/src/test/java/example/ContainerTemplateDemo.java new file mode 100644 index 000000000000..4baadb340b23 --- /dev/null +++ b/documentation/src/test/java/example/ContainerTemplateDemo.java @@ -0,0 +1,99 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package example; + +import static java.util.Collections.singletonList; +import static java.util.Collections.unmodifiableList; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import example.ContainerTemplateDemo.MyContainerTemplateInvocationContextProvider; + +import org.junit.jupiter.api.ContainerTemplate; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ContainerTemplateInvocationContext; +import org.junit.jupiter.api.extension.ContainerTemplateInvocationContextProvider; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestInstancePostProcessor; + +// tag::user_guide[] +@ContainerTemplate +@ExtendWith(MyContainerTemplateInvocationContextProvider.class) +class ContainerTemplateDemo { + + static final List WELL_KNOWN_FRUITS + // tag::custom_line_break[] + = unmodifiableList(Arrays.asList("apple", "banana", "lemon")); + + private String fruit; + + @Test + void notNull() { + assertNotNull(fruit); + } + + @Test + void wellKnown() { + assertTrue(WELL_KNOWN_FRUITS.contains(fruit)); + } + + // end::user_guide[] + static + // tag::user_guide[] + public class MyContainerTemplateInvocationContextProvider + // tag::custom_line_break[] + implements ContainerTemplateInvocationContextProvider { + + @Override + public boolean supportsContainerTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream + // tag::custom_line_break[] + provideContainerTemplateInvocationContexts(ExtensionContext context) { + + return Stream.of(invocationContext("apple"), invocationContext("banana")); + } + + private ContainerTemplateInvocationContext invocationContext(String parameter) { + return new ContainerTemplateInvocationContext() { + @Override + public String getDisplayName(int invocationIndex) { + return parameter; + } + + // end::user_guide[] + @SuppressWarnings("Convert2Lambda") + // tag::user_guide[] + @Override + public List getAdditionalExtensions() { + return singletonList(new TestInstancePostProcessor() { + @Override + public void postProcessTestInstance( + // tag::custom_line_break[] + Object testInstance, ExtensionContext context) { + ((ContainerTemplateDemo) testInstance).fruit = parameter; + } + }); + } + }; + } + } +} +// end::user_guide[] diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AfterAll.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AfterAll.java index 171f530fb324..bce909ae8d2d 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AfterAll.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AfterAll.java @@ -25,7 +25,13 @@ * executed after all tests in the current test class. * *

In contrast to {@link AfterEach @AfterEach} methods, {@code @AfterAll} - * methods are only executed once for a given test class. + * methods are only executed once per execution of a given test class. If the + * test class is annotated with {@link ContainerTemplate @ContainerTemplate}, + * the {@code @AfterAll} methods are executed once after the last invocation of + * the container template. If a {@link Nested @Nested} test class is declared in + * a {@link ContainerTemplate @ContainerTemplate} class, its {@code @AfterAll} + * methods are called once per execution of the nested test class, namely, once + * per invocation of the outer container template. * *

Method Signatures

* diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/BeforeAll.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/BeforeAll.java index e327653c46c3..6b0332d66d53 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/BeforeAll.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/BeforeAll.java @@ -25,7 +25,13 @@ * executed before all tests in the current test class. * *

In contrast to {@link BeforeEach @BeforeEach} methods, {@code @BeforeAll} - * methods are only executed once for a given test class. + * methods are only executed once per execution of a given test class. If the + * test class is annotated with {@link ContainerTemplate @ContainerTemplate}, + * the {@code @BeforeAll} methods are executed once before the first invocation + * of the container template. If a {@link Nested @Nested} test class is declared + * in a {@link ContainerTemplate @ContainerTemplate} class, its + * {@code @BeforeAll} methods are called once per execution of the nested test + * class, namely, once per invocation of the outer container template. * *

Method Signatures

* diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/ContainerTemplate.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/ContainerTemplate.java new file mode 100644 index 000000000000..6330e63c31d2 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/ContainerTemplate.java @@ -0,0 +1,65 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.api; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; +import org.junit.platform.commons.annotation.Testable; + +/** + * {@code @ContainerTemplate} is used to signal that the annotated class is a + * container template. + * + *

In contrast to regular test classes, a container template is not directly + * a test class but rather a template for a set of test cases. As such, it is + * designed to be invoked multiple times depending on the number of {@linkplain + * org.junit.jupiter.api.extension.ContainerTemplateInvocationContext invocation + * contexts} returned by the registered {@linkplain + * org.junit.jupiter.api.extension.ContainerTemplateInvocationContextProvider + * providers}. Must be used together with at least one provider. Otherwise, + * execution will fail. + * + *

Each invocation of a container template method behaves like the execution + * of a regular test class with full support for the same lifecycle callbacks + * and extensions. + * + *

{@code @ContainerTemplate} may be combined with {@link Nested @Nested} and + * a container template may contain regular nested test classes or nested + * container templates. + * + *

{@code @ContainerTemplate} may also be used as a meta-annotation in order + * to create a custom composed annotation that inherits the semantics + * of {@code @ContainerTemplate}. + * + *

Inheritance

+ * + *

The {@code @ContainerTemplate} annotation is not inherited to subclasses + * but needs to be declared on each container template class individually. + * + * @since 5.13 + * @see TestTemplate + * @see org.junit.jupiter.api.extension.ContainerTemplateInvocationContext + * @see org.junit.jupiter.api.extension.ContainerTemplateInvocationContextProvider + */ +@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@API(status = EXPERIMENTAL, since = "5.13") +@Testable +public @interface ContainerTemplate { +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Nested.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Nested.java index 4d96618238d0..476efdb83d12 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Nested.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Nested.java @@ -31,6 +31,9 @@ *

{@code @Nested} test classes may be ordered via * {@link TestClassOrder @TestClassOrder} or a global {@link ClassOrderer}. * + *

{@code @Nested} may be combined with + * {@link ContainerTemplate @ContainerTemplate}. + * *

Test Instance Lifecycle

* *
    @@ -42,6 +45,7 @@ *
* * @since 5.0 + * @see ContainerTemplate * @see Test * @see TestInstance * @see TestClassOrder diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestInstance.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestInstance.java index 80f904a41987..0202e3017f64 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestInstance.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestInstance.java @@ -86,7 +86,12 @@ enum Lifecycle { /** * When using this mode, a new test instance will be created once per - * test class. + * test or container template class. + * + *

For {@link Nested @Nested}

test classes declared inside an + * enclosing {@link ContainerTemplate @ContainerTemplate} test class, an + * instance of the {@code @Nested} class will be created for each + * invocation of the {@code @ContainerTemplate} test class. * * @see #PER_METHOD */ diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestTemplate.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestTemplate.java index 9636a2d01679..2a60a011e1a6 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestTemplate.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestTemplate.java @@ -79,6 +79,7 @@ * * @since 5.0 * @see Test + * @see ContainerTemplate * @see org.junit.jupiter.api.extension.TestTemplateInvocationContext * @see org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider */ diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ContainerTemplateInvocationContext.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ContainerTemplateInvocationContext.java new file mode 100644 index 000000000000..41477537df6c --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ContainerTemplateInvocationContext.java @@ -0,0 +1,70 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.api.extension; + +import static java.util.Collections.emptyList; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.util.List; + +import org.apiguardian.api.API; + +/** + * {@code ContainerTemplateInvocationContext} represents the context of + * a single invocation of a {@linkplain org.junit.jupiter.api.ContainerTemplate + * container template}. + * + *

Each context is provided by a + * {@link ContainerTemplateInvocationContextProvider}. + * + * @since 5.13 + * @see org.junit.jupiter.api.ContainerTemplate + * @see ContainerTemplateInvocationContextProvider + */ +@API(status = EXPERIMENTAL, since = "5.13") +public interface ContainerTemplateInvocationContext { + + /** + * Get the display name for this invocation. + * + *

The supplied {@code invocationIndex} is incremented by the framework + * with each container invocation. Thus, in the case of multiple active + * {@linkplain ContainerTemplateInvocationContextProvider providers}, only + * the first active provider receives indices starting with {@code 1}. + * + *

The default implementation returns the supplied {@code invocationIndex} + * wrapped in brackets — for example, {@code [1]}, {@code [42]}, etc. + * + * @param invocationIndex the index of this invocation (1-based). + * @return the display name for this invocation; never {@code null} or blank + */ + default String getDisplayName(int invocationIndex) { + return "[" + invocationIndex + "]"; + } + + /** + * Get the additional {@linkplain Extension extensions} for this invocation. + * + *

The extensions provided by this method will only be used for this + * invocation of the container template. Thus, it does not make sense to + * return an extension that acts solely on the container level (e.g. + * {@link BeforeAllCallback}). + * + *

The default implementation returns an empty list. + * + * @return the additional extensions for this invocation; never {@code null} + * or containing {@code null} elements, but potentially empty + */ + default List getAdditionalExtensions() { + return emptyList(); + } + +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ContainerTemplateInvocationContextProvider.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ContainerTemplateInvocationContextProvider.java new file mode 100644 index 000000000000..f0b62eca95dc --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ContainerTemplateInvocationContextProvider.java @@ -0,0 +1,121 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.api.extension; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.util.stream.Stream; + +import org.apiguardian.api.API; + +/** + * {@code ContainerTemplateInvocationContextProvider} defines the API for + * {@link Extension Extensions} that wish to provide one or multiple contexts + * for the invocation of a + * {@link org.junit.jupiter.api.ContainerTemplate @ContainerTemplate} class. + * + *

This extension point makes it possible to execute a container template in + * different contexts — for example, with different parameters, by + * preparing the test class instance differently, or multiple times without + * modifying the context. + * + *

This interface defines two main methods: + * {@link #supportsContainerTemplate} and + * {@link #provideContainerTemplateInvocationContexts}. The former is called by + * the framework to determine whether this extension wants to act on a container + * template that is about to be executed. If so, the latter is called and must + * return a {@link Stream} of {@link ContainerTemplateInvocationContext} + * instances. Otherwise, this provider is ignored for the execution of the + * current container template. + * + *

A provider that has returned {@code true} from its + * {@link #supportsContainerTemplate} method is called active. When + * multiple providers are active for a container template class, the + * {@code Streams} returned by their + * {@link #provideContainerTemplateInvocationContexts} methods will be chained, + * and the container template method will be invoked using the contexts of all + * active providers. + * + *

An active provider may return zero invocation contexts from its + * {@link #provideContainerTemplateInvocationContexts} method if it overrides + * {@link #mayReturnZeroContainerTemplateInvocationContexts} to return + * {@code true}. + * + *

Constructor Requirements

+ * + *

Consult the documentation in {@link Extension} for details on + * constructor requirements. + * + * @since 5.13 + * @see org.junit.jupiter.api.ContainerTemplate + * @see ContainerTemplateInvocationContext + */ +@API(status = EXPERIMENTAL, since = "5.13") +public interface ContainerTemplateInvocationContextProvider extends Extension { + + /** + * Determine if this provider supports providing invocation contexts for the + * container template class represented by the supplied {@code context}. + * + * @param context the extension context for the container template class + * about to be invoked; never {@code null} + * @return {@code true} if this provider can provide invocation contexts + * @see #provideContainerTemplateInvocationContexts + * @see ExtensionContext + */ + boolean supportsContainerTemplate(ExtensionContext context); + + /** + * Provide + * {@linkplain ContainerTemplateInvocationContext invocation contexts} for + * the container template class represented by the supplied + * {@code context}. + * + *

This method is only called by the framework if + * {@link #supportsContainerTemplate} previously returned {@code true} for + * the same {@link ExtensionContext}; this method is allowed to return an + * empty {@code Stream} but not {@code null}. + * + *

The returned {@code Stream} will be properly closed by calling + * {@link Stream#close()}, making it safe to use a resource such as + * {@link java.nio.file.Files#lines(java.nio.file.Path) Files.lines()}. + * + * @param context the extension context for the container template class + * about to be invoked; never {@code null} + * @return a {@code Stream} of {@code ContainerTemplateInvocationContext} + * instances for the invocation of the container template class; never {@code null} + * @see #supportsContainerTemplate + * @see ExtensionContext + */ + Stream provideContainerTemplateInvocationContexts( + ExtensionContext context); + + /** + * Signal that this provider may provide zero + * {@linkplain ContainerTemplateInvocationContext invocation contexts} for + * the container template class represented by the supplied {@code context}. + * + *

If this method returns {@code false} (which is the default) and the + * provider returns an empty stream from + * {@link #provideContainerTemplateInvocationContexts}, this will be considered + * an execution error. Override this method to return {@code true} to ignore + * the absence of invocation contexts for this provider. + * + * @param context the extension context for the container template class + * about to be invoked; never {@code null} + * @return {@code true} to allow zero contexts, {@code false} to fail + * execution in case of zero contexts + */ + default boolean mayReturnZeroContainerTemplateInvocationContexts(ExtensionContext context) { + return false; + } + +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLock.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLock.java index 24c85cb268c8..4619fe11d6f0 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLock.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/parallel/ResourceLock.java @@ -21,6 +21,7 @@ import java.lang.annotation.Target; import org.apiguardian.api.API; +import org.junit.jupiter.api.ContainerTemplate; /** * {@code @ResourceLock} is used to declare that the annotated test class or test @@ -70,6 +71,11 @@ * attribute remains applicable, and the target of "dynamic" shared resources added * via implementations of {@link ResourceLocksProvider} is not changed. * + *

Shared resources declared on or provided for methods or nested test + * classes in a {@link ContainerTemplate @ContainerTemplate} are propagated as + * if they were declared on the outermost enclosing {@code @ContainerTemplate} + * class itself. + * * @see Isolated * @see Resources * @see ResourceAccessMode diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java index 5c514362e8d9..0077d5d7f512 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java @@ -82,7 +82,8 @@ * @since 5.5 */ @API(status = INTERNAL, since = "5.5") -public abstract class ClassBasedTestDescriptor extends JupiterTestDescriptor implements ResourceLockAware { +public abstract class ClassBasedTestDescriptor extends JupiterTestDescriptor + implements ResourceLockAware, TestClassAware { private static final InterceptingExecutableInvoker executableInvoker = new InterceptingExecutableInvoker(); @@ -107,37 +108,49 @@ public abstract class ClassBasedTestDescriptor extends JupiterTestDescriptor imp this.exclusiveResourceCollector = ExclusiveResourceCollector.from(testClass); } - // --- TestDescriptor ------------------------------------------------------ + ClassBasedTestDescriptor(UniqueId uniqueId, Class testClass, String displayName, + JupiterConfiguration configuration) { + super(uniqueId, displayName, ClassSource.from(testClass), configuration); + this.testClass = testClass; + this.tags = getTags(testClass); + this.lifecycle = getTestInstanceLifecycle(testClass, configuration); + this.defaultChildExecutionMode = (this.lifecycle == Lifecycle.PER_CLASS ? ExecutionMode.SAME_THREAD : null); + this.exclusiveResourceCollector = ExclusiveResourceCollector.from(testClass); + } + + // --- TestClassAware ------------------------------------------------------ + + @Override public final Class getTestClass() { return this.testClass; } - public abstract List> getEnclosingTestClasses(); + // --- TestDescriptor ------------------------------------------------------ @Override - public Type getType() { + public final Type getType() { return Type.CONTAINER; } @Override - public String getLegacyReportingName() { + public final String getLegacyReportingName() { return this.testClass.getName(); } // --- Node ---------------------------------------------------------------- @Override - protected Optional getExplicitExecutionMode() { + protected final Optional getExplicitExecutionMode() { return getExecutionModeFromAnnotation(getTestClass()); } @Override - protected Optional getDefaultChildExecutionMode() { + protected final Optional getDefaultChildExecutionMode() { return Optional.ofNullable(this.defaultChildExecutionMode); } - public void setDefaultChildExecutionMode(ExecutionMode defaultChildExecutionMode) { + public final void setDefaultChildExecutionMode(ExecutionMode defaultChildExecutionMode) { this.defaultChildExecutionMode = defaultChildExecutionMode; } @@ -147,7 +160,7 @@ public final ExclusiveResourceCollector getExclusiveResourceCollector() { } @Override - public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext context) { + public final JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext context) { MutableExtensionRegistry registry = populateNewExtensionRegistryFromExtendWithAnnotation( context.getExtensionRegistry(), this.testClass); @@ -194,7 +207,7 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte } @Override - public JupiterEngineExecutionContext before(JupiterEngineExecutionContext context) { + public final JupiterEngineExecutionContext before(JupiterEngineExecutionContext context) { ThrowableCollector throwableCollector = context.getThrowableCollector(); if (isPerClassLifecycle(context)) { @@ -223,7 +236,7 @@ public JupiterEngineExecutionContext before(JupiterEngineExecutionContext contex } @Override - public void after(JupiterEngineExecutionContext context) { + public final void after(JupiterEngineExecutionContext context) { ThrowableCollector throwableCollector = context.getThrowableCollector(); Throwable previousThrowable = throwableCollector.getThrowable(); @@ -303,8 +316,8 @@ protected abstract TestInstances instantiateTestClass(JupiterEngineExecutionCont ExtensionContextSupplier extensionContext, ExtensionRegistry registry, JupiterEngineExecutionContext context); - protected TestInstances instantiateTestClass(Optional outerInstances, ExtensionRegistry registry, - ExtensionContextSupplier extensionContext) { + protected final TestInstances instantiateTestClass(Optional outerInstances, + ExtensionRegistry registry, ExtensionContextSupplier extensionContext) { Optional outerInstance = outerInstances.map(TestInstances::getInnermostInstance); invokeTestInstancePreConstructCallbacks(new DefaultTestInstanceFactoryContext(this.testClass, outerInstance), diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java index aace0e86e16d..546472781200 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java @@ -34,20 +34,6 @@ final class ClassExtensionContext extends AbstractExtensionContext testClass, JupiterConfigu super(uniqueId, testClass, createDisplayNameSupplierForClass(testClass, configuration), configuration); } + private ClassTestDescriptor(UniqueId uniqueId, Class testClass, String displayName, + JupiterConfiguration configuration) { + super(uniqueId, testClass, displayName, configuration); + } + + // --- JupiterTestDescriptor ----------------------------------------------- + + @Override + protected ClassTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer) { + return new ClassTestDescriptor(uniqueIdTransformer.apply(getUniqueId()), getTestClass(), getDisplayName(), + configuration); + } + // --- TestDescriptor ------------------------------------------------------ @Override @@ -59,6 +73,8 @@ public Set getTags() { return new LinkedHashSet<>(this.tags); } + // --- TestClassAware ------------------------------------------------------ + @Override public List> getEnclosingTestClasses() { return emptyList(); @@ -72,6 +88,8 @@ public ExecutionMode getExecutionMode() { () -> JupiterTestDescriptor.toExecutionMode(configuration.getDefaultClassesExecutionMode())); } + // --- ClassBasedTestDescriptor -------------------------------------------- + @Override protected TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext, ExtensionContextSupplier extensionContext, ExtensionRegistry registry, @@ -79,6 +97,8 @@ protected TestInstances instantiateTestClass(JupiterEngineExecutionContext paren return instantiateTestClass(Optional.empty(), registry, extensionContext); } + // --- ResourceLockAware --------------------------------------------------- + @Override public Function> getResourceLocksProviderEvaluator() { return provider -> provider.provideForClass(getTestClass()); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateInvocationExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateInvocationExtensionContext.java new file mode 100644 index 000000000000..82a045824373 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateInvocationExtensionContext.java @@ -0,0 +1,77 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.engine.descriptor; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.Optional; + +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestInstances; +import org.junit.jupiter.engine.config.JupiterConfiguration; +import org.junit.jupiter.engine.extension.ExtensionRegistry; +import org.junit.platform.engine.EngineExecutionListener; +import org.junit.platform.engine.support.hierarchical.Node; + +/** + * @since 5.13 + */ +final class ContainerTemplateInvocationExtensionContext + extends AbstractExtensionContext { + + ContainerTemplateInvocationExtensionContext(ExtensionContext parent, + EngineExecutionListener engineExecutionListener, ContainerTemplateInvocationTestDescriptor testDescriptor, + JupiterConfiguration configuration, ExtensionRegistry extensionRegistry) { + super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry); + } + + @Override + public Optional getElement() { + return Optional.of(getTestDescriptor().getTestClass()); + } + + @Override + public Optional> getTestClass() { + return Optional.of(getTestDescriptor().getTestClass()); + } + + @Override + public Optional getTestInstanceLifecycle() { + return getParent().flatMap(ExtensionContext::getTestInstanceLifecycle); + } + + @Override + public Optional getTestInstance() { + return getParent().flatMap(ExtensionContext::getTestInstance); + } + + @Override + public Optional getTestInstances() { + return getParent().flatMap(ExtensionContext::getTestInstances); + } + + @Override + public Optional getTestMethod() { + return Optional.empty(); + } + + @Override + public Optional getExecutionException() { + return Optional.empty(); + } + + @Override + protected Node.ExecutionMode getPlatformExecutionMode() { + return getTestDescriptor().getExecutionMode(); + } + +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateInvocationTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateInvocationTestDescriptor.java new file mode 100644 index 000000000000..64cbdbe4296d --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateInvocationTestDescriptor.java @@ -0,0 +1,141 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.engine.descriptor; + +import static org.apiguardian.api.API.Status.INTERNAL; +import static org.junit.jupiter.engine.extension.MutableExtensionRegistry.createRegistryFrom; + +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.ContainerTemplateInvocationContext; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.parallel.ResourceLocksProvider; +import org.junit.jupiter.engine.config.JupiterConfiguration; +import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; +import org.junit.jupiter.engine.extension.MutableExtensionRegistry; +import org.junit.platform.engine.TestSource; +import org.junit.platform.engine.UniqueId; + +/** + * @since 5.13 + */ +@API(status = INTERNAL, since = "5.13") +public class ContainerTemplateInvocationTestDescriptor extends JupiterTestDescriptor + implements TestClassAware, ResourceLockAware { + + public static final String SEGMENT_TYPE = "container-template-invocation"; + + private final ContainerTemplateTestDescriptor parent; + private ContainerTemplateInvocationContext invocationContext; + private final int index; + + public ContainerTemplateInvocationTestDescriptor(UniqueId uniqueId, ContainerTemplateTestDescriptor parent, + ContainerTemplateInvocationContext invocationContext, int index, TestSource source, + JupiterConfiguration configuration) { + super(uniqueId, invocationContext.getDisplayName(index), source, configuration); + this.parent = parent; + this.invocationContext = invocationContext; + this.index = index; + } + + public int getIndex() { + return index; + } + + // --- JupiterTestDescriptor ----------------------------------------------- + + @Override + protected ContainerTemplateInvocationTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer) { + return new ContainerTemplateInvocationTestDescriptor(uniqueIdTransformer.apply(getUniqueId()), parent, + this.invocationContext, this.index, getSource().orElse(null), this.configuration); + } + + // --- TestDescriptor ------------------------------------------------------ + + @Override + public Type getType() { + return Type.CONTAINER; + } + + @Override + public String getLegacyReportingName() { + return getTestClass().getName() + "[" + index + "]"; + } + + // --- TestClassAware ------------------------------------------------------ + + @Override + public Class getTestClass() { + return parent.getTestClass(); + } + + @Override + public List> getEnclosingTestClasses() { + return parent.getEnclosingTestClasses(); + } + + // --- ResourceLockAware --------------------------------------------------- + + @Override + public ExclusiveResourceCollector getExclusiveResourceCollector() { + return parent.getExclusiveResourceCollector(); + } + + @Override + public Function> getResourceLocksProviderEvaluator() { + return parent.getResourceLocksProviderEvaluator(); + } + + // --- Node ---------------------------------------------------------------- + + @Override + public SkipResult shouldBeSkipped(JupiterEngineExecutionContext context) { + return SkipResult.doNotSkip(); + } + + @Override + public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext context) { + MutableExtensionRegistry registry = context.getExtensionRegistry(); + List additionalExtensions = this.invocationContext.getAdditionalExtensions(); + if (!additionalExtensions.isEmpty()) { + MutableExtensionRegistry childRegistry = createRegistryFrom(registry, Stream.empty()); + additionalExtensions.forEach( + extension -> childRegistry.registerExtension(extension, this.invocationContext)); + registry = childRegistry; + } + ExtensionContext extensionContext = new ContainerTemplateInvocationExtensionContext( + context.getExtensionContext(), context.getExecutionListener(), this, context.getConfiguration(), registry); + return context.extend() // + .withExtensionContext(extensionContext) // + .withExtensionRegistry(registry) // + .build(); + } + + @Override + public JupiterEngineExecutionContext execute(JupiterEngineExecutionContext context, + DynamicTestExecutor dynamicTestExecutor) throws Exception { + Visitor visitor = context.getExecutionListener()::dynamicTestRegistered; + getChildren().forEach(child -> child.accept(visitor)); + return context; + } + + @Override + public void cleanUp(JupiterEngineExecutionContext context) { + // forget invocationContext so it can be garbage collected + this.invocationContext = null; + } +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateTestDescriptor.java new file mode 100644 index 000000000000..d59debe38cc8 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateTestDescriptor.java @@ -0,0 +1,263 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.engine.descriptor; + +import static java.util.stream.Collectors.toCollection; +import static java.util.stream.Collectors.toList; +import static org.apiguardian.api.API.Status.INTERNAL; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.ContainerTemplate; +import org.junit.jupiter.api.extension.ContainerTemplateInvocationContext; +import org.junit.jupiter.api.extension.ContainerTemplateInvocationContextProvider; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestInstances; +import org.junit.jupiter.api.parallel.ResourceLocksProvider; +import org.junit.jupiter.engine.execution.ExtensionContextSupplier; +import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; +import org.junit.jupiter.engine.extension.ExtensionRegistry; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestTag; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.hierarchical.ExclusiveResource; +import org.junit.platform.engine.support.hierarchical.Node; + +/** + * @since 5.13 + */ +@API(status = INTERNAL, since = "5.13") +public class ContainerTemplateTestDescriptor extends ClassBasedTestDescriptor implements Filterable { + + public static final String STATIC_CLASS_SEGMENT_TYPE = "container-template"; + public static final String NESTED_CLASS_SEGMENT_TYPE = "nested-container-template"; + + private final Map> childrenPrototypesByIndex = new HashMap<>(); + private final List childrenPrototypes = new ArrayList<>(); + private final ClassBasedTestDescriptor delegate; + private final DynamicDescendantFilter dynamicDescendantFilter; + + public ContainerTemplateTestDescriptor(UniqueId uniqueId, ClassBasedTestDescriptor delegate) { + this(uniqueId, delegate, new DynamicDescendantFilter()); + } + + private ContainerTemplateTestDescriptor(UniqueId uniqueId, ClassBasedTestDescriptor delegate, + DynamicDescendantFilter dynamicDescendantFilter) { + super(uniqueId, delegate.getTestClass(), delegate.getDisplayName(), delegate.configuration); + this.delegate = delegate; + this.dynamicDescendantFilter = dynamicDescendantFilter; + } + + // --- TestDescriptor ------------------------------------------------------ + + @Override + public Set getTags() { + // return modifiable copy + return new LinkedHashSet<>(this.tags); + } + + // --- Filterable ---------------------------------------------------------- + + @Override + public DynamicDescendantFilter getDynamicDescendantFilter() { + return dynamicDescendantFilter; + } + + // --- JupiterTestDescriptor ----------------------------------------------- + + @Override + protected JupiterTestDescriptor copyIncludingDescendants(UnaryOperator uniqueIdTransformer) { + ContainerTemplateTestDescriptor copy = (ContainerTemplateTestDescriptor) super.copyIncludingDescendants( + uniqueIdTransformer); + this.childrenPrototypes.forEach(oldChild -> { + TestDescriptor newChild = ((JupiterTestDescriptor) oldChild).copyIncludingDescendants(uniqueIdTransformer); + copy.childrenPrototypes.add(newChild); + }); + this.childrenPrototypesByIndex.forEach((index, oldChildren) -> { + List newChildren = oldChildren.stream() // + .map(oldChild -> ((JupiterTestDescriptor) oldChild).copyIncludingDescendants(uniqueIdTransformer)) // + .collect(toList()); + copy.childrenPrototypesByIndex.put(index, newChildren); + }); + return copy; + } + + @Override + protected ContainerTemplateTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer) { + return new ContainerTemplateTestDescriptor(uniqueIdTransformer.apply(getUniqueId()), this.delegate, + this.dynamicDescendantFilter.copy(uniqueIdTransformer)); + } + + @Override + public void prunePriorToFiltering() { + // do nothing to allow PostDiscoveryFilters to be applied first + } + + // --- TestDescriptor ------------------------------------------------------ + + @Override + public void prune() { + super.prune(); + this.children.forEach(child -> child.accept(TestDescriptor::prune)); + // Second iteration to avoid processing children that were pruned in the first iteration + this.children.forEach(child -> { + if (child instanceof ContainerTemplateInvocationTestDescriptor) { + int index = ((ContainerTemplateInvocationTestDescriptor) child).getIndex(); + this.dynamicDescendantFilter.allowIndex(index - 1); + this.childrenPrototypesByIndex.put(index, child.getChildren()); + } + else { + this.childrenPrototypes.add(child); + } + }); + this.children.clear(); + } + + @Override + public boolean mayRegisterTests() { + return !childrenPrototypes.isEmpty() || !childrenPrototypesByIndex.isEmpty(); + } + + // --- TestClassAware ------------------------------------------------------ + + @Override + public List> getEnclosingTestClasses() { + return delegate.getEnclosingTestClasses(); + } + + // --- ClassBasedTestDescriptor -------------------------------------------- + + @Override + public TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext, + ExtensionContextSupplier extensionContext, ExtensionRegistry registry, + JupiterEngineExecutionContext context) { + return delegate.instantiateTestClass(parentExecutionContext, extensionContext, registry, context); + } + + // --- ResourceLockAware --------------------------------------------------- + + @Override + public Function> getResourceLocksProviderEvaluator() { + return delegate.getResourceLocksProviderEvaluator(); + } + + // --- Node ---------------------------------------------------------------- + + @Override + public Set getExclusiveResources() { + Set result = determineExclusiveResources().collect(toCollection(HashSet::new)); + Visitor visitor = testDescriptor -> { + if (testDescriptor instanceof Node) { + result.addAll(((Node) testDescriptor).getExclusiveResources()); + } + }; + this.childrenPrototypes.forEach(child -> child.accept(visitor)); + this.childrenPrototypesByIndex.values() // + .forEach(prototypes -> prototypes // + .forEach(child -> child.accept(visitor))); + return result; + } + + @Override + public void cleanUp(JupiterEngineExecutionContext context) { + this.childrenPrototypes.clear(); + this.childrenPrototypesByIndex.clear(); + this.dynamicDescendantFilter.allowAll(); + } + + @Override + public JupiterEngineExecutionContext execute(JupiterEngineExecutionContext context, + DynamicTestExecutor dynamicTestExecutor) throws Exception { + + new ContainerTemplateExecutor().execute(context, dynamicTestExecutor); + return context; + } + + class ContainerTemplateExecutor + extends TemplateExecutor { + + public ContainerTemplateExecutor() { + super(ContainerTemplateTestDescriptor.this, ContainerTemplateInvocationContextProvider.class); + } + + @Override + boolean supports(ContainerTemplateInvocationContextProvider provider, ExtensionContext extensionContext) { + return provider.supportsContainerTemplate(extensionContext); + } + + @Override + protected String getNoRegisteredProviderErrorMessage() { + return String.format("You must register at least one %s that supports @%s class [%s]", + ContainerTemplateInvocationContextProvider.class.getSimpleName(), + ContainerTemplate.class.getSimpleName(), getTestClass().getName()); + } + + @Override + Stream provideContexts( + ContainerTemplateInvocationContextProvider provider, ExtensionContext extensionContext) { + return provider.provideContainerTemplateInvocationContexts(extensionContext); + } + + @Override + boolean mayReturnZeroContexts(ContainerTemplateInvocationContextProvider provider, + ExtensionContext extensionContext) { + return provider.mayReturnZeroContainerTemplateInvocationContexts(extensionContext); + } + + @Override + protected String getZeroContextsProvidedErrorMessage(ContainerTemplateInvocationContextProvider provider) { + return String.format( + "Provider [%s] did not provide any invocation contexts, but was expected to do so. " + + "You may override mayReturnZeroContainerTemplateInvocationContexts() to allow this.", + provider.getClass().getSimpleName()); + } + + @Override + UniqueId createInvocationUniqueId(UniqueId parentUniqueId, int index) { + return parentUniqueId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#" + index); + } + + @Override + TestDescriptor createInvocationTestDescriptor(UniqueId uniqueId, + ContainerTemplateInvocationContext invocationContext, int index) { + ContainerTemplateInvocationTestDescriptor containerInvocationDescriptor = new ContainerTemplateInvocationTestDescriptor( + uniqueId, ContainerTemplateTestDescriptor.this, invocationContext, index, getSource().orElse(null), + ContainerTemplateTestDescriptor.this.configuration); + + collectChildren(index, uniqueId) // + .forEach(containerInvocationDescriptor::addChild); + + return containerInvocationDescriptor; + } + + private Stream collectChildren(int index, UniqueId invocationUniqueId) { + if (ContainerTemplateTestDescriptor.this.childrenPrototypesByIndex.containsKey(index)) { + return ContainerTemplateTestDescriptor.this.childrenPrototypesByIndex.remove(index).stream(); + } + UnaryOperator transformer = new UniqueIdPrefixTransformer(getUniqueId(), invocationUniqueId); + return ContainerTemplateTestDescriptor.this.childrenPrototypes.stream() // + .map(JupiterTestDescriptor.class::cast) // + .map(it -> it.copyIncludingDescendants(transformer)); + } + } + +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicContainerTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicContainerTestDescriptor.java index 96b7d6e132ba..8f157b9f8726 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicContainerTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicContainerTestDescriptor.java @@ -14,6 +14,7 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.UnaryOperator; import java.util.stream.Stream; import org.junit.jupiter.api.DynamicContainer; @@ -46,6 +47,12 @@ class DynamicContainerTestDescriptor extends DynamicNodeTestDescriptor { this.dynamicDescendantFilter = dynamicDescendantFilter; } + @Override + protected DynamicContainerTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer) { + return new DynamicContainerTestDescriptor(uniqueIdTransformer.apply(getUniqueId()), this.index, + this.dynamicContainer, this.testSource, this.dynamicDescendantFilter, this.configuration); + } + @Override public Type getType() { return Type.CONTAINER; diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicDescendantFilter.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicDescendantFilter.java index 15b059ca47b4..ded323498b1a 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicDescendantFilter.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicDescendantFilter.java @@ -15,6 +15,7 @@ import java.util.HashSet; import java.util.Set; import java.util.function.BiPredicate; +import java.util.function.UnaryOperator; import org.apiguardian.api.API; import org.junit.platform.engine.TestDescriptor; @@ -40,6 +41,12 @@ public void allowUniqueIdPrefix(UniqueId uniqueId) { } } + public void allowIndex(int index) { + if (this.mode == Mode.EXPLICIT) { + this.allowedIndices.add(index); + } + } + public void allowIndex(Set indices) { if (this.mode == Mode.EXPLICIT) { this.allowedIndices.addAll(indices); @@ -79,6 +86,18 @@ private enum Mode { EXPLICIT, ALLOW_ALL } + public DynamicDescendantFilter copy(UnaryOperator uniqueIdTransformer) { + return configure(uniqueIdTransformer, new DynamicDescendantFilter()); + } + + protected DynamicDescendantFilter configure(UnaryOperator uniqueIdTransformer, + DynamicDescendantFilter copy) { + this.allowedUniqueIds.stream().map(uniqueIdTransformer).forEach(copy.allowedUniqueIds::add); + copy.allowedIndices.addAll(this.allowedIndices); + copy.mode = this.mode; + return copy; + } + private class WithoutIndexFiltering extends DynamicDescendantFilter { @Override @@ -90,5 +109,10 @@ public boolean test(UniqueId uniqueId, Integer index) { public DynamicDescendantFilter withoutIndexFiltering() { return this; } + + @Override + public DynamicDescendantFilter copy(UnaryOperator uniqueIdTransformer) { + return configure(uniqueIdTransformer, new WithoutIndexFiltering()); + } } } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java index c5e83da28df1..9ed1f81ca6b1 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java @@ -11,6 +11,7 @@ package org.junit.jupiter.engine.descriptor; import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; import org.junit.platform.engine.TestDescriptor; @@ -24,7 +25,7 @@ */ abstract class DynamicNodeTestDescriptor extends JupiterTestDescriptor { - private final int index; + protected final int index; DynamicNodeTestDescriptor(UniqueId uniqueId, int index, DynamicNode dynamicNode, TestSource testSource, JupiterConfiguration configuration) { @@ -44,7 +45,7 @@ public String getLegacyReportingName() { @Override public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext context) { - DynamicExtensionContext extensionContext = new DynamicExtensionContext(context.getExtensionContext(), + ExtensionContext extensionContext = new DynamicExtensionContext(context.getExtensionContext(), context.getExecutionListener(), this, context.getConfiguration(), context.getExtensionRegistry()); // @formatter:off return context.extend() diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicTestTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicTestTestDescriptor.java index e504b311a5df..9209b8c1f3d6 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicTestTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicTestTestDescriptor.java @@ -10,6 +10,8 @@ package org.junit.jupiter.engine.descriptor; +import java.util.function.UnaryOperator; + import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.extension.DynamicTestInvocationContext; import org.junit.jupiter.api.extension.ExtensionContext; @@ -41,6 +43,12 @@ class DynamicTestTestDescriptor extends DynamicNodeTestDescriptor { this.dynamicTest = dynamicTest; } + @Override + protected DynamicTestTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer) { + return new DynamicTestTestDescriptor(uniqueIdTransformer.apply(getUniqueId()), this.index, this.dynamicTest, + this.getSource().orElse(null), this.configuration); + } + @Override public Type getType() { return Type.TEST; diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java index 0101f9d2b5c6..27344fc499f9 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java @@ -26,11 +26,13 @@ import java.util.Optional; import java.util.Set; import java.util.function.Supplier; +import java.util.function.UnaryOperator; import org.apiguardian.api.API; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.extension.ConditionEvaluationResult; import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.execution.ConditionEvaluator; @@ -202,7 +204,8 @@ private SkipResult toSkipResult(ConditionEvaluationResult evaluationResult) { } /** - * Must be overridden and return a new context so cleanUp() does not accidentally close the parent context. + * Must be overridden and return a new context with a new {@link ExtensionContext} + * so cleanUp() does not accidentally close the parent context. */ @Override public abstract JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext context) throws Exception; @@ -212,6 +215,27 @@ public void cleanUp(JupiterEngineExecutionContext context) throws Exception { context.close(); } + public void prunePriorToFiltering() { + prune(); + } + + /** + * {@return a deep copy (with copies of children) of this descriptor with the supplied unique ID} + */ + protected JupiterTestDescriptor copyIncludingDescendants(UnaryOperator uniqueIdTransformer) { + JupiterTestDescriptor result = withUniqueId(uniqueIdTransformer); + getChildren().forEach(oldChild -> { + TestDescriptor newChild = ((JupiterTestDescriptor) oldChild).copyIncludingDescendants(uniqueIdTransformer); + result.addChild(newChild); + }); + return result; + } + + /** + * {@return shallow copy (without children) of this descriptor with the supplied unique ID} + */ + protected abstract JupiterTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer); + /** * @since 5.5 */ diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java index 3a5b785591f8..c38ecd72fd77 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodBasedTestDescriptor.java @@ -108,9 +108,9 @@ public Function> getResou private List> getEnclosingTestClasses() { return getParent() // - .filter(ClassBasedTestDescriptor.class::isInstance) // - .map(ClassBasedTestDescriptor.class::cast) // - .map(ClassBasedTestDescriptor::getEnclosingTestClasses) // + .filter(TestClassAware.class::isInstance) // + .map(TestClassAware.class::cast) // + .map(TestClassAware::getEnclosingTestClasses) // .orElseGet(Collections::emptyList); } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/NestedClassTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/NestedClassTestDescriptor.java index b0619fa09cff..aad262a66109 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/NestedClassTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/NestedClassTestDescriptor.java @@ -22,6 +22,7 @@ import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; +import java.util.function.UnaryOperator; import org.apiguardian.api.API; import org.junit.jupiter.api.extension.TestInstances; @@ -55,6 +56,19 @@ public NestedClassTestDescriptor(UniqueId uniqueId, Class testClass, createDisplayNameSupplierForNestedClass(enclosingInstanceTypes, testClass, configuration), configuration); } + private NestedClassTestDescriptor(UniqueId uniqueId, Class testClass, String displayName, + JupiterConfiguration configuration) { + super(uniqueId, testClass, displayName, configuration); + } + + // --- JupiterTestDescriptor ----------------------------------------------- + + @Override + protected NestedClassTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer) { + return new NestedClassTestDescriptor(uniqueIdTransformer.apply(getUniqueId()), getTestClass(), getDisplayName(), + configuration); + } + // --- TestDescriptor ------------------------------------------------------ @Override @@ -65,6 +79,8 @@ public final Set getTags() { return allTags; } + // --- TestClassAware ------------------------------------------------------ + @Override public List> getEnclosingTestClasses() { return getEnclosingTestClasses(getParent().orElse(null)); @@ -72,8 +88,8 @@ public List> getEnclosingTestClasses() { @API(status = INTERNAL, since = "5.12") public static List> getEnclosingTestClasses(TestDescriptor parent) { - if (parent instanceof ClassBasedTestDescriptor) { - ClassBasedTestDescriptor parentClassDescriptor = (ClassBasedTestDescriptor) parent; + if (parent instanceof TestClassAware) { + TestClassAware parentClassDescriptor = (TestClassAware) parent; List> result = new ArrayList<>(parentClassDescriptor.getEnclosingTestClasses()); result.add(parentClassDescriptor.getTestClass()); return result; @@ -81,7 +97,7 @@ public static List> getEnclosingTestClasses(TestDescriptor parent) { return emptyList(); } - // --- Node ---------------------------------------------------------------- + // --- ClassBasedTestDescriptor -------------------------------------------- @Override protected TestInstances instantiateTestClass(JupiterEngineExecutionContext parentExecutionContext, @@ -95,6 +111,8 @@ protected TestInstances instantiateTestClass(JupiterEngineExecutionContext paren return instantiateTestClass(Optional.of(outerInstances), registry, extensionContext); } + // --- ResourceLockAware --------------------------------------------------- + @Override public Function> getResourceLocksProviderEvaluator() { return enclosingInstanceTypesDependentResourceLocksProviderEvaluator(this::getEnclosingTestClasses, (provider, diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TemplateExecutor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TemplateExecutor.java new file mode 100644 index 000000000000..64988963685f --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TemplateExecutor.java @@ -0,0 +1,100 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.engine.descriptor; + +import static java.util.stream.Collectors.toList; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; +import org.junit.jupiter.engine.extension.ExtensionRegistry; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.hierarchical.Node; + +abstract class TemplateExecutor

{ + + private final TestDescriptor parent; + private final Class

providerType; + private final DynamicDescendantFilter dynamicDescendantFilter; + + TemplateExecutor(T parent, Class

providerType) { + this.parent = parent; + this.providerType = providerType; + this.dynamicDescendantFilter = parent.getDynamicDescendantFilter(); + } + + void execute(JupiterEngineExecutionContext context, Node.DynamicTestExecutor dynamicTestExecutor) { + ExtensionContext extensionContext = context.getExtensionContext(); + List

providers = validateProviders(extensionContext, context.getExtensionRegistry()); + AtomicInteger invocationIndex = new AtomicInteger(); + for (P provider : providers) { + executeForProvider(provider, invocationIndex, dynamicTestExecutor, extensionContext); + } + } + + private void executeForProvider(P provider, AtomicInteger invocationIndex, + Node.DynamicTestExecutor dynamicTestExecutor, ExtensionContext extensionContext) { + + int initialValue = invocationIndex.get(); + + try (Stream stream = provideContexts(provider, extensionContext)) { + stream.forEach(invocationContext -> createInvocationTestDescriptor(invocationContext, + invocationIndex.incrementAndGet()) // + .ifPresent(testDescriptor -> execute(dynamicTestExecutor, testDescriptor))); + } + + Preconditions.condition( + invocationIndex.get() != initialValue || mayReturnZeroContexts(provider, extensionContext), + getZeroContextsProvidedErrorMessage(provider)); + } + + private List

validateProviders(ExtensionContext extensionContext, ExtensionRegistry extensionRegistry) { + List

providers = extensionRegistry.stream(providerType) // + .filter(provider -> supports(provider, extensionContext)) // + .collect(toList()); + return Preconditions.notEmpty(providers, this::getNoRegisteredProviderErrorMessage); + } + + private Optional createInvocationTestDescriptor(C invocationContext, int index) { + UniqueId invocationUniqueId = createInvocationUniqueId(parent.getUniqueId(), index); + if (this.dynamicDescendantFilter.test(invocationUniqueId, index - 1)) { + return Optional.of(createInvocationTestDescriptor(invocationUniqueId, invocationContext, index)); + } + return Optional.empty(); + } + + private void execute(Node.DynamicTestExecutor dynamicTestExecutor, TestDescriptor testDescriptor) { + testDescriptor.setParent(parent); + dynamicTestExecutor.execute(testDescriptor); + } + + abstract boolean supports(P provider, ExtensionContext extensionContext); + + protected abstract String getNoRegisteredProviderErrorMessage(); + + abstract Stream provideContexts(P provider, ExtensionContext extensionContext); + + abstract boolean mayReturnZeroContexts(P provider, ExtensionContext extensionContext); + + protected abstract String getZeroContextsProvidedErrorMessage(P provider); + + abstract UniqueId createInvocationUniqueId(UniqueId parentUniqueId, int index); + + abstract TestDescriptor createInvocationTestDescriptor(UniqueId uniqueId, C invocationContext, int index); + +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestClassAware.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestClassAware.java new file mode 100644 index 000000000000..f337b53ca046 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestClassAware.java @@ -0,0 +1,29 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.engine.descriptor; + +import static org.apiguardian.api.API.Status.INTERNAL; + +import java.util.List; + +import org.apiguardian.api.API; + +/** + * @since 5.13 + */ +@API(status = INTERNAL, since = "5.13") +public interface TestClassAware { + + Class getTestClass(); + + List> getEnclosingTestClasses(); + +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java index f0d37814bbf2..f145e5531a7c 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Optional; import java.util.function.Supplier; +import java.util.function.UnaryOperator; import java.util.stream.Stream; import org.apiguardian.api.API; @@ -61,11 +62,27 @@ public class TestFactoryTestDescriptor extends TestMethodTestDescriptor implemen private static final ReflectiveInterceptorCall interceptorCall = InvocationInterceptor::interceptTestFactoryMethod; private static final InterceptingExecutableInvoker executableInvoker = new InterceptingExecutableInvoker(); - private final DynamicDescendantFilter dynamicDescendantFilter = new DynamicDescendantFilter(); + private final DynamicDescendantFilter dynamicDescendantFilter; public TestFactoryTestDescriptor(UniqueId uniqueId, Class testClass, Method testMethod, Supplier>> enclosingInstanceTypes, JupiterConfiguration configuration) { super(uniqueId, testClass, testMethod, enclosingInstanceTypes, configuration); + this.dynamicDescendantFilter = new DynamicDescendantFilter(); + } + + private TestFactoryTestDescriptor(UniqueId uniqueId, String displayName, Class testClass, Method testMethod, + JupiterConfiguration configuration, DynamicDescendantFilter dynamicDescendantFilter) { + super(uniqueId, displayName, testClass, testMethod, configuration); + this.dynamicDescendantFilter = dynamicDescendantFilter; + } + + // --- JupiterTestDescriptor ----------------------------------------------- + + @Override + protected TestFactoryTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer) { + // TODO #871 Check that dynamic descendant filter is copied correctly + return new TestFactoryTestDescriptor(uniqueIdTransformer.apply(getUniqueId()), getDisplayName(), getTestClass(), + getTestMethod(), this.configuration, this.dynamicDescendantFilter.copy(uniqueIdTransformer)); } // --- Filterable ---------------------------------------------------------- diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java index 67fa136a52d1..8d3b14a82537 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java @@ -19,6 +19,7 @@ import java.lang.reflect.Method; import java.util.List; import java.util.function.Supplier; +import java.util.function.UnaryOperator; import org.apiguardian.api.API; import org.junit.jupiter.api.TestInstance.Lifecycle; @@ -82,12 +83,27 @@ public TestMethodTestDescriptor(UniqueId uniqueId, Class testClass, Method te this.interceptorCall = defaultInterceptorCall; } + TestMethodTestDescriptor(UniqueId uniqueId, String displayName, Class testClass, Method testMethod, + JupiterConfiguration configuration) { + this(uniqueId, displayName, testClass, testMethod, configuration, defaultInterceptorCall); + } + TestMethodTestDescriptor(UniqueId uniqueId, String displayName, Class testClass, Method testMethod, JupiterConfiguration configuration, ReflectiveInterceptorCall interceptorCall) { super(uniqueId, displayName, testClass, testMethod, configuration); this.interceptorCall = interceptorCall; } + // --- JupiterTestDescriptor ----------------------------------------------- + + @Override + protected TestMethodTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer) { + return new TestMethodTestDescriptor(uniqueIdTransformer.apply(getUniqueId()), getDisplayName(), getTestClass(), + getTestMethod(), this.configuration, interceptorCall); + } + + // --- TestDescriptor ------------------------------------------------------ + @Override public Type getType() { return Type.TEST; diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateInvocationTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateInvocationTestDescriptor.java index cbb66f12d9b9..2437c297bd3a 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateInvocationTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateInvocationTestDescriptor.java @@ -15,6 +15,7 @@ import java.lang.reflect.Method; import java.util.Set; +import java.util.function.UnaryOperator; import org.apiguardian.api.API; import org.junit.jupiter.api.extension.InvocationInterceptor; @@ -51,6 +52,16 @@ public class TestTemplateInvocationTestDescriptor extends TestMethodTestDescript this.index = index; } + // --- JupiterTestDescriptor ----------------------------------------------- + + @Override + protected TestTemplateInvocationTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer) { + return new TestTemplateInvocationTestDescriptor(uniqueIdTransformer.apply(getUniqueId()), getTestClass(), + getTestMethod(), this.invocationContext, this.index, this.configuration); + } + + // --- TestDescriptor ------------------------------------------------------ + @Override public Set getExclusiveResources() { // Resources are already collected and returned by the enclosing container diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java index e592ba3a6326..05ef02c0c991 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java @@ -10,27 +10,24 @@ package org.junit.jupiter.engine.descriptor; -import static java.util.stream.Collectors.toList; import static org.apiguardian.api.API.Status.INTERNAL; import static org.junit.jupiter.engine.descriptor.ExtensionUtils.populateNewExtensionRegistryFromExtendWithAnnotation; import java.lang.reflect.Method; import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; +import java.util.function.UnaryOperator; import java.util.stream.Stream; import org.apiguardian.api.API; +import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestInstances; import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; -import org.junit.jupiter.engine.extension.ExtensionRegistry; import org.junit.jupiter.engine.extension.MutableExtensionRegistry; -import org.junit.platform.commons.util.Preconditions; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.UniqueId; @@ -44,11 +41,27 @@ public class TestTemplateTestDescriptor extends MethodBasedTestDescriptor implements Filterable { public static final String SEGMENT_TYPE = "test-template"; - private final DynamicDescendantFilter dynamicDescendantFilter = new DynamicDescendantFilter(); + private final DynamicDescendantFilter dynamicDescendantFilter; public TestTemplateTestDescriptor(UniqueId uniqueId, Class testClass, Method templateMethod, Supplier>> enclosingInstanceTypes, JupiterConfiguration configuration) { super(uniqueId, testClass, templateMethod, enclosingInstanceTypes, configuration); + this.dynamicDescendantFilter = new DynamicDescendantFilter(); + } + + private TestTemplateTestDescriptor(UniqueId uniqueId, String displayName, Class testClass, Method templateMethod, + JupiterConfiguration configuration, DynamicDescendantFilter dynamicDescendantFilter) { + super(uniqueId, displayName, testClass, templateMethod, configuration); + this.dynamicDescendantFilter = dynamicDescendantFilter; + } + + // --- JupiterTestDescriptor ----------------------------------------------- + + @Override + protected TestTemplateTestDescriptor withUniqueId(UnaryOperator uniqueIdTransformer) { + return new TestTemplateTestDescriptor(uniqueIdTransformer.apply(getUniqueId()), getDisplayName(), + getTestClass(), getTestMethod(), this.configuration, + this.dynamicDescendantFilter.copy(uniqueIdTransformer)); } // --- Filterable ---------------------------------------------------------- @@ -95,65 +108,59 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte public JupiterEngineExecutionContext execute(JupiterEngineExecutionContext context, DynamicTestExecutor dynamicTestExecutor) throws Exception { - ExtensionContext extensionContext = context.getExtensionContext(); - List providers = validateProviders(extensionContext, - context.getExtensionRegistry()); - AtomicInteger invocationIndex = new AtomicInteger(); - for (TestTemplateInvocationContextProvider provider : providers) { - executeForProvider(provider, invocationIndex, dynamicTestExecutor, extensionContext); - } + new TestTemplateExecutor().execute(context, dynamicTestExecutor); return context; } - private void executeForProvider(TestTemplateInvocationContextProvider provider, AtomicInteger invocationIndex, - DynamicTestExecutor dynamicTestExecutor, ExtensionContext extensionContext) { - - int initialValue = invocationIndex.get(); + private class TestTemplateExecutor + extends TemplateExecutor { - try (Stream stream = invocationContexts(provider, extensionContext)) { - stream.forEach(invocationContext -> toTestDescriptor(invocationContext, invocationIndex.incrementAndGet()) // - .ifPresent(testDescriptor -> execute(dynamicTestExecutor, testDescriptor))); + TestTemplateExecutor() { + super(TestTemplateTestDescriptor.this, TestTemplateInvocationContextProvider.class); } - Preconditions.condition( - invocationIndex.get() != initialValue - || provider.mayReturnZeroTestTemplateInvocationContexts(extensionContext), - String.format( - "Provider [%s] did not provide any invocation contexts, but was expected to do so. " - + "You may override mayReturnZeroTestTemplateInvocationContexts() to allow this.", - provider.getClass().getSimpleName())); - } + @Override + boolean supports(TestTemplateInvocationContextProvider provider, ExtensionContext extensionContext) { + return provider.supportsTestTemplate(extensionContext); + } - private static Stream invocationContexts( - TestTemplateInvocationContextProvider provider, ExtensionContext extensionContext) { - return provider.provideTestTemplateInvocationContexts(extensionContext); - } + @Override + protected String getNoRegisteredProviderErrorMessage() { + return String.format("You must register at least one %s that supports @%s method [%s]", + TestTemplateInvocationContextProvider.class.getSimpleName(), TestTemplate.class.getSimpleName(), + getTestMethod()); + } - private List validateProviders(ExtensionContext extensionContext, - ExtensionRegistry extensionRegistry) { + @Override + Stream provideContexts(TestTemplateInvocationContextProvider provider, + ExtensionContext extensionContext) { + return provider.provideTestTemplateInvocationContexts(extensionContext); + } - // @formatter:off - List providers = extensionRegistry.stream(TestTemplateInvocationContextProvider.class) - .filter(provider -> provider.supportsTestTemplate(extensionContext)) - .collect(toList()); - // @formatter:on + @Override + boolean mayReturnZeroContexts(TestTemplateInvocationContextProvider provider, + ExtensionContext extensionContext) { + return provider.mayReturnZeroTestTemplateInvocationContexts(extensionContext); + } - return Preconditions.notEmpty(providers, - () -> String.format("You must register at least one %s that supports @TestTemplate method [%s]", - TestTemplateInvocationContextProvider.class.getSimpleName(), getTestMethod())); - } + @Override + protected String getZeroContextsProvidedErrorMessage(TestTemplateInvocationContextProvider provider) { + return String.format( + "Provider [%s] did not provide any invocation contexts, but was expected to do so. " + + "You may override mayReturnZeroTestTemplateInvocationContexts() to allow this.", + provider.getClass().getSimpleName()); + } - private Optional toTestDescriptor(TestTemplateInvocationContext invocationContext, int index) { - UniqueId uniqueId = getUniqueId().append(TestTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#" + index); - if (getDynamicDescendantFilter().test(uniqueId, index - 1)) { - return Optional.of(new TestTemplateInvocationTestDescriptor(uniqueId, getTestClass(), getTestMethod(), - invocationContext, index, configuration)); + @Override + UniqueId createInvocationUniqueId(UniqueId parentUniqueId, int index) { + return parentUniqueId.append(TestTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#" + index); } - return Optional.empty(); - } - private void execute(DynamicTestExecutor dynamicTestExecutor, TestDescriptor testDescriptor) { - testDescriptor.setParent(this); - dynamicTestExecutor.execute(testDescriptor); + @Override + TestDescriptor createInvocationTestDescriptor(UniqueId uniqueId, + TestTemplateInvocationContext invocationContext, int index) { + return new TestTemplateInvocationTestDescriptor(uniqueId, getTestClass(), getTestMethod(), + invocationContext, index, TestTemplateTestDescriptor.this.configuration); + } } } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/UniqueIdPrefixTransformer.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/UniqueIdPrefixTransformer.java new file mode 100644 index 000000000000..20a85ba1382b --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/UniqueIdPrefixTransformer.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.engine.descriptor; + +import java.util.List; +import java.util.function.UnaryOperator; + +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.engine.UniqueId; + +/** + * @since 5.13 + */ +class UniqueIdPrefixTransformer implements UnaryOperator { + + private final UniqueId oldPrefix; + private final UniqueId newPrefix; + private final int oldPrefixLength; + + UniqueIdPrefixTransformer(UniqueId oldPrefix, UniqueId newPrefix) { + this.oldPrefix = oldPrefix; + this.newPrefix = newPrefix; + this.oldPrefixLength = oldPrefix.getSegments().size(); + } + + @Override + public UniqueId apply(UniqueId uniqueId) { + Preconditions.condition(uniqueId.hasPrefix(oldPrefix), + () -> String.format("Unique ID %s does not have the expected prefix %s", uniqueId, oldPrefix)); + List oldSegments = uniqueId.getSegments(); + List suffix = oldSegments.subList(oldPrefixLength, oldSegments.size()); + UniqueId newValue = newPrefix; + for (UniqueId.Segment segment : suffix) { + newValue = newValue.append(segment); + } + return newValue; + } +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/ClassSelectorResolver.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/ClassSelectorResolver.java index 0f809dcbde47..b39c360829e0 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/ClassSelectorResolver.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/ClassSelectorResolver.java @@ -10,10 +10,13 @@ package org.junit.jupiter.engine.discovery; +import static java.util.Collections.emptyList; import static java.util.function.Predicate.isEqual; import static java.util.stream.Collectors.toCollection; +import static java.util.stream.Collectors.toSet; import static org.junit.jupiter.engine.descriptor.NestedClassTestDescriptor.getEnclosingTestClasses; import static org.junit.jupiter.engine.discovery.predicates.IsTestClassWithTests.isTestOrTestFactoryOrTestTemplateMethod; +import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated; import static org.junit.platform.commons.support.HierarchyTraversalMode.TOP_DOWN; import static org.junit.platform.commons.support.ReflectionSupport.findMethods; import static org.junit.platform.commons.support.ReflectionSupport.streamNestedClasses; @@ -27,14 +30,21 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Stream; +import org.junit.jupiter.api.ContainerTemplate; +import org.junit.jupiter.api.extension.ContainerTemplateInvocationContext; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor; import org.junit.jupiter.engine.descriptor.ClassTestDescriptor; +import org.junit.jupiter.engine.descriptor.ContainerTemplateInvocationTestDescriptor; +import org.junit.jupiter.engine.descriptor.ContainerTemplateTestDescriptor; +import org.junit.jupiter.engine.descriptor.Filterable; import org.junit.jupiter.engine.descriptor.NestedClassTestDescriptor; +import org.junit.jupiter.engine.descriptor.TestClassAware; import org.junit.jupiter.engine.discovery.predicates.IsNestedTestClass; import org.junit.jupiter.engine.discovery.predicates.IsTestClassWithTests; import org.junit.platform.commons.support.ReflectionSupport; @@ -43,6 +53,7 @@ import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.discovery.ClassSelector; import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.engine.discovery.IterationSelector; import org.junit.platform.engine.discovery.NestedClassSelector; import org.junit.platform.engine.discovery.UniqueIdSelector; import org.junit.platform.engine.support.discovery.SelectorResolver; @@ -54,6 +65,8 @@ class ClassSelectorResolver implements SelectorResolver { private static final IsTestClassWithTests isTestClassWithTests = new IsTestClassWithTests(); private static final IsNestedTestClass isNestedTestClass = new IsNestedTestClass(); + private static final Predicate> isAnnotatedWithContainerTemplate = testClass -> isAnnotated(testClass, + ContainerTemplate.class); private final Predicate classNameFilter; private final JupiterConfiguration configuration; @@ -70,12 +83,12 @@ public Resolution resolve(ClassSelector selector, Context context) { // Nested tests are never filtered out if (classNameFilter.test(testClass.getName())) { return toResolution( - context.addToParent(parent -> Optional.of(newClassTestDescriptor(parent, testClass)))); + context.addToParent(parent -> Optional.of(newStaticClassTestDescriptor(parent, testClass)))); } } else if (isNestedTestClass.test(testClass)) { return toResolution(context.addToParent(() -> DiscoverySelectors.selectClass(testClass.getEnclosingClass()), - parent -> Optional.of(newNestedClassTestDescriptor(parent, testClass)))); + parent -> Optional.of(newMemberClassTestDescriptor(parent, testClass)))); } return unresolved(); } @@ -84,7 +97,7 @@ else if (isNestedTestClass.test(testClass)) { public Resolution resolve(NestedClassSelector selector, Context context) { if (isNestedTestClass.test(selector.getNestedClass())) { return toResolution(context.addToParent(() -> selectClass(selector.getEnclosingClasses()), - parent -> Optional.of(newNestedClassTestDescriptor(parent, selector.getNestedClass())))); + parent -> Optional.of(newMemberClassTestDescriptor(parent, selector.getNestedClass())))); } return unresolved(); } @@ -94,55 +107,177 @@ public Resolution resolve(UniqueIdSelector selector, Context context) { UniqueId uniqueId = selector.getUniqueId(); UniqueId.Segment lastSegment = uniqueId.getLastSegment(); if (ClassTestDescriptor.SEGMENT_TYPE.equals(lastSegment.getType())) { - String className = lastSegment.getValue(); - return ReflectionSupport.tryToLoadClass(className).toOptional().filter(isTestClassWithTests).map( - testClass -> toResolution( - context.addToParent(parent -> Optional.of(newClassTestDescriptor(parent, testClass))))).orElse( - unresolved()); + return resolveStaticClassUniqueId(context, lastSegment, __ -> true, this::newClassTestDescriptor); + } + if (ContainerTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE.equals(lastSegment.getType())) { + return resolveStaticClassUniqueId(context, lastSegment, isAnnotatedWithContainerTemplate, + this::newStaticContainerTemplateTestDescriptor); } if (NestedClassTestDescriptor.SEGMENT_TYPE.equals(lastSegment.getType())) { - String simpleClassName = lastSegment.getValue(); - return toResolution(context.addToParent(() -> selectUniqueId(uniqueId.removeLastSegment()), parent -> { - if (parent instanceof ClassBasedTestDescriptor) { - Class parentTestClass = ((ClassBasedTestDescriptor) parent).getTestClass(); - return ReflectionSupport.findNestedClasses(parentTestClass, - isNestedTestClass.and( - where(Class::getSimpleName, isEqual(simpleClassName)))).stream().findFirst().flatMap( - testClass -> Optional.of(newNestedClassTestDescriptor(parent, testClass))); - } - return Optional.empty(); - })); + return resolveNestedClassUniqueId(context, uniqueId, __ -> true, this::newNestedClassTestDescriptor); + } + if (ContainerTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE.equals(lastSegment.getType())) { + return resolveNestedClassUniqueId(context, uniqueId, isAnnotatedWithContainerTemplate, + this::newNestedContainerTemplateTestDescriptor); + } + if (ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE.equals(lastSegment.getType())) { + Optional testDescriptor = context.addToParent( + () -> selectUniqueId(uniqueId.removeLastSegment()), parent -> { + int index = Integer.parseInt(lastSegment.getValue().substring(1)); + return Optional.of(newDummyContainerTemplateInvocationTestDescriptor(parent, index)); + }); + return toInvocationMatch(testDescriptor) // + .map(Resolution::match) // + .orElse(unresolved()); + } + return unresolved(); + } + + @Override + public Resolution resolve(IterationSelector selector, Context context) { + DiscoverySelector parentSelector = selector.getParentSelector(); + if (parentSelector instanceof ClassSelector + && isAnnotatedWithContainerTemplate.test(((ClassSelector) parentSelector).getJavaClass())) { + return resolveIterations(selector, context); + } + if (parentSelector instanceof NestedClassSelector + && isAnnotatedWithContainerTemplate.test(((NestedClassSelector) parentSelector).getNestedClass())) { + return resolveIterations(selector, context); } return unresolved(); } + private Resolution resolveIterations(IterationSelector selector, Context context) { + DiscoverySelector parentSelector = selector.getParentSelector(); + Set matches = selector.getIterationIndices().stream() // + .map(index -> context.addToParent(() -> parentSelector, + parent -> Optional.of(newDummyContainerTemplateInvocationTestDescriptor(parent, index + 1)))) // + .map(this::toInvocationMatch) // + .filter(Optional::isPresent) // + .map(Optional::get) // + .collect(toSet()); + return matches.isEmpty() ? unresolved() : Resolution.matches(matches); + } + + private Resolution resolveStaticClassUniqueId(Context context, UniqueId.Segment lastSegment, + Predicate> condition, + BiFunction, ClassBasedTestDescriptor> factory) { + + String className = lastSegment.getValue(); + return ReflectionSupport.tryToLoadClass(className).toOptional() // + .filter(isTestClassWithTests) // + .filter(condition) // + .map(testClass -> toResolution( + context.addToParent(parent -> Optional.of(factory.apply(parent, testClass))))) // + .orElse(unresolved()); + } + + private Resolution resolveNestedClassUniqueId(Context context, UniqueId uniqueId, + Predicate> condition, + BiFunction, ClassBasedTestDescriptor> factory) { + + String simpleClassName = uniqueId.getLastSegment().getValue(); + return toResolution(context.addToParent(() -> selectUniqueId(uniqueId.removeLastSegment()), parent -> { + Class parentTestClass = ((TestClassAware) parent).getTestClass(); + return ReflectionSupport.findNestedClasses(parentTestClass, + isNestedTestClass.and(where(Class::getSimpleName, isEqual(simpleClassName)))).stream() // + .findFirst() // + .filter(condition) // + .map(testClass -> factory.apply(parent, testClass)); + })); + } + + private ContainerTemplateInvocationTestDescriptor newDummyContainerTemplateInvocationTestDescriptor( + TestDescriptor parent, int index) { + UniqueId uniqueId = parent.getUniqueId().append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#" + index); + return new ContainerTemplateInvocationTestDescriptor(uniqueId, (ContainerTemplateTestDescriptor) parent, + DummyContainerTemplateInvocationContext.INSTANCE, index, parent.getSource().orElse(null), configuration); + } + + private ClassBasedTestDescriptor newStaticClassTestDescriptor(TestDescriptor parent, Class testClass) { + return isAnnotatedWithContainerTemplate.test(testClass) // + ? newStaticContainerTemplateTestDescriptor(parent, testClass) // + : newClassTestDescriptor(parent, testClass); + } + + private ContainerTemplateTestDescriptor newStaticContainerTemplateTestDescriptor(TestDescriptor parent, + Class testClass) { + return newContainerTemplateTestDescriptor(parent, ContainerTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + newClassTestDescriptor(parent, testClass)); + } + private ClassTestDescriptor newClassTestDescriptor(TestDescriptor parent, Class testClass) { return new ClassTestDescriptor( parent.getUniqueId().append(ClassTestDescriptor.SEGMENT_TYPE, testClass.getName()), testClass, configuration); } + private ClassBasedTestDescriptor newMemberClassTestDescriptor(TestDescriptor parent, Class testClass) { + return isAnnotatedWithContainerTemplate.test(testClass) // + ? newNestedContainerTemplateTestDescriptor(parent, testClass) // + : newNestedClassTestDescriptor(parent, testClass); + } + + private ContainerTemplateTestDescriptor newNestedContainerTemplateTestDescriptor(TestDescriptor parent, + Class testClass) { + return newContainerTemplateTestDescriptor(parent, ContainerTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE, + newNestedClassTestDescriptor(parent, testClass)); + } + private NestedClassTestDescriptor newNestedClassTestDescriptor(TestDescriptor parent, Class testClass) { UniqueId uniqueId = parent.getUniqueId().append(NestedClassTestDescriptor.SEGMENT_TYPE, testClass.getSimpleName()); return new NestedClassTestDescriptor(uniqueId, testClass, () -> getEnclosingTestClasses(parent), configuration); } + private ContainerTemplateTestDescriptor newContainerTemplateTestDescriptor(TestDescriptor parent, + String segmentType, ClassBasedTestDescriptor delegate) { + + delegate.setParent(parent); + String segmentValue = delegate.getUniqueId().getLastSegment().getValue(); + UniqueId uniqueId = parent.getUniqueId().append(segmentType, segmentValue); + return new ContainerTemplateTestDescriptor(uniqueId, delegate); + } + + private Optional toInvocationMatch(Optional testDescriptor) { + return testDescriptor // + .map(it -> Match.exact(it, expansionCallback(it, + () -> it.getParent().map(parent -> getTestClasses((TestClassAware) parent)).orElse(emptyList())))); + } + private Resolution toResolution(Optional testDescriptor) { - return testDescriptor.map(it -> { - Class testClass = it.getTestClass(); - List> testClasses = new ArrayList<>(it.getEnclosingTestClasses()); - testClasses.add(testClass); - // @formatter:off - return Resolution.match(Match.exact(it, () -> { - Stream methods = findMethods(testClass, isTestOrTestFactoryOrTestTemplateMethod, TOP_DOWN).stream() - .map(method -> selectMethod(testClasses, method)); - Stream nestedClasses = streamNestedClasses(testClass, isNestedTestClass) - .map(nestedClass -> DiscoverySelectors.selectNestedClass(testClasses, nestedClass)); - return Stream.concat(methods, nestedClasses).collect(toCollection((Supplier>) LinkedHashSet::new)); - })); - // @formatter:on - }).orElse(unresolved()); + return testDescriptor // + .map(it -> Resolution.match(Match.exact(it, expansionCallback(it)))) // + .orElse(unresolved()); + } + + private Supplier> expansionCallback(ClassBasedTestDescriptor testDescriptor) { + return expansionCallback(testDescriptor, () -> getTestClasses(testDescriptor)); + } + + private static List> getTestClasses(TestClassAware testDescriptor) { + List> testClasses = new ArrayList<>(testDescriptor.getEnclosingTestClasses()); + testClasses.add(testDescriptor.getTestClass()); + return testClasses; + } + + private Supplier> expansionCallback(TestDescriptor testDescriptor, + Supplier>> testClassesSupplier) { + return () -> { + if (testDescriptor instanceof Filterable) { + Filterable filterable = (Filterable) testDescriptor; + filterable.getDynamicDescendantFilter().allowAll(); + } + List> testClasses = testClassesSupplier.get(); + Class testClass = testClasses.get(testClasses.size() - 1); + Stream methods = findMethods(testClass, isTestOrTestFactoryOrTestTemplateMethod, + TOP_DOWN).stream().map(method -> selectMethod(testClasses, method)); + Stream nestedClasses = streamNestedClasses(testClass, isNestedTestClass).map( + nestedClass -> DiscoverySelectors.selectNestedClass(testClasses, nestedClass)); + return Stream.concat(methods, nestedClasses).collect( + toCollection((Supplier>) LinkedHashSet::new)); + }; } private DiscoverySelector selectClass(List> classes) { @@ -161,4 +296,7 @@ private DiscoverySelector selectMethod(List> classes, Method method) { return DiscoverySelectors.selectNestedMethod(classes.subList(0, lastIndex), classes.get(lastIndex), method); } + static class DummyContainerTemplateInvocationContext implements ContainerTemplateInvocationContext { + private static final DummyContainerTemplateInvocationContext INSTANCE = new DummyContainerTemplateInvocationContext(); + } } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/DiscoverySelectorResolver.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/DiscoverySelectorResolver.java index a828889cc000..97f4e8da22fd 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/DiscoverySelectorResolver.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/DiscoverySelectorResolver.java @@ -13,11 +13,14 @@ import static org.apiguardian.api.API.Status.INTERNAL; import org.apiguardian.api.API; +import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor; +import org.junit.jupiter.engine.descriptor.JupiterTestDescriptor; import org.junit.jupiter.engine.discovery.predicates.IsTestClassWithTests; import org.junit.platform.engine.EngineDiscoveryRequest; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolver; +import org.junit.platform.engine.support.discovery.EngineDiscoveryRequestResolver.InitializationContext; /** * {@code DiscoverySelectorResolver} resolves {@link TestDescriptor TestDescriptors} @@ -33,16 +36,22 @@ @API(status = INTERNAL, since = "5.0") public class DiscoverySelectorResolver { - // @formatter:off - private static final EngineDiscoveryRequestResolver resolver = EngineDiscoveryRequestResolver.builder() - .addClassContainerSelectorResolver(new IsTestClassWithTests()) - .addSelectorResolver(context -> new ClassSelectorResolver(context.getClassNameFilter(), context.getEngineDescriptor().getConfiguration())) - .addSelectorResolver(context -> new MethodSelectorResolver(context.getEngineDescriptor().getConfiguration())) - .addTestDescriptorVisitor(context -> new ClassOrderingVisitor(context.getEngineDescriptor().getConfiguration())) - .addTestDescriptorVisitor(context -> new MethodOrderingVisitor(context.getEngineDescriptor().getConfiguration())) - .addTestDescriptorVisitor(context -> TestDescriptor::prune) + private static final EngineDiscoveryRequestResolver resolver = EngineDiscoveryRequestResolver. builder() // + .addClassContainerSelectorResolver(new IsTestClassWithTests()) // + .addSelectorResolver(ctx -> new ClassSelectorResolver(ctx.getClassNameFilter(), getConfiguration(ctx))) // + .addSelectorResolver(ctx -> new MethodSelectorResolver(getConfiguration(ctx))) // + .addTestDescriptorVisitor(ctx -> new ClassOrderingVisitor(getConfiguration(ctx))) // + .addTestDescriptorVisitor(ctx -> new MethodOrderingVisitor(getConfiguration(ctx))) // + .addTestDescriptorVisitor(ctx -> descriptor -> { + if (descriptor instanceof JupiterTestDescriptor) { + ((JupiterTestDescriptor) descriptor).prunePriorToFiltering(); + } + }) // .build(); - // @formatter:on + + private static JupiterConfiguration getConfiguration(InitializationContext context) { + return context.getEngineDescriptor().getConfiguration(); + } public void resolveSelectors(EngineDiscoveryRequest request, JupiterEngineDescriptor engineDescriptor) { resolver.resolve(request, engineDescriptor); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/MethodSelectorResolver.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/MethodSelectorResolver.java index 9d5af96aa103..44479f7eb0c3 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/MethodSelectorResolver.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/discovery/MethodSelectorResolver.java @@ -30,8 +30,8 @@ import java.util.stream.Stream; import org.junit.jupiter.engine.config.JupiterConfiguration; -import org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor; import org.junit.jupiter.engine.descriptor.Filterable; +import org.junit.jupiter.engine.descriptor.TestClassAware; import org.junit.jupiter.engine.descriptor.TestFactoryTestDescriptor; import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor; import org.junit.jupiter.engine.descriptor.TestTemplateInvocationTestDescriptor; @@ -208,8 +208,7 @@ private Optional resolve(List> enclosingClasses, Class< return Optional.empty(); } return context.addToParent(() -> selectClass(enclosingClasses, testClass), // - parent -> Optional.of( - createTestDescriptor((ClassBasedTestDescriptor) parent, testClass, method, configuration))); + parent -> Optional.of(createTestDescriptor(parent, testClass, method, configuration))); } private DiscoverySelector selectClass(List> enclosingClasses, Class testClass) { @@ -225,11 +224,11 @@ private Optional resolveUniqueIdIntoTestDescriptor(UniqueId uniq if (segmentType.equals(lastSegment.getType())) { return context.addToParent(() -> selectUniqueId(uniqueId.removeLastSegment()), parent -> { String methodSpecPart = lastSegment.getValue(); - Class testClass = ((ClassBasedTestDescriptor) parent).getTestClass(); + Class testClass = ((TestClassAware) parent).getTestClass(); // @formatter:off return methodFinder.findMethod(methodSpecPart, testClass) .filter(methodPredicate) - .map(method -> createTestDescriptor((ClassBasedTestDescriptor) parent, testClass, method, configuration)); + .map(method -> createTestDescriptor(parent, testClass, method, configuration)); // @formatter:on }); } @@ -239,10 +238,11 @@ private Optional resolveUniqueIdIntoTestDescriptor(UniqueId uniq return Optional.empty(); } - private TestDescriptor createTestDescriptor(ClassBasedTestDescriptor parent, Class testClass, Method method, + private TestDescriptor createTestDescriptor(TestDescriptor parent, Class testClass, Method method, JupiterConfiguration configuration) { UniqueId uniqueId = createUniqueId(method, parent); - return createTestDescriptor(uniqueId, testClass, method, parent::getEnclosingTestClasses, configuration); + return createTestDescriptor(uniqueId, testClass, method, ((TestClassAware) parent)::getEnclosingTestClasses, + configuration); } private UniqueId createUniqueId(Method method, TestDescriptor parent) { diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ConditionEvaluator.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ConditionEvaluator.java index e1e0a4a89617..e51126bec2ef 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ConditionEvaluator.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ConditionEvaluator.java @@ -72,7 +72,7 @@ private ConditionEvaluationResult evaluate(ExecutionCondition condition, Extensi private void logResult(Class conditionType, ConditionEvaluationResult result, ExtensionContext context) { logger.trace(() -> format("Evaluation of condition [%s] on [%s] resulted in: %s", conditionType.getName(), - context.getElement().get(), result)); + context.getElement().orElse(null), result)); } private ConditionEvaluationException evaluationException(Class conditionType, Exception ex) { diff --git a/junit-jupiter-engine/src/testFixtures/java/org/junit/jupiter/engine/discovery/JupiterUniqueIdBuilder.java b/junit-jupiter-engine/src/testFixtures/java/org/junit/jupiter/engine/discovery/JupiterUniqueIdBuilder.java index 063d0527103f..5504efad7eaf 100644 --- a/junit-jupiter-engine/src/testFixtures/java/org/junit/jupiter/engine/discovery/JupiterUniqueIdBuilder.java +++ b/junit-jupiter-engine/src/testFixtures/java/org/junit/jupiter/engine/discovery/JupiterUniqueIdBuilder.java @@ -12,13 +12,18 @@ import static org.junit.platform.commons.util.ReflectionUtils.isInnerClass; +import org.junit.jupiter.api.ContainerTemplate; import org.junit.jupiter.engine.descriptor.ClassTestDescriptor; +import org.junit.jupiter.engine.descriptor.ContainerTemplateInvocationTestDescriptor; +import org.junit.jupiter.engine.descriptor.ContainerTemplateTestDescriptor; import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor; import org.junit.jupiter.engine.descriptor.NestedClassTestDescriptor; import org.junit.jupiter.engine.descriptor.TestFactoryTestDescriptor; import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor; import org.junit.jupiter.engine.descriptor.TestTemplateInvocationTestDescriptor; import org.junit.jupiter.engine.descriptor.TestTemplateTestDescriptor; +import org.junit.platform.commons.support.AnnotationSupport; +import org.junit.platform.commons.support.ReflectionSupport; import org.junit.platform.engine.UniqueId; /** @@ -31,16 +36,30 @@ public class JupiterUniqueIdBuilder { public static UniqueId uniqueIdForClass(Class clazz) { - UniqueId containerId = engineId(); if (isInnerClass(clazz)) { - containerId = uniqueIdForClass(clazz.getEnclosingClass()); - return containerId.append(NestedClassTestDescriptor.SEGMENT_TYPE, clazz.getSimpleName()); + var segmentType = classSegmentType(clazz, NestedClassTestDescriptor.SEGMENT_TYPE, + ContainerTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE); + return uniqueIdForClass(clazz.getEnclosingClass()).append(segmentType, clazz.getSimpleName()); } - return containerId.append(ClassTestDescriptor.SEGMENT_TYPE, clazz.getName()); + return uniqueIdForStaticClass(clazz.getName()); } - public static UniqueId uniqueIdForTopLevelClass(String className) { - return engineId().append(ClassTestDescriptor.SEGMENT_TYPE, className); + public static UniqueId uniqueIdForStaticClass(String className) { + return engineId().append(staticClassSegmentType(className), className); + } + + private static String staticClassSegmentType(String className) { + return ReflectionSupport.tryToLoadClass(className).toOptional() // + .map(it -> classSegmentType(it, ClassTestDescriptor.SEGMENT_TYPE, + ContainerTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE)) // + .orElse(ClassTestDescriptor.SEGMENT_TYPE); + } + + private static String classSegmentType(Class clazz, String regularSegmentType, + String containerTemplateSegmentType) { + return AnnotationSupport.isAnnotated(clazz, ContainerTemplate.class) // + ? containerTemplateSegmentType // + : regularSegmentType; } public static UniqueId uniqueIdForMethod(Class clazz, String methodPart) { @@ -59,6 +78,10 @@ public static UniqueId appendTestTemplateInvocationSegment(UniqueId parentId, in return parentId.append(TestTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#" + index); } + public static UniqueId appendContainerTemplateInvocationSegment(UniqueId parentId, int index) { + return parentId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#" + index); + } + public static UniqueId engineId() { return UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); } diff --git a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EventConditions.java b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EventConditions.java index 5f260fe3dc14..4be237c109e0 100644 --- a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EventConditions.java +++ b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EventConditions.java @@ -249,6 +249,32 @@ public static Condition dynamicTestRegistered(Condition condition) return allOf(type(DYNAMIC_TEST_REGISTERED), condition); } + /** + * Create a new {@link Condition} that matches if and only if the + * {@linkplain TestDescriptor#getUniqueId() unique id} of an {@link Event}'s + * {@linkplain Event#getTestDescriptor() test descriptor} is equal to the + * {@link UniqueId} parsed from the supplied {@link String}. + * + * @since 1.13 + */ + @API(status = EXPERIMENTAL, since = "1.13") + public static Condition uniqueId(String uniqueId) { + return uniqueId(UniqueId.parse(uniqueId)); + } + + /** + * Create a new {@link Condition} that matches if and only if the + * {@linkplain TestDescriptor#getUniqueId() unique id} of an {@link Event}'s + * {@linkplain Event#getTestDescriptor() test descriptor} is equal to the + * supplied {@link UniqueId}. + * + * @since 1.13 + */ + @API(status = EXPERIMENTAL, since = "1.13") + public static Condition uniqueId(UniqueId uniqueId) { + return uniqueId(new Condition<>(isEqual(uniqueId), "equal to '%s'", uniqueId)); + } + /** * Create a new {@link Condition} that matches if and only if the * {@linkplain TestDescriptor#getUniqueId() unique id} of an @@ -260,11 +286,22 @@ public static Condition uniqueIdSubstring(String uniqueIdSubstring) { String text = segment.getType() + ":" + segment.getValue(); return text.contains(uniqueIdSubstring); }; + return uniqueId(new Condition<>(uniqueId -> uniqueId.getSegments().stream().anyMatch(predicate), + "substring '%s'", uniqueIdSubstring)); + } - return new Condition<>( - byTestDescriptor( - where(TestDescriptor::getUniqueId, uniqueId -> uniqueId.getSegments().stream().anyMatch(predicate))), - "descriptor with uniqueId substring '%s'", uniqueIdSubstring); + /** + * Create a new {@link Condition} that matches if and only if the + * {@linkplain TestDescriptor#getUniqueId() unique id} of an {@link Event}'s + * {@linkplain Event#getTestDescriptor() test descriptor} matches the + * supplied {@link Condition}. + * + * @since 1.13 + */ + @API(status = EXPERIMENTAL, since = "1.13") + public static Condition uniqueId(Condition condition) { + return new Condition<>(byTestDescriptor(where(TestDescriptor::getUniqueId, condition::matches)), + "descriptor with uniqueId %s", condition.description().value()); } /** @@ -315,6 +352,21 @@ public static Condition displayName(String displayName) { "descriptor with display name '%s'", displayName); } + /** + * Create a new {@link Condition} that matches if and only if the + * {@linkplain TestDescriptor#getLegacyReportingName()} () legacy reporting name} + * of an {@link Event}'s {@linkplain Event#getTestDescriptor() test descriptor} + * is equal to the supplied {@link String}. + * + * @since 1.13 + */ + @API(status = EXPERIMENTAL, since = "1.13") + public static Condition legacyReportingName(String legacyReportingName) { + return new Condition<>( + byTestDescriptor(where(TestDescriptor::getLegacyReportingName, isEqual(legacyReportingName))), + "descriptor with legacy reporting name '%s'", legacyReportingName); + } + /** * Create a new {@link Condition} that matches if and only if an * {@link Event}'s {@linkplain Event#getType() type} is diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/DisplayNameGenerationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/DisplayNameGenerationTests.java index 898dfc1ea9e8..61550ce2676f 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/api/DisplayNameGenerationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/DisplayNameGenerationTests.java @@ -22,7 +22,12 @@ import java.util.EmptyStackException; import java.util.List; import java.util.Stack; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.ContainerTemplateInvocationContext; +import org.junit.jupiter.api.extension.ContainerTemplateInvocationContextProvider; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; import org.junit.platform.engine.TestDescriptor; @@ -225,6 +230,32 @@ void indicativeSentencesOnSubClass() { ); } + @Test + void indicativeSentencesOnContainerTemplate() { + check(ContainerTemplateTestCase.class, // + "CONTAINER: Container template", // + "TEST: Container template, some test", // + "CONTAINER: Container template, Regular Nested Test Case", // + "TEST: Container template, Regular Nested Test Case, some nested test", // + "CONTAINER: Container template, Nested Container Template", // + "TEST: Container template, Nested Container Template, some nested test" // + ); + + assertThat(executeTestsForClass(ContainerTemplateTestCase.class).allEvents().started().stream()) // + .map(event -> event.getTestDescriptor().getDisplayName()) // + .containsExactly( // + "JUnit Jupiter", // + "Container template", // + "[1] Container template", // + "Container template, some test", // + "Container template, Regular Nested Test Case", // + "Container template, Regular Nested Test Case, some nested test", // + "Container template, Nested Container Template", // + "[1] Container template, Nested Container Template", // + "Container template, Nested Container Template, some nested test" // + ); + } + private void check(Class testClass, String... expectedDisplayNames) { var request = request().selectors(selectClass(testClass)).build(); var descriptors = discoverTests(request).getDescendants(); @@ -470,4 +501,55 @@ void is_no_longer_empty() { } } } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ContainerTemplate + @ExtendWith(ContainerTemplateTestCase.Once.class) + @DisplayName("Container template") + @IndicativeSentencesGeneration(generator = DisplayNameGenerator.ReplaceUnderscores.class) + @TestClassOrder(ClassOrderer.OrderAnnotation.class) + static class ContainerTemplateTestCase { + + @Test + void some_test() { + } + + @Nested + @Order(1) + class Regular_Nested_Test_Case { + @Test + void some_nested_test() { + } + } + + @Nested + @Order(2) + @ContainerTemplate + class Nested_Container_Template { + @Test + void some_nested_test() { + } + } + + private static class Once implements ContainerTemplateInvocationContextProvider { + + @Override + public boolean supportsContainerTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideContainerTemplateInvocationContexts( + ExtensionContext context) { + return Stream.of(new ContainerTemplateInvocationContext() { + @Override + public String getDisplayName(int invocationIndex) { + return "%s %s".formatted( + ContainerTemplateInvocationContext.super.getDisplayName(invocationIndex), + context.getDisplayName()); + } + }); + } + } + } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/KitchenSinkExtension.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/KitchenSinkExtension.java index 0d77cac0882e..73c0e3edc3d8 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/KitchenSinkExtension.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/KitchenSinkExtension.java @@ -55,8 +55,9 @@ public class KitchenSinkExtension implements // Conditional Test Execution ExecutionCondition, - // @TestTemplate + // @TestTemplate and @ContainerTemplate TestTemplateInvocationContextProvider, + ContainerTemplateInvocationContextProvider, // Miscellaneous TestWatcher, @@ -174,6 +175,24 @@ public boolean mayReturnZeroTestTemplateInvocationContexts(ExtensionContext cont return false; } + // --- @ContainerTemplate ------------------------------------------------------- + + @Override + public boolean supportsContainerTemplate(ExtensionContext context) { + return false; + } + + @Override + public Stream provideContainerTemplateInvocationContexts( + ExtensionContext context) { + return null; + } + + @Override + public boolean mayReturnZeroContainerTemplateInvocationContexts(ExtensionContext context) { + return false; + } + // --- TestWatcher --------------------------------------------------------- @Override diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/parallel/ResourceLockAnnotationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/parallel/ResourceLockAnnotationTests.java index 60e7c36b808f..337224b61eff 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/api/parallel/ResourceLockAnnotationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/parallel/ResourceLockAnnotationTests.java @@ -12,6 +12,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.util.Throwables.getRootCause; +import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectIteration; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; import static org.junit.platform.engine.support.hierarchical.ExclusiveResource.LockMode; import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; @@ -25,6 +29,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.ContainerTemplate; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Nested; @@ -34,12 +39,14 @@ import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.descriptor.ClassTestDescriptor; +import org.junit.jupiter.engine.descriptor.JupiterTestDescriptor; import org.junit.jupiter.engine.descriptor.NestedClassTestDescriptor; import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.junit.platform.commons.JUnitException; +import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.support.hierarchical.ExclusiveResource; @@ -180,6 +187,76 @@ void addSharedResourcesViaAnnotationValueAndProviders() { // @formatter:on } + @Test + void addSharedResourcesViaAnnotationValueAndProvidersForContainerTemplate() { + var engineDescriptor = discoverTests( + selectClass(SharedResourcesViaAnnotationValueAndProvidersContainerTemplateTestCase.class)); + engineDescriptor.accept(TestDescriptor::prune); + + var containerTemplateTestDescriptor = (JupiterTestDescriptor) getOnlyElement(engineDescriptor.getChildren()); + + var expectedResources = List.of( // + new ExclusiveResource("a1", LockMode.READ_WRITE), // + new ExclusiveResource("a2", LockMode.READ_WRITE), // + new ExclusiveResource("a3", LockMode.READ), // + new ExclusiveResource("b1", LockMode.READ), // + new ExclusiveResource("b2", LockMode.READ), // + new ExclusiveResource("c1", LockMode.READ_WRITE), // + new ExclusiveResource("c2", LockMode.READ_WRITE), // + new ExclusiveResource("c3", LockMode.READ_WRITE), // + new ExclusiveResource("d1", LockMode.READ_WRITE), // + new ExclusiveResource("d2", LockMode.READ) // + ); + + assertThat(containerTemplateTestDescriptor.getExclusiveResources()) // + .containsExactlyInAnyOrderElementsOf(expectedResources); + } + + @Test + void addSharedResourcesViaAnnotationValueAndProvidersForContainerTemplateInvocation() { + var engineDescriptor = discoverTests(selectIteration( + selectClass(SharedResourcesViaAnnotationValueAndProvidersContainerTemplateTestCase.class), 0)); + engineDescriptor.accept(TestDescriptor::prune); + + var containerTemplateTestDescriptor = (JupiterTestDescriptor) getOnlyElement(engineDescriptor.getChildren()); + + var expectedResources = List.of( // + new ExclusiveResource("a1", LockMode.READ_WRITE), // + new ExclusiveResource("a2", LockMode.READ_WRITE), // + new ExclusiveResource("a3", LockMode.READ), // + new ExclusiveResource("b1", LockMode.READ), // + new ExclusiveResource("b2", LockMode.READ), // + new ExclusiveResource("c1", LockMode.READ_WRITE), // + new ExclusiveResource("c2", LockMode.READ_WRITE), // + new ExclusiveResource("c3", LockMode.READ_WRITE), // + new ExclusiveResource("d1", LockMode.READ_WRITE), // + new ExclusiveResource("d2", LockMode.READ) // + ); + + assertThat(containerTemplateTestDescriptor.getExclusiveResources()) // + .containsExactlyInAnyOrderElementsOf(expectedResources); + } + + @Test + void addSharedResourcesViaAnnotationValueAndProvidersForMethodInContainerTemplate() { + var engineDescriptor = discoverTests( + selectMethod(SharedResourcesViaAnnotationValueAndProvidersContainerTemplateTestCase.class, "test")); + engineDescriptor.accept(TestDescriptor::prune); + + var containerTemplateTestDescriptor = (JupiterTestDescriptor) getOnlyElement(engineDescriptor.getChildren()); + + var expectedResources = List.of( // + new ExclusiveResource("a1", LockMode.READ_WRITE), // + new ExclusiveResource("a2", LockMode.READ_WRITE), // + new ExclusiveResource("a3", LockMode.READ), // + new ExclusiveResource("b1", LockMode.READ), // + new ExclusiveResource("b2", LockMode.READ) // + ); + + assertThat(containerTemplateTestDescriptor.getExclusiveResources()) // + .containsExactlyInAnyOrderElementsOf(expectedResources); + } + @Test void sharedResourcesHavingTheSameValueAndModeAreDeduplicated() { // @formatter:off @@ -523,4 +600,71 @@ class NestedClass { } } + @SuppressWarnings("JUnitMalformedDeclaration") + @ContainerTemplate + @ResourceLock( // + value = "a1", // + providers = SharedResourcesViaAnnotationValueAndProvidersContainerTemplateTestCase.FirstClassLevelProvider.class // + ) + @ResourceLock( // + value = "a2", // + target = ResourceLockTarget.CHILDREN, // + providers = SharedResourcesViaAnnotationValueAndProvidersContainerTemplateTestCase.SecondClassLevelProvider.class // + ) + static class SharedResourcesViaAnnotationValueAndProvidersContainerTemplateTestCase { + + @Test + @ResourceLock(value = "b1", mode = ResourceAccessMode.READ) + void test() { + } + + @Nested + @ResourceLock(providers = NestedClassLevelProvider.class) + class NestedClass { + @Test + @ResourceLock("c1") + void test() { + } + } + + @Nested + @ContainerTemplate + @ResourceLock(value = "d1", target = ResourceLockTarget.CHILDREN) + class NestedContainerTemplate { + @Test + @ResourceLock(value = "d2", mode = ResourceAccessMode.READ) + void test() { + } + } + + static class FirstClassLevelProvider implements ResourceLocksProvider { + + @Override + public Set provideForClass(Class testClass) { + return Set.of(new Lock("a3", ResourceAccessMode.READ)); + } + } + + static class SecondClassLevelProvider implements ResourceLocksProvider { + + @Override + public Set provideForMethod(List> enclosingInstanceTypes, Class testClass, + Method testMethod) { + return Set.of(new Lock("b2", ResourceAccessMode.READ)); + } + + @Override + public Set provideForNestedClass(List> enclosingInstanceTypes, Class testClass) { + return Set.of(new Lock("c2")); + } + } + + static class NestedClassLevelProvider implements ResourceLocksProvider { + + @Override + public Set provideForNestedClass(List> enclosingInstanceTypes, Class testClass) { + return Set.of(new Lock("c3")); + } + } + } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/AbstractJupiterTestEngineTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/AbstractJupiterTestEngineTests.java index 3ec1035ebb5c..194abc7393db 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/AbstractJupiterTestEngineTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/AbstractJupiterTestEngineTests.java @@ -13,10 +13,12 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; +import static org.junit.platform.launcher.LauncherConstants.STACKTRACE_PRUNING_ENABLED_PROPERTY_NAME; import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import java.util.Set; +import java.util.function.Consumer; import org.junit.platform.engine.DiscoverySelector; import org.junit.platform.engine.TestDescriptor; @@ -40,7 +42,13 @@ protected EngineExecutionResults executeTestsForClass(Class testClass) { } protected EngineExecutionResults executeTests(DiscoverySelector... selectors) { - return executeTests(request().selectors(selectors).outputDirectoryProvider(dummyOutputDirectoryProvider())); + return executeTests(request -> request.selectors(selectors)); + } + + protected EngineExecutionResults executeTests(Consumer configurer) { + var builder = defaultRequest(); + configurer.accept(builder); + return executeTests(builder); } protected EngineExecutionResults executeTests(LauncherDiscoveryRequestBuilder builder) { @@ -52,8 +60,13 @@ protected EngineExecutionResults executeTests(LauncherDiscoveryRequest request) } protected TestDescriptor discoverTests(DiscoverySelector... selectors) { - return discoverTests( - request().selectors(selectors).outputDirectoryProvider(dummyOutputDirectoryProvider()).build()); + return discoverTests(defaultRequest().selectors(selectors).build()); + } + + private static LauncherDiscoveryRequestBuilder defaultRequest() { + return request() // + .outputDirectoryProvider(dummyOutputDirectoryProvider()) // + .configurationParameter(STACKTRACE_PRUNING_ENABLED_PROPERTY_NAME, String.valueOf(false)); } protected TestDescriptor discoverTests(LauncherDiscoveryRequest request) { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/ContainerTemplateInvocationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/ContainerTemplateInvocationTests.java new file mode 100644 index 000000000000..e621393f1f80 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/ContainerTemplateInvocationTests.java @@ -0,0 +1,1257 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.engine; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectIteration; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectNestedClass; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectNestedMethod; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; +import static org.junit.platform.launcher.TagFilter.excludeTags; +import static org.junit.platform.launcher.TagFilter.includeTags; +import static org.junit.platform.testkit.engine.EventConditions.container; +import static org.junit.platform.testkit.engine.EventConditions.displayName; +import static org.junit.platform.testkit.engine.EventConditions.dynamicTestRegistered; +import static org.junit.platform.testkit.engine.EventConditions.engine; +import static org.junit.platform.testkit.engine.EventConditions.event; +import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully; +import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; +import static org.junit.platform.testkit.engine.EventConditions.legacyReportingName; +import static org.junit.platform.testkit.engine.EventConditions.started; +import static org.junit.platform.testkit.engine.EventConditions.test; +import static org.junit.platform.testkit.engine.EventConditions.uniqueId; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.ContainerTemplate; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestReporter; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ContainerTemplateInvocationContext; +import org.junit.jupiter.api.extension.ContainerTemplateInvocationContextProvider; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.engine.descriptor.ClassTestDescriptor; +import org.junit.jupiter.engine.descriptor.ContainerTemplateInvocationTestDescriptor; +import org.junit.jupiter.engine.descriptor.ContainerTemplateTestDescriptor; +import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor; +import org.junit.jupiter.engine.descriptor.NestedClassTestDescriptor; +import org.junit.jupiter.engine.descriptor.TestFactoryTestDescriptor; +import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor; +import org.junit.jupiter.engine.descriptor.TestTemplateInvocationTestDescriptor; +import org.junit.jupiter.engine.descriptor.TestTemplateTestDescriptor; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.engine.reporting.ReportEntry; + +/** + * @since 5.13 + */ +public class ContainerTemplateInvocationTests extends AbstractJupiterTestEngineTests { + + @ParameterizedTest + @ValueSource(strings = { // + "class:org.junit.jupiter.engine.ContainerTemplateInvocationTests$TwoInvocationsTestCase", // + "uid:[engine:junit-jupiter]/[container-template:org.junit.jupiter.engine.ContainerTemplateInvocationTests$TwoInvocationsTestCase]" // + }) + void executesContainerTemplateClassTwice(String selectorIdentifier) { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var containerTemplateId = engineId.append(ContainerTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + TwoInvocationsTestCase.class.getName()); + var invocationId1 = containerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#1"); + var invocation1MethodAId = invocationId1.append(TestMethodTestDescriptor.SEGMENT_TYPE, "a()"); + var invocation1NestedClassId = invocationId1.append(NestedClassTestDescriptor.SEGMENT_TYPE, "NestedTestCase"); + var invocation1NestedMethodBId = invocation1NestedClassId.append(TestMethodTestDescriptor.SEGMENT_TYPE, "b()"); + var invocationId2 = containerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var invocation2MethodAId = invocationId2.append(TestMethodTestDescriptor.SEGMENT_TYPE, "a()"); + var invocation2NestedClassId = invocationId2.append(NestedClassTestDescriptor.SEGMENT_TYPE, "NestedTestCase"); + var invocation2NestedMethodBId = invocation2NestedClassId.append(TestMethodTestDescriptor.SEGMENT_TYPE, "b()"); + + var results = executeTests(DiscoverySelectors.parse(selectorIdentifier).orElseThrow()); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(containerTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId1)), displayName("[1] A of TwoInvocationsTestCase"), + legacyReportingName("%s[1]".formatted(TwoInvocationsTestCase.class.getName()))), // + event(container(uniqueId(invocationId1)), started()), // + event(dynamicTestRegistered(uniqueId(invocation1MethodAId))), // + event(dynamicTestRegistered(uniqueId(invocation1NestedClassId))), // + event(dynamicTestRegistered(uniqueId(invocation1NestedMethodBId))), // + event(test(uniqueId(invocation1MethodAId)), started()), // + event(test(uniqueId(invocation1MethodAId)), finishedSuccessfully()), // + event(container(uniqueId(invocation1NestedClassId)), started()), // + event(test(uniqueId(invocation1NestedMethodBId)), started()), // + event(test(uniqueId(invocation1NestedMethodBId)), finishedSuccessfully()), // + event(container(uniqueId(invocation1NestedClassId)), finishedSuccessfully()), // + event(container(uniqueId(invocationId1)), finishedSuccessfully()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), displayName("[2] B of TwoInvocationsTestCase"), + legacyReportingName("%s[2]".formatted(TwoInvocationsTestCase.class.getName()))), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(invocation2MethodAId))), // + event(dynamicTestRegistered(uniqueId(invocation2NestedClassId))), // + event(dynamicTestRegistered(uniqueId(invocation2NestedMethodBId))), // + event(test(uniqueId(invocation2MethodAId)), started()), // + event(test(uniqueId(invocation2MethodAId)), finishedSuccessfully()), // + event(container(uniqueId(invocation2NestedClassId)), started()), // + event(test(uniqueId(invocation2NestedMethodBId)), started()), // + event(test(uniqueId(invocation2NestedMethodBId)), finishedSuccessfully()), // + event(container(uniqueId(invocation2NestedClassId)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(containerTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void executesOnlySelectedMethodsDeclaredInContainerTemplate() { + var results = executeTests(selectMethod(TwoInvocationsTestCase.class, "a")); + + results.testEvents() // + .assertStatistics(stats -> stats.started(2).succeeded(2)) // + .assertEventsMatchLoosely(event(test(displayName("a()")), finishedSuccessfully())); + } + + @Test + void executesOnlySelectedMethodsDeclaredInNestedClassOfContainerTemplate() { + var results = executeTests(selectNestedMethod(List.of(TwoInvocationsTestCase.class), + TwoInvocationsTestCase.NestedTestCase.class, "b")); + + results.testEvents().assertStatistics(stats -> stats.started(2).succeeded(2)) // + .assertEventsMatchLoosely(event(test(displayName("b()")), finishedSuccessfully())); + } + + @Test + void executesOnlyTestsPassingPostDiscoveryFilter() { + var results = executeTests(request -> request // + .selectors(selectClass(TwoInvocationsTestCase.class)) // + .filters(includeTags("nested"))); + + results.testEvents().assertStatistics(stats -> stats.started(2).succeeded(2)) // + .assertEventsMatchLoosely(event(test(displayName("b()")), finishedSuccessfully())); + } + + @Test + void prunesEmptyNestedTestClasses() { + var results = executeTests(request -> request // + .selectors(selectClass(TwoInvocationsTestCase.class)) // + .filters(excludeTags("nested"))); + + results.containerEvents().assertThatEvents() // + .noneMatch(container(TwoInvocationsTestCase.NestedTestCase.class.getSimpleName())::matches); + + results.testEvents().assertStatistics(stats -> stats.started(2).succeeded(2)) // + .assertEventsMatchLoosely(event(test(displayName("a()")), finishedSuccessfully())); + } + + @Test + void executesNestedContainerTemplateClassTwiceWithClassSelectorForEnclosingClass() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var classId = engineId.append(ClassTestDescriptor.SEGMENT_TYPE, + NestedContainerTemplateWithTwoInvocationsTestCase.class.getName()); + var methodAId = classId.append(TestMethodTestDescriptor.SEGMENT_TYPE, "a()"); + var nestedContainerTemplateId = classId.append(ContainerTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE, + "NestedTestCase"); + var invocationId1 = nestedContainerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#1"); + var invocation1NestedMethodBId = invocationId1.append(TestMethodTestDescriptor.SEGMENT_TYPE, "b()"); + var invocationId2 = nestedContainerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#2"); + var invocation2NestedMethodBId = invocationId2.append(TestMethodTestDescriptor.SEGMENT_TYPE, "b()"); + + var results = executeTestsForClass(NestedContainerTemplateWithTwoInvocationsTestCase.class); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(classId)), started()), // + + event(test(uniqueId(methodAId)), started()), // + event(test(uniqueId(methodAId)), finishedSuccessfully()), // + + event(container(uniqueId(nestedContainerTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId1)), displayName("[1] A of NestedTestCase"), + legacyReportingName("%s[1]".formatted( + NestedContainerTemplateWithTwoInvocationsTestCase.NestedTestCase.class.getName()))), // + event(container(uniqueId(invocationId1)), started()), // + event(dynamicTestRegistered(uniqueId(invocation1NestedMethodBId))), // + event(test(uniqueId(invocation1NestedMethodBId)), started()), // + event(test(uniqueId(invocation1NestedMethodBId)), finishedSuccessfully()), // + event(container(uniqueId(invocationId1)), finishedSuccessfully()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), displayName("[2] B of NestedTestCase"), + legacyReportingName("%s[2]".formatted( + NestedContainerTemplateWithTwoInvocationsTestCase.NestedTestCase.class.getName()))), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(invocation2NestedMethodBId))), // + event(test(uniqueId(invocation2NestedMethodBId)), started()), // + event(test(uniqueId(invocation2NestedMethodBId)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(nestedContainerTemplateId)), finishedSuccessfully()), // + + event(container(uniqueId(classId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void executesNestedContainerTemplateClassTwiceWithNestedClassSelector() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var classId = engineId.append(ClassTestDescriptor.SEGMENT_TYPE, + NestedContainerTemplateWithTwoInvocationsTestCase.class.getName()); + var nestedContainerTemplateId = classId.append(ContainerTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE, + "NestedTestCase"); + var invocationId1 = nestedContainerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#1"); + var invocation1NestedMethodBId = invocationId1.append(TestMethodTestDescriptor.SEGMENT_TYPE, "b()"); + var invocationId2 = nestedContainerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#2"); + var invocation2NestedMethodBId = invocationId2.append(TestMethodTestDescriptor.SEGMENT_TYPE, "b()"); + + var results = executeTestsForClass(NestedContainerTemplateWithTwoInvocationsTestCase.NestedTestCase.class); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(classId)), started()), // + + event(container(uniqueId(nestedContainerTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId1)), displayName("[1] A of NestedTestCase")), // + event(container(uniqueId(invocationId1)), started()), // + event(dynamicTestRegistered(uniqueId(invocation1NestedMethodBId))), // + event(test(uniqueId(invocation1NestedMethodBId)), started()), // + event(test(uniqueId(invocation1NestedMethodBId)), finishedSuccessfully()), // + event(container(uniqueId(invocationId1)), finishedSuccessfully()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), displayName("[2] B of NestedTestCase")), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(invocation2NestedMethodBId))), // + event(test(uniqueId(invocation2NestedMethodBId)), started()), // + event(test(uniqueId(invocation2NestedMethodBId)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(nestedContainerTemplateId)), finishedSuccessfully()), // + + event(container(uniqueId(classId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void executesNestedContainerTemplatesTwiceEach() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var outerContainerTemplateId = engineId.append(ContainerTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + TwoTimesTwoInvocationsTestCase.class.getName()); + + var outerInvocation1Id = outerContainerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#1"); + var outerInvocation1NestedContainerTemplateId = outerInvocation1Id.append( + ContainerTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE, "NestedTestCase"); + var outerInvocation1InnerInvocation1Id = outerInvocation1NestedContainerTemplateId.append( + ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#1"); + var outerInvocation1InnerInvocation1NestedMethodId = outerInvocation1InnerInvocation1Id.append( + TestMethodTestDescriptor.SEGMENT_TYPE, "test()"); + var outerInvocation1InnerInvocation2Id = outerInvocation1NestedContainerTemplateId.append( + ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var outerInvocation1InnerInvocation2NestedMethodId = outerInvocation1InnerInvocation2Id.append( + TestMethodTestDescriptor.SEGMENT_TYPE, "test()"); + + var outerInvocation2Id = outerContainerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#2"); + var outerInvocation2NestedContainerTemplateId = outerInvocation2Id.append( + ContainerTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE, "NestedTestCase"); + var outerInvocation2InnerInvocation1Id = outerInvocation2NestedContainerTemplateId.append( + ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#1"); + var outerInvocation2InnerInvocation1NestedMethodId = outerInvocation2InnerInvocation1Id.append( + TestMethodTestDescriptor.SEGMENT_TYPE, "test()"); + var outerInvocation2InnerInvocation2Id = outerInvocation2NestedContainerTemplateId.append( + ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var outerInvocation2InnerInvocation2NestedMethodId = outerInvocation2InnerInvocation2Id.append( + TestMethodTestDescriptor.SEGMENT_TYPE, "test()"); + + var results = executeTestsForClass(TwoTimesTwoInvocationsTestCase.class); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(outerContainerTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation1Id)), + displayName("[1] A of TwoTimesTwoInvocationsTestCase")), // + event(container(uniqueId(outerInvocation1Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation1NestedContainerTemplateId))), // + event(container(uniqueId(outerInvocation1NestedContainerTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation1InnerInvocation1Id)), + displayName("[1] A of NestedTestCase")), // + event(container(uniqueId(outerInvocation1InnerInvocation1Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation1InnerInvocation1NestedMethodId))), // + event(test(uniqueId(outerInvocation1InnerInvocation1NestedMethodId)), started()), // + event(test(uniqueId(outerInvocation1InnerInvocation1NestedMethodId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation1InnerInvocation1Id)), finishedSuccessfully()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation1InnerInvocation2Id)), + displayName("[2] B of NestedTestCase")), // + event(container(uniqueId(outerInvocation1InnerInvocation2Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation1InnerInvocation2NestedMethodId))), // + event(test(uniqueId(outerInvocation1InnerInvocation2NestedMethodId)), started()), // + event(test(uniqueId(outerInvocation1InnerInvocation2NestedMethodId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation1InnerInvocation2Id)), finishedSuccessfully()), // + + event(container(uniqueId(outerInvocation1NestedContainerTemplateId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation1Id)), finishedSuccessfully()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation2Id)), + displayName("[2] B of TwoTimesTwoInvocationsTestCase")), // + event(container(uniqueId(outerInvocation2Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation2NestedContainerTemplateId))), // + event(container(uniqueId(outerInvocation2NestedContainerTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation2InnerInvocation1Id)), + displayName("[1] A of NestedTestCase")), // + event(container(uniqueId(outerInvocation2InnerInvocation1Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation2InnerInvocation1NestedMethodId))), // + event(test(uniqueId(outerInvocation2InnerInvocation1NestedMethodId)), started()), // + event(test(uniqueId(outerInvocation2InnerInvocation1NestedMethodId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation2InnerInvocation1Id)), finishedSuccessfully()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation2InnerInvocation2Id)), + displayName("[2] B of NestedTestCase")), // + event(container(uniqueId(outerInvocation2InnerInvocation2Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation2InnerInvocation2NestedMethodId))), // + event(test(uniqueId(outerInvocation2InnerInvocation2NestedMethodId)), started()), // + event(test(uniqueId(outerInvocation2InnerInvocation2NestedMethodId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation2InnerInvocation2Id)), finishedSuccessfully()), // + + event(container(uniqueId(outerInvocation2NestedContainerTemplateId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation2Id)), finishedSuccessfully()), // + + event(container(uniqueId(outerContainerTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void invocationContextProviderCanRegisterAdditionalExtensions() { + var results = executeTestsForClass(AdditionalExtensionRegistrationTestCase.class); + + results.testEvents().assertStatistics(stats -> stats.started(2).succeeded(2)); + } + + @Test + void eachInvocationHasSeparateExtensionContext() { + var results = executeTestsForClass(SeparateExtensionContextTestCase.class); + + results.testEvents().assertStatistics(stats -> stats.started(2).succeeded(2)); + } + + @Test + void supportsTestTemplateMethodsInsideContainerTemplateClasses() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var containerTemplateId = engineId.append(ContainerTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + CombinationWithTestTemplateTestCase.class.getName()); + var invocationId1 = containerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#1"); + var testTemplateId1 = invocationId1.append(TestTemplateTestDescriptor.SEGMENT_TYPE, "test(int)"); + var testTemplate1InvocationId1 = testTemplateId1.append(TestTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#1"); + var testTemplate1InvocationId2 = testTemplateId1.append(TestTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#2"); + var invocationId2 = containerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var testTemplateId2 = invocationId2.append(TestTemplateTestDescriptor.SEGMENT_TYPE, "test(int)"); + var testTemplate2InvocationId1 = testTemplateId2.append(TestTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#1"); + var testTemplate2InvocationId2 = testTemplateId2.append(TestTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#2"); + + var results = executeTestsForClass(CombinationWithTestTemplateTestCase.class); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(containerTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId1)), + displayName("[1] A of CombinationWithTestTemplateTestCase")), // + event(container(uniqueId(invocationId1)), started()), // + event(dynamicTestRegistered(uniqueId(testTemplateId1))), // + event(container(uniqueId(testTemplateId1)), started()), // + event(dynamicTestRegistered(uniqueId(testTemplate1InvocationId1))), // + event(test(uniqueId(testTemplate1InvocationId1)), started()), // + event(test(uniqueId(testTemplate1InvocationId1)), finishedSuccessfully()), // + event(dynamicTestRegistered(uniqueId(testTemplate1InvocationId2))), // + event(test(uniqueId(testTemplate1InvocationId2)), started()), // + event(test(uniqueId(testTemplate1InvocationId2)), finishedSuccessfully()), // + event(container(uniqueId(testTemplateId1)), finishedSuccessfully()), // + event(container(uniqueId(invocationId1)), finishedSuccessfully()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), + displayName("[2] B of CombinationWithTestTemplateTestCase")), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(testTemplateId2))), // + event(container(uniqueId(testTemplateId2)), started()), // + event(dynamicTestRegistered(uniqueId(testTemplate2InvocationId1))), // + event(test(uniqueId(testTemplate2InvocationId1)), started()), // + event(test(uniqueId(testTemplate2InvocationId1)), finishedSuccessfully()), // + event(dynamicTestRegistered(uniqueId(testTemplate2InvocationId2))), // + event(test(uniqueId(testTemplate2InvocationId2)), started()), // + event(test(uniqueId(testTemplate2InvocationId2)), finishedSuccessfully()), // + event(container(uniqueId(testTemplateId2)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(containerTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void testTemplateInvocationInsideContainerTemplateClassCanBeSelectedByUniqueId() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var containerTemplateId = engineId.append(ContainerTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + CombinationWithTestTemplateTestCase.class.getName()); + var invocationId2 = containerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var testTemplateId2 = invocationId2.append(TestTemplateTestDescriptor.SEGMENT_TYPE, "test(int)"); + var testTemplate2InvocationId2 = testTemplateId2.append(TestTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#2"); + + var results = executeTests(selectUniqueId(testTemplate2InvocationId2)); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(containerTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), + displayName("[2] B of CombinationWithTestTemplateTestCase")), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(testTemplateId2))), // + event(container(uniqueId(testTemplateId2)), started()), // + event(dynamicTestRegistered(uniqueId(testTemplate2InvocationId2))), // + event(test(uniqueId(testTemplate2InvocationId2)), started()), // + event(test(uniqueId(testTemplate2InvocationId2)), finishedSuccessfully()), // + event(container(uniqueId(testTemplateId2)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(containerTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void supportsTestFactoryMethodsInsideContainerTemplateClasses() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var containerTemplateId = engineId.append(ContainerTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + CombinationWithTestFactoryTestCase.class.getName()); + var invocationId1 = containerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#1"); + var testFactoryId1 = invocationId1.append(TestFactoryTestDescriptor.SEGMENT_TYPE, "test()"); + var testFactory1DynamicTestId1 = testFactoryId1.append(TestFactoryTestDescriptor.DYNAMIC_TEST_SEGMENT_TYPE, + "#1"); + var testFactory1DynamicTestId2 = testFactoryId1.append(TestFactoryTestDescriptor.DYNAMIC_TEST_SEGMENT_TYPE, + "#2"); + var invocationId2 = containerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var testFactoryId2 = invocationId2.append(TestFactoryTestDescriptor.SEGMENT_TYPE, "test()"); + var testFactory2DynamicTestId1 = testFactoryId2.append(TestFactoryTestDescriptor.DYNAMIC_TEST_SEGMENT_TYPE, + "#1"); + var testFactory2DynamicTestId2 = testFactoryId2.append(TestFactoryTestDescriptor.DYNAMIC_TEST_SEGMENT_TYPE, + "#2"); + + var results = executeTestsForClass(CombinationWithTestFactoryTestCase.class); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(containerTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId1)), + displayName("[1] A of CombinationWithTestFactoryTestCase")), // + event(container(uniqueId(invocationId1)), started()), // + event(dynamicTestRegistered(uniqueId(testFactoryId1))), // + event(container(uniqueId(testFactoryId1)), started()), // + event(dynamicTestRegistered(uniqueId(testFactory1DynamicTestId1))), // + event(test(uniqueId(testFactory1DynamicTestId1)), started()), // + event(test(uniqueId(testFactory1DynamicTestId1)), finishedSuccessfully()), // + event(dynamicTestRegistered(uniqueId(testFactory1DynamicTestId2))), // + event(test(uniqueId(testFactory1DynamicTestId2)), started()), // + event(test(uniqueId(testFactory1DynamicTestId2)), finishedSuccessfully()), // + event(container(uniqueId(testFactoryId1)), finishedSuccessfully()), // + event(container(uniqueId(invocationId1)), finishedSuccessfully()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), + displayName("[2] B of CombinationWithTestFactoryTestCase")), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(testFactoryId2))), // + event(container(uniqueId(testFactoryId2)), started()), // + event(dynamicTestRegistered(uniqueId(testFactory2DynamicTestId1))), // + event(test(uniqueId(testFactory2DynamicTestId1)), started()), // + event(test(uniqueId(testFactory2DynamicTestId1)), finishedSuccessfully()), // + event(dynamicTestRegistered(uniqueId(testFactory2DynamicTestId2))), // + event(test(uniqueId(testFactory2DynamicTestId2)), started()), // + event(test(uniqueId(testFactory2DynamicTestId2)), finishedSuccessfully()), // + event(container(uniqueId(testFactoryId2)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(containerTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void specificDynamicTestInsideContainerTemplateClassCanBeSelectedByUniqueId() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var containerTemplateId = engineId.append(ContainerTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + CombinationWithTestFactoryTestCase.class.getName()); + var invocationId2 = containerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var testFactoryId2 = invocationId2.append(TestFactoryTestDescriptor.SEGMENT_TYPE, "test()"); + var testFactory2DynamicTestId2 = testFactoryId2.append(TestFactoryTestDescriptor.DYNAMIC_TEST_SEGMENT_TYPE, + "#2"); + + var results = executeTests(selectUniqueId(testFactory2DynamicTestId2)); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(containerTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), + displayName("[2] B of CombinationWithTestFactoryTestCase")), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(testFactoryId2))), // + event(container(uniqueId(testFactoryId2)), started()), // + event(dynamicTestRegistered(uniqueId(testFactory2DynamicTestId2))), // + event(test(uniqueId(testFactory2DynamicTestId2)), started()), // + event(test(uniqueId(testFactory2DynamicTestId2)), finishedSuccessfully()), // + event(container(uniqueId(testFactoryId2)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(containerTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void failsIfProviderReturnsZeroInvocationContextWithoutOptIn() { + var results = executeTestsForClass(InvalidZeroInvocationTestCase.class); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(InvalidZeroInvocationTestCase.class), started()), // + event(container(InvalidZeroInvocationTestCase.class), + finishedWithFailure( + message("Provider [Ext] did not provide any invocation contexts, but was expected to do so. " + + "You may override mayReturnZeroContainerTemplateInvocationContexts() to allow this."))), // + event(engine(), finishedSuccessfully())); + } + + @Test + void succeedsIfProviderReturnsZeroInvocationContextWithOptIn() { + var results = executeTestsForClass(ValidZeroInvocationTestCase.class); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(ValidZeroInvocationTestCase.class), started()), // + event(container(ValidZeroInvocationTestCase.class), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @ParameterizedTest + @ValueSource(classes = { NoProviderRegisteredTestCase.class, NoSupportingProviderRegisteredTestCase.class }) + void failsIfNoSupportingProviderIsRegistered(Class testClass) { + var results = executeTestsForClass(testClass); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(testClass), started()), // + event(container(testClass), + finishedWithFailure( + message("You must register at least one ContainerTemplateInvocationContextProvider that supports " + + "@ContainerTemplate class [" + testClass.getName() + "]"))), // + event(engine(), finishedSuccessfully())); + } + + @Test + void containerTemplateInvocationCanBeSelectedByUniqueId() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var containerTemplateId = engineId.append(ContainerTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + TwoInvocationsTestCase.class.getName()); + var invocationId2 = containerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var methodAId = invocationId2.append(TestMethodTestDescriptor.SEGMENT_TYPE, "a()"); + var nestedClassId = invocationId2.append(NestedClassTestDescriptor.SEGMENT_TYPE, "NestedTestCase"); + var nestedMethodBId = nestedClassId.append(TestMethodTestDescriptor.SEGMENT_TYPE, "b()"); + + var results = executeTests(selectUniqueId(invocationId2)); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(containerTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), displayName("[2] B of TwoInvocationsTestCase")), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(methodAId))), // + event(dynamicTestRegistered(uniqueId(nestedClassId))), // + event(dynamicTestRegistered(uniqueId(nestedMethodBId))), // + event(test(uniqueId(methodAId)), started()), // + event(test(uniqueId(methodAId)), finishedSuccessfully()), // + event(container(uniqueId(nestedClassId)), started()), // + event(test(uniqueId(nestedMethodBId)), started()), // + event(test(uniqueId(nestedMethodBId)), finishedSuccessfully()), // + event(container(uniqueId(nestedClassId)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(containerTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void containerTemplateInvocationCanBeSelectedByIteration() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var containerTemplateId = engineId.append(ContainerTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + TwoInvocationsTestCase.class.getName()); + var invocationId2 = containerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var methodAId = invocationId2.append(TestMethodTestDescriptor.SEGMENT_TYPE, "a()"); + var nestedClassId = invocationId2.append(NestedClassTestDescriptor.SEGMENT_TYPE, "NestedTestCase"); + var nestedMethodBId = nestedClassId.append(TestMethodTestDescriptor.SEGMENT_TYPE, "b()"); + + var results = executeTests(selectIteration(selectClass(TwoInvocationsTestCase.class), 1)); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(containerTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), displayName("[2] B of TwoInvocationsTestCase")), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(methodAId))), // + event(dynamicTestRegistered(uniqueId(nestedClassId))), // + event(dynamicTestRegistered(uniqueId(nestedMethodBId))), // + event(test(uniqueId(methodAId)), started()), // + event(test(uniqueId(methodAId)), finishedSuccessfully()), // + event(container(uniqueId(nestedClassId)), started()), // + event(test(uniqueId(nestedMethodBId)), started()), // + event(test(uniqueId(nestedMethodBId)), finishedSuccessfully()), // + event(container(uniqueId(nestedClassId)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(containerTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @ParameterizedTest + @ValueSource(strings = { // + "class:org.junit.jupiter.engine.ContainerTemplateInvocationTests$TwoInvocationsTestCase", // + "uid:[engine:junit-jupiter]/[container-template:org.junit.jupiter.engine.ContainerTemplateInvocationTests$TwoInvocationsTestCase]" // + }) + void executesAllInvocationsForRedundantSelectors(String containerTemplateSelectorIdentifier) { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var containerTemplateId = engineId.append(ContainerTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + TwoInvocationsTestCase.class.getName()); + var invocationId2 = containerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + + var results = executeTests(selectUniqueId(invocationId2), + DiscoverySelectors.parse(containerTemplateSelectorIdentifier).orElseThrow()); + + results.testEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + } + + @Test + void methodInContainerTemplateInvocationCanBeSelectedByUniqueId() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var containerTemplateId = engineId.append(ContainerTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + TwoInvocationsTestCase.class.getName()); + var invocationId2 = containerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var methodAId = invocationId2.append(TestMethodTestDescriptor.SEGMENT_TYPE, "a()"); + + var results = executeTests(selectUniqueId(methodAId)); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(containerTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), displayName("[2] B of TwoInvocationsTestCase")), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(methodAId))), // + event(test(uniqueId(methodAId)), started()), // + event(test(uniqueId(methodAId)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(containerTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void nestedMethodInContainerTemplateInvocationCanBeSelectedByUniqueId() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var containerTemplateId = engineId.append(ContainerTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + TwoInvocationsTestCase.class.getName()); + var invocationId2 = containerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var nestedClassId = invocationId2.append(NestedClassTestDescriptor.SEGMENT_TYPE, "NestedTestCase"); + var nestedMethodBId = nestedClassId.append(TestMethodTestDescriptor.SEGMENT_TYPE, "b()"); + + var results = executeTests(selectUniqueId(nestedMethodBId)); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(containerTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(invocationId2)), displayName("[2] B of TwoInvocationsTestCase")), // + event(container(uniqueId(invocationId2)), started()), // + event(dynamicTestRegistered(uniqueId(nestedClassId))), // + event(dynamicTestRegistered(uniqueId(nestedMethodBId))), // + event(container(uniqueId(nestedClassId)), started()), // + event(test(uniqueId(nestedMethodBId)), started()), // + event(test(uniqueId(nestedMethodBId)), finishedSuccessfully()), // + event(container(uniqueId(nestedClassId)), finishedSuccessfully()), // + event(container(uniqueId(invocationId2)), finishedSuccessfully()), // + + event(container(uniqueId(containerTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void nestedContainerTemplateInvocationCanBeSelectedByUniqueId() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var outerContainerTemplateId = engineId.append(ContainerTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + TwoTimesTwoInvocationsWithMultipleMethodsTestCase.class.getName()); + var outerInvocation2Id = outerContainerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#2"); + var outerInvocation2NestedContainerTemplateId = outerInvocation2Id.append( + ContainerTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE, "NestedTestCase"); + var outerInvocation2InnerInvocation2Id = outerInvocation2NestedContainerTemplateId.append( + ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var outerInvocation2InnerInvocation2NestedMethodId = outerInvocation2InnerInvocation2Id.append( + TestMethodTestDescriptor.SEGMENT_TYPE, "b()"); + + var results = executeTests(selectUniqueId(outerInvocation2InnerInvocation2NestedMethodId)); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(outerContainerTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation2Id)), + displayName("[2] B of TwoTimesTwoInvocationsWithMultipleMethodsTestCase")), // + event(container(uniqueId(outerInvocation2Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation2NestedContainerTemplateId))), // + event(container(uniqueId(outerInvocation2NestedContainerTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation2InnerInvocation2Id)), + displayName("[2] B of NestedTestCase")), // + event(container(uniqueId(outerInvocation2InnerInvocation2Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation2InnerInvocation2NestedMethodId))), // + event(test(uniqueId(outerInvocation2InnerInvocation2NestedMethodId)), started()), // + event(test(uniqueId(outerInvocation2InnerInvocation2NestedMethodId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation2InnerInvocation2Id)), finishedSuccessfully()), // + + event(container(uniqueId(outerInvocation2NestedContainerTemplateId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation2Id)), finishedSuccessfully()), // + + event(container(uniqueId(outerContainerTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void nestedContainerTemplateInvocationCanBeSelectedByIteration() { + var engineId = UniqueId.forEngine(JupiterEngineDescriptor.ENGINE_ID); + var outerContainerTemplateId = engineId.append(ContainerTemplateTestDescriptor.STATIC_CLASS_SEGMENT_TYPE, + TwoTimesTwoInvocationsTestCase.class.getName()); + var outerInvocation1Id = outerContainerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#1"); + var outerInvocation1NestedContainerTemplateId = outerInvocation1Id.append( + ContainerTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE, "NestedTestCase"); + var outerInvocation1InnerInvocation2Id = outerInvocation1NestedContainerTemplateId.append( + ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var outerInvocation1InnerInvocation2NestedMethodId = outerInvocation1InnerInvocation2Id.append( + TestMethodTestDescriptor.SEGMENT_TYPE, "test()"); + var outerInvocation2Id = outerContainerTemplateId.append(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, + "#2"); + var outerInvocation2NestedContainerTemplateId = outerInvocation2Id.append( + ContainerTemplateTestDescriptor.NESTED_CLASS_SEGMENT_TYPE, "NestedTestCase"); + var outerInvocation2InnerInvocation2Id = outerInvocation2NestedContainerTemplateId.append( + ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE, "#2"); + var outerInvocation2InnerInvocation2NestedMethodId = outerInvocation2InnerInvocation2Id.append( + TestMethodTestDescriptor.SEGMENT_TYPE, "test()"); + + var results = executeTests(selectIteration(selectNestedClass(List.of(TwoTimesTwoInvocationsTestCase.class), + TwoTimesTwoInvocationsTestCase.NestedTestCase.class), 1)); + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(uniqueId(outerContainerTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation1Id)), + displayName("[1] A of TwoTimesTwoInvocationsTestCase")), // + event(container(uniqueId(outerInvocation1Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation1NestedContainerTemplateId))), // + event(container(uniqueId(outerInvocation1NestedContainerTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation1InnerInvocation2Id)), + displayName("[2] B of NestedTestCase")), // + event(container(uniqueId(outerInvocation1InnerInvocation2Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation1InnerInvocation2NestedMethodId))), // + event(test(uniqueId(outerInvocation1InnerInvocation2NestedMethodId)), started()), // + event(test(uniqueId(outerInvocation1InnerInvocation2NestedMethodId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation1InnerInvocation2Id)), finishedSuccessfully()), // + + event(container(uniqueId(outerInvocation1NestedContainerTemplateId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation1Id)), finishedSuccessfully()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation2Id)), + displayName("[2] B of TwoTimesTwoInvocationsTestCase")), // + event(container(uniqueId(outerInvocation2Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation2NestedContainerTemplateId))), // + event(container(uniqueId(outerInvocation2NestedContainerTemplateId)), started()), // + + event(dynamicTestRegistered(uniqueId(outerInvocation2InnerInvocation2Id)), + displayName("[2] B of NestedTestCase")), // + event(container(uniqueId(outerInvocation2InnerInvocation2Id)), started()), // + event(dynamicTestRegistered(uniqueId(outerInvocation2InnerInvocation2NestedMethodId))), // + event(test(uniqueId(outerInvocation2InnerInvocation2NestedMethodId)), started()), // + event(test(uniqueId(outerInvocation2InnerInvocation2NestedMethodId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation2InnerInvocation2Id)), finishedSuccessfully()), // + + event(container(uniqueId(outerInvocation2NestedContainerTemplateId)), finishedSuccessfully()), // + event(container(uniqueId(outerInvocation2Id)), finishedSuccessfully()), // + + event(container(uniqueId(outerContainerTemplateId)), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @Test + void executesLifecycleCallbackMethodsInNestedContainerTemplates() { + var results = executeTestsForClass(TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase.class); + + results.containerEvents().assertStatistics(stats -> stats.started(10).succeeded(10)); + results.testEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + + var callSequence = results.allEvents().reportingEntryPublished() // + .map(event -> event.getRequiredPayload(ReportEntry.class)) // + .map(ReportEntry::getKeyValuePairs) // + .map(Map::values) // + .flatMap(Collection::stream); + // @formatter:off + assertThat(callSequence).containsExactly( + "beforeAll: TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase", + "beforeAll: NestedTestCase", + "beforeEach: test [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test [NestedTestCase]", + "test", + "afterEach: test [NestedTestCase]", + "afterEach: test [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test [NestedTestCase]", + "test", + "afterEach: test [NestedTestCase]", + "afterEach: test [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "afterAll: NestedTestCase", + "beforeAll: NestedTestCase", + "beforeEach: test [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test [NestedTestCase]", + "test", + "afterEach: test [NestedTestCase]", + "afterEach: test [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "beforeEach: test [NestedTestCase]", + "test", + "afterEach: test [NestedTestCase]", + "afterEach: test [TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase]", + "afterAll: NestedTestCase", + "afterAll: TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase" + ); + // @formatter:on + } + + // ------------------------------------------------------------------- + + @SuppressWarnings("JUnitMalformedDeclaration") + @ContainerTemplate + @ExtendWith(TwoInvocationsContainerTemplateInvocationContextProvider.class) + static class TwoInvocationsTestCase { + @Test + void a() { + } + + @Nested + class NestedTestCase { + @Test + @Tag("nested") + void b() { + } + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + static class NestedContainerTemplateWithTwoInvocationsTestCase { + @Test + void a() { + } + + @Nested + @ContainerTemplate + @ExtendWith(TwoInvocationsContainerTemplateInvocationContextProvider.class) + class NestedTestCase { + @Test + void b() { + } + } + } + + @ExtendWith(TwoInvocationsContainerTemplateInvocationContextProvider.class) + @ContainerTemplate + static class TwoTimesTwoInvocationsTestCase { + @Nested + @ContainerTemplate + class NestedTestCase { + @Test + void test() { + } + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ContainerTemplate + @ExtendWith(TwoInvocationsContainerTemplateInvocationContextProvider.class) + static class TwoInvocationsWithExtensionTestCase { + @Test + void a() { + } + + @Nested + class NestedTestCase { + @Test + @Tag("nested") + void b() { + } + } + } + + static class TwoInvocationsContainerTemplateInvocationContextProvider + implements ContainerTemplateInvocationContextProvider { + + @Override + public boolean supportsContainerTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideContainerTemplateInvocationContexts(ExtensionContext context) { + var suffix = " of %s".formatted(context.getRequiredTestClass().getSimpleName()); + return Stream.of(new Ctx("A" + suffix), new Ctx("B" + suffix)); + } + + record Ctx(String displayName) implements ContainerTemplateInvocationContext { + @Override + public String getDisplayName(int invocationIndex) { + var defaultDisplayName = ContainerTemplateInvocationContext.super.getDisplayName(invocationIndex); + return "%s %s".formatted(defaultDisplayName, displayName); + } + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ContainerTemplate + @ExtendWith(AdditionalExtensionRegistrationTestCase.Ext.class) + static class AdditionalExtensionRegistrationTestCase { + + @Test + void test(Data data) { + assertNotNull(data); + assertNotNull(data.value()); + } + + static class Ext implements ContainerTemplateInvocationContextProvider { + @Override + public boolean supportsContainerTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideContainerTemplateInvocationContexts(ExtensionContext context) { + return Stream.of(new Data("A"), new Data("B")).map(Ctx::new); + } + } + + record Ctx(Data data) implements ContainerTemplateInvocationContext { + @Override + public String getDisplayName(int invocationIndex) { + return this.data.value(); + } + + @Override + public List getAdditionalExtensions() { + return List.of(new ParameterResolver() { + @Override + public boolean supportsParameter(ParameterContext parameterContext, + ExtensionContext extensionContext) throws ParameterResolutionException { + return Data.class.equals(parameterContext.getParameter().getType()); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return Ctx.this.data; + } + }); + } + } + + record Data(String value) { + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ContainerTemplate + @ExtendWith(TwoInvocationsContainerTemplateInvocationContextProvider.class) + @ExtendWith(SeparateExtensionContextTestCase.SomeResourceExtension.class) + static class SeparateExtensionContextTestCase { + + @Test + void test(SomeResource someResource) { + assertFalse(someResource.closed); + } + + static class SomeResourceExtension implements BeforeAllCallback, ParameterResolver { + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + context.getStore(ExtensionContext.Namespace.GLOBAL).put("someResource", new SomeResource()); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + var parentContext = extensionContext.getParent().orElseThrow(); + assertAll( // + () -> assertEquals(SeparateExtensionContextTestCase.class, parentContext.getRequiredTestClass()), // + () -> assertEquals(SeparateExtensionContextTestCase.class, + parentContext.getElement().orElseThrow()), // + () -> assertEquals(TestInstance.Lifecycle.PER_METHOD, + parentContext.getTestInstanceLifecycle().orElseThrow()) // + ); + return SomeResource.class.equals(parameterContext.getParameter().getType()); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return extensionContext.getStore(ExtensionContext.Namespace.GLOBAL).get("someResource"); + } + } + + static class SomeResource implements CloseableResource { + private boolean closed; + + @Override + public void close() { + this.closed = true; + } + } + } + + @ContainerTemplate + @ExtendWith(TwoInvocationsContainerTemplateInvocationContextProvider.class) + static class CombinationWithTestTemplateTestCase { + + @ParameterizedTest + @ValueSource(ints = { 1, 2 }) + void test(int i) { + assertNotEquals(0, i); + } + } + + @ContainerTemplate + @ExtendWith(TwoInvocationsContainerTemplateInvocationContextProvider.class) + static class CombinationWithTestFactoryTestCase { + + @TestFactory + Stream test() { + return IntStream.of(1, 2) // + .mapToObj(i -> dynamicTest("test" + i, () -> assertNotEquals(0, i))); + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ContainerTemplate + @ExtendWith(InvalidZeroInvocationTestCase.Ext.class) + static class InvalidZeroInvocationTestCase { + + @Test + void test() { + fail("should not be called"); + } + + static class Ext implements ContainerTemplateInvocationContextProvider { + + @Override + public boolean supportsContainerTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideContainerTemplateInvocationContexts( + ExtensionContext context) { + return Stream.empty(); + } + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ContainerTemplate + @ExtendWith(ValidZeroInvocationTestCase.Ext.class) + static class ValidZeroInvocationTestCase { + + @Test + void test() { + fail("should not be called"); + } + + static class Ext implements ContainerTemplateInvocationContextProvider { + + @Override + public boolean supportsContainerTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideContainerTemplateInvocationContexts( + ExtensionContext context) { + return Stream.empty(); + } + + @Override + public boolean mayReturnZeroContainerTemplateInvocationContexts(ExtensionContext context) { + return true; + } + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ContainerTemplate + static class NoProviderRegisteredTestCase { + + @Test + void test() { + fail("should not be called"); + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ContainerTemplate + @ExtendWith(NoSupportingProviderRegisteredTestCase.Ext.class) + static class NoSupportingProviderRegisteredTestCase { + + @Test + void test() { + fail("should not be called"); + } + + static class Ext implements ContainerTemplateInvocationContextProvider { + + @Override + public boolean supportsContainerTemplate(ExtensionContext context) { + return false; + } + + @Override + public Stream provideContainerTemplateInvocationContexts( + ExtensionContext context) { + throw new RuntimeException("should not be called"); + } + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ExtendWith(TwoInvocationsContainerTemplateInvocationContextProvider.class) + @ContainerTemplate + static class TwoTimesTwoInvocationsWithMultipleMethodsTestCase { + + @Test + void test() { + } + + @Nested + @ContainerTemplate + class NestedTestCase { + @Test + void a() { + } + + @Test + void b() { + } + } + + @Nested + @ContainerTemplate + class AnotherNestedTestCase { + @Test + void test() { + } + } + } + + @ExtendWith(TwoInvocationsContainerTemplateInvocationContextProvider.class) + @ContainerTemplate + static class TwoTimesTwoInvocationsWithLifecycleCallbacksTestCase extends LifecycleCallbacks { + @Nested + @ContainerTemplate + class NestedTestCase extends LifecycleCallbacks { + @Test + @DisplayName("test") + void test(TestReporter testReporter) { + testReporter.publishEntry("test"); + } + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + static class LifecycleCallbacks { + @BeforeAll + static void beforeAll(TestReporter testReporter, TestInfo testInfo) { + testReporter.publishEntry("beforeAll: " + testInfo.getTestClass().orElseThrow().getSimpleName()); + } + + @BeforeEach + void beforeEach(TestReporter testReporter, TestInfo testInfo) { + testReporter.publishEntry( + "beforeEach: " + testInfo.getDisplayName() + " [" + getClass().getSimpleName() + "]"); + } + + @AfterEach + void afterEach(TestReporter testReporter, TestInfo testInfo) { + testReporter.publishEntry( + "afterEach: " + testInfo.getDisplayName() + " [" + getClass().getSimpleName() + "]"); + } + + @AfterAll + static void afterAll(TestReporter testReporter, TestInfo testInfo) { + testReporter.publishEntry("afterAll: " + testInfo.getTestClass().orElseThrow().getSimpleName()); + } + } +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleTests.java index 91676086da86..784df0e19a9f 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestInstanceLifecycleTests.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -30,12 +31,14 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Stream; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.ContainerTemplate; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; @@ -47,6 +50,8 @@ import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ContainerTemplateInvocationContext; +import org.junit.jupiter.api.extension.ContainerTemplateInvocationContextProvider; import org.junit.jupiter.api.extension.ExecutionCondition; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtensionContext; @@ -56,6 +61,8 @@ import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; import org.junit.jupiter.engine.execution.DefaultTestInstances; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.platform.testkit.engine.EngineExecutionResults; /** @@ -601,6 +608,44 @@ void instancePerMethodOnOuterTestClassWithInstancePerClassOnNestedTestClass() { assertThat(lifecyclesMap.get(nestedTestClass).stream()).allMatch(Lifecycle.PER_CLASS::equals); } + @ParameterizedTest + @EnumSource(Lifecycle.class) + void containerTemplate(Lifecycle lifecycle) { + var containerTemplate = ContainerTemplateWithDefaultLifecycleTestCase.class; + + var results = executeTests(r -> r // + .selectors(selectClass(containerTemplate)) // + .configurationParameter(Constants.DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME, lifecycle.name())); + + results.allEvents().assertStatistics(stats -> stats.failed(0)); + results.testEvents().assertStatistics(stats -> stats.succeeded(4)); + + assertThat(instanceCount).containsExactly(entry(containerTemplate, lifecycle == Lifecycle.PER_CLASS ? 1 : 4)); + assertThat(lifecyclesMap.keySet()).containsExactly(containerTemplate); + assertThat(lifecyclesMap.get(containerTemplate)).filteredOn(Objects::nonNull).containsOnly(lifecycle); + } + + @ParameterizedTest + @EnumSource(Lifecycle.class) + void containerTemplateWithNestedClass(Lifecycle lifecycle) { + var containerTemplate = ContainerTemplateWithDefaultLifecycleAndNestedClassTestCase.class; + var nestedClass = ContainerTemplateWithDefaultLifecycleAndNestedClassTestCase.InnerTestCase.class; + + var results = executeTests(r -> r // + .selectors(selectClass(containerTemplate)) // + .configurationParameter(Constants.DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME, lifecycle.name())); + + results.allEvents().assertStatistics(stats -> stats.failed(0)); + results.testEvents().assertStatistics(stats -> stats.succeeded(4)); + + assertThat(instanceCount).containsExactly( // + entry(containerTemplate, lifecycle == Lifecycle.PER_CLASS ? 1 : 4), // + entry(nestedClass, lifecycle == Lifecycle.PER_CLASS ? 2 : 4)); + assertThat(lifecyclesMap.keySet()).containsExactlyInAnyOrder(containerTemplate, nestedClass); + assertThat(lifecyclesMap.get(containerTemplate)).filteredOn(Objects::nonNull).containsOnly(lifecycle); + assertThat(lifecyclesMap.get(nestedClass)).filteredOn(Objects::nonNull).containsOnly(lifecycle); + } + private void performAssertions(Class testClass, int numContainers, int numTests, Map.Entry, Integer>[] instanceCountEntries, int allMethods, int eachMethods) { @@ -623,7 +668,7 @@ private void performAssertions(Class testClass, int numContainers, int numTes @SafeVarargs @SuppressWarnings("varargs") - private final Map.Entry, Integer>[] instanceCounts(Map.Entry, Integer>... entries) { + private Map.Entry, Integer>[] instanceCounts(Map.Entry, Integer>... entries) { return entries; } @@ -983,7 +1028,9 @@ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext con String testMethod = context.getTestMethod().map(Method::getName).orElse(null); if (testMethod == null) { assertThat(context.getTestInstance()).isNotPresent(); - assertThat(instanceCount.getOrDefault(context.getRequiredTestClass(), 0)).isEqualTo(0); + if (!isAnnotated(context.getRequiredTestClass().getEnclosingClass(), ContainerTemplate.class)) { + assertThat(instanceCount.getOrDefault(context.getRequiredTestClass(), 0)).isEqualTo(0); + } } instanceMap.put(executionConditionKey(context.getRequiredTestClass(), testMethod), context.getTestInstances().orElse(null)); @@ -1067,4 +1114,66 @@ private static void trackLifecycle(ExtensionContext context) { @interface SingletonTest { } + @SuppressWarnings("JUnitMalformedDeclaration") + @ContainerTemplate + @ExtendWith(Twice.class) + @ExtendWith(InstanceTrackingExtension.class) + static class ContainerTemplateWithDefaultLifecycleTestCase { + + ContainerTemplateWithDefaultLifecycleTestCase() { + incrementInstanceCount(ContainerTemplateWithDefaultLifecycleTestCase.class); + } + + @Test + void test1() { + } + + @Test + void test2() { + } + } + + @ContainerTemplate + @ExtendWith(Twice.class) + @ExtendWith(InstanceTrackingExtension.class) + static class ContainerTemplateWithDefaultLifecycleAndNestedClassTestCase { + + ContainerTemplateWithDefaultLifecycleAndNestedClassTestCase() { + incrementInstanceCount(ContainerTemplateWithDefaultLifecycleAndNestedClassTestCase.class); + } + + @Nested + class InnerTestCase { + + public InnerTestCase() { + incrementInstanceCount(InnerTestCase.class); + } + + @Test + void test1() { + } + + @Test + void test2() { + } + } + } + + private static class Twice implements ContainerTemplateInvocationContextProvider { + + @Override + public boolean supportsContainerTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideContainerTemplateInvocationContexts( + ExtensionContext context) { + return Stream.of(new Ctx(), new Ctx()); + } + + private record Ctx() implements ContainerTemplateInvocationContext { + } + } + } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java index 48682ff17e7a..11b50f5dfd2b 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Named.named; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD; import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import static org.junit.platform.launcher.core.OutputDirectoryProviders.hierarchicalOutputDirectoryProvider; import static org.mockito.ArgumentMatchers.eq; @@ -116,8 +117,8 @@ void fromClassTestDescriptor() { var nestedClassDescriptor = nestedClassDescriptor(); var outerClassDescriptor = outerClassDescriptor(nestedClassDescriptor); - var outerExtensionContext = new ClassExtensionContext(null, null, outerClassDescriptor, configuration, - extensionRegistry, null); + var outerExtensionContext = new ClassExtensionContext(null, null, outerClassDescriptor, PER_METHOD, + configuration, extensionRegistry, null); // @formatter:off assertAll("outerContext", @@ -136,15 +137,15 @@ void fromClassTestDescriptor() { // @formatter:on var nestedExtensionContext = new ClassExtensionContext(outerExtensionContext, null, nestedClassDescriptor, - configuration, extensionRegistry, null); + PER_METHOD, configuration, extensionRegistry, null); assertThat(nestedExtensionContext.getParent()).containsSame(outerExtensionContext); } @Test void ExtensionContext_With_ExtensionRegistry_getExtensions() { var classTestDescriptor = nestedClassDescriptor(); - try (var ctx = new ClassExtensionContext(null, null, classTestDescriptor, configuration, extensionRegistry, - null)) { + try (var ctx = new ClassExtensionContext(null, null, classTestDescriptor, PER_METHOD, configuration, + extensionRegistry, null)) { Extension ext = mock(); when(extensionRegistry.getExtensions(Extension.class)).thenReturn(List.of(ext)); @@ -161,14 +162,14 @@ void tagsCanBeRetrievedInExtensionContext() { var methodTestDescriptor = methodDescriptor(); outerClassDescriptor.addChild(methodTestDescriptor); - var outerExtensionContext = new ClassExtensionContext(null, null, outerClassDescriptor, configuration, - extensionRegistry, null); + var outerExtensionContext = new ClassExtensionContext(null, null, outerClassDescriptor, PER_METHOD, + configuration, extensionRegistry, null); assertThat(outerExtensionContext.getTags()).containsExactly("outer-tag"); assertThat(outerExtensionContext.getRoot()).isSameAs(outerExtensionContext); var nestedExtensionContext = new ClassExtensionContext(outerExtensionContext, null, nestedClassDescriptor, - configuration, extensionRegistry, null); + PER_METHOD, configuration, extensionRegistry, null); assertThat(nestedExtensionContext.getTags()).containsExactlyInAnyOrder("outer-tag", "nested-tag"); assertThat(nestedExtensionContext.getRoot()).isSameAs(outerExtensionContext); @@ -193,7 +194,7 @@ void fromMethodTestDescriptor() { var engineExtensionContext = new JupiterEngineExtensionContext(null, engineDescriptor, configuration, extensionRegistry); var classExtensionContext = new ClassExtensionContext(engineExtensionContext, null, classTestDescriptor, - configuration, extensionRegistry, null); + PER_METHOD, configuration, extensionRegistry, null); var methodExtensionContext = new MethodExtensionContext(classExtensionContext, null, methodTestDescriptor, configuration, extensionRegistry, new OpenTest4JAwareThrowableCollector()); methodExtensionContext.setTestInstances(DefaultTestInstances.of(testInstance)); @@ -221,7 +222,7 @@ void reportEntriesArePublishedToExecutionListener() { var classTestDescriptor = outerClassDescriptor(null); var engineExecutionListener = spy(EngineExecutionListener.class); ExtensionContext extensionContext = new ClassExtensionContext(null, engineExecutionListener, - classTestDescriptor, configuration, extensionRegistry, null); + classTestDescriptor, PER_METHOD, configuration, extensionRegistry, null); var map1 = Collections.singletonMap("key", "value"); var map2 = Collections.singletonMap("other key", "other value"); @@ -346,7 +347,7 @@ private ExtensionContext createExtensionContextForFilePublishing(Path tempDir, EngineExecutionListener engineExecutionListener, ClassTestDescriptor classTestDescriptor) { when(configuration.getOutputDirectoryProvider()) // .thenReturn(hierarchicalOutputDirectoryProvider(tempDir)); - return new ClassExtensionContext(null, engineExecutionListener, classTestDescriptor, configuration, + return new ClassExtensionContext(null, engineExecutionListener, classTestDescriptor, PER_METHOD, configuration, extensionRegistry, null); } @@ -355,8 +356,8 @@ private ExtensionContext createExtensionContextForFilePublishing(Path tempDir, void usingStore() { var methodTestDescriptor = methodDescriptor(); var classTestDescriptor = outerClassDescriptor(methodTestDescriptor); - ExtensionContext parentContext = new ClassExtensionContext(null, null, classTestDescriptor, configuration, - extensionRegistry, null); + ExtensionContext parentContext = new ClassExtensionContext(null, null, classTestDescriptor, PER_METHOD, + configuration, extensionRegistry, null); var childContext = new MethodExtensionContext(parentContext, null, methodTestDescriptor, configuration, extensionRegistry, new OpenTest4JAwareThrowableCollector()); childContext.setTestInstances(DefaultTestInstances.of(new OuterClass())); @@ -415,8 +416,8 @@ void configurationParameter(Function { var classUniqueId = UniqueId.parse("[engine:junit-jupiter]/[class:MyClass]"); var classTestDescriptor = new ClassTestDescriptor(classUniqueId, testClass, configuration); - return new ClassExtensionContext(null, null, classTestDescriptor, configuration, extensionRegistry, - null); + return new ClassExtensionContext(null, null, classTestDescriptor, PER_METHOD, configuration, + extensionRegistry, null); }), // named("method", (JupiterConfiguration configuration) -> { var method = ReflectionSupport.findMethod(testClass, "extensionContextFactories").orElseThrow(); diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptorTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptorTests.java index 6ad584b728df..ad1ef49fc983 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptorTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptorTests.java @@ -48,6 +48,31 @@ */ class TestFactoryTestDescriptorTests { + @Test + void copyIncludesTransformedDynamicDescendantFilter() throws Exception { + var rootUniqueId = UniqueId.forEngine("engine"); + var parentUniqueId = rootUniqueId.append("class", "myClass"); + var originalUniqueId = parentUniqueId.append("old", "testFactory()"); + + var configuration = mock(JupiterConfiguration.class); + when(configuration.getDefaultDisplayNameGenerator()).thenReturn(new CustomDisplayNameGenerator()); + Method testMethod = CustomStreamTestCase.class.getDeclaredMethod("customStream"); + var original = new TestFactoryTestDescriptor(originalUniqueId, CustomStreamTestCase.class, testMethod, List::of, + configuration); + + original.getDynamicDescendantFilter().allowUniqueIdPrefix(originalUniqueId.append("foo", "bar")); + original.getDynamicDescendantFilter().allowIndex(42); + + var newUniqueId = parentUniqueId.append("new", "testFactory()"); + + var copy = original.withUniqueId(new UniqueIdPrefixTransformer(originalUniqueId, newUniqueId)); + + assertThat(copy.getUniqueId()).isEqualTo(newUniqueId); + assertThat(copy.getDynamicDescendantFilter().test(newUniqueId, 0)).isTrue(); + assertThat(copy.getDynamicDescendantFilter().test(newUniqueId, 42)).isTrue(); + assertThat(copy.getDynamicDescendantFilter().test(originalUniqueId, 1)).isFalse(); + } + /** * @since 5.3 */ diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptorTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptorTests.java index c0dab4d6a66e..59a5ec415b7a 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptorTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptorTests.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.Set; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -33,20 +34,22 @@ * @since 5.0 */ class TestTemplateTestDescriptorTests { + private JupiterConfiguration jupiterConfiguration = mock(); + @BeforeEach + void prepareJupiterConfiguration() { + when(jupiterConfiguration.getDefaultDisplayNameGenerator()).thenReturn(new DisplayNameGenerator.Standard()); + } + @Test void inheritsTagsFromParent() throws Exception { - UniqueId rootUniqueId = UniqueId.root("segment", "template"); - UniqueId parentUniqueId = rootUniqueId.append("class", "myClass"); - AbstractTestDescriptor parent = containerTestDescriptorWithTags(parentUniqueId, - singleton(TestTag.create("foo"))); - - when(jupiterConfiguration.getDefaultDisplayNameGenerator()).thenReturn(new DisplayNameGenerator.Standard()); + var rootUniqueId = UniqueId.root("segment", "template"); + var parentUniqueId = rootUniqueId.append("class", "myClass"); + var parent = containerTestDescriptorWithTags(parentUniqueId, singleton(TestTag.create("foo"))); - TestTemplateTestDescriptor testDescriptor = new TestTemplateTestDescriptor( - parentUniqueId.append("tmp", "testTemplate()"), MyTestCase.class, - MyTestCase.class.getDeclaredMethod("testTemplate"), List::of, jupiterConfiguration); + var testDescriptor = new TestTemplateTestDescriptor(parentUniqueId.append("tmp", "testTemplate()"), + MyTestCase.class, MyTestCase.class.getDeclaredMethod("testTemplate"), List::of, jupiterConfiguration); parent.addChild(testDescriptor); assertThat(testDescriptor.getTags()).containsExactlyInAnyOrder(TestTag.create("foo"), TestTag.create("bar"), @@ -55,16 +58,14 @@ void inheritsTagsFromParent() throws Exception { @Test void shouldUseCustomDisplayNameGeneratorIfPresentFromConfiguration() throws Exception { - UniqueId rootUniqueId = UniqueId.root("segment", "template"); - UniqueId parentUniqueId = rootUniqueId.append("class", "myClass"); - AbstractTestDescriptor parent = containerTestDescriptorWithTags(parentUniqueId, - singleton(TestTag.create("foo"))); + var rootUniqueId = UniqueId.root("segment", "template"); + var parentUniqueId = rootUniqueId.append("class", "myClass"); + var parent = containerTestDescriptorWithTags(parentUniqueId, singleton(TestTag.create("foo"))); when(jupiterConfiguration.getDefaultDisplayNameGenerator()).thenReturn(new CustomDisplayNameGenerator()); - TestTemplateTestDescriptor testDescriptor = new TestTemplateTestDescriptor( - parentUniqueId.append("tmp", "testTemplate()"), MyTestCase.class, - MyTestCase.class.getDeclaredMethod("testTemplate"), List::of, jupiterConfiguration); + var testDescriptor = new TestTemplateTestDescriptor(parentUniqueId.append("tmp", "testTemplate()"), + MyTestCase.class, MyTestCase.class.getDeclaredMethod("testTemplate"), List::of, jupiterConfiguration); parent.addChild(testDescriptor); assertThat(testDescriptor.getDisplayName()).isEqualTo("method-display-name"); @@ -72,21 +73,39 @@ void shouldUseCustomDisplayNameGeneratorIfPresentFromConfiguration() throws Exce @Test void shouldUseStandardDisplayNameGeneratorIfConfigurationNotPresent() throws Exception { - UniqueId rootUniqueId = UniqueId.root("segment", "template"); - UniqueId parentUniqueId = rootUniqueId.append("class", "myClass"); - AbstractTestDescriptor parent = containerTestDescriptorWithTags(parentUniqueId, - singleton(TestTag.create("foo"))); - - when(jupiterConfiguration.getDefaultDisplayNameGenerator()).thenReturn(new DisplayNameGenerator.Standard()); + var rootUniqueId = UniqueId.root("segment", "template"); + var parentUniqueId = rootUniqueId.append("class", "myClass"); + var parent = containerTestDescriptorWithTags(parentUniqueId, singleton(TestTag.create("foo"))); - TestTemplateTestDescriptor testDescriptor = new TestTemplateTestDescriptor( - parentUniqueId.append("tmp", "testTemplate()"), MyTestCase.class, - MyTestCase.class.getDeclaredMethod("testTemplate"), List::of, jupiterConfiguration); + var testDescriptor = new TestTemplateTestDescriptor(parentUniqueId.append("tmp", "testTemplate()"), + MyTestCase.class, MyTestCase.class.getDeclaredMethod("testTemplate"), List::of, jupiterConfiguration); parent.addChild(testDescriptor); assertThat(testDescriptor.getDisplayName()).isEqualTo("testTemplate()"); } + @Test + void copyIncludesTransformedDynamicDescendantFilter() throws Exception { + var rootUniqueId = UniqueId.root("segment", "template"); + var parentUniqueId = rootUniqueId.append("class", "myClass"); + var originalUniqueId = parentUniqueId.append("old", "testTemplate()"); + + var original = new TestTemplateTestDescriptor(originalUniqueId, MyTestCase.class, + MyTestCase.class.getDeclaredMethod("testTemplate"), List::of, jupiterConfiguration); + + original.getDynamicDescendantFilter().allowUniqueIdPrefix(originalUniqueId.append("foo", "bar")); + original.getDynamicDescendantFilter().allowIndex(42); + + var newUniqueId = parentUniqueId.append("new", "testTemplate()"); + + var copy = original.withUniqueId(new UniqueIdPrefixTransformer(originalUniqueId, newUniqueId)); + + assertThat(copy.getUniqueId()).isEqualTo(newUniqueId); + assertThat(copy.getDynamicDescendantFilter().test(newUniqueId, 0)).isTrue(); + assertThat(copy.getDynamicDescendantFilter().test(newUniqueId, 42)).isTrue(); + assertThat(copy.getDynamicDescendantFilter().test(originalUniqueId, 1)).isFalse(); + } + private AbstractTestDescriptor containerTestDescriptorWithTags(UniqueId uniqueId, Set tags) { return new AbstractTestDescriptor(uniqueId, "testDescriptor with tags") { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/DiscoverySelectorResolverTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/DiscoverySelectorResolverTests.java index 6c74b09dbbb4..8af8e24cb92a 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/DiscoverySelectorResolverTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/discovery/DiscoverySelectorResolverTests.java @@ -17,12 +17,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.engine.descriptor.TestFactoryTestDescriptor.DYNAMIC_CONTAINER_SEGMENT_TYPE; import static org.junit.jupiter.engine.descriptor.TestFactoryTestDescriptor.DYNAMIC_TEST_SEGMENT_TYPE; +import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.appendContainerTemplateInvocationSegment; import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.engineId; import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForClass; import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForMethod; +import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForStaticClass; import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForTestFactoryMethod; import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForTestTemplateMethod; -import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForTopLevelClass; import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement; import static org.junit.platform.engine.SelectorResolutionResult.Status.FAILED; import static org.junit.platform.engine.SelectorResolutionResult.Status.RESOLVED; @@ -53,6 +54,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.ContainerTemplate; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Nested; @@ -192,6 +194,49 @@ void classResolutionOfStaticNestedClass() { assertThat(uniqueIds).contains(uniqueIdForMethod(OtherTestClass.NestedTestClass.class, "test6()")); } + @Test + void classResolutionOfContainerTemplate() { + var selector = selectClass(ContainerTemplateTestCase.class); + + resolve(request().selectors(selector)); + + assertThat(engineDescriptor.getChildren()).hasSize(1); + + TestDescriptor containerTemplateDescriptor = getOnlyElement(engineDescriptor.getChildren()); + assertThat(containerTemplateDescriptor.mayRegisterTests()).isFalse(); + assertThat(containerTemplateDescriptor.getDescendants()).hasSize(1); + + var containerTemplateSegment = containerTemplateDescriptor.getUniqueId().getLastSegment(); + assertThat(containerTemplateSegment.getType()).isEqualTo("container-template"); + assertThat(containerTemplateSegment.getValue()).isEqualTo(ContainerTemplateTestCase.class.getName()); + + containerTemplateDescriptor.prune(); + assertThat(containerTemplateDescriptor.mayRegisterTests()).isTrue(); + assertThat(containerTemplateDescriptor.getDescendants()).isEmpty(); + } + + @Test + void uniqueIdResolutionOfContainerTemplateInvocation() { + var selector = selectUniqueId( + appendContainerTemplateInvocationSegment(uniqueIdForClass(ContainerTemplateTestCase.class), 1)); + + resolve(request().selectors(selector)); + + assertThat(engineDescriptor.getChildren()).hasSize(1); + + TestDescriptor containerTemplateDescriptor = getOnlyElement(engineDescriptor.getChildren()); + + containerTemplateDescriptor.prune(); + assertThat(engineDescriptor.getChildren()).hasSize(1); + assertThat(containerTemplateDescriptor.mayRegisterTests()).isTrue(); + assertThat(containerTemplateDescriptor.getDescendants()).isEmpty(); + + containerTemplateDescriptor.prune(); + assertThat(engineDescriptor.getChildren()).hasSize(1); + assertThat(containerTemplateDescriptor.mayRegisterTests()).isTrue(); + assertThat(containerTemplateDescriptor.getDescendants()).isEmpty(); + } + @Test void methodResolution() throws NoSuchMethodException { Method test1 = MyTestClass.class.getDeclaredMethod("test1"); @@ -502,8 +547,8 @@ void classpathResolutionForJarFiles() throws Exception { resolve(request().selectors(selectors)); assertThat(uniqueIds()) // - .contains(uniqueIdForTopLevelClass("com.example.project.FirstTest")) // - .contains(uniqueIdForTopLevelClass("com.example.project.SecondTest")); + .contains(uniqueIdForStaticClass("com.example.project.FirstTest")) // + .contains(uniqueIdForStaticClass("com.example.project.SecondTest")); } finally { Thread.currentThread().setContextClassLoader(originalClassLoader); @@ -899,3 +944,10 @@ class OtherClass { void test() { } } + +@ContainerTemplate +class ContainerTemplateTestCase { + @Test + void test() { + } +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/OrderedClassTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/OrderedClassTests.java index ff8560830382..42af0d90dc04 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/OrderedClassTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/OrderedClassTests.java @@ -20,16 +20,22 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.LogRecord; +import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.ClassOrderer; +import org.junit.jupiter.api.ContainerTemplate; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestClassOrder; import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.ContainerTemplateInvocationContext; +import org.junit.jupiter.api.extension.ContainerTemplateInvocationContextProvider; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.fixtures.TrackLogRecords; import org.junit.platform.commons.logging.LogRecordListener; import org.junit.platform.engine.DiscoverySelector; @@ -128,6 +134,42 @@ void random() { .assertStatistics(stats -> stats.succeeded(callSequence.size())); } + @Test + void containerTemplateWithLocalConfig() { + var containerTemplate = ContainerTemplateWithLocalConfigTestCase.class; + var inner0 = ContainerTemplateWithLocalConfigTestCase.Inner0.class; + var inner1 = ContainerTemplateWithLocalConfigTestCase.Inner1.class; + var inner1Inner1 = ContainerTemplateWithLocalConfigTestCase.Inner1.Inner1Inner1.class; + var inner1Inner0 = ContainerTemplateWithLocalConfigTestCase.Inner1.Inner1Inner0.class; + + executeTests(ClassOrderer.Random.class, selectClass(containerTemplate))// + .assertStatistics(stats -> stats.succeeded(callSequence.size())); + + var inner1InvocationCallSequence = Stream.of(inner1, inner1Inner1, inner1Inner0, inner1Inner0).toList(); + var inner1CallSequence = twice(inner1InvocationCallSequence).toList(); + var outerCallSequence = Stream.concat(Stream.of(containerTemplate), + Stream.concat(inner1CallSequence.stream(), Stream.of(inner0))).toList(); + var expectedCallSequence = twice(outerCallSequence).map(Class::getSimpleName).toList(); + + assertThat(callSequence).containsExactlyElementsOf(expectedCallSequence); + } + + private static Stream twice(List values) { + return Stream.concat(values.stream(), values.stream()); + } + + @Test + void containerTemplateWithGlobalConfig() { + var containerTemplate = ContainerTemplateWithLocalConfigTestCase.class; + var otherClass = A_TestCase.class; + + executeTests(ClassOrderer.OrderAnnotation.class, selectClass(otherClass), selectClass(containerTemplate))// + .assertStatistics(stats -> stats.succeeded(callSequence.size())); + + assertThat(callSequence)// + .containsSubsequence(containerTemplate.getSimpleName(), otherClass.getSimpleName()); + } + private Events executeTests(Class classOrderer) { return executeTests(classOrderer, selectClass(A_TestCase.class), selectClass(B_TestCase.class), selectClass(C_TestCase.class)); @@ -266,4 +308,73 @@ void test() { } } + @SuppressWarnings("JUnitMalformedDeclaration") + @Order(1) + @TestClassOrder(ClassOrderer.OrderAnnotation.class) + @ContainerTemplate + @ExtendWith(ContainerTemplateWithLocalConfigTestCase.Twice.class) + static class ContainerTemplateWithLocalConfigTestCase { + + @Test + void test() { + callSequence.add(ContainerTemplateWithLocalConfigTestCase.class.getSimpleName()); + } + + @Nested + @Order(1) + class Inner0 { + @Test + void test() { + callSequence.add(getClass().getSimpleName()); + } + } + + @Nested + @ContainerTemplate + @Order(0) + class Inner1 { + + @Test + void test() { + callSequence.add(getClass().getSimpleName()); + } + + @Nested + @ContainerTemplate + @Order(2) + class Inner1Inner0 { + @Test + void test() { + callSequence.add(getClass().getSimpleName()); + } + } + + @Nested + @Order(1) + class Inner1Inner1 { + @Test + void test() { + callSequence.add(getClass().getSimpleName()); + } + } + } + + private static class Twice implements ContainerTemplateInvocationContextProvider { + + @Override + public boolean supportsContainerTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideContainerTemplateInvocationContexts( + ExtensionContext context) { + return Stream.of(new Ctx(), new Ctx()); + } + + private record Ctx() implements ContainerTemplateInvocationContext { + } + } + } + } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/OrderedMethodTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/OrderedMethodTests.java index 0e245db1a2a1..3d555d44f3ee 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/OrderedMethodTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/OrderedMethodTests.java @@ -52,6 +52,8 @@ import org.junit.jupiter.api.TestReporter; import org.junit.jupiter.api.fixtures.TrackLogRecords; import org.junit.jupiter.engine.JupiterTestEngine; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.junit.platform.commons.logging.LogRecordListener; import org.junit.platform.commons.util.ClassUtils; import org.junit.platform.testkit.engine.EngineTestKit; @@ -173,9 +175,10 @@ void random() { assertThat(threadNames).hasSize(1); } - @Test - void defaultOrderer() { - var tests = executeTestsInParallel(WithoutTestMethodOrderTestCase.class, OrderAnnotation.class); + @ParameterizedTest + @ValueSource(classes = { WithoutTestMethodOrderTestCase.class, ContainerTemplateTestCase.class }) + void defaultOrderer(Class testClass) { + var tests = executeTestsInParallel(testClass, OrderAnnotation.class); tests.assertStatistics(stats -> stats.succeeded(callSequence.size())); @@ -845,4 +848,7 @@ void test1() { } + static class ContainerTemplateTestCase extends WithoutTestMethodOrderTestCase { + } + } diff --git a/platform-tooling-support-tests/projects/jupiter-starter/src/test/java/com/example/project/CalculatorContainerTemplateTests.java b/platform-tooling-support-tests/projects/jupiter-starter/src/test/java/com/example/project/CalculatorContainerTemplateTests.java new file mode 100644 index 000000000000..00b58cce65e8 --- /dev/null +++ b/platform-tooling-support-tests/projects/jupiter-starter/src/test/java/com/example/project/CalculatorContainerTemplateTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package com.example.project; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.ContainerTemplate; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ContainerTemplateInvocationContext; +import org.junit.jupiter.api.extension.ContainerTemplateInvocationContextProvider; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@ContainerTemplate +@ExtendWith(CalculatorContainerTemplateTests.Twice.class) +class CalculatorContainerTemplateTests { + + @Test + void regularTest() { + Calculator calculator = new Calculator(); + assertEquals(2, calculator.add(1, 1), "1 + 1 should equal 2"); + } + + @ParameterizedTest + @ValueSource(ints = { 1, 2 }) + void parameterizedTest(int i) { + Calculator calculator = new Calculator(); + assertEquals(i, calculator.add(i, 0)); + } + + static class Twice implements ContainerTemplateInvocationContextProvider { + + @Override + public boolean supportsContainerTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideContainerTemplateInvocationContexts( + ExtensionContext context) { + return Stream.of(new Ctx(), new Ctx()); + } + + static class Ctx implements ContainerTemplateInvocationContext { + } + } +} diff --git a/platform-tooling-support-tests/projects/jupiter-starter/src/test/resources/junit-platform.properties b/platform-tooling-support-tests/projects/jupiter-starter/src/test/resources/junit-platform.properties new file mode 100644 index 000000000000..daf7418ffd20 --- /dev/null +++ b/platform-tooling-support-tests/projects/jupiter-starter/src/test/resources/junit-platform.properties @@ -0,0 +1,2 @@ +junit.jupiter.testclass.order.default = \ + org.junit.jupiter.api.ClassOrderer$ClassName diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/AntStarterTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/AntStarterTests.java index 171548801968..084cdcb89032 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/AntStarterTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/AntStarterTests.java @@ -51,13 +51,15 @@ void ant_starter(@TempDir Path workspace, @FilePrefix("ant") OutputFiles outputF assertLinesMatch(List.of(">> HEAD >>", // "test.junit.launcher:", // ">>>>", // + "\\[junitlauncher\\] Tests run: 6, Failures: 0, Aborted: 0, Skipped: 0, Time elapsed: .+ sec", // + "\\[junitlauncher\\] Running com.example.project.CalculatorTests", // "\\[junitlauncher\\] Tests run: 5, Failures: 0, Aborted: 0, Skipped: 0, Time elapsed: .+ sec", // ">>>>", // "test.console.launcher:", // ">>>>", // " \\[java\\] Test run finished after [\\d]+ ms", // ">>>>", // - " \\[java\\] \\[ 5 tests successful \\]", // + " \\[java\\] \\[ 11 tests successful \\]", // " \\[java\\] \\[ 0 tests failed \\]", // ">> TAIL >>"), // result.stdOutLines()); diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/GradleStarterTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/GradleStarterTests.java index b25ca6589027..07756177eef8 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/GradleStarterTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/GradleStarterTests.java @@ -21,9 +21,11 @@ import de.skuzzle.test.snapshots.Snapshot; import de.skuzzle.test.snapshots.junit5.EnableSnapshotTests; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.platform.tests.process.OutputFiles; +import org.junit.platform.tests.process.ProcessResult; import org.opentest4j.TestAbortedException; import platform.tooling.support.Helper; @@ -36,23 +38,71 @@ @EnableSnapshotTests class GradleStarterTests { + @TempDir + Path workspace; + + @BeforeEach + void prepareWorkspace() throws Exception { + copyToWorkspace(Projects.JUPITER_STARTER, workspace); + } + @Test - void gradle_wrapper(@TempDir Path workspace, @FilePrefix("gradle") OutputFiles outputFiles, Snapshot snapshot) - throws Exception { + void buildJupiterStarterProject(@FilePrefix("gradle") OutputFiles outputFiles, Snapshot snapshot) throws Exception { + + var result = runGradle(outputFiles, "build"); + + assertThat(result.stdOut()) // + .contains( // + "CalculatorContainerTemplateTests > [1] > regularTest() PASSED", // + "CalculatorContainerTemplateTests > [2] > regularTest() PASSED", // + "CalculatorContainerTemplateTests > [1] > parameterizedTest(int)", // + "CalculatorContainerTemplateTests > [2] > parameterizedTest(int)", // + "Using Java version: 1.8", // + "CalculatorTests > 1 + 1 = 2 PASSED", // + "CalculatorTests > add(int, int, int) > 0 + 1 = 1 PASSED", // + "CalculatorTests > add(int, int, int) > 1 + 2 = 3 PASSED", // + "CalculatorTests > add(int, int, int) > 49 + 51 = 100 PASSED", // + "CalculatorTests > add(int, int, int) > 1 + 100 = 101 PASSED" // + ); + var testResultsDir = workspace.resolve("build/test-results/test"); + verifyContainsExpectedStartedOpenTestReport(testResultsDir, snapshot); + } + + @Test + void runOnlyOneMethodInContainerTemplate(@FilePrefix("gradle") OutputFiles outputFiles) throws Exception { + + var result = runGradle(outputFiles, "test", "--tests", "CalculatorContainer*.regular*"); + + assertThat(result.stdOut()) // + .contains( // + "CalculatorContainerTemplateTests > [1] > regularTest() PASSED", // + "CalculatorContainerTemplateTests > [2] > regularTest() PASSED" // + ) // + .doesNotContain("parameterizedTest(int)", "CalculatorTests"); + + result = runGradle(outputFiles, "test", "--tests", "*ContainerTemplateTests.parameterized*"); + + assertThat(result.stdOut()) // + .contains( // + "CalculatorContainerTemplateTests > [1] > parameterizedTest(int)", // + "CalculatorContainerTemplateTests > [2] > parameterizedTest(int)" // + ) // + .doesNotContain("regularTest()", "CalculatorTests"); + } + + private ProcessResult runGradle(OutputFiles outputFiles, String... extraArgs) throws InterruptedException { var result = ProcessStarters.gradlew() // - .workingDir(copyToWorkspace(Projects.JUPITER_STARTER, workspace)) // + .workingDir(workspace) // .addArguments("-Dmaven.repo=" + MavenRepo.dir()) // - .addArguments("build", "--no-daemon", "--stacktrace", "--no-build-cache", "--warning-mode=fail") // - .putEnvironment("JDK8", Helper.getJavaHome("8").orElseThrow(TestAbortedException::new).toString()) // + .addArguments("--stacktrace", "--no-build-cache", "--warning-mode=fail") // + .addArguments(extraArgs).putEnvironment("JDK8", + Helper.getJavaHome("8").orElseThrow(TestAbortedException::new).toString()) // .redirectOutput(outputFiles) // .startAndWait(); assertEquals(0, result.exitCode()); assertTrue(result.stdOut().lines().anyMatch(line -> line.contains("BUILD SUCCESSFUL"))); - assertThat(result.stdOut()).contains("Using Java version: 1.8"); - - var testResultsDir = workspace.resolve("build/test-results/test"); - verifyContainsExpectedStartedOpenTestReport(testResultsDir, snapshot); + return result; } } diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/MavenStarterTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/MavenStarterTests.java index 11ef756bd352..cece033b4aa5 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/MavenStarterTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/MavenStarterTests.java @@ -12,7 +12,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static platform.tooling.support.tests.Projects.copyToWorkspace; import static platform.tooling.support.tests.XmlAssertions.verifyContainsExpectedStartedOpenTestReport; @@ -21,9 +20,11 @@ import de.skuzzle.test.snapshots.Snapshot; import de.skuzzle.test.snapshots.junit5.EnableSnapshotTests; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.platform.tests.process.OutputFiles; +import org.junit.platform.tests.process.ProcessResult; import org.opentest4j.TestAbortedException; import platform.tooling.support.Helper; @@ -42,25 +43,54 @@ class MavenStarterTests { @ManagedResource MavenRepoProxy mavenRepoProxy; + @TempDir + Path workspace; + + @BeforeEach + void prepareWorkspace() throws Exception { + copyToWorkspace(Projects.JUPITER_STARTER, workspace); + } + @Test - void verifyJupiterStarterProject(@TempDir Path workspace, @FilePrefix("maven") OutputFiles outputFiles, - Snapshot snapshot) throws Exception { + void verifyJupiterStarterProject(@FilePrefix("maven") OutputFiles outputFiles, Snapshot snapshot) throws Exception { + + var result = runMaven(outputFiles, "verify"); + + assertThat(result.stdOutLines()).contains("[INFO] Tests run: 11, Failures: 0, Errors: 0, Skipped: 0"); + assertThat(result.stdOut()).contains("Using Java version: 1.8"); + var testResultsDir = workspace.resolve("target/surefire-reports"); + verifyContainsExpectedStartedOpenTestReport(testResultsDir, snapshot); + } + + @Test + void runOnlyOneMethodInContainerTemplate(@FilePrefix("maven") OutputFiles outputFiles) throws Exception { + + var result = runMaven(outputFiles, "test", "-Dtest=CalculatorContainerTemplateTests#regularTest"); + + assertThat(result.stdOutLines()) // + .doesNotContain("CalculatorTests") // + .contains("[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0"); + + result = runMaven(outputFiles, "test", "-Dtest=CalculatorContainerTemplateTests#parameterizedTest"); + + assertThat(result.stdOutLines()) // + .doesNotContain("CalculatorTests") // + .contains("[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0"); + } + + private ProcessResult runMaven(OutputFiles outputFiles, String... extraArgs) throws InterruptedException { var result = ProcessStarters.maven(Helper.getJavaHome("8").orElseThrow(TestAbortedException::new)) // - .workingDir(copyToWorkspace(Projects.JUPITER_STARTER, workspace)) // + .workingDir(workspace) // .addArguments(localMavenRepo.toCliArgument(), "-Dmaven.repo=" + MavenRepo.dir()) // .addArguments("-Dsnapshot.repo.url=" + mavenRepoProxy.getBaseUri()) // - .addArguments("--update-snapshots", "--batch-mode", "verify") // - .redirectOutput(outputFiles) // + .addArguments("--update-snapshots", "--batch-mode") // + .addArguments(extraArgs).redirectOutput(outputFiles) // .startAndWait(); assertEquals(0, result.exitCode()); assertEquals("", result.stdErr()); - assertTrue(result.stdOutLines().contains("[INFO] BUILD SUCCESS")); - assertTrue(result.stdOutLines().contains("[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0")); - assertThat(result.stdOut()).contains("Using Java version: 1.8"); - - var testResultsDir = workspace.resolve("target/surefire-reports"); - verifyContainsExpectedStartedOpenTestReport(testResultsDir, snapshot); + assertThat(result.stdOutLines()).contains("[INFO] BUILD SUCCESS"); + return result; } } diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/UnalignedClasspathTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/UnalignedClasspathTests.java index 80028afebcd4..6ce670bc5ecf 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/UnalignedClasspathTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/UnalignedClasspathTests.java @@ -12,7 +12,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD; import static platform.tooling.support.ProcessStarters.currentJdkHome; import static platform.tooling.support.tests.Projects.copyToWorkspace; @@ -60,7 +59,7 @@ void verifyErrorMessageForUnalignedClasspath(JRE jre, Path javaHome, @TempDir Pa assertEquals(1, result.exitCode()); assertEquals("", result.stdErr()); - assertTrue(result.stdOutLines().contains("[INFO] BUILD FAILURE")); + assertThat(result.stdOutLines()).contains("[INFO] BUILD FAILURE"); assertThat(result.stdOut()) // .contains("The wrapped NoClassDefFoundError is likely caused by the versions of JUnit jars " + "on the classpath/module path not being properly aligned"); diff --git a/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/AntStarterTests_snapshots/open-test-report.xml.snapshot b/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/AntStarterTests_snapshots/open-test-report.xml.snapshot index a947aa10f920..7144e4830014 100644 --- a/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/AntStarterTests_snapshots/open-test-report.xml.snapshot +++ b/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/AntStarterTests_snapshots/open-test-report.xml.snapshot @@ -21,7 +21,7 @@ test-method: ant_starter - + [engine:junit-jupiter] JUnit Jupiter @@ -29,7 +29,172 @@ test-method: ant_starter - + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests] + com.example.project.CalculatorContainerTemplateTests + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#1] + com.example.project.CalculatorContainerTemplateTests[1] + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#1]/[method:regularTest()] + regularTest() + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#1]/[test-template:parameterizedTest(int)] + parameterizedTest(int) + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#1]/[test-template:parameterizedTest(int)]/[test-template-invocation:#1] + parameterizedTest(int)[1] + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#1]/[test-template:parameterizedTest(int)]/[test-template-invocation:#2] + parameterizedTest(int)[2] + TEST + + + + + + + + + + + + + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#2] + com.example.project.CalculatorContainerTemplateTests[2] + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#2]/[method:regularTest()] + regularTest() + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#2]/[test-template:parameterizedTest(int)] + parameterizedTest(int) + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#2]/[test-template:parameterizedTest(int)]/[test-template-invocation:#1] + parameterizedTest(int)[1] + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#2]/[test-template:parameterizedTest(int)]/[test-template-invocation:#2] + parameterizedTest(int)[2] + TEST + + + + + + + + + + + + + + + + + + + + + + + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests] com.example.project.CalculatorTests @@ -40,7 +205,7 @@ test-method: ant_starter - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[method:addsTwoNumbers()] addsTwoNumbers() @@ -51,11 +216,11 @@ test-method: ant_starter - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)] add(int, int, int) @@ -66,7 +231,7 @@ test-method: ant_starter - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#1] add(int, int, int)[1] @@ -77,11 +242,11 @@ test-method: ant_starter - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#2] add(int, int, int)[2] @@ -92,11 +257,11 @@ test-method: ant_starter - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#3] add(int, int, int)[3] @@ -107,11 +272,11 @@ test-method: ant_starter - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#4] add(int, int, int)[4] @@ -122,19 +287,19 @@ test-method: ant_starter - + - + - + - + diff --git a/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/GradleStarterTests_snapshots/open-test-report.xml.snapshot b/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/GradleStarterTests_snapshots/open-test-report.xml.snapshot index 3be1b286c9bb..020b5160c082 100644 --- a/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/GradleStarterTests_snapshots/open-test-report.xml.snapshot +++ b/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/GradleStarterTests_snapshots/open-test-report.xml.snapshot @@ -2,7 +2,7 @@ dynamic-directory: false snapshot-name: open-test-report.xml snapshot-number: 0 test-class: platform.tooling.support.tests.GradleStarterTests -test-method: gradle_wrapper +test-method: buildJupiterStarterProject - + [engine:junit-jupiter] JUnit Jupiter @@ -29,7 +29,172 @@ test-method: gradle_wrapper - + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests] + com.example.project.CalculatorContainerTemplateTests + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#1] + com.example.project.CalculatorContainerTemplateTests[1] + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#1]/[method:regularTest()] + regularTest() + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#1]/[test-template:parameterizedTest(int)] + parameterizedTest(int) + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#1]/[test-template:parameterizedTest(int)]/[test-template-invocation:#1] + parameterizedTest(int)[1] + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#1]/[test-template:parameterizedTest(int)]/[test-template-invocation:#2] + parameterizedTest(int)[2] + TEST + + + + + + + + + + + + + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#2] + com.example.project.CalculatorContainerTemplateTests[2] + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#2]/[method:regularTest()] + regularTest() + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#2]/[test-template:parameterizedTest(int)] + parameterizedTest(int) + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#2]/[test-template:parameterizedTest(int)]/[test-template-invocation:#1] + parameterizedTest(int)[1] + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#2]/[test-template:parameterizedTest(int)]/[test-template-invocation:#2] + parameterizedTest(int)[2] + TEST + + + + + + + + + + + + + + + + + + + + + + + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests] com.example.project.CalculatorTests @@ -40,7 +205,7 @@ test-method: gradle_wrapper - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[method:addsTwoNumbers()] addsTwoNumbers() @@ -51,11 +216,11 @@ test-method: gradle_wrapper - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)] add(int, int, int) @@ -66,7 +231,7 @@ test-method: gradle_wrapper - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#1] add(int, int, int)[1] @@ -77,11 +242,11 @@ test-method: gradle_wrapper - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#2] add(int, int, int)[2] @@ -92,11 +257,11 @@ test-method: gradle_wrapper - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#3] add(int, int, int)[3] @@ -107,11 +272,11 @@ test-method: gradle_wrapper - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#4] add(int, int, int)[4] @@ -122,19 +287,19 @@ test-method: gradle_wrapper - + - + - + - + diff --git a/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/MavenStarterTests_snapshots/open-test-report.xml.snapshot b/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/MavenStarterTests_snapshots/open-test-report.xml.snapshot index 385bb5aff6a5..4c7776b69c9d 100644 --- a/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/MavenStarterTests_snapshots/open-test-report.xml.snapshot +++ b/platform-tooling-support-tests/src/test/resources/platform/tooling/support/tests/MavenStarterTests_snapshots/open-test-report.xml.snapshot @@ -21,7 +21,7 @@ test-method: verifyJupiterStarterProject - + [engine:junit-jupiter] JUnit Jupiter @@ -29,7 +29,172 @@ test-method: verifyJupiterStarterProject - + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests] + com.example.project.CalculatorContainerTemplateTests + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#1] + com.example.project.CalculatorContainerTemplateTests[1] + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#1]/[method:regularTest()] + regularTest() + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#1]/[test-template:parameterizedTest(int)] + parameterizedTest(int) + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#1]/[test-template:parameterizedTest(int)]/[test-template-invocation:#1] + parameterizedTest(int)[1] + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#1]/[test-template:parameterizedTest(int)]/[test-template-invocation:#2] + parameterizedTest(int)[2] + TEST + + + + + + + + + + + + + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#2] + com.example.project.CalculatorContainerTemplateTests[2] + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#2]/[method:regularTest()] + regularTest() + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#2]/[test-template:parameterizedTest(int)] + parameterizedTest(int) + CONTAINER + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#2]/[test-template:parameterizedTest(int)]/[test-template-invocation:#1] + parameterizedTest(int)[1] + TEST + + + + + + + + + + + + + [engine:junit-jupiter]/[container-template:com.example.project.CalculatorContainerTemplateTests]/[container-template-invocation:#2]/[test-template:parameterizedTest(int)]/[test-template-invocation:#2] + parameterizedTest(int)[2] + TEST + + + + + + + + + + + + + + + + + + + + + + + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests] com.example.project.CalculatorTests @@ -40,7 +205,7 @@ test-method: verifyJupiterStarterProject - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[method:addsTwoNumbers()] addsTwoNumbers() @@ -51,11 +216,11 @@ test-method: verifyJupiterStarterProject - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)] add(int, int, int) @@ -66,7 +231,7 @@ test-method: verifyJupiterStarterProject - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#1] add(int, int, int)[1] @@ -77,11 +242,11 @@ test-method: verifyJupiterStarterProject - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#2] add(int, int, int)[2] @@ -92,11 +257,11 @@ test-method: verifyJupiterStarterProject - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#3] add(int, int, int)[3] @@ -107,11 +272,11 @@ test-method: verifyJupiterStarterProject - + - + [engine:junit-jupiter]/[class:com.example.project.CalculatorTests]/[test-template:add(int, int, int)]/[test-template-invocation:#4] add(int, int, int)[4] @@ -122,19 +287,19 @@ test-method: verifyJupiterStarterProject - + - + - + - + From 129337652195efbb946a79122e6a2e09aad1c026 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 19 Feb 2025 10:22:49 +0100 Subject: [PATCH 012/167] Evaluate `Arguments.get()` at most once --- ...ameterizedTestNameFormatterBenchmarks.java | 2 +- .../params/ArgumentCountValidator.java | 13 ++- .../jupiter/params/EvaluatedArgumentSet.java | 101 ++++++++++++++++++ .../ParameterizedTestInvocationContext.java | 20 ++-- .../ParameterizedTestNameFormatter.java | 34 +++--- .../ParameterizedTestParameterResolver.java | 22 +--- .../ParameterizedTestIntegrationTests.java | 28 +++++ .../ParameterizedTestNameFormatterTests.java | 2 +- 8 files changed, 167 insertions(+), 55 deletions(-) create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/EvaluatedArgumentSet.java diff --git a/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedTestNameFormatterBenchmarks.java b/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedTestNameFormatterBenchmarks.java index dad2ce3e2115..d6c62b3fcc20 100644 --- a/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedTestNameFormatterBenchmarks.java +++ b/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedTestNameFormatterBenchmarks.java @@ -51,7 +51,7 @@ public void formatTestNames(Blackhole blackhole) throws Exception { 512); for (int i = 0; i < argumentsList.size(); i++) { Arguments arguments = argumentsList.get(i); - blackhole.consume(formatter.format(i, arguments, arguments.get())); + blackhole.consume(formatter.format(i, EvaluatedArgumentSet.allOf(arguments))); } } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java index 42a1f48ea54c..556543b35319 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java @@ -18,7 +18,6 @@ import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.InvocationInterceptor; import org.junit.jupiter.api.extension.ReflectiveInvocationContext; -import org.junit.jupiter.params.provider.Arguments; import org.junit.platform.commons.logging.Logger; import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.util.Preconditions; @@ -31,9 +30,9 @@ class ArgumentCountValidator implements InvocationInterceptor { ArgumentCountValidator.class); private final ParameterizedTestMethodContext methodContext; - private final Arguments arguments; + private final EvaluatedArgumentSet arguments; - ArgumentCountValidator(ParameterizedTestMethodContext methodContext, Arguments arguments) { + ArgumentCountValidator(ParameterizedTestMethodContext methodContext, EvaluatedArgumentSet arguments) { this.methodContext = methodContext; this.arguments = arguments; } @@ -41,7 +40,7 @@ class ArgumentCountValidator implements InvocationInterceptor { @Override public void interceptTestTemplateMethod(InvocationInterceptor.Invocation invocation, ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { - validateArgumentCount(extensionContext, arguments); + validateArgumentCount(extensionContext); invocation.proceed(); } @@ -49,7 +48,7 @@ private ExtensionContext.Store getStore(ExtensionContext context) { return context.getRoot().getStore(NAMESPACE); } - private void validateArgumentCount(ExtensionContext extensionContext, Arguments arguments) { + private void validateArgumentCount(ExtensionContext extensionContext) { ArgumentCountValidationMode argumentCountValidationMode = getArgumentCountValidationMode(extensionContext); switch (argumentCountValidationMode) { case DEFAULT: @@ -57,10 +56,10 @@ private void validateArgumentCount(ExtensionContext extensionContext, Arguments return; case STRICT: int testParamCount = extensionContext.getRequiredTestMethod().getParameterCount(); - int argumentsCount = arguments.get().length; + int argumentsCount = arguments.getTotalLength(); Preconditions.condition(testParamCount == argumentsCount, () -> String.format( "Configuration error: the @ParameterizedTest has %s argument(s) but there were %s argument(s) provided.%nNote: the provided arguments are %s", - testParamCount, argumentsCount, Arrays.toString(arguments.get()))); + testParamCount, argumentsCount, Arrays.toString(arguments.getAllPayloads()))); break; default: throw new ExtensionConfigurationException( diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/EvaluatedArgumentSet.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/EvaluatedArgumentSet.java new file mode 100644 index 000000000000..c4a72daccee1 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/EvaluatedArgumentSet.java @@ -0,0 +1,101 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import java.util.Arrays; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.IntUnaryOperator; + +import org.junit.jupiter.api.Named; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.platform.commons.util.Preconditions; + +/** + * Encapsulates the evaluation of an {@link Arguments} instance (so it happens + * only once) and access to the resulting argument values. + * + *

The provided accessor methods are focused on the different use cases and + * make it less error-prone to access the argument values. + * + * @since 5.13 + */ +class EvaluatedArgumentSet { + + static EvaluatedArgumentSet allOf(Arguments arguments) { + Object[] all = arguments.get(); + return create(all, all, arguments); + } + + static EvaluatedArgumentSet of(Arguments arguments, IntUnaryOperator consumedLengthComputer) { + Object[] all = arguments.get(); + Object[] consumed = dropSurplus(all, consumedLengthComputer.applyAsInt(all.length)); + return create(all, consumed, arguments); + } + + private static EvaluatedArgumentSet create(Object[] all, Object[] consumed, Arguments arguments) { + return new EvaluatedArgumentSet(all, consumed, determineName(arguments)); + } + + private final Object[] all; + private final Object[] consumed; + private final Optional name; + + private EvaluatedArgumentSet(Object[] all, Object[] consumed, Optional name) { + this.all = all; + this.consumed = consumed; + this.name = name; + } + + int getTotalLength() { + return this.all.length; + } + + Object[] getAllPayloads() { + return extractFromNamed(this.all, Named::getPayload); + } + + int getConsumedLength() { + return this.consumed.length; + } + + Object[] getConsumedNames() { + return extractFromNamed(this.consumed, Named::getName); + } + + Object[] getConsumedPayloads() { + return extractFromNamed(this.consumed, Named::getPayload); + } + + Optional getName() { + return this.name; + } + + private static Object[] dropSurplus(Object[] arguments, int newLength) { + Preconditions.condition(newLength <= arguments.length, + () -> String.format("New length %d must be less than or equal to the total length %d", newLength, + arguments.length)); + return arguments.length > newLength ? Arrays.copyOf(arguments, newLength) : arguments; + } + + private static Optional determineName(Arguments arguments) { + if (arguments instanceof Arguments.ArgumentSet) { + return Optional.of(((Arguments.ArgumentSet) arguments).getName()); + } + return Optional.empty(); + } + + private static Object[] extractFromNamed(Object[] arguments, Function, Object> mapper) { + return Arrays.stream(arguments) // + .map(argument -> argument instanceof Named ? mapper.apply((Named) argument) : argument) // + .toArray(); + } +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestInvocationContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestInvocationContext.java index a6adfd3e5c5f..b11f4c54eb08 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestInvocationContext.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestInvocationContext.java @@ -24,8 +24,7 @@ class ParameterizedTestInvocationContext implements TestTemplateInvocationContex private final ParameterizedTestNameFormatter formatter; private final ParameterizedTestMethodContext methodContext; - private final Arguments arguments; - private final Object[] consumedArguments; + private final EvaluatedArgumentSet arguments; private final int invocationIndex; ParameterizedTestInvocationContext(ParameterizedTestNameFormatter formatter, @@ -33,29 +32,26 @@ class ParameterizedTestInvocationContext implements TestTemplateInvocationContex this.formatter = formatter; this.methodContext = methodContext; - this.arguments = arguments; - this.consumedArguments = consumedArguments(methodContext, arguments.get()); + this.arguments = EvaluatedArgumentSet.of(arguments, this::determineConsumedArgumentCount); this.invocationIndex = invocationIndex; } @Override public String getDisplayName(int invocationIndex) { - return this.formatter.format(invocationIndex, this.arguments, this.consumedArguments); + return this.formatter.format(invocationIndex, this.arguments); } @Override public List getAdditionalExtensions() { return Arrays.asList( - new ParameterizedTestParameterResolver(this.methodContext, this.consumedArguments, this.invocationIndex), + new ParameterizedTestParameterResolver(this.methodContext, this.arguments, this.invocationIndex), new ArgumentCountValidator(this.methodContext, this.arguments)); } - private static Object[] consumedArguments(ParameterizedTestMethodContext methodContext, Object[] arguments) { - if (methodContext.hasAggregator()) { - return arguments; - } - int parameterCount = methodContext.getParameterCount(); - return arguments.length > parameterCount ? Arrays.copyOf(arguments, parameterCount) : arguments; + private int determineConsumedArgumentCount(int totalLength) { + return methodContext.hasAggregator() // + ? totalLength // + : Math.min(totalLength, methodContext.getParameterCount()); } } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestNameFormatter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestNameFormatter.java index 12cd141ec38c..dc5cadc31b66 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestNameFormatter.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestNameFormatter.java @@ -27,6 +27,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -35,8 +36,6 @@ import org.junit.jupiter.api.Named; import org.junit.jupiter.api.extension.ExtensionConfigurationException; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.Arguments.ArgumentSet; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.util.StringUtils; @@ -59,9 +58,9 @@ class ParameterizedTestNameFormatter { } } - String format(int invocationIndex, Arguments arguments, Object[] consumedArguments) { + String format(int invocationIndex, EvaluatedArgumentSet arguments) { try { - return formatSafely(invocationIndex, arguments, consumedArguments); + return formatSafely(invocationIndex, arguments); } catch (Exception ex) { String message = "Failed to format display name for parameterized test. " @@ -70,9 +69,9 @@ String format(int invocationIndex, Arguments arguments, Object[] consumedArgumen } } - private String formatSafely(int invocationIndex, Arguments arguments, Object[] consumedArguments) { - ArgumentsContext context = new ArgumentsContext(invocationIndex, arguments, - extractNamedArguments(consumedArguments)); + private String formatSafely(int invocationIndex, EvaluatedArgumentSet arguments) { + ArgumentsContext context = new ArgumentsContext(invocationIndex, arguments.getConsumedNames(), + arguments.getName()); StringBuffer result = new StringBuffer(); // used instead of StringBuilder so MessageFormat can append directly for (PartialFormatter partialFormatter : this.partialFormatters) { partialFormatter.append(context, result); @@ -150,7 +149,7 @@ private PartialFormatters createPartialFormatters(String displayName, Parameteri formatters.put(ARGUMENTS_PLACEHOLDER, new CachingByArgumentsLengthPartialFormatter( length -> new MessageFormatPartialFormatter(argumentsPattern(length), argumentMaxLength))); formatters.put(ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER, (context, result) -> { - PartialFormatter formatterToUse = context.arguments instanceof ArgumentSet // + PartialFormatter formatterToUse = context.argumentSetName.isPresent() // ? PartialFormatter.ARGUMENT_SET_NAME // : argumentsWithNamesFormatter; formatterToUse.append(context, result); @@ -186,13 +185,13 @@ private static class PlaceholderPosition { private static class ArgumentsContext { private final int invocationIndex; - private final Arguments arguments; private final Object[] consumedArguments; + private final Optional argumentSetName; - ArgumentsContext(int invocationIndex, Arguments arguments, Object[] consumedArguments) { + ArgumentsContext(int invocationIndex, Object[] consumedArguments, Optional argumentSetName) { this.invocationIndex = invocationIndex; - this.arguments = arguments; this.consumedArguments = consumedArguments; + this.argumentSetName = argumentSetName; } } @@ -202,13 +201,14 @@ private interface PartialFormatter { PartialFormatter INDEX = (context, result) -> result.append(context.invocationIndex); PartialFormatter ARGUMENT_SET_NAME = (context, result) -> { - if (!(context.arguments instanceof ArgumentSet)) { - throw new ExtensionConfigurationException( - String.format("When the display name pattern for a @ParameterizedTest contains %s, " - + "the arguments must be supplied as an ArgumentSet.", - ARGUMENT_SET_NAME_PLACEHOLDER)); + if (context.argumentSetName.isPresent()) { + result.append(context.argumentSetName.get()); + return; } - result.append(((ArgumentSet) context.arguments).getName()); + throw new ExtensionConfigurationException( + String.format("When the display name pattern for a @ParameterizedTest contains %s, " + + "the arguments must be supplied as an ArgumentSet.", + ARGUMENT_SET_NAME_PLACEHOLDER)); }; void append(ArgumentsContext context, StringBuffer result); diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestParameterResolver.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestParameterResolver.java index d49cdc29284e..ce93eee5bda4 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestParameterResolver.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestParameterResolver.java @@ -15,7 +15,6 @@ import java.util.Arrays; import java.util.concurrent.atomic.AtomicInteger; -import org.junit.jupiter.api.Named; import org.junit.jupiter.api.extension.AfterTestExecutionCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; @@ -33,10 +32,10 @@ class ParameterizedTestParameterResolver implements ParameterResolver, AfterTest private static final Namespace NAMESPACE = Namespace.create(ParameterizedTestParameterResolver.class); private final ParameterizedTestMethodContext methodContext; - private final Object[] arguments; + private final EvaluatedArgumentSet arguments; private final int invocationIndex; - ParameterizedTestParameterResolver(ParameterizedTestMethodContext methodContext, Object[] arguments, + ParameterizedTestParameterResolver(ParameterizedTestMethodContext methodContext, EvaluatedArgumentSet arguments, int invocationIndex) { this.methodContext = methodContext; @@ -72,13 +71,13 @@ public boolean supportsParameter(ParameterContext parameterContext, ExtensionCon } // Else fallback to behavior for parameterized test methods without aggregators. - return parameterIndex < this.arguments.length; + return parameterIndex < this.arguments.getConsumedLength(); } @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { - return this.methodContext.resolve(parameterContext, extensionContext, extractPayloads(this.arguments), + return this.methodContext.resolve(parameterContext, extensionContext, this.arguments.getConsumedPayloads(), this.invocationIndex); } @@ -96,7 +95,7 @@ public void afterTestExecution(ExtensionContext context) { Store store = context.getStore(NAMESPACE); AtomicInteger argumentIndex = new AtomicInteger(); - Arrays.stream(this.arguments) // + Arrays.stream(this.arguments.getConsumedPayloads()) // .filter(AutoCloseable.class::isInstance) // .map(AutoCloseable.class::cast) // .map(CloseableArgument::new) // @@ -118,15 +117,4 @@ public void close() throws Throwable { } - private Object[] extractPayloads(Object[] arguments) { - return Arrays.stream(arguments) // - .map(argument -> { - if (argument instanceof Named) { - return ((Named) argument).getPayload(); - } - return argument; - }) // - .toArray(); - } - } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java index eabe6470ec83..9f2a91c2786a 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java @@ -1180,6 +1180,17 @@ void executesWithMethodSourceProvidingUnusedArguments() { .haveExactly(1, event(test(), displayName("[2] argument=b"), finishedWithFailure(message("b")))); } + @Test + void evaluatesArgumentsAtMostOnce() { + var results = execute(ArgumentCountValidationMode.STRICT, UnusedArgumentsTestCase.class, + "testWithEvaluationReportingArgumentsProvider", String.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, event(finishedWithFailure(message(String.format( + "Configuration error: the @ParameterizedTest has 1 argument(s) but there were 2 argument(s) provided.%nNote: the provided arguments are [foo, unused]"))))); + results.allEvents().reportingEntryPublished().assertThatEvents() // + .haveExactly(1, event(EventConditions.reportEntry(Map.of("evaluated", "true")))); + } + private EngineExecutionResults execute(ArgumentCountValidationMode configurationValue, Class javaClass, String methodName, Class... methodParameterTypes) { return EngineTestKit.engine(new JupiterTestEngine()) // @@ -2123,6 +2134,23 @@ void testWithNoneArgumentCountValidation(String argument) { void testWithCsvSourceContainingDifferentNumbersOfArguments(String argument) { fail(argument); } + + @ParameterizedTest + @ArgumentsSource(EvaluationReportingArgumentsProvider.class) + void testWithEvaluationReportingArgumentsProvider(String argument) { + fail(argument); + } + + private static class EvaluationReportingArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of(() -> { + context.publishReportEntry("evaluated", "true"); + return List.of("foo", "unused").toArray(); + }); + } + } } static class LifecycleTestCase { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestNameFormatterTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestNameFormatterTests.java index fc07dae767a9..749ed94bced4 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestNameFormatterTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestNameFormatterTests.java @@ -336,7 +336,7 @@ private static ParameterizedTestNameFormatter formatter(String pattern, String d } private static String format(ParameterizedTestNameFormatter formatter, int invocationIndex, Arguments arguments) { - return formatter.format(invocationIndex, arguments, arguments.get()); + return formatter.format(invocationIndex, EvaluatedArgumentSet.allOf(arguments)); } private static class ToStringReturnsNull { From 8f8b694ea19375c1494a6de4fd5df2d7f98756fb Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 19 Feb 2025 10:42:36 +0100 Subject: [PATCH 013/167] Close all arguments, not just consumed ones --- .../release-notes/release-notes-5.13.0-M1.adoc | 6 +++++- .../params/ParameterizedTestParameterResolver.java | 2 +- .../params/ParameterizedTestIntegrationTests.java | 14 +++++++------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc index 15c3a64fbb5f..e9f2b631992c 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc @@ -35,7 +35,11 @@ repository on GitHub. [[release-notes-5.13.0-M1-junit-jupiter-bug-fixes]] ==== Bug Fixes -* ❓ +* If `@ParameterizedTest(autoCloseArguments = true)`, all arguments returned by the used + `ArgumentsProvider` implementations are now closed even if the test method declares + fewer parameters. +* `AutoCloseable` arguments returned by an `ArgumentsProvider` are now closed even if they + are wrapped with `Named`. [[release-notes-5.13.0-M1-junit-jupiter-deprecations-and-breaking-changes]] ==== Deprecations and Breaking Changes diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestParameterResolver.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestParameterResolver.java index ce93eee5bda4..50a9a14349d6 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestParameterResolver.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestParameterResolver.java @@ -95,7 +95,7 @@ public void afterTestExecution(ExtensionContext context) { Store store = context.getStore(NAMESPACE); AtomicInteger argumentIndex = new AtomicInteger(); - Arrays.stream(this.arguments.getConsumedPayloads()) // + Arrays.stream(this.arguments.getAllPayloads()) // .filter(AutoCloseable.class::isInstance) // .map(AutoCloseable.class::cast) // .map(CloseableArgument::new) // diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java index 9f2a91c2786a..26f0353a51d0 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java @@ -14,7 +14,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.within; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -127,8 +126,9 @@ class ParameterizedTestIntegrationTests { private final Locale originalLocale = Locale.getDefault(Locale.Category.FORMAT); @AfterEach - void restoreLocale() { + void reset() { Locale.setDefault(Locale.Category.FORMAT, originalLocale); + AutoCloseableArgument.closeCounter = 0; } @ParameterizedTest @@ -1310,7 +1310,7 @@ void closeAutoCloseableArgumentsAfterTest() { results.allEvents().assertThatEvents() // .haveExactly(1, event(test(), finishedSuccessfully())); - assertTrue(AutoCloseableArgument.isClosed); + assertEquals(2, AutoCloseableArgument.closeCounter); } @Test @@ -1441,7 +1441,7 @@ void testWithIgnoreLeadingAndTrailingWhitespaceSetToTrueForCsvFileSource(String @ParameterizedTest @ArgumentsSource(AutoCloseableArgumentProvider.class) void testWithAutoCloseableArgument(AutoCloseableArgument autoCloseable) { - assertFalse(AutoCloseableArgument.isClosed); + assertEquals(0, AutoCloseableArgument.closeCounter); } @ParameterizedTest @@ -2521,17 +2521,17 @@ private static class AutoCloseableArgumentProvider implements ArgumentsProvider @Override public Stream provideArguments(ExtensionContext context) { - return Stream.of(arguments(new AutoCloseableArgument())); + return Stream.of(arguments(new AutoCloseableArgument(), Named.of("unused", new AutoCloseableArgument()))); } } static class AutoCloseableArgument implements AutoCloseable { - static boolean isClosed = false; + static int closeCounter = 0; @Override public void close() { - isClosed = true; + closeCounter++; } } From 96ec3cdbcbf60e5cfbd8a0f9fcffc92a9513b85c Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 19 Feb 2025 10:43:54 +0100 Subject: [PATCH 014/167] Use existing static import --- .../jupiter/params/ParameterizedTestIntegrationTests.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java index 26f0353a51d0..4c452216862a 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java @@ -1129,7 +1129,7 @@ void failsWithArgumentsSourceProvidingUnusedArguments() { var results = execute(ArgumentCountValidationMode.STRICT, UnusedArgumentsTestCase.class, "testWithTwoUnusedStringArgumentsProvider", String.class); results.allEvents().assertThatEvents() // - .haveExactly(1, event(EventConditions.finishedWithFailure(message(String.format( + .haveExactly(1, event(finishedWithFailure(message(String.format( "Configuration error: the @ParameterizedTest has 1 argument(s) but there were 2 argument(s) provided.%nNote: the provided arguments are [foo, unused1]"))))); } @@ -1138,7 +1138,7 @@ void failsWithMethodSourceProvidingUnusedArguments() { var results = execute(ArgumentCountValidationMode.STRICT, UnusedArgumentsTestCase.class, "testWithMethodSourceProvidingUnusedArguments", String.class); results.allEvents().assertThatEvents() // - .haveExactly(1, event(EventConditions.finishedWithFailure(message(String.format( + .haveExactly(1, event(finishedWithFailure(message(String.format( "Configuration error: the @ParameterizedTest has 1 argument(s) but there were 2 argument(s) provided.%nNote: the provided arguments are [foo, unused1]"))))); } @@ -1147,7 +1147,7 @@ void failsWithCsvSourceUnusedArgumentsAndStrictArgumentCountValidationAnnotation var results = execute(ArgumentCountValidationMode.NONE, UnusedArgumentsTestCase.class, "testWithStrictArgumentCountValidation", String.class); results.allEvents().assertThatEvents() // - .haveExactly(1, event(EventConditions.finishedWithFailure(message(String.format( + .haveExactly(1, event(finishedWithFailure(message(String.format( "Configuration error: the @ParameterizedTest has 1 argument(s) but there were 2 argument(s) provided.%nNote: the provided arguments are [foo, unused1]"))))); } @@ -1156,7 +1156,7 @@ void failsWithCsvSourceUnusedArgumentsButExecutesRemainingArgumentsWhereThereIsN var results = execute(ArgumentCountValidationMode.STRICT, UnusedArgumentsTestCase.class, "testWithCsvSourceContainingDifferentNumbersOfArguments", String.class); results.allEvents().assertThatEvents() // - .haveExactly(1, event(EventConditions.finishedWithFailure(message(String.format( + .haveExactly(1, event(finishedWithFailure(message(String.format( "Configuration error: the @ParameterizedTest has 1 argument(s) but there were 2 argument(s) provided.%nNote: the provided arguments are [foo, unused1]"))))) // .haveExactly(1, event(test(), displayName("[2] argument=bar"), finishedWithFailure(message("bar")))); From c5359ec2473615e9072ba38b3ee1b56e3763ecf4 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 19 Feb 2025 11:33:46 +0100 Subject: [PATCH 015/167] Close all arguments, despite early failures --- .../release-notes-5.13.0-M1.adoc | 6 ++ .../ContainerTemplateInvocationContext.java | 12 +++ .../TestTemplateInvocationContext.java | 15 +++ ...ainerTemplateInvocationTestDescriptor.java | 4 +- .../ContainerTemplateTestDescriptor.java | 3 +- .../descriptor/TestMethodTestDescriptor.java | 5 + .../TestTemplateInvocationTestDescriptor.java | 10 +- .../ParameterizedTestInvocationContext.java | 35 +++++++ .../ParameterizedTestParameterResolver.java | 46 +--------- .../ContainerTemplateInvocationTests.java | 91 ++++++++++++++++++- .../engine/TestTemplateInvocationTests.java | 83 +++++++++++++++++ .../ParameterizedTestIntegrationTests.java | 24 +++++ 12 files changed, 283 insertions(+), 51 deletions(-) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc index e9f2b631992c..7cf976bb5bdb 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc @@ -40,6 +40,8 @@ repository on GitHub. fewer parameters. * `AutoCloseable` arguments returned by an `ArgumentsProvider` are now closed even if they are wrapped with `Named`. +* `AutoCloseable` arguments returned by an `ArgumentsProvider` are now closed even if a + failure happens prior to invoking the parameterized method. [[release-notes-5.13.0-M1-junit-jupiter-deprecations-and-breaking-changes]] ==== Deprecations and Breaking Changes @@ -54,6 +56,10 @@ repository on GitHub. times. This may be used, for example, to inject different parameters to be used by all tests in the container template class or to set up each invocation of the container template differently. +* New `TestTemplateInvocationContext.prepareInvocation(ExtensionContext)` callback method + allows preparing the `ExtensionContext` before the test template method is invoked. This + may be used, for example, to store entries in its `Store` to benefit from its cleanup + support or for retrieval by other extensions. [[release-notes-5.13.0-M1-junit-vintage]] diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ContainerTemplateInvocationContext.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ContainerTemplateInvocationContext.java index 41477537df6c..3510ba2700b7 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ContainerTemplateInvocationContext.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ContainerTemplateInvocationContext.java @@ -67,4 +67,16 @@ default List getAdditionalExtensions() { return emptyList(); } + /** + * Prepare the imminent invocation of the container template. + * + *

This may be used, for example, to store entries in the + * {@link ExtensionContext.Store Store} to benefit from its cleanup support + * or for retrieval by other extensions. + * + * @param context The invocation-level extension context. + */ + default void prepareInvocation(ExtensionContext context) { + } + } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestTemplateInvocationContext.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestTemplateInvocationContext.java index 37a11b3b923d..1d74bed2dd7d 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestTemplateInvocationContext.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestTemplateInvocationContext.java @@ -11,6 +11,7 @@ package org.junit.jupiter.api.extension; import static java.util.Collections.emptyList; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import java.util.List; @@ -66,4 +67,18 @@ default List getAdditionalExtensions() { return emptyList(); } + /** + * Prepare the imminent invocation of the test template. + * + *

This may be used, for example, to store entries in the + * {@link ExtensionContext.Store Store} to benefit from its cleanup support + * or for retrieval by other extensions. + * + * @param context The invocation-level extension context. + * @since 5.13 + */ + @API(status = EXPERIMENTAL, since = "5.13") + default void prepareInvocation(ExtensionContext context) { + } + } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateInvocationTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateInvocationTestDescriptor.java index 64cbdbe4296d..e24fa1b53129 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateInvocationTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateInvocationTestDescriptor.java @@ -119,6 +119,7 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte } ExtensionContext extensionContext = new ContainerTemplateInvocationExtensionContext( context.getExtensionContext(), context.getExecutionListener(), this, context.getConfiguration(), registry); + this.invocationContext.prepareInvocation(extensionContext); return context.extend() // .withExtensionContext(extensionContext) // .withExtensionRegistry(registry) // @@ -134,8 +135,9 @@ public JupiterEngineExecutionContext execute(JupiterEngineExecutionContext conte } @Override - public void cleanUp(JupiterEngineExecutionContext context) { + public void cleanUp(JupiterEngineExecutionContext context) throws Exception { // forget invocationContext so it can be garbage collected this.invocationContext = null; + super.cleanUp(context); } } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateTestDescriptor.java index d59debe38cc8..f430f33bfe6f 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateTestDescriptor.java @@ -178,10 +178,11 @@ public Set getExclusiveResources() { } @Override - public void cleanUp(JupiterEngineExecutionContext context) { + public void cleanUp(JupiterEngineExecutionContext context) throws Exception { this.childrenPrototypes.clear(); this.childrenPrototypesByIndex.clear(); this.dynamicDescendantFilter.allowAll(); + super.cleanUp(context); } @Override diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java index 8d3b14a82537..59cf610de724 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java @@ -117,6 +117,7 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte ThrowableCollector throwableCollector = createThrowableCollector(); MethodExtensionContext extensionContext = new MethodExtensionContext(context.getExtensionContext(), context.getExecutionListener(), this, context.getConfiguration(), registry, throwableCollector); + throwableCollector.execute(() -> prepareExtensionContext(extensionContext)); // @formatter:off JupiterEngineExecutionContext newContext = context.extend() .withExtensionRegistry(registry) @@ -131,6 +132,10 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte return newContext; } + protected void prepareExtensionContext(ExtensionContext extensionContext) { + // nothing to do by default + } + protected MutableExtensionRegistry populateNewExtensionRegistry(JupiterEngineExecutionContext context) { MutableExtensionRegistry registry = populateNewExtensionRegistryFromExtendWithAnnotation( context.getExtensionRegistry(), getTestMethod()); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateInvocationTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateInvocationTestDescriptor.java index 2437c297bd3a..88d494a9731d 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateInvocationTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateInvocationTestDescriptor.java @@ -18,6 +18,7 @@ import java.util.function.UnaryOperator; import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.InvocationInterceptor; import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import org.junit.jupiter.engine.config.JupiterConfiguration; @@ -76,15 +77,20 @@ public String getLegacyReportingName() { @Override protected MutableExtensionRegistry populateNewExtensionRegistry(JupiterEngineExecutionContext context) { MutableExtensionRegistry registry = super.populateNewExtensionRegistry(context); - invocationContext.getAdditionalExtensions().forEach( + this.invocationContext.getAdditionalExtensions().forEach( extension -> registry.registerExtension(extension, invocationContext)); return registry; } + @Override + protected void prepareExtensionContext(ExtensionContext extensionContext) { + this.invocationContext.prepareInvocation(extensionContext); + } + @Override public void after(JupiterEngineExecutionContext context) { // forget invocationContext so it can be garbage collected - invocationContext = null; + this.invocationContext = null; } } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestInvocationContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestInvocationContext.java index b11f4c54eb08..8e94ec5ed5bb 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestInvocationContext.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestInvocationContext.java @@ -12,8 +12,12 @@ import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ExtensionContext.Store; import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import org.junit.jupiter.params.provider.Arguments; @@ -22,6 +26,8 @@ */ class ParameterizedTestInvocationContext implements TestTemplateInvocationContext { + private static final Namespace NAMESPACE = Namespace.create(ParameterizedTestInvocationContext.class); + private final ParameterizedTestNameFormatter formatter; private final ParameterizedTestMethodContext methodContext; private final EvaluatedArgumentSet arguments; @@ -48,10 +54,39 @@ public List getAdditionalExtensions() { new ArgumentCountValidator(this.methodContext, this.arguments)); } + @Override + public void prepareInvocation(ExtensionContext context) { + if (this.methodContext.annotation.autoCloseArguments()) { + Store store = context.getStore(NAMESPACE); + AtomicInteger argumentIndex = new AtomicInteger(); + + Arrays.stream(this.arguments.getAllPayloads()) // + .filter(AutoCloseable.class::isInstance) // + .map(AutoCloseable.class::cast) // + .map(CloseableArgument::new) // + .forEach(closeable -> store.put(argumentIndex.incrementAndGet(), closeable)); + } + } + private int determineConsumedArgumentCount(int totalLength) { return methodContext.hasAggregator() // ? totalLength // : Math.min(totalLength, methodContext.getParameterCount()); } + private static class CloseableArgument implements Store.CloseableResource { + + private final AutoCloseable autoCloseable; + + CloseableArgument(AutoCloseable autoCloseable) { + this.autoCloseable = autoCloseable; + } + + @Override + public void close() throws Throwable { + this.autoCloseable.close(); + } + + } + } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestParameterResolver.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestParameterResolver.java index 50a9a14349d6..cf1196bac37b 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestParameterResolver.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestParameterResolver.java @@ -12,24 +12,16 @@ import java.lang.reflect.Executable; import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.jupiter.api.extension.AfterTestExecutionCallback; import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.ExtensionContext.Namespace; -import org.junit.jupiter.api.extension.ExtensionContext.Store; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ParameterResolver; -import org.junit.platform.commons.support.AnnotationSupport; /** * @since 5.0 */ -class ParameterizedTestParameterResolver implements ParameterResolver, AfterTestExecutionCallback { - - private static final Namespace NAMESPACE = Namespace.create(ParameterizedTestParameterResolver.class); +class ParameterizedTestParameterResolver implements ParameterResolver { private final ParameterizedTestMethodContext methodContext; private final EvaluatedArgumentSet arguments; @@ -81,40 +73,4 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte this.invocationIndex); } - /** - * @since 5.8 - */ - @Override - public void afterTestExecution(ExtensionContext context) { - ParameterizedTest parameterizedTest = AnnotationSupport.findAnnotation(context.getRequiredTestMethod(), - ParameterizedTest.class).get(); - if (!parameterizedTest.autoCloseArguments()) { - return; - } - - Store store = context.getStore(NAMESPACE); - AtomicInteger argumentIndex = new AtomicInteger(); - - Arrays.stream(this.arguments.getAllPayloads()) // - .filter(AutoCloseable.class::isInstance) // - .map(AutoCloseable.class::cast) // - .map(CloseableArgument::new) // - .forEach(closeable -> store.put("closeableArgument#" + argumentIndex.incrementAndGet(), closeable)); - } - - private static class CloseableArgument implements Store.CloseableResource { - - private final AutoCloseable autoCloseable; - - CloseableArgument(AutoCloseable autoCloseable) { - this.autoCloseable = autoCloseable; - } - - @Override - public void close() throws Throwable { - this.autoCloseable.close(); - } - - } - } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/ContainerTemplateInvocationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/ContainerTemplateInvocationTests.java index e621393f1f80..f56f63f191b3 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/ContainerTemplateInvocationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/ContainerTemplateInvocationTests.java @@ -16,6 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.DynamicTest.dynamicTest; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; @@ -65,6 +66,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; @@ -887,6 +889,14 @@ void executesLifecycleCallbackMethodsInNestedContainerTemplates() { // @formatter:on } + @Test + void templateWithPreparations() { + var results = executeTestsForClass(ContainerTemplateWithPreparationsTestCase.class); + + results.allEvents().debug().assertStatistics(stats -> stats.started(6).succeeded(6)); + assertTrue(CustomCloseableResource.closed, "resource in store was closed"); + } + // ------------------------------------------------------------------- @SuppressWarnings("JUnitMalformedDeclaration") @@ -1040,7 +1050,7 @@ static class SomeResourceExtension implements BeforeAllCallback, ParameterResolv @Override public void beforeAll(ExtensionContext context) throws Exception { - context.getStore(ExtensionContext.Namespace.GLOBAL).put("someResource", new SomeResource()); + context.getStore(Namespace.GLOBAL).put("someResource", new SomeResource()); } @Override @@ -1060,7 +1070,7 @@ public boolean supportsParameter(ParameterContext parameterContext, ExtensionCon @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { - return extensionContext.getStore(ExtensionContext.Namespace.GLOBAL).get("someResource"); + return extensionContext.getStore(Namespace.GLOBAL).get("someResource"); } } @@ -1254,4 +1264,81 @@ static void afterAll(TestReporter testReporter, TestInfo testInfo) { testReporter.publishEntry("afterAll: " + testInfo.getTestClass().orElseThrow().getSimpleName()); } } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ContainerTemplate + @ExtendWith({ PreparingContainerTemplateInvocationContextProvider.class, CompanionExtension.class }) + static class ContainerTemplateWithPreparationsTestCase { + + @Test + void test(CustomCloseableResource resource) { + assertNotNull(resource); + assertFalse(CustomCloseableResource.closed, "should not be closed yet"); + } + + } + + private static class PreparingContainerTemplateInvocationContextProvider + implements ContainerTemplateInvocationContextProvider { + + static final Namespace NAMESPACE = Namespace.create(PreparingContainerTemplateInvocationContextProvider.class); + + @Override + public boolean supportsContainerTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideContainerTemplateInvocationContexts( + ExtensionContext context) { + var invocationContext = new PreparingContainerTemplateInvocationContext(); + return Stream.of(invocationContext, invocationContext); + } + + } + + private static class PreparingContainerTemplateInvocationContext implements ContainerTemplateInvocationContext { + + @Override + public void prepareInvocation(ExtensionContext context) { + CustomCloseableResource.closed = false; + context.getStore(PreparingContainerTemplateInvocationContextProvider.NAMESPACE) // + .put("resource", new CustomCloseableResource()); + } + + } + + private static class CompanionExtension implements ParameterResolver { + + @Override + public ExtensionContextScope getTestInstantiationExtensionContextScope(ExtensionContext rootContext) { + return ExtensionContextScope.TEST_METHOD; + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return CustomCloseableResource.class.equals(parameterContext.getParameter().getType()); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return extensionContext.getStore(PreparingContainerTemplateInvocationContextProvider.NAMESPACE).get( + "resource"); + } + + } + + private static class CustomCloseableResource implements CloseableResource { + + static boolean closed; + + @Override + public void close() { + closed = true; + } + + } + } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestTemplateInvocationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestTemplateInvocationTests.java index b9427e78e6e6..677df08911b0 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestTemplateInvocationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestTemplateInvocationTests.java @@ -14,6 +14,9 @@ import static java.util.Collections.singletonList; 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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; @@ -57,6 +60,8 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ParameterResolver; @@ -408,6 +413,14 @@ void templateWithCloseableStream() { event(container("templateWithCloseableStream"), finishedSuccessfully()))); } + @Test + void templateWithPreparations() { + var results = executeTestsForClass(TestTemplateWithPreparationsTestCase.class); + + assertTrue(CustomCloseableResource.closed, "resource in store was closed"); + results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + } + private TestDescriptor findTestDescriptor(EngineExecutionResults executionResults, Condition condition) { // @formatter:off return executionResults.allEvents() @@ -852,4 +865,74 @@ private static TestTemplateInvocationContext emptyTestTemplateInvocationContext( }; } + static class TestTemplateWithPreparationsTestCase { + + @TestTemplate + @ExtendWith({ PreparingTestTemplateInvocationContextProvider.class, CompanionExtension.class }) + void test(CustomCloseableResource resource) { + assertNotNull(resource); + assertFalse(CustomCloseableResource.closed, "should not be closed yet"); + } + + } + + private static class PreparingTestTemplateInvocationContextProvider + implements TestTemplateInvocationContextProvider { + + static final Namespace NAMESPACE = Namespace.create(PreparingTestTemplateInvocationContextProvider.class); + + @Override + public boolean supportsTestTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { + return Stream.of(new PreparingTestTemplateInvocationContext()); + } + + } + + private static class PreparingTestTemplateInvocationContext implements TestTemplateInvocationContext { + + @Override + public void prepareInvocation(ExtensionContext context) { + context.getStore(PreparingTestTemplateInvocationContextProvider.NAMESPACE) // + .put("resource", new CustomCloseableResource()); + } + + } + + private static class CompanionExtension implements ParameterResolver { + + @Override + public ExtensionContextScope getTestInstantiationExtensionContextScope(ExtensionContext rootContext) { + return ExtensionContextScope.TEST_METHOD; + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return CustomCloseableResource.class.equals(parameterContext.getParameter().getType()); + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return extensionContext.getStore(PreparingTestTemplateInvocationContextProvider.NAMESPACE).get("resource"); + } + + } + + private static class CustomCloseableResource implements CloseableResource { + + static boolean closed; + + @Override + public void close() { + closed = true; + } + + } + } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java index 4c452216862a..e8afc956fbff 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java @@ -1313,6 +1313,15 @@ void closeAutoCloseableArgumentsAfterTest() { assertEquals(2, AutoCloseableArgument.closeCounter); } + @Test + void closeAutoCloseableArgumentsAfterTestDespiteEarlyFailure() { + var results = execute(FailureInBeforeEachTestCase.class, "test", AutoCloseableArgument.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, event(test(), finishedWithFailure(message("beforeEach")))); + + assertEquals(2, AutoCloseableArgument.closeCounter); + } + @Test void executesTwoIterationsBasedOnIterationAndUniqueIdSelector() { var methodId = uniqueIdForTestTemplateMethod(TestCase.class, "testWithThreeIterations(int)"); @@ -2548,4 +2557,19 @@ static Book factory(String title) { } } + static class FailureInBeforeEachTestCase { + + @BeforeEach + void beforeEach() { + fail("beforeEach"); + } + + @ParameterizedTest + @ArgumentsSource(AutoCloseableArgumentProvider.class) + void test(AutoCloseableArgument autoCloseable) { + assertNotNull(autoCloseable); + assertEquals(0, AutoCloseableArgument.closeCounter); + } + } + } From dc7a0b538d4a4c38e95a0da39b2f6334db57ea8d Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Fri, 21 Feb 2025 13:07:28 +0100 Subject: [PATCH 016/167] Remove duplicated reference to `SimpleArgumentConverter` --- .../org/junit/jupiter/params/converter/ArgumentConverter.java | 1 - 1 file changed, 1 deletion(-) diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ArgumentConverter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ArgumentConverter.java index 78e4cc55e4d6..2e5495eae0da 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ArgumentConverter.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ArgumentConverter.java @@ -40,7 +40,6 @@ * the {@link ParameterContext} to perform the conversion. * * @since 5.0 - * @see SimpleArgumentConverter * @see org.junit.jupiter.params.ParameterizedTest * @see org.junit.jupiter.params.converter.ConvertWith * @see org.junit.jupiter.params.support.AnnotationConsumer From 0f255052e7007c1d8eb0ba31825771b059c72eb6 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Fri, 21 Feb 2025 13:20:35 +0100 Subject: [PATCH 017/167] Upgradle to 8.13-rc-2 --- .../junitbuild.jacoco-aggregation-conventions.gradle.kts | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- .../platform-tooling-support-tests.gradle.kts | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.jacoco-aggregation-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.jacoco-aggregation-conventions.gradle.kts index 17a613090463..140f1f001fac 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.jacoco-aggregation-conventions.gradle.kts +++ b/gradle/plugins/common/src/main/kotlin/junitbuild.jacoco-aggregation-conventions.gradle.kts @@ -4,7 +4,7 @@ plugins { } val jacocoRootReport by reporting.reports.creating(JacocoCoverageReport::class) { - testType = TestSuiteType.UNIT_TEST + testSuiteName = "test" } val classesView = configurations["aggregateCodeCoverageReportResults"].incoming.artifactView { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d71047787f80..24d1c4af3c3c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=8d97a97984f6cbd2b85fe4c60a743440a347544bf18818048e611f5288d46c94 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip +distributionSha256Sum=264353f17a13391626fc8d0e86ae8023f30ea334a470caae0cfee02fe6cd1f3d +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-rc-2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/platform-tooling-support-tests/platform-tooling-support-tests.gradle.kts b/platform-tooling-support-tests/platform-tooling-support-tests.gradle.kts index 01826462be5d..32e310ce4411 100644 --- a/platform-tooling-support-tests/platform-tooling-support-tests.gradle.kts +++ b/platform-tooling-support-tests/platform-tooling-support-tests.gradle.kts @@ -1,8 +1,6 @@ - import com.gradle.develocity.agent.gradle.internal.test.TestDistributionConfigurationInternal import junitbuild.extensions.capitalized import org.gradle.api.tasks.PathSensitivity.RELATIVE -import org.gradle.jvm.toolchain.internal.NoToolchainAvailableException import org.gradle.kotlin.dsl.support.listFilesOrdered import java.time.Duration @@ -216,7 +214,7 @@ class JavaHomeDir(project: Project, @Input val version: Int, testDistributionEna project.javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(version) }.get() - } catch (e: NoToolchainAvailableException) { + } catch (e: Exception) { null } }) From 3a7ea6f93b2789e5213b6ff6eaedcfc9643ab119 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Fri, 21 Feb 2025 13:20:47 +0100 Subject: [PATCH 018/167] Update log suppressions for tests --- platform-tests/src/test/resources/log4j2-test.xml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/platform-tests/src/test/resources/log4j2-test.xml b/platform-tests/src/test/resources/log4j2-test.xml index 653e2017872e..050b674d12b8 100644 --- a/platform-tests/src/test/resources/log4j2-test.xml +++ b/platform-tests/src/test/resources/log4j2-test.xml @@ -7,13 +7,15 @@ - + + + + - + - From f0064eea40dc3f4859167bce4c88858e33eee47b Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Fri, 21 Feb 2025 13:21:21 +0100 Subject: [PATCH 019/167] Reapply "Use Gradle's new daemon JVM criteria feature" This reverts commit 292f6a09fef591b3cb945cb7fcd8f7b48000f80b. --- gradle/gradle-daemon-jvm.properties | 2 ++ gradle/plugins/settings.gradle.kts | 6 ------ 2 files changed, 2 insertions(+), 6 deletions(-) create mode 100644 gradle/gradle-daemon-jvm.properties diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties new file mode 100644 index 000000000000..63e5bbdf4845 --- /dev/null +++ b/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,2 @@ +#This file is generated by updateDaemonJvm +toolchainVersion=21 diff --git a/gradle/plugins/settings.gradle.kts b/gradle/plugins/settings.gradle.kts index 163866db2805..41935db0aa36 100644 --- a/gradle/plugins/settings.gradle.kts +++ b/gradle/plugins/settings.gradle.kts @@ -1,9 +1,3 @@ -val expectedJavaVersion = JavaVersion.VERSION_21 -val actualJavaVersion = JavaVersion.current() -require(actualJavaVersion == expectedJavaVersion) { - "The JUnit 5 build must be executed with Java ${expectedJavaVersion.majorVersion}. Currently executing with Java ${actualJavaVersion.majorVersion}." -} - dependencyResolutionManagement { versionCatalogs { create("libs") { From 25dd6e5a8c66adf5b6e2f2de11c839e05a2f4fa9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Feb 2025 12:23:51 +0000 Subject: [PATCH 020/167] Update dependency org.apache.maven.plugins:maven-compiler-plugin to v3.14.0 --- platform-tooling-support-tests/projects/java-versions/pom.xml | 2 +- platform-tooling-support-tests/projects/jupiter-starter/pom.xml | 2 +- .../projects/maven-surefire-compatibility/pom.xml | 2 +- .../projects/multi-release-jar/pom.xml | 2 +- platform-tooling-support-tests/projects/vintage/pom.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/platform-tooling-support-tests/projects/java-versions/pom.xml b/platform-tooling-support-tests/projects/java-versions/pom.xml index d9d22bbec6b6..cdbb8cf6aaf0 100644 --- a/platform-tooling-support-tests/projects/java-versions/pom.xml +++ b/platform-tooling-support-tests/projects/java-versions/pom.xml @@ -31,7 +31,7 @@ maven-compiler-plugin - 3.13.0 + 3.14.0 1.8 1.8 diff --git a/platform-tooling-support-tests/projects/jupiter-starter/pom.xml b/platform-tooling-support-tests/projects/jupiter-starter/pom.xml index d03ec1ff3367..a0b01281e086 100644 --- a/platform-tooling-support-tests/projects/jupiter-starter/pom.xml +++ b/platform-tooling-support-tests/projects/jupiter-starter/pom.xml @@ -49,7 +49,7 @@ maven-compiler-plugin - 3.13.0 + 3.14.0 maven-surefire-plugin diff --git a/platform-tooling-support-tests/projects/maven-surefire-compatibility/pom.xml b/platform-tooling-support-tests/projects/maven-surefire-compatibility/pom.xml index 3ac0876b2d8b..47aba716ed6a 100644 --- a/platform-tooling-support-tests/projects/maven-surefire-compatibility/pom.xml +++ b/platform-tooling-support-tests/projects/maven-surefire-compatibility/pom.xml @@ -37,7 +37,7 @@ maven-compiler-plugin - 3.13.0 + 3.14.0 maven-surefire-plugin diff --git a/platform-tooling-support-tests/projects/multi-release-jar/pom.xml b/platform-tooling-support-tests/projects/multi-release-jar/pom.xml index 9c96f86949bf..b9e4b86ed82f 100644 --- a/platform-tooling-support-tests/projects/multi-release-jar/pom.xml +++ b/platform-tooling-support-tests/projects/multi-release-jar/pom.xml @@ -31,7 +31,7 @@ maven-compiler-plugin - 3.13.0 + 3.14.0 11 diff --git a/platform-tooling-support-tests/projects/vintage/pom.xml b/platform-tooling-support-tests/projects/vintage/pom.xml index 164570994740..3553f799ee3a 100644 --- a/platform-tooling-support-tests/projects/vintage/pom.xml +++ b/platform-tooling-support-tests/projects/vintage/pom.xml @@ -38,7 +38,7 @@ maven-compiler-plugin - 3.13.0 + 3.14.0 maven-surefire-plugin From 7b4f1224ef8054c5cac6fb8529b4630f42ae3847 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Fri, 21 Feb 2025 14:24:55 +0100 Subject: [PATCH 021/167] Release 5.12.0 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b8302e82cc62..be040f0b3e7c 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ This repository is the home of _JUnit 5_. ## Latest Releases -- General Availability (GA): [JUnit 5.11.4](https://github.com/junit-team/junit5/releases/tag/r5.11.4) (December 16, 2024) -- Preview (Milestone/Release Candidate): [JUnit 5.12.0-RC2](https://github.com/junit-team/junit5/releases/tag/r5.12.0-RC2) (February 12, 2025) +- General Availability (GA): [JUnit 5.12.0](https://github.com/junit-team/junit5/releases/tag/r5.12.0) (February 21, 2025) +- Preview (Milestone/Release Candidate): N/A ## Documentation From dd9cb1d6f7b2394f4112a1b44d5a56da6a7f8fce Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Fri, 21 Feb 2025 14:59:53 +0100 Subject: [PATCH 022/167] Update supported versions --- SECURITY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 788b41ebd7e7..4d4bad5f6641 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,8 +11,8 @@ You'll find more information about the key here: [KEYS](./KEYS) | Version | Supported | |---------| ------------------ | -| 5.11.x | :white_check_mark: | -| < 5.11 | :x: | +| 5.12.x | :white_check_mark: | +| < 5.12 | :x: | ## Reporting a Vulnerability From dc8a0151dd836804642703ebc4f80aecdb989c5d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Feb 2025 22:34:48 +0000 Subject: [PATCH 023/167] Update actions/upload-artifact digest to 4cec3d8 --- .github/actions/main-build/action.yml | 2 +- .github/workflows/cross-version.yml | 4 ++-- .github/workflows/ossf-scorecard.yml | 2 +- .github/workflows/release.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/actions/main-build/action.yml b/.github/actions/main-build/action.yml index ecafefb92517..98c9f4017a6e 100644 --- a/.github/actions/main-build/action.yml +++ b/.github/actions/main-build/action.yml @@ -16,7 +16,7 @@ runs: with: arguments: ${{ inputs.arguments }} encryptionKey: ${{ inputs.encryptionKey }} - - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 + - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4 if: ${{ always() }} with: name: Open Test Reports (${{ github.job }}) diff --git a/.github/workflows/cross-version.yml b/.github/workflows/cross-version.yml index 534ea6c7b1a8..f4647bd2c7b0 100644 --- a/.github/workflows/cross-version.yml +++ b/.github/workflows/cross-version.yml @@ -67,7 +67,7 @@ jobs: -Dscan.tag.JDK_${{ matrix.jdk.version }} \ build \ --no-configuration-cache #Disable configuration cache due to https://github.com/diffplug/spotless/issues/2318 - - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 + - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4 if: ${{ always() }} with: name: Open Test Reports (${{ github.job }} ${{ matrix.jdk.version }} (${{ matrix.jdk.release || matrix.jdk.type }})) @@ -109,7 +109,7 @@ jobs: -Dscan.tag.OpenJ9 \ build \ --no-configuration-cache # Disable configuration cache due to https://github.com/diffplug/spotless/issues/2318 - - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 + - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4 if: ${{ always() }} with: name: Open Test Reports (${{ github.job }}) diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml index 9a935daabb85..5468cddcba32 100644 --- a/.github/workflows/ossf-scorecard.yml +++ b/.github/workflows/ossf-scorecard.yml @@ -48,7 +48,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.pre.node20 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.pre.node20 with: name: SARIF file path: results.sarif diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1ff8f451505..685c5aed3e80 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,7 +55,7 @@ jobs: with: subject-path: build/repo/**/*.jar - name: Upload local repository for later jobs - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4 with: name: local-maven-repository path: build/repo From a224d41ad3d445958785d7d5059d1d612989dc46 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 22 Feb 2025 09:37:48 +0000 Subject: [PATCH 024/167] Update github/codeql-action digest to b56ba49 (#4331) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 4 ++-- .github/workflows/ossf-scorecard.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b5aa4b56c8dd..39786bbd4a29 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,7 +34,7 @@ jobs: - name: Check out repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Initialize CodeQL - uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3 + uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3 with: languages: ${{ matrix.language }} tools: linked @@ -47,4 +47,4 @@ jobs: -Dscan.tag.CodeQL \ allMainClasses - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3 + uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3 diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml index 5468cddcba32..024517909384 100644 --- a/.github/workflows/ossf-scorecard.yml +++ b/.github/workflows/ossf-scorecard.yml @@ -57,6 +57,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3 + uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3 with: sarif_file: results.sarif From 962b926babcea0ded1671208d75fedcb8df2bd11 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 22 Feb 2025 09:38:23 +0000 Subject: [PATCH 025/167] Update ossf/scorecard-action action to v2.4.1 --- .github/workflows/ossf-scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml index 024517909384..34a7c08fca41 100644 --- a/.github/workflows/ossf-scorecard.yml +++ b/.github/workflows/ossf-scorecard.yml @@ -26,7 +26,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 + uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 with: results_file: results.sarif results_format: sarif From 34084d8476092a3c7f3726e778d5eaa71b5feebe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 23 Feb 2025 17:55:53 +0000 Subject: [PATCH 026/167] Update dependency com.puppycrawl.tools:checkstyle to v10.21.3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a842479988ce..542f88ff1f16 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ asciidoctorj-pdf = "2.3.19" asciidoctor-plugins = "4.0.4" # Check if workaround in documentation.gradle.kts can be removed when upgrading assertj = "3.27.3" bnd = "7.1.0" -checkstyle = "10.21.2" +checkstyle = "10.21.3" eclipse = "4.34.0" jackson = "2.18.2" jacoco = "0.8.12" From 0ad7faeea355b7a72a10fe3ba6a8d2c2abbbb525 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 24 Feb 2025 13:21:58 +0100 Subject: [PATCH 027/167] Remove obsolete method --- .../jupiter/params/ParameterizedTestNameFormatter.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestNameFormatter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestNameFormatter.java index dc5cadc31b66..754b1ad2ec66 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestNameFormatter.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestNameFormatter.java @@ -34,7 +34,6 @@ import java.util.function.Function; import java.util.stream.IntStream; -import org.junit.jupiter.api.Named; import org.junit.jupiter.api.extension.ExtensionConfigurationException; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.util.StringUtils; @@ -79,12 +78,6 @@ private String formatSafely(int invocationIndex, EvaluatedArgumentSet arguments) return result.toString(); } - private Object[] extractNamedArguments(Object[] arguments) { - return Arrays.stream(arguments) // - .map(argument -> argument instanceof Named ? ((Named) argument).getName() : argument) // - .toArray(); - } - private PartialFormatter[] parse(String pattern, String displayName, ParameterizedTestMethodContext methodContext, int argumentMaxLength) { From 02cd2f6a1e4648d4cead9ac12e4e33bb8d234c50 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 24 Feb 2025 13:22:08 +0100 Subject: [PATCH 028/167] Polishing --- .../java/org/junit/jupiter/params/EvaluatedArgumentSet.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/EvaluatedArgumentSet.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/EvaluatedArgumentSet.java index c4a72daccee1..ae9f0a4dc18e 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/EvaluatedArgumentSet.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/EvaluatedArgumentSet.java @@ -17,6 +17,7 @@ import org.junit.jupiter.api.Named; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.Arguments.ArgumentSet; import org.junit.platform.commons.util.Preconditions; /** @@ -87,8 +88,8 @@ private static Object[] dropSurplus(Object[] arguments, int newLength) { } private static Optional determineName(Arguments arguments) { - if (arguments instanceof Arguments.ArgumentSet) { - return Optional.of(((Arguments.ArgumentSet) arguments).getName()); + if (arguments instanceof ArgumentSet) { + return Optional.of(((ArgumentSet) arguments).getName()); } return Optional.empty(); } @@ -98,4 +99,5 @@ private static Object[] extractFromNamed(Object[] arguments, Function, .map(argument -> argument instanceof Named ? mapper.apply((Named) argument) : argument) // .toArray(); } + } From 6804dd94d6aa1a0c95844d0c44be50e01a47c629 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 24 Feb 2025 13:37:21 +0100 Subject: [PATCH 029/167] Adhere to project coding and style guidelines --- .../api/AssertTimeoutPreemptively.java | 7 +++++-- .../jupiter/api/AssertionFailureBuilder.java | 9 +++++---- .../org/junit/jupiter/api/TestReporter.java | 16 ++++++++-------- .../api/extension/ExtensionContext.java | 8 ++++---- .../jupiter/api/extension/MediaType.java | 8 +++++--- .../platform/commons/support/Resource.java | 19 ++++++++++++------- .../scanning/DefaultClasspathScanner.java | 7 ++++--- .../platform/commons/util/PackageUtils.java | 11 ++++++----- 8 files changed, 49 insertions(+), 36 deletions(-) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeoutPreemptively.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeoutPreemptively.java index 82b0148780a9..1eaea9ce4f3e 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeoutPreemptively.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeoutPreemptively.java @@ -143,14 +143,17 @@ private static class ExecutionTimeoutException extends JUnitException { /** * The thread factory used for preemptive timeout. - *

- * The factory creates threads with meaningful names, helpful for debugging purposes. + * + *

The factory creates threads with meaningful names, helpful for debugging + * purposes. */ private static class TimeoutThreadFactory implements ThreadFactory { private static final AtomicInteger threadNumber = new AtomicInteger(1); + @Override public Thread newThread(Runnable r) { return new Thread(r, "junit-timeout-thread-" + threadNumber.getAndIncrement()); } } + } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertionFailureBuilder.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertionFailureBuilder.java index ab65c8048db7..9e98fb06f3a9 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertionFailureBuilder.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertionFailureBuilder.java @@ -21,8 +21,8 @@ /** * Builder for {@link AssertionFailedError AssertionFailedErrors}. - *

- * Using this builder ensures consistency in how failure message are formatted + * + *

Using this builder ensures consistency in how failure message are formatted * within JUnit Jupiter and for custom user-defined assertions. * * @since 5.9 @@ -51,8 +51,8 @@ private AssertionFailureBuilder() { /** * Set the user-defined message of the assertion. - *

- * The {@code message} may be passed as a {@link Supplier} or plain + * + *

The {@code message} may be passed as a {@link Supplier} or plain * {@link String}. If any other type is passed, it is converted to * {@code String} as per {@link StringUtils#nullSafeToString(Object)}. * @@ -202,4 +202,5 @@ private static String getClassName(Object obj) { return (obj == null ? "null" : obj instanceof Class ? getCanonicalName((Class) obj) : obj.getClass().getName()); } + } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestReporter.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestReporter.java index 85e28acc3bce..7805ab93ab29 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestReporter.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestReporter.java @@ -89,8 +89,8 @@ default void publishEntry(String value) { /** * Publish the supplied file and attach it to the current test or container. - *

- * The file will be copied to the report output directory replacing any + * + *

The file will be copied to the report output directory replacing any * potentially existing file with the same name. * * @param file the file to be attached; never {@code null} or blank @@ -108,8 +108,8 @@ default void publishFile(Path file, MediaType mediaType) { /** * Publish the supplied directory and attach it to the current test or * container. - *

- * The entire directory will be copied to the report output directory + * + *

The entire directory will be copied to the report output directory * replacing any potentially existing files with the same name. * * @param directory the file to be attached; never {@code null} or blank @@ -142,8 +142,8 @@ default void publishDirectory(Path directory) { /** * Publish a file or directory with the supplied name and media type written * by the supplied action and attach it to the current test or container. - *

- * The {@link Path} passed to the supplied action will be relative to the + * + *

The {@link Path} passed to the supplied action will be relative to the * report output directory, but it's up to the action to write the file. * * @param name the name of the file to be attached; never {@code null} or @@ -161,8 +161,8 @@ default void publishFile(String name, MediaType mediaType, ThrowingConsumer - * The {@link Path} passed to the supplied action will be relative to the + * + *

The {@link Path} passed to the supplied action will be relative to the * report output directory and point to an existing directory, but it's up * to the action to write files to it. * diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java index a2bfd4db7d80..3e9e3dd03430 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java @@ -370,8 +370,8 @@ default void publishReportEntry(String value) { /** * Publish a file with the supplied name written by the supplied action and * attach it to the current test or container. - *

- * The file will be resolved in the report output directory prior to + * + *

The file will be resolved in the report output directory prior to * invoking the supplied action. * * @param name the name of the file to be attached; never {@code null} or @@ -388,8 +388,8 @@ default void publishReportEntry(String value) { /** * Publish a directory with the supplied name written by the supplied action * and attach it to the current test or container. - *

- * The directory will be resolved and created in the report output directory + * + *

The directory will be resolved and created in the report output directory * prior to invoking the supplied action. * * @param name the name of the directory to be attached; never {@code null} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/MediaType.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/MediaType.java index 00ad02dadfd2..82513d94bcc2 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/MediaType.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/MediaType.java @@ -86,8 +86,8 @@ public class MediaType { /** * Parse the given media type value. - *

- * Must be valid according to + * + *

Must be valid according to * RFC 2045. * * @param value the media type value to parse; never {@code null} @@ -142,8 +142,9 @@ public String toString() { @Override public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) + if (o == null || getClass() != o.getClass()) { return false; + } MediaType that = (MediaType) o; return Objects.equals(this.value, that.value); } @@ -152,4 +153,5 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hashCode(value); } + } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/Resource.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/Resource.java index da3d6df59fd1..4080d12a5bbe 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/Resource.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/Resource.java @@ -20,7 +20,8 @@ import org.apiguardian.api.API; /** - * Represents a resource on the classpath. + * {@code Resource} represents a resource on the classpath. + * * @since 1.11 * @see ReflectionSupport#findAllResourcesInClasspathRoot(URI, Predicate) * @see ReflectionSupport#findAllResourcesInPackage(String, Predicate) @@ -33,24 +34,27 @@ public interface Resource { /** - * Get the resource name. - *

- * The resource name is a {@code /}-separated path. The path is relative to - * the classpath root in which the resource is located. + * Get the name of this resource. + * + *

The resource name is a {@code /}-separated path. The path is relative + * to the classpath root in which the resource is located. * * @return the resource name; never {@code null} */ String getName(); /** - * Get URI to a resource. + * Get the URI of this resource. * * @return the uri of the resource; never {@code null} */ URI getUri(); /** - * Returns an input stream for reading this resource. + * Get an {@link InputStream} for reading this resource. + * + *

The default implementation delegates to {@link java.net.URL#openStream()} + * for this resource's {@link #getUri() URI}. * * @return an input stream for this resource; never {@code null} * @throws IOException if an I/O exception occurs @@ -58,4 +62,5 @@ public interface Resource { default InputStream getInputStream() throws IOException { return getUri().toURL().openStream(); } + } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/scanning/DefaultClasspathScanner.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/scanning/DefaultClasspathScanner.java index de9ec7de7b66..f4ff533b000c 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/scanning/DefaultClasspathScanner.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/scanning/DefaultClasspathScanner.java @@ -243,9 +243,10 @@ private String determineFullyQualifiedClassName(Path baseDir, String basePackage /** * The fully qualified resource name is a {@code /}-separated path. - *

- * The path is relative to the classpath root in which the resource is located. - + * + *

The path is relative to the classpath root in which the resource is + * located. + * * @return the resource name; never {@code null} */ private String determineFullyQualifiedResourceName(Path baseDir, String basePackageName, Path resourceFile) { diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/PackageUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/PackageUtils.java index 884587c2928d..5e56191f4ad1 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/PackageUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/PackageUtils.java @@ -95,17 +95,17 @@ public static Optional getAttribute(Class type, String name) { return Optional.ofNullable(mainAttributes.getValue(name)); } } - catch (Exception e) { + catch (Exception ex) { return Optional.empty(); } } /** * Get the module or implementation version for the supplied {@code type}. - *

- * The former is only available if the type is part of a versioned module on - * the module path; the latter only if the type is part of a JAR file with a - * manifest that contains an {@code Implementation-Version} attribute. + * + *

The former is only available if the type is part of a versioned module + * on the module path; the latter only if the type is part of a JAR file with + * a manifest that contains an {@code Implementation-Version} attribute. * * @since 1.11 */ @@ -117,4 +117,5 @@ public static Optional getModuleOrImplementationVersion(Class type) { } return getAttribute(type, Package::getImplementationVersion); } + } From c110619baa1bc3bcebfc1cb9d09bdf582af383f7 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 24 Feb 2025 13:55:55 +0100 Subject: [PATCH 030/167] =?UTF-8?q?Add=20missing=20@=E2=81=A0since=20tag?= =?UTF-8?q?=20and=20polish/format=20Javadoc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/junit/platform/engine/TestDescriptor.java | 10 +++++----- .../java/org/junit/vintage/engine/Constants.java | 14 ++++++++------ .../vintage/engine/execution/VintageExecutor.java | 1 + 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/TestDescriptor.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/TestDescriptor.java index ba423fd07452..c0d0a737a88c 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/TestDescriptor.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/TestDescriptor.java @@ -177,16 +177,16 @@ default Set getDescendants() { void removeFromHierarchy(); /** - * Order the children from this descriptor. + * Order the children of this descriptor. * *

The {@code orderer} is provided a modifiable list of child test - * descriptors in this test descriptor; never {@code null}. The + * descriptors of this test descriptor; never {@code null}. The * {@code orderer} must return a list containing the same descriptors in any * order; potentially the same list, but never {@code null}. If descriptors - * were added or removed, an exception is thrown. + * are added or removed, an exception is thrown. * - * @param orderer a unary operator to order the children of this test - * descriptor. + * @param orderer a unary operator to order the children of this test descriptor + * @since 1.12 */ @API(since = "1.12", status = EXPERIMENTAL) default void orderChildren(UnaryOperator> orderer) { diff --git a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/Constants.java b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/Constants.java index 78e48d641bd2..3d97979d2276 100644 --- a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/Constants.java +++ b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/Constants.java @@ -47,10 +47,11 @@ public final class Constants { public static final String PARALLEL_POOL_SIZE = "junit.vintage.execution.parallel.pool-size"; /** - * Indicates whether parallel execution is enabled for test classes in the JUnit Vintage engine. + * Indicates whether parallel execution is enabled for test classes in the + * JUnit Vintage engine. * - *

Set this property to {@code true} to enable parallel execution of test classes. - * Defaults to {@code false}. + *

Set this property to {@code true} to enable parallel execution of test + * classes. Defaults to {@code false}. * * @since 5.12 */ @@ -58,10 +59,11 @@ public final class Constants { public static final String PARALLEL_CLASS_EXECUTION = "junit.vintage.execution.parallel.classes"; /** - * Indicates whether parallel execution is enabled for test methods in the JUnit Vintage engine. + * Indicates whether parallel execution is enabled for test methods in the + * JUnit Vintage engine. * - *

Set this property to {@code true} to enable parallel execution of test methods. - * Defaults to {@code false}. + *

Set this property to {@code true} to enable parallel execution of test + * methods. Defaults to {@code false}. * * @since 5.12 */ diff --git a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/execution/VintageExecutor.java b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/execution/VintageExecutor.java index f20446863d2a..0ca61c6655fe 100644 --- a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/execution/VintageExecutor.java +++ b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/execution/VintageExecutor.java @@ -186,4 +186,5 @@ private void shutdownExecutorService(ExecutorService executorService) { Thread.currentThread().interrupt(); } } + } From 21b76daac3ad82d1e15cf012acf12580f1e62445 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Tue, 25 Feb 2025 18:52:07 +0100 Subject: [PATCH 031/167] Handle `state_reason` "duplicate" --- .github/workflows/sanitize-closed-issues.yml | 36 +++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/.github/workflows/sanitize-closed-issues.yml b/.github/workflows/sanitize-closed-issues.yml index 6f0721a0d2db..6a7ae78d889e 100644 --- a/.github/workflows/sanitize-closed-issues.yml +++ b/.github/workflows/sanitize-closed-issues.yml @@ -28,7 +28,7 @@ jobs: labels: newLabels, }); } - if (issue.data.state_reason === "not_planned") { + if (issue.data.state_reason === "not_planned" || issue.data.state_reason === "duplicate") { if (issue.data.milestone) { await github.rest.issues.update({ issue_number: issue.data.number, @@ -39,18 +39,28 @@ jobs: } const statusLabels = newLabels.filter(l => l.startsWith("status: ")); if (statusLabels.length === 0) { - await github.rest.issues.createComment({ - issue_number: issue.data.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: "Please assign a status label to this issue.", - }); - await github.rest.issues.update({ - issue_number: issue.data.number, - owner: context.repo.owner, - repo: context.repo.repo, - state: "open", - }); + if (issue.data.state_reason === "not_planned") { + await github.rest.issues.createComment({ + issue_number: issue.data.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: "Please assign a status label to this issue.", + }); + await github.rest.issues.update({ + issue_number: issue.data.number, + owner: context.repo.owner, + repo: context.repo.repo, + state: "open", + }); + } else { + newLabels.push("status: duplicate"); + await github.rest.issues.update({ + issue_number: issue.data.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: newLabels, + }); + } } } else { if (!(newLabels.includes("type: task") || newLabels.includes("type: question")) && !issue.data.milestone) { From 826d6f815088d1cf4c9dc286febaff2f03e298c3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 18:10:38 +0000 Subject: [PATCH 032/167] Update dependency gradle to v8.13 (#4340) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/wrapper/gradle-wrapper.jar | Bin 43583 -> 43705 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- gradlew | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b9530d66f5e68d973ea569d8e19de379189..9bbc975c742b298b441bfb90dbc124400a3751b9 100644 GIT binary patch delta 34744 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 delta 34727 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�{ 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 a%EQ2*fnt2> Date: Tue, 25 Feb 2025 18:11:12 +0000 Subject: [PATCH 033/167] Update dependency org.slf4j:slf4j-jdk14 to v2.0.17 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 542f88ff1f16..a9ba03143349 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,7 +64,7 @@ openTestReporting-events = { module = "org.opentest4j.reporting:open-test-report openTestReporting-tooling-core = { module = "org.opentest4j.reporting:open-test-reporting-tooling-core", version.ref = "openTestReporting" } openTestReporting-tooling-spi = { module = "org.opentest4j.reporting:open-test-reporting-tooling-spi", version.ref = "openTestReporting" } picocli = { module = "info.picocli:picocli", version = "4.7.6" } -slf4j-julBinding = { module = "org.slf4j:slf4j-jdk14", version = "2.0.16" } +slf4j-julBinding = { module = "org.slf4j:slf4j-jdk14", version = "2.0.17" } snapshotTests-junit5 = { module = "de.skuzzle.test:snapshot-tests-junit5", version.ref = "snapshotTests" } snapshotTests-xml = { module = "de.skuzzle.test:snapshot-tests-xml", version.ref = "snapshotTests" } spock1 = { module = "org.spockframework:spock-core", version = "1.3-groovy-2.5" } From ab0009273e3495c463de4c032bc745fea0902fa4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 22:27:56 +0000 Subject: [PATCH 034/167] Update dependency ch.qos.logback:logback-core to v1.5.17 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a9ba03143349..f489f4da946c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ junit4 = "4.13.2" junit4Min = "4.12" ktlint = "1.5.0" log4j = "2.24.3" -logback = "1.5.16" +logback = "1.5.17" mockito = "5.15.2" opentest4j = "1.3.0" openTestReporting = "0.2.0" From 5c8a6431de53279f32f33e5eb76d398f81bd406b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2025 15:38:43 +0000 Subject: [PATCH 035/167] Update actions/download-artifact digest to cc20338 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 685c5aed3e80..9df5e6dcb436 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -191,7 +191,7 @@ jobs: fetch-depth: 1 ref: "refs/tags/${{ env.RELEASE_TAG }}" - name: Download local Maven repository - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4 + uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4 with: name: local-maven-repository path: build/repo From 275b2398f7b3c936a3d1b2ffd9aaca48425f88a6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2025 22:39:35 +0000 Subject: [PATCH 036/167] Update actions/attest-build-provenance action to v2.2.1 --- .github/workflows/main.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 03341e634fc5..8df55ac8c26f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -93,7 +93,7 @@ jobs: publish -x check \ prepareGitHubAttestation - name: Generate build provenance attestations - uses: actions/attest-build-provenance@520d128f165991a6c774bcb264f323e3d70747f4 # v2.2.0 + uses: actions/attest-build-provenance@f9eaf234fc1c2e333c1eca18177db0f44fa6ba52 # v2.2.1 with: subject-path: documentation/build/attestation/*.jar diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9df5e6dcb436..b65687f0ad25 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,7 +51,7 @@ jobs: :verifyArtifactsInStagingRepositoryAreReproducible \ --remote-repo-url=${{ env.STAGING_REPO_URL }} - name: Generate build provenance attestations - uses: actions/attest-build-provenance@520d128f165991a6c774bcb264f323e3d70747f4 # v2.2.0 + uses: actions/attest-build-provenance@f9eaf234fc1c2e333c1eca18177db0f44fa6ba52 # v2.2.1 with: subject-path: build/repo/**/*.jar - name: Upload local repository for later jobs From 189233686a743bfe0e0dfc2a84bb43c688f782bd Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 27 Feb 2025 12:03:53 +0100 Subject: [PATCH 037/167] Extract not(IndicativeSentences.class) to static Predicate --- .../org/junit/jupiter/api/DisplayNameGenerator.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java index 9852c79bffd5..750ec72bf226 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java @@ -15,6 +15,8 @@ import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.INTERNAL; import static org.apiguardian.api.API.Status.STABLE; +import static org.junit.jupiter.api.DisplayNameGenerator.getDisplayNameGenerator; +import static org.junit.jupiter.api.DisplayNameGenerator.parameterTypesAsString; import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation; import static org.junit.platform.commons.support.ModifierSupport.isStatic; @@ -310,6 +312,8 @@ class IndicativeSentences implements DisplayNameGenerator { static final DisplayNameGenerator INSTANCE = new IndicativeSentences(); + private static final Predicate> notIndicativeSentences = clazz -> clazz != IndicativeSentences.class; + public IndicativeSentences() { } @@ -346,7 +350,7 @@ private String getSentenceBeginning(Class testClass, List> enclosing Class generatorClass = findDisplayNameGeneration(testClass, enclosingInstanceTypes)// .map(DisplayNameGeneration::value)// - .filter(not(IndicativeSentences.class))// + .filter(notIndicativeSentences)// .orElse(null); if (generatorClass != null) { return getDisplayNameGenerator(generatorClass).generateDisplayNameForClass(testClass); @@ -411,7 +415,7 @@ private static String getFragmentSeparator(Class testClass, List> en private static DisplayNameGenerator getGeneratorFor(Class testClass, List> enclosingInstanceTypes) { return findIndicativeSentencesGeneration(testClass, enclosingInstanceTypes)// .map(IndicativeSentencesGeneration::generator)// - .filter(not(IndicativeSentences.class))// + .filter(notIndicativeSentences)// .map(DisplayNameGenerator::getDisplayNameGenerator)// .orElseGet(() -> getDisplayNameGenerator(IndicativeSentencesGeneration.DEFAULT_GENERATOR)); } @@ -447,10 +451,6 @@ private static Optional findIndicativeSentencesGe return findAnnotation(testClass, IndicativeSentencesGeneration.class, enclosingInstanceTypes); } - private static Predicate> not(Class clazz) { - return ((Predicate>) clazz::equals).negate(); - } - } /** From af4df143ede2e6c3bb37c3e5fdde68e53f490e48 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 27 Feb 2025 12:25:36 +0100 Subject: [PATCH 038/167] Remove unnecessary static imports --- .../main/java/org/junit/jupiter/api/DisplayNameGenerator.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java index 750ec72bf226..177264785288 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DisplayNameGenerator.java @@ -15,8 +15,6 @@ import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.INTERNAL; import static org.apiguardian.api.API.Status.STABLE; -import static org.junit.jupiter.api.DisplayNameGenerator.getDisplayNameGenerator; -import static org.junit.jupiter.api.DisplayNameGenerator.parameterTypesAsString; import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation; import static org.junit.platform.commons.support.ModifierSupport.isStatic; From b4ab5d7064285fc0403b91ae3a40f9d344bda4ab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 15:29:34 +0000 Subject: [PATCH 039/167] Update actions/attest-build-provenance action to v2.2.2 --- .github/workflows/main.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8df55ac8c26f..4eda347564a7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -93,7 +93,7 @@ jobs: publish -x check \ prepareGitHubAttestation - name: Generate build provenance attestations - uses: actions/attest-build-provenance@f9eaf234fc1c2e333c1eca18177db0f44fa6ba52 # v2.2.1 + uses: actions/attest-build-provenance@bd77c077858b8d561b7a36cbe48ef4cc642ca39d # v2.2.2 with: subject-path: documentation/build/attestation/*.jar diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b65687f0ad25..644e6392e693 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,7 +51,7 @@ jobs: :verifyArtifactsInStagingRepositoryAreReproducible \ --remote-repo-url=${{ env.STAGING_REPO_URL }} - name: Generate build provenance attestations - uses: actions/attest-build-provenance@f9eaf234fc1c2e333c1eca18177db0f44fa6ba52 # v2.2.1 + uses: actions/attest-build-provenance@bd77c077858b8d561b7a36cbe48ef4cc642ca39d # v2.2.2 with: subject-path: build/repo/**/*.jar - name: Upload local repository for later jobs From 556594cb6ffce9eb44b2a96e7ac49e262336f349 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Fri, 28 Feb 2025 11:54:50 +0100 Subject: [PATCH 040/167] Track exact versions to make Renovate updates easier to understand --- .github/actions/main-build/action.yml | 2 +- .github/actions/run-gradle/action.yml | 4 +-- .github/actions/setup-test-jdk/action.yml | 2 +- .github/workflows/close-inactive-issues.yml | 2 +- .github/workflows/codeql-analysis.yml | 6 ++--- .github/workflows/cross-version.yml | 12 ++++----- .../gradle-dependency-submission.yml | 6 ++--- .github/workflows/label-opened-issues.yml | 2 +- .github/workflows/main.yml | 14 +++++----- .github/workflows/ossf-scorecard.yml | 6 ++--- .github/workflows/release.yml | 26 +++++++++---------- .github/workflows/reproducible-build.yml | 2 +- .github/workflows/sanitize-closed-issues.yml | 2 +- 13 files changed, 43 insertions(+), 43 deletions(-) diff --git a/.github/actions/main-build/action.yml b/.github/actions/main-build/action.yml index 98c9f4017a6e..ec0e33284939 100644 --- a/.github/actions/main-build/action.yml +++ b/.github/actions/main-build/action.yml @@ -16,7 +16,7 @@ runs: with: arguments: ${{ inputs.arguments }} encryptionKey: ${{ inputs.encryptionKey }} - - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4 + - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 if: ${{ always() }} with: name: Open Test Reports (${{ github.job }}) diff --git a/.github/actions/run-gradle/action.yml b/.github/actions/run-gradle/action.yml index 86436e44afb3..ad901772a30c 100644 --- a/.github/actions/run-gradle/action.yml +++ b/.github/actions/run-gradle/action.yml @@ -11,13 +11,13 @@ inputs: runs: using: "composite" steps: - - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4 + - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 id: setup-gradle-jdk with: distribution: temurin java-version: 21 check-latest: true - - uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4 + - uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0 with: cache-encryption-key: ${{ inputs.encryptionKey }} - shell: bash diff --git a/.github/actions/setup-test-jdk/action.yml b/.github/actions/setup-test-jdk/action.yml index 48b4a3c11e1f..3fd0f16bdc48 100644 --- a/.github/actions/setup-test-jdk/action.yml +++ b/.github/actions/setup-test-jdk/action.yml @@ -8,7 +8,7 @@ inputs: runs: using: "composite" steps: - - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4 + - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: distribution: ${{ inputs.distribution }} java-version: 8 diff --git a/.github/workflows/close-inactive-issues.yml b/.github/workflows/close-inactive-issues.yml index a443402a9720..4453ca192264 100644 --- a/.github/workflows/close-inactive-issues.yml +++ b/.github/workflows/close-inactive-issues.yml @@ -11,7 +11,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9 + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 with: only-labels: "status: waiting-for-feedback" days-before-stale: 14 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 39786bbd4a29..8b35fe6c0bec 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -32,9 +32,9 @@ jobs: - javascript steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Initialize CodeQL - uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3 + uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 with: languages: ${{ matrix.language }} tools: linked @@ -47,4 +47,4 @@ jobs: -Dscan.tag.CodeQL \ allMainClasses - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3 + uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 diff --git a/.github/workflows/cross-version.yml b/.github/workflows/cross-version.yml index f4647bd2c7b0..478c508b8512 100644 --- a/.github/workflows/cross-version.yml +++ b/.github/workflows/cross-version.yml @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 - name: Set up Test JDK @@ -49,7 +49,7 @@ jobs: version: latest - name: "Set up JDK ${{ matrix.jdk.version }} (${{ matrix.jdk.distribution || 'temurin' }})" if: matrix.jdk.type == 'ga' - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: distribution: ${{ matrix.jdk.distribution || 'temurin' }} java-version: ${{ matrix.jdk.version }} @@ -67,7 +67,7 @@ jobs: -Dscan.tag.JDK_${{ matrix.jdk.version }} \ build \ --no-configuration-cache #Disable configuration cache due to https://github.com/diffplug/spotless/issues/2318 - - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4 + - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 if: ${{ always() }} with: name: Open Test Reports (${{ github.job }} ${{ matrix.jdk.version }} (${{ matrix.jdk.release || matrix.jdk.type }})) @@ -81,7 +81,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 - name: Set up Test JDK @@ -89,7 +89,7 @@ jobs: with: distribution: semeru - name: 'Set up JDK ${{ matrix.jdk }}' - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: distribution: semeru java-version: ${{ matrix.jdk }} @@ -109,7 +109,7 @@ jobs: -Dscan.tag.OpenJ9 \ build \ --no-configuration-cache # Disable configuration cache due to https://github.com/diffplug/spotless/issues/2318 - - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4 + - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 if: ${{ always() }} with: name: Open Test Reports (${{ github.job }}) diff --git a/.github/workflows/gradle-dependency-submission.yml b/.github/workflows/gradle-dependency-submission.yml index 6dff6b23897a..1e1c037ef8af 100644 --- a/.github/workflows/gradle-dependency-submission.yml +++ b/.github/workflows/gradle-dependency-submission.yml @@ -15,14 +15,14 @@ jobs: contents: write steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 - name: Setup Java - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: distribution: temurin java-version: 21 check-latest: true - name: Generate and submit dependency graph - uses: gradle/actions/dependency-submission@94baf225fe0a508e581a564467443d0e2379123b # v4 + uses: gradle/actions/dependency-submission@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0 diff --git a/.github/workflows/label-opened-issues.yml b/.github/workflows/label-opened-issues.yml index bbf37c72db6e..7cbf56726edf 100644 --- a/.github/workflows/label-opened-issues.yml +++ b/.github/workflows/label-opened-issues.yml @@ -10,7 +10,7 @@ jobs: permissions: issues: write steps: - - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | const issue = await github.rest.issues.get({ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4eda347564a7..0b2a0305171e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,11 +21,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 - name: Install GraalVM - uses: graalvm/setup-graalvm@b0cb26a8da53cb3e97cdc0c827d8e3071240e730 # v1 + uses: graalvm/setup-graalvm@b0cb26a8da53cb3e97cdc0c827d8e3071240e730 # v1.3.1 with: distribution: graalvm-community version: 'latest' @@ -41,7 +41,7 @@ jobs: jacocoRootReport \ --no-configuration-cache # Disable configuration cache due to https://github.com/diffplug/spotless/issues/2318 - name: Upload to Codecov.io - uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5 + uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.4.0 with: token: ${{ secrets.CODECOV_TOKEN }} @@ -49,7 +49,7 @@ jobs: runs-on: windows-latest steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 - name: Build @@ -61,7 +61,7 @@ jobs: runs-on: macos-latest steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 - name: Build @@ -79,7 +79,7 @@ jobs: if: github.event_name == 'push' && github.repository == 'junit-team/junit5' && (startsWith(github.ref, 'refs/heads/releases/') || github.ref == 'refs/heads/main') steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 - name: Publish @@ -106,7 +106,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 - name: Install Graphviz diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml index 34a7c08fca41..6a36c77a930e 100644 --- a/.github/workflows/ossf-scorecard.yml +++ b/.github/workflows/ossf-scorecard.yml @@ -21,7 +21,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false @@ -48,7 +48,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.pre.node20 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: SARIF file path: results.sarif @@ -57,6 +57,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3 + uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 with: sarif_file: results.sarif diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 644e6392e693..923c585f6eac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: id-token: write # required for build provenance attestation steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 ref: "refs/tags/${{ env.RELEASE_TAG }}" @@ -55,7 +55,7 @@ jobs: with: subject-path: build/repo/**/*.jar - name: Upload local repository for later jobs - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4 + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 with: name: local-maven-repository path: build/repo @@ -65,17 +65,17 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out samples repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: repository: ${{ github.repository_owner }}/junit5-samples token: ${{ secrets.GH_TOKEN }} fetch-depth: 1 - name: Set up JDK - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 21 distribution: temurin - - uses: sbt/setup-sbt@96cf3f09dc501acdad7807fffe97dba9fa0709be # v1 + - uses: sbt/setup-sbt@96cf3f09dc501acdad7807fffe97dba9fa0709be # v1.1.5 - name: Update JUnit dependencies in samples run: java src/Updater.java ${{ github.event.inputs.releaseVersion }} - name: Inject staging repository URL @@ -90,7 +90,7 @@ jobs: issues: write steps: - name: Close GitHub milestone - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: result-encoding: string script: | @@ -122,7 +122,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 ref: "refs/tags/${{ env.RELEASE_TAG }}" @@ -143,7 +143,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 ref: "refs/tags/${{ env.RELEASE_TAG }}" @@ -186,7 +186,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 ref: "refs/tags/${{ env.RELEASE_TAG }}" @@ -206,17 +206,17 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out samples repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: repository: ${{ github.repository_owner }}/junit5-samples token: ${{ secrets.GH_TOKEN }} fetch-depth: 1 - name: Set up JDK - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4 + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 with: java-version: 21 distribution: temurin - - uses: sbt/setup-sbt@96cf3f09dc501acdad7807fffe97dba9fa0709be # v1 + - uses: sbt/setup-sbt@96cf3f09dc501acdad7807fffe97dba9fa0709be # v1.1.5 - name: Update JUnit dependencies in samples run: java src/Updater.java ${{ github.event.inputs.releaseVersion }} - name: Build samples @@ -244,7 +244,7 @@ jobs: contents: write steps: - name: Create GitHub release - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | const releaseVersion = "${{ github.event.inputs.releaseVersion }}"; diff --git a/.github/workflows/reproducible-build.yml b/.github/workflows/reproducible-build.yml index 6d5f3bb7a1cf..546ff5b9cd18 100644 --- a/.github/workflows/reproducible-build.yml +++ b/.github/workflows/reproducible-build.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 1 - name: Restore Gradle cache and display toolchains diff --git a/.github/workflows/sanitize-closed-issues.yml b/.github/workflows/sanitize-closed-issues.yml index 6a7ae78d889e..046be82f7804 100644 --- a/.github/workflows/sanitize-closed-issues.yml +++ b/.github/workflows/sanitize-closed-issues.yml @@ -10,7 +10,7 @@ jobs: permissions: issues: write steps: - - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | const issue = await github.rest.issues.get({ From fb89b3bdfd83285b82214c8818a45426c303e3b9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 10:55:55 +0000 Subject: [PATCH 041/167] Update codecov/codecov-action digest to 0565863 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0b2a0305171e..900d4686ce72 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -41,7 +41,7 @@ jobs: jacocoRootReport \ --no-configuration-cache # Disable configuration cache due to https://github.com/diffplug/spotless/issues/2318 - name: Upload to Codecov.io - uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.4.0 + uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5.4.0 with: token: ${{ secrets.CODECOV_TOKEN }} From 8cab7b8cb48726733cd0dee90aea7304d091faa1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 10:55:58 +0000 Subject: [PATCH 042/167] Update dependency org.apache.groovy:groovy to v4.0.26 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f489f4da946c..ac8c6ba16e22 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,7 +36,7 @@ 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.179" } commons-io = { module = "commons-io:commons-io", version = "2.18.0" } -groovy4 = { module = "org.apache.groovy:groovy", version = "4.0.25" } +groovy4 = { module = "org.apache.groovy:groovy", version = "4.0.26" } groovy2-bom = { module = "org.codehaus.groovy:groovy-bom", version = "2.5.23" } hamcrest = { module = "org.hamcrest:hamcrest", version = "3.0" } jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson" } From c4faa6c26918fbbb8e3dbbde0e8d5077c8873d7c Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Fri, 28 Feb 2025 16:22:38 +0100 Subject: [PATCH 043/167] Introduce support for parameterized classes (#4342) This commit introduces @ParameterizedClass which builds on @ContainerTemplate and allows declaring a top-level or @Nested test class as a parameterized container to be invoked multiple times with different arguments. The same @...Source annotations as for @ParameterizedTest may be used to provide arguments via constructor or field injection. Resolves #878. --- .../src/docs/asciidoc/link-attributes.adoc | 5 + .../release-notes-5.13.0-M1.adoc | 9 +- .../docs/asciidoc/user-guide/appendix.adoc | 2 +- .../docs/asciidoc/user-guide/extensions.adoc | 7 +- .../asciidoc/user-guide/writing-tests.adoc | 419 +++-- .../java/example/ParameterizedClassDemo.java | 150 ++ .../java/example/ParameterizedTestDemo.java | 19 +- .../example/ParameterizedRecordDemo.java | 51 + .../test/resources/junit-platform.properties | 2 + ...uild.kotlin-library-conventions.gradle.kts | 1 + .../descriptor/ClassBasedTestDescriptor.java | 4 +- ...ainerTemplateInvocationTestDescriptor.java | 19 +- .../descriptor/JupiterTestDescriptor.java | 2 +- .../descriptor/TestMethodTestDescriptor.java | 2 +- .../execution/ParameterResolutionUtils.java | 4 + ...zedInvocationNameFormatterBenchmarks.java} | 12 +- .../params/ArgumentCountValidationMode.java | 29 +- .../params/ArgumentCountValidator.java | 57 +- ...rTemplateConstructorParameterResolver.java | 38 + ...tanceFieldInjectingBeforeEachCallback.java | 39 + ...teInstanceFieldInjectingPostProcessor.java | 41 + .../jupiter/params/EvaluatedArgumentSet.java | 10 +- .../ExecutableParameterDeclaration.java | 49 + .../params/FieldParameterDeclaration.java | 57 + .../org/junit/jupiter/params/Parameter.java | 65 + .../jupiter/params/ParameterizedClass.java | 239 +++ .../params/ParameterizedClassContext.java | 111 ++ .../params/ParameterizedClassExtension.java | 135 ++ .../ParameterizedClassInvocationContext.java | 80 + .../ParameterizedDeclarationContext.java | 43 + .../ParameterizedInvocationConstants.java | 131 ++ .../ParameterizedInvocationContext.java | 75 + ...arameterizedInvocationContextProvider.java | 77 + ...ParameterizedInvocationNameFormatter.java} | 98 +- ...ameterizedInvocationParameterResolver.java | 63 + .../jupiter/params/ParameterizedTest.java | 168 +- .../params/ParameterizedTestContext.java | 78 + .../params/ParameterizedTestExtension.java | 103 +- .../ParameterizedTestInvocationContext.java | 66 +- .../ParameterizedTestMethodContext.java | 282 ---- ...ameterizedTestMethodParameterResolver.java | 37 + .../ParameterizedTestParameterResolver.java | 76 - .../junit/jupiter/params/ResolutionCache.java | 41 + .../junit/jupiter/params/ResolverFacade.java | 541 +++++++ .../params/aggregator/AggregateWith.java | 10 +- .../aggregator/ArgumentsAggregator.java | 27 + .../aggregator/DefaultArgumentsAccessor.java | 33 +- .../aggregator/SimpleArgumentsAggregator.java | 48 + .../AnnotationBasedArgumentConverter.java | 6 + .../params/converter/ArgumentConverter.java | 25 +- .../jupiter/params/converter/ConvertWith.java | 12 +- .../converter/DefaultArgumentConverter.java | 18 +- .../converter/JavaTimeConversionPattern.java | 7 +- .../converter/SimpleArgumentConverter.java | 6 + .../converter/TypedArgumentConverter.java | 14 +- .../AnnotationBasedArgumentsProvider.java | 21 +- .../params/provider/ArgumentsProvider.java | 46 +- .../params/provider/ArgumentsSource.java | 7 +- .../params/provider/ArgumentsSources.java | 2 +- .../params/provider/CsvArgumentsProvider.java | 4 +- .../provider/CsvFileArgumentsProvider.java | 15 +- .../params/provider/CsvFileSource.java | 22 +- .../params/provider/CsvFileSources.java | 2 +- .../jupiter/params/provider/CsvSource.java | 18 +- .../jupiter/params/provider/CsvSources.java | 2 +- .../provider/EmptyArgumentsProvider.java | 21 +- .../jupiter/params/provider/EmptySource.java | 8 +- .../provider/EnumArgumentsProvider.java | 33 +- .../jupiter/params/provider/EnumSource.java | 7 +- .../jupiter/params/provider/EnumSources.java | 2 +- .../provider/FieldArgumentsProvider.java | 11 +- .../jupiter/params/provider/FieldSource.java | 20 +- .../jupiter/params/provider/FieldSources.java | 2 +- .../provider/MethodArgumentsProvider.java | 36 +- .../jupiter/params/provider/MethodSource.java | 19 +- .../params/provider/MethodSources.java | 2 +- .../params/provider/NullAndEmptySource.java | 9 +- .../provider/NullArgumentsProvider.java | 11 +- .../jupiter/params/provider/NullSource.java | 6 +- .../provider/ValueArgumentsProvider.java | 4 +- .../jupiter/params/provider/ValueSource.java | 6 +- .../jupiter/params/provider/ValueSources.java | 2 +- .../AnnotationConsumerInitializer.java | 2 +- .../jupiter/params/support/FieldContext.java | 50 + .../params/support/ParameterDeclaration.java | 55 + .../params/support/ParameterDeclarations.java | 83 + .../ParameterizedClassIntegrationTests.java | 1384 +++++++++++++++++ ...eterizedInvocationNameFormatterTests.java} | 45 +- ...ava => ParameterizedTestContextTests.java} | 18 +- .../ParameterizedTestExtensionTests.java | 33 +- .../ParameterizedTestIntegrationTests.java | 106 +- .../AggregatorIntegrationTests.java | 44 +- .../DefaultArgumentsAccessorTests.java | 2 +- ...AnnotationBasedArgumentsProviderTests.java | 13 +- .../provider/CsvArgumentsProviderTests.java | 3 +- .../CsvFileArgumentsProviderTests.java | 2 +- .../provider/EnumArgumentsProviderTests.java | 35 +- .../provider/FieldArgumentsProviderTests.java | 4 +- .../MethodArgumentsProviderTests.java | 7 +- .../provider/ValueArgumentsProviderTests.java | 3 +- .../AnnotationConsumerInitializerTests.java | 5 +- .../params/ParameterizedDataClassTestCase.kt | 35 + ...nvocationNameFormatterIntegrationTests.kt} | 6 +- .../ArgumentsAccessorKotlinTests.kt | 2 +- .../params/aggregator/DisplayNameTests.kt | 2 +- .../discovery/IterationSelectorTests.java | 18 +- 106 files changed, 4964 insertions(+), 1074 deletions(-) create mode 100644 documentation/src/test/java/example/ParameterizedClassDemo.java create mode 100644 documentation/src/test/java21/example/ParameterizedRecordDemo.java rename junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/{ParameterizedTestNameFormatterBenchmarks.java => ParameterizedInvocationNameFormatterBenchmarks.java} (78%) create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/ContainerTemplateConstructorParameterResolver.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/ContainerTemplateInstanceFieldInjectingBeforeEachCallback.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/ContainerTemplateInstanceFieldInjectingPostProcessor.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/ExecutableParameterDeclaration.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/FieldParameterDeclaration.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/Parameter.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClass.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassExtension.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedDeclarationContext.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationConstants.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContextProvider.java rename junit-jupiter-params/src/main/java/org/junit/jupiter/params/{ParameterizedTestNameFormatter.java => ParameterizedInvocationNameFormatter.java} (70%) create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationParameterResolver.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestContext.java delete mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodContext.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodParameterResolver.java delete mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestParameterResolver.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolutionCache.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/SimpleArgumentsAggregator.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/FieldContext.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterDeclaration.java create mode 100644 junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterDeclarations.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java rename jupiter-tests/src/test/java/org/junit/jupiter/params/{ParameterizedTestNameFormatterTests.java => ParameterizedInvocationNameFormatterTests.java} (84%) rename jupiter-tests/src/test/java/org/junit/jupiter/params/{ParameterizedTestMethodContextTests.java => ParameterizedTestContextTests.java} (77%) create mode 100644 jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedDataClassTestCase.kt rename jupiter-tests/src/test/kotlin/org/junit/jupiter/params/{ParameterizedTestNameFormatterIntegrationTests.kt => ParameterizedInvocationNameFormatterIntegrationTests.kt} (92%) diff --git a/documentation/src/docs/asciidoc/link-attributes.adoc b/documentation/src/docs/asciidoc/link-attributes.adoc index 9862695d6d76..3806284591f1 100644 --- a/documentation/src/docs/asciidoc/link-attributes.adoc +++ b/documentation/src/docs/asciidoc/link-attributes.adoc @@ -185,6 +185,9 @@ endif::[] :params-provider-package: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/package-summary.html[org.junit.jupiter.params.provider] :AnnotationBasedArgumentConverter: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/converter/AnnotationBasedArgumentConverter.html[AnnotationBasedArgumentConverter] :AnnotationBasedArgumentsProvider: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProvider.html[AnnotationBasedArgumentsProvider] +:AggregateWith: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/aggregator/AggregateWith.html[@AggregateWith] +:Arguments: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/Arguments.html[Arguments] +:ArgumentsProvider: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/ArgumentsProvider.html[ArgumentsProvider] :ArgumentsAccessor: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/aggregator/ArgumentsAccessor.html[ArgumentsAccessor] :ArgumentsAggregator: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/aggregator/ArgumentsAggregator.html[ArgumentsAggregator] :CsvArgumentsProvider: {junit5-repo}/blob/main/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvArgumentsProvider.java[CsvArgumentsProvider] @@ -193,6 +196,8 @@ endif::[] :MethodSource: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/MethodSource.html[@MethodSource] :NullAndEmptySource: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/NullAndEmptySource.html[@NullAndEmptySource] :NullSource: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/NullSource.html[@NullSource] +:Parameter: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/Parameter.html[@Parameter] +:ParameterizedClass: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/ParameterizedClass.html[@ParameterizedClass] :ParameterizedTest: {javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/ParameterizedTest.html[@ParameterizedTest] :ValueArgumentsProvider: {junit5-repo}/blob/main/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueArgumentsProvider.java[ValueArgumentsProvider] // Jupiter Engine diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc index 7cf976bb5bdb..2254a47c6b2a 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc @@ -55,7 +55,14 @@ repository on GitHub. allow declaring a top-level or `@Nested` test class as a template to be invoked multiple times. This may be used, for example, to inject different parameters to be used by all tests in the container template class or to set up each invocation of the container - template differently. + template differently. Please refer to the + <<../user-guide/index.adoc#writing-tests-container-templates, User Guide>> for details. +* Introduce `@ParameterizedClass` concept that builds on `@ContainerTemplate` and allows + declaring a top-level or `@Nested` test class as a parameterized test class to be + invoked multiple times with different arguments. The same `@...Source` annotations as + for `@ParameterizedTest` may be used to provide arguments via constructor or field + injection. Please refer to the + <<../user-guide/index.adoc#writing-tests-parameterized-tests, User Guide>> for details. * New `TestTemplateInvocationContext.prepareInvocation(ExtensionContext)` callback method allows preparing the `ExtensionContext` before the test template method is invoked. This may be used, for example, to store entries in its `Store` to benefit from its cleanup diff --git a/documentation/src/docs/asciidoc/user-guide/appendix.adoc b/documentation/src/docs/asciidoc/user-guide/appendix.adoc index ab63a5a90b3b..9df8622629d5 100644 --- a/documentation/src/docs/asciidoc/user-guide/appendix.adoc +++ b/documentation/src/docs/asciidoc/user-guide/appendix.adoc @@ -105,7 +105,7 @@ Please refer to the corresponding sections for <> in JUnit Jupiter. + Support for <> in JUnit Jupiter. `junit-jupiter-migrationsupport`:: Support for migrating from JUnit 4 to JUnit Jupiter; only required for support for JUnit 4's `@Ignore` annotation and for running selected JUnit 4 rules. diff --git a/documentation/src/docs/asciidoc/user-guide/extensions.adoc b/documentation/src/docs/asciidoc/user-guide/extensions.adoc index 47bbd8947661..65e7c77485a6 100644 --- a/documentation/src/docs/asciidoc/user-guide/extensions.adoc +++ b/documentation/src/docs/asciidoc/user-guide/extensions.adoc @@ -804,6 +804,9 @@ implementing different kinds of tests that rely on repetitive invocation of _all methods in a test class albeit in different contexts — for example, with different parameters, by preparing the test class instance differently, or multiple times without modifying the context. +Please refer to the implementations of +<> which uses this extension +point to provide its functionality. [[extensions-test-templates]] === Providing Invocation Contexts for Test Templates @@ -839,8 +842,8 @@ implementing different kinds of tests that rely on repetitive invocation of a te method albeit in different contexts — for example, with different parameters, by preparing the test class instance differently, or multiple times without modifying the context. Please refer to the implementations of <> or -<> which use this extension point to provide their -functionality. +<> which use this extension point +to provide their functionality. [[extensions-keeping-state]] === Keeping State in Extensions diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index 7ea683a04df6..2926648fe3e9 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -1,4 +1,5 @@ :testDir: ../../../../src/test/java +:testResourcesDir: ../../../../src/test/resources :testRelease21Dir: ../../../../src/test/java21 :kotlinTestDir: ../../../../src/test/kotlin @@ -42,6 +43,7 @@ in the `junit-jupiter-api` module. | `@AfterEach` | Denotes that the annotated method should be executed _after_ *each* `@Test`, `@RepeatedTest`, `@ParameterizedTest`, or `@TestFactory` method in the current class; analogous to JUnit 4's `@After`. Such methods are inherited unless they are overridden. | `@BeforeAll` | Denotes that the annotated method should be executed _before_ *all* `@Test`, `@RepeatedTest`, `@ParameterizedTest`, and `@TestFactory` methods in the current class; analogous to JUnit 4's `@BeforeClass`. Such methods are inherited unless they are overridden and must be `static` unless the "per-class" <> is used. | `@AfterAll` | Denotes that the annotated method should be executed _after_ *all* `@Test`, `@RepeatedTest`, `@ParameterizedTest`, and `@TestFactory` methods in the current class; analogous to JUnit 4's `@AfterClass`. Such methods are inherited unless they are overridden and must be `static` unless the "per-class" <> is used. +| `@ParameterizedClass` | Denotes that the annotated class is a <>. | `@ContainerTemplate` | Denotes that the annotated class is a <> designed to be executed multiple times depending on the number of invocation contexts returned by the registered <>. | `@Nested` | Denotes that the annotated class is a non-static <>. On Java 8 through Java 15, `@BeforeAll` and `@AfterAll` methods cannot be used directly in a `@Nested` test class unless the "per-class" <> is used. Beginning with Java 16, `@BeforeAll` and `@AfterAll` methods can be declared as `static` in a `@Nested` test class with either test instance lifecycle mode. Such annotations are not inherited. | `@Tag` | Used to declare <>, either at the class or method level; analogous to test groups in TestNG or Categories in JUnit 4. Such annotations are inherited at the class level but not at the method level. @@ -1052,6 +1054,47 @@ class with `@TestInstance(Lifecycle.PER_CLASS)` (see `@BeforeAll` and `@AfterAll` methods can be declared as `static` in `@Nested` test classes, and this restriction no longer applies. +[[writing-tests-nested-interoperability]] +==== Interoperability + +`@Nested` may be combined with +<> in which case the nested test +class is parameterized. + +The following example illustrates how to combine `@Nested` with `@ParameterizedClass` and +`@ParameterizedTest`. + +[source,java,indent=0] +---- +include::{testDir}/example/ParameterizedClassDemo.java[tags=nested] +---- + +Executing the above test class yields the following output: + +.... +FruitTests ✔ +├─ [1] fruit=apple ✔ +│ └─ QuantityTests ✔ +│ ├─ [1] quantity=23 ✔ +│ │ └─ test(Duration) ✔ +│ │ ├─ [1] duration=PT1H ✔ +│ │ └─ [2] duration=PT2H ✔ +│ └─ [2] quantity=42 ✔ +│ └─ test(Duration) ✔ +│ ├─ [1] duration=PT1H ✔ +│ └─ [2] duration=PT2H ✔ +└─ [2] fruit=banana ✔ + └─ QuantityTests ✔ + ├─ [1] quantity=23 ✔ + │ └─ test(Duration) ✔ + │ ├─ [1] duration=PT1H ✔ + │ └─ [2] duration=PT2H ✔ + └─ [2] quantity=42 ✔ + └─ test(Duration) ✔ + ├─ [1] duration=PT1H ✔ + └─ [2] duration=PT2H ✔ +.... + [[writing-tests-dependency-injection]] === Dependency Injection for Constructors and Methods @@ -1403,13 +1446,26 @@ When using the `ConsoleLauncher` with the unicode theme enabled, execution of [[writing-tests-parameterized-tests]] -=== Parameterized Tests +=== Parameterized Classes and Tests -Parameterized tests make it possible to run a test multiple times with different +_Parameterized tests_ make it possible to run a test method multiple times with different arguments. They are declared just like regular `@Test` methods but use the -`{ParameterizedTest}` annotation instead. In addition, you must declare at least one -_source_ that will provide the arguments for each invocation and then _consume_ the -arguments in the test method. +`{ParameterizedTest}` annotation instead. + +_Parameterized classes_ make it possible to run _all_ tests in test class, including +<>, multiple times with different arguments. They are declared just +like regular test classes and may contain any supported test method type (including +`@ParameterizedTest`) but annotated with the `{ParameterizedClass}` annotation. + +WARNING: _Parameterized classes_ are currently an _experimental_ feature. You're invited +to give it a try and provide feedback to the JUnit team so they can improve and eventually +<> this feature. + +Regardless of whether you are parameterizing a test method or a test class, you must +declare at least one <> that will +provide the arguments for each invocation and then +<> the arguments in the +parameterized method or class, respectively. The following example demonstrates a parameterized test that uses the `@ValueSource` annotation to specify a `String` array as the source of arguments. @@ -1430,18 +1486,46 @@ palindromes(String) ✔ └─ [3] candidate=able was I ere I saw elba ✔ .... +The same `@ValueSource` annotation can be used to specify the source of arguments for a +`@ParameterizedClass`. + +[source,java,indent=0] +---- +include::{testDir}/example/ParameterizedClassDemo.java[tags=first_example] +---- + +When executing the above parameterized test class, each invocation will be reported +separately. For instance, the `ConsoleLauncher` will print output similar to the +following. + +.... +PalindromeTests ✔ +├─ [1] candidate=racecar ✔ +│ ├─ palindrome() ✔ +│ └─ reversePalindrome() ✔ +├─ [2] candidate=radar ✔ +│ ├─ palindrome() ✔ +│ └─ reversePalindrome() ✔ +└─ [3] candidate=able was I ere I saw elba ✔ + ├─ palindrome() ✔ + └─ reversePalindrome() ✔ +.... + [[writing-tests-parameterized-tests-setup]] ==== Required Setup -In order to use parameterized tests you need to add a dependency on the +In order to use parameterized classes or tests you need to add a dependency on the `junit-jupiter-params` artifact. Please refer to <> for details. [[writing-tests-parameterized-tests-consuming-arguments]] ==== Consuming Arguments -Parameterized test methods typically _consume_ arguments directly from the configured -source (see <>) following a one-to-one -correlation between argument source index and method parameter index (see examples in +[[writing-tests-parameterized-tests-consuming-arguments-methods]] +===== Parameterized Tests + +Parameterized test methods _consume_ arguments directly from the configured source (see +<>) following a one-to-one correlation between +argument source index and method parameter index (see examples in <>). However, a parameterized test method may also choose to _aggregate_ arguments from the source into a single object passed to the method (see <>). @@ -1449,29 +1533,96 @@ Additional arguments may also be provided by a `ParameterResolver` (e.g., to obt instance of `TestInfo`, `TestReporter`, etc.). Specifically, a parameterized test method must declare formal parameters according to the following rules. -* Zero or more _indexed arguments_ must be declared first. +* Zero or more _indexed parameters_ must be declared first. * Zero or more _aggregators_ must be declared next. * Zero or more arguments supplied by a `ParameterResolver` must be declared last. -In this context, an _indexed argument_ is an argument for a given index in the -`Arguments` provided by an `ArgumentsProvider` that is passed as an argument to the +In this context, an _indexed parameter_ is an argument for a given index in the +`{Arguments}` provided by an `{ArgumentsProvider}` that is passed as an argument to the parameterized method at the same index in the method's formal parameter list. An -_aggregator_ is any parameter of type `ArgumentsAccessor` or any parameter annotated with -`@AggregateWith`. +_aggregator_ is any parameter of type `{ArgumentsAccessor}` or any parameter annotated +with `{AggregateWith}`. + +[[writing-tests-parameterized-tests-consuming-arguments-classes]] +===== Parameterized Classes + +Parameterized classes _consume_ arguments directly from the configured source (see +<>); either via their unique constructor or via +field injection. If a `{Parameter}`-annotated field is declared in the parameterized class +or one of its superclasses, field injection will be used. Otherwise, constructor injection +will be used. + +[[writing-tests-parameterized-tests-consuming-arguments-constructor-injection]] +====== Constructor Injection + +WARNING: Constructor injection can only be used with the (default) `PER_METHOD` +<> mode. Please use +<> +with the `PER_CLASS` mode instead. + +For constructor injection, the same rules apply as defined for +<> +above. In the following example, two arguments are injected into the constructor of the +test class. + +[source,java,indent=0] +---- +include::{testDir}/example/ParameterizedClassDemo.java[tags=constructor_injection] +---- + +If your programming language level you are using supports _records_ -- for example, Java +16 or higher -- you may use them to implement parameterized classes that avoid the +boilerplate code of declaring a test class constructor. + +[source,java,indent=0] +---- +include::{testRelease21Dir}/example/ParameterizedRecordDemo.java[tags=example] +---- + +[[writing-tests-parameterized-tests-consuming-arguments-field-injection]] +====== Field Injection + +For field injection, the following rules apply for fields annotated with `@Parameter`. + +* Zero or more _indexed parameters_ may be declared; each must have a unique index + specified in its `@Parameter(index)` annotation. The index may be omitted if there is + only one indexed parameter. If there are at least two indexed parameter declarations, + there must be declarations for all indexes from 0 to the largest declared index. +* Zero or more _aggregators_ may be declared; each without specifying an index in its + `@Parameter` annotation. +* Zero or more other fields may be declared as usual as long as they're not annotated with + `@Parameter`. + +In this context, an _indexed parameter_ is an argument for a given index in the +`{Arguments}` provided by an `{ArgumentsProvider}` that is injected into a field annotated +with `@Parameter(index)`. An _aggregator_ is any `@Parameter`-annotated field of type +{ArgumentsAccessor} or any field annotated with {AggregateWith}. + +The following example demonstrates how to use field injection to consume multiple +arguments in a parameterized class. + +[source,java,indent=0] +---- +include::{testDir}/example/ParameterizedClassDemo.java[tags=field_injection] +---- + +If field injection is used, no constructor parameters will be resolved with arguments from +the source. Other <> +may resolve constructor parameters as usual, though. [NOTE] .AutoCloseable arguments ==== Arguments that implement `java.lang.AutoCloseable` (or `java.io.Closeable` which extends -`java.lang.AutoCloseable`) will be automatically closed after `@AfterEach` methods and -`AfterEachCallback` extensions have been called for the current parameterized test -invocation. +`java.lang.AutoCloseable`) will be automatically closed after the parameterized class or +test invocation. To prevent this from happening, set the `autoCloseArguments` attribute in `@ParameterizedTest` to `false`. Specifically, if an argument that implements -`AutoCloseable` is reused for multiple invocations of the same parameterized test method, -you must annotate the method with `@ParameterizedTest(autoCloseArguments = false)` to -ensure that the argument is not closed between invocations. +`AutoCloseable` is reused for multiple invocations of the same parameterized class or test +method, you must specify the `autoCloseArguments = false` on the `{ParameterizedClass}` or +`{ParameterizedTest}` annotation to ensure that the argument is not closed between +invocations. ==== [[writing-tests-parameterized-tests-sources]] @@ -1482,6 +1633,10 @@ following subsections provides a brief overview and an example for each of them. refer to the Javadoc in the `{params-provider-package}` package for additional information. +TIP: All source annotations in this section are applicable to both `{ParameterizedClass}` +and `{ParameterizedTest}`. For the sake of brevity, the examples in this section will only +show how to use them with `{ParameterizedTest}` methods. + [[writing-tests-parameterized-tests-sources-ValueSource]] ===== @ValueSource @@ -1518,22 +1673,23 @@ supplied _bad input_, it can be useful to have `null` and _empty_ values supplie parameterized tests. The following annotations serve as sources of `null` and empty values for parameterized tests that accept a single argument. -* `{NullSource}`: provides a single `null` argument to the annotated `@ParameterizedTest` - method. +* `{NullSource}`: provides a single `null` argument to the annotated `@ParameterizedClass` + or `@ParameterizedTest`. - `@NullSource` cannot be used for a parameter that has a primitive type. * `{EmptySource}`: provides a single _empty_ argument to the annotated - `@ParameterizedTest` method for parameters of the following types: `java.lang.String`, - `java.util.Collection` (and concrete subtypes with a `public` no-arg constructor), - `java.util.List`, `java.util.Set`, `java.util.SortedSet`, `java.util.NavigableSet`, - `java.util.Map` (and concrete subtypes with a `public` no-arg constructor), - `java.util.SortedMap`, `java.util.NavigableMap`, primitive arrays (e.g., `int[]`, - `char[][]`, etc.), object arrays (e.g., `String[]`, `Integer[][]`, etc.). + `@ParameterizedClass` or `@ParameterizedTest` for parameters of the following types: + `java.lang.String`, `java.util.Collection` (and concrete subtypes with a `public` no-arg + constructor), `java.util.List`, `java.util.Set`, `java.util.SortedSet`, + `java.util.NavigableSet`, `java.util.Map` (and concrete subtypes with a `public` no-arg + constructor), `java.util.SortedMap`, `java.util.NavigableMap`, primitive arrays (e.g., + `int[]`, `char[][]`, etc.), object arrays (e.g., `String[]`, `Integer[][]`, etc.). * `{NullAndEmptySource}`: a _composed annotation_ that combines the functionality of `@NullSource` and `@EmptySource`. -If you need to supply multiple varying types of _blank_ strings to a parameterized test, -you can achieve that using <> -- -for example, `@ValueSource(strings = {"{nbsp}", "{nbsp}{nbsp}{nbsp}", "\t", "\n"})`. +If you need to supply multiple varying types of _blank_ strings to a parameterized +class or test, you can achieve that using +<> -- for example, +`@ValueSource(strings = {"{nbsp}", "{nbsp}{nbsp}{nbsp}", "\t", "\n"})`. You can also combine `@NullSource`, `@EmptySource`, and `@ValueSource` to test a wider range of `null`, _empty_, and _blank_ input. The following example demonstrates how to @@ -1567,7 +1723,7 @@ include::{testDir}/example/ParameterizedTestDemo.java[tags=EnumSource_example] ---- The annotation's `value` attribute is optional. When omitted, the declared type of the -first method parameter is used. The test will fail if it does not reference an enum type. +first parameter is used. The test will fail if it does not reference an enum type. Thus, the `value` attribute is required in the above example because the method parameter is declared as `TemporalUnit`, i.e. the interface implemented by `ChronoUnit`, which isn't an enum type. Changing the method parameter type to `ChronoUnit` allows you to omit the @@ -1636,14 +1792,14 @@ must always be `static`. Each factory method must generate a _stream_ of _arguments_, and each set of arguments within the stream will be provided as the physical arguments for individual invocations -of the annotated `@ParameterizedTest` method. Generally speaking this translates to a -`Stream` of `Arguments` (i.e., `Stream`); however, the actual concrete return -type can take on many forms. In this context, a "stream" is anything that JUnit can -reliably convert into a `Stream`, such as `Stream`, `DoubleStream`, `LongStream`, -`IntStream`, `Collection`, `Iterator`, `Iterable`, an array of objects, or an array of -primitives. The "arguments" within the stream can be supplied as an instance of -`Arguments`, an array of objects (e.g., `Object[]`), or a single value if the -parameterized test method accepts a single argument. +of the annotated `@ParameterizedClass` or `@ParameterizedTest`. Generally speaking this +translates to a `Stream` of `Arguments` (i.e., `Stream`); however, the actual +concrete return type can take on many forms. In this context, a "stream" is anything that +JUnit can reliably convert into a `Stream`, such as `Stream`, `DoubleStream`, +`LongStream`, `IntStream`, `Collection`, `Iterator`, `Iterable`, an array of objects, or +an array of primitives. The "arguments" within the stream can be supplied as an instance +of `Arguments`, an array of objects (e.g., `Object[]`), or a single value if the +parameterized class or test method accepts a single argument. If you only need a single parameter, you can return a `Stream` of instances of the parameter type as demonstrated in the following example. @@ -1653,8 +1809,9 @@ parameter type as demonstrated in the following example. include::{testDir}/example/ParameterizedTestDemo.java[tags=simple_MethodSource_example] ---- -If you do not explicitly provide a factory method name via `@MethodSource`, JUnit Jupiter -will search for a _factory_ method that has the same name as the current +For a `@ParameterizedClass`, providing a factory method name via `@MethodSource` is +mandatory. For a `@ParameterizedTest`, if you do not explicitly provide a factory method +name, JUnit Jupiter will search for a _factory_ method with the same name as the current `@ParameterizedTest` method by convention. This is demonstrated in the following example. [source,java,indent=0] @@ -1670,11 +1827,11 @@ supported as demonstrated by the following example. include::{testDir}/example/ParameterizedTestDemo.java[tags=primitive_MethodSource_example] ---- -If a parameterized test method declares multiple parameters, you need to return a -collection, stream, or array of `Arguments` instances or object arrays as shown below -(see the Javadoc for `{MethodSource}` for further details on supported return types). -Note that `arguments(Object...)` is a static factory method defined in the `Arguments` -interface. In addition, `Arguments.of(Object...)` may be used as an alternative to +If a parameterized class or test method declares multiple parameters, you need to return a +collection, stream, or array of `Arguments` instances or object arrays as shown below (see +the Javadoc for `{MethodSource}` for further details on supported return types). Note that +`arguments(Object...)` is a static factory method defined in the `Arguments` interface. In +addition, `Arguments.of(Object...)` may be used as an alternative to `arguments(Object...)`. [source,java,indent=0] @@ -1718,7 +1875,7 @@ Fields within the test class must be `static` unless the test class is annotated Each field must be able to supply a _stream_ of arguments, and each set of "arguments" within the "stream" will be provided as the physical arguments for individual invocations -of the annotated `@ParameterizedTest` method. +of the annotated `@ParameterizedClass` or `@ParameterizedTest`. In this context, a "stream" is anything that JUnit can reliably convert to a `Stream`; however, the actual concrete field type can take on many forms. Generally speaking this @@ -1726,8 +1883,8 @@ translates to a `Collection`, an `Iterable`, a `Supplier` of a stream (`Stream`, `DoubleStream`, `LongStream`, or `IntStream`), a `Supplier` of an `Iterator`, an array of objects, or an array of primitives. Each set of "arguments" within the "stream" can be supplied as an instance of `Arguments`, an array of objects (for example, `Object[]`, -`String[]`, etc.), or a single value if the parameterized test method accepts a single -argument. +`String[]`, etc.), or a single value if the parameterized class or test method accepts a +single argument. [WARNING] ==== @@ -1740,11 +1897,13 @@ these types, you can wrap it in a `Supplier` — for example, `Supplier> to `strict`. -To change this behavior for a single test, -use the `argumentCountValidation` attribute of the `@ParameterizedTest` annotation: +To change this behavior for a single parameterized class or test method, +use the `argumentCountValidation` attribute of the `@ParameterizedClass` or +`@ParameterizedTest` annotation: [source,java,indent=0] ---- @@ -2094,10 +2257,10 @@ include::{testDir}/example/ParameterizedTestDemo.java[tags=argument_count_valida JUnit Jupiter supports https://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html#jls-5.1.2[Widening Primitive -Conversion] for arguments supplied to a `@ParameterizedTest`. For example, a -parameterized test annotated with `@ValueSource(ints = { 1, 2, 3 })` can be declared to -accept not only an argument of type `int` but also an argument of type `long`, `float`, -or `double`. +Conversion] for arguments supplied to a `@ParameterizedClass` or `@ParameterizedTest`. +For example, a parameterized class or test method annotated with +`@ValueSource(ints = { 1, 2, 3 })` can be declared to accept not only an argument of type +`int` but also an argument of type `long`, `float`, or `double`. [[writing-tests-parameterized-tests-argument-conversion-implicit]] ===== Implicit Conversion @@ -2106,9 +2269,9 @@ To support use cases like `@CsvSource`, JUnit Jupiter provides a number of built implicit type converters. The conversion process depends on the declared type of each method parameter. -For example, if a `@ParameterizedTest` declares a parameter of type `TimeUnit` and the -actual type supplied by the declared source is a `String`, the string will be -automatically converted into the corresponding `TimeUnit` enum constant. +For example, if a `@ParameterizedClass` or `@ParameterizedTest` declares a parameter +of type `TimeUnit` and the actual type supplied by the declared source is a `String`, the +string will be automatically converted into the corresponding `TimeUnit` enum constant. [source,java,indent=0] ---- @@ -2239,9 +2402,10 @@ If you wish to implement a custom `ArgumentConverter` that also consumes an anno [[writing-tests-parameterized-tests-argument-aggregation]] ==== Argument Aggregation -By default, each _argument_ provided to a `@ParameterizedTest` method corresponds to a -single method parameter. Consequently, argument sources which are expected to supply a -large number of arguments can lead to large method signatures. +By default, each _argument_ provided to a `@ParameterizedClass` or `@ParameterizedTest` +corresponds to a single method parameter. Consequently, argument sources which are +expected to supply a large number of arguments can lead to large constructor or method +signatures, respectively. In such cases, an `{ArgumentsAccessor}` can be used instead of multiple parameters. Using this API, you can access the provided arguments through a single argument passed to your @@ -2262,16 +2426,16 @@ _An instance of `ArgumentsAccessor` is automatically injected into any parameter [[writing-tests-parameterized-tests-argument-aggregation-custom]] ===== Custom Aggregators -Apart from direct access to a `@ParameterizedTest` method's arguments using an -`ArgumentsAccessor`, JUnit Jupiter also supports the usage of custom, reusable -_aggregators_. +Apart from direct access to the arguments of a `@ParameterizedClass` or +`@ParameterizedTest` using an `ArgumentsAccessor`, JUnit Jupiter also supports the usage +of custom, reusable _aggregators_. To use a custom aggregator, implement the `{ArgumentsAggregator}` interface and register -it via the `@AggregateWith` annotation on a compatible parameter in the -`@ParameterizedTest` method. The result of the aggregation will then be provided as an -argument for the corresponding parameter when the parameterized test is invoked. Note -that an implementation of `ArgumentsAggregator` must be declared as either a top-level -class or as a `static` nested class. +it via the `@AggregateWith` annotation on a compatible parameter of the +`@ParameterizedClass` or `@ParameterizedTest`. The result of the aggregation will then be +provided as an argument for the corresponding parameter when the parameterized test is +invoked. Note that an implementation of `ArgumentsAggregator` must be declared as either a +top-level class or as a `static` nested class. [source,java,indent=0] ---- @@ -2284,8 +2448,8 @@ include::{testDir}/example/ParameterizedTestDemo.java[tags=ArgumentsAggregator_e ---- If you find yourself repeatedly declaring `@AggregateWith(MyTypeAggregator.class)` for -multiple parameterized test methods across your codebase, you may wish to create a custom -_composed annotation_ such as `@CsvToMyType` that is meta-annotated with +multiple parameterized classes or methods across your codebase, you may wish to create a +custom _composed annotation_ such as `@CsvToMyType` that is meta-annotated with `@AggregateWith(MyTypeAggregator.class)`. The following example demonstrates this in action with a custom `@CsvToPerson` annotation. @@ -2303,14 +2467,15 @@ include::{testDir}/example/ParameterizedTestDemo.java[tags=ArgumentsAggregator_w [[writing-tests-parameterized-tests-display-names]] ==== Customizing Display Names -By default, the display name of a parameterized test invocation contains the invocation -index and the `String` representation of all arguments for that specific invocation. Each -argument is preceded by its parameter name (unless the argument is only available via an -`ArgumentsAccessor` or `ArgumentAggregator`), if the parameter name is present in the -bytecode (for Java, test code must be compiled with the `-parameters` compiler flag). +By default, the display name of a parameterized class or test invocation contains the +invocation index and the `String` representation of all arguments for that specific +invocation. Each argument is preceded by its parameter name (unless the argument is only +available via an `ArgumentsAccessor` or `ArgumentAggregator`), if the parameter name is +present in the bytecode (for Java, test code must be compiled with the `-parameters` +compiler flag; for Kotlin, with `-java-parameters`). However, you can customize invocation display names via the `name` attribute of the -`@ParameterizedTest` annotation like in the following example. +`@ParameterizedClass` or `@ParameterizedTest` annotation as in the following example. ====== [source,java,indent=0] @@ -2339,15 +2504,15 @@ The following placeholders are supported within custom display names. [cols="20,80"] |=== -| Placeholder | Description - -| `{displayName}` | the display name of the method -| `{index}` | the current invocation index (1-based) -| `{arguments}` | the complete, comma-separated arguments list -| `{argumentsWithNames}` | the complete, comma-separated arguments list with parameter names -| `{argumentSetName}` | the name of the argument set -| `{argumentSetNameOrArgumentsWithNames}` | `{argumentSetName}` or `{argumentsWithNames}`, depending on how the arguments are supplied -| `{0}`, `{1}`, ... | an individual argument +| Placeholder | Description + +| `\{displayName}` | the display name of the method +| `\{index}` | the current invocation index (1-based) +| `\{arguments}` | the complete, comma-separated arguments list +| `\{argumentsWithNames}` | the complete, comma-separated arguments list with parameter names +| `\{argumentSetName}` | the name of the argument set +| `\{argumentSetNameOrArgumentsWithNames}` | `\{argumentSetName}` or `\{argumentsWithNames}`, depending on how the arguments are supplied +| `\{0}`, `\{1}`, ... | an individual argument |=== NOTE: When including arguments in display names, their string representations are truncated @@ -2412,9 +2577,9 @@ Note that `argumentSet(String, Object...)` is a static factory method defined in `org.junit.jupiter.params.provider.Arguments` interface. ==== -If you'd like to set a default name pattern for all parameterized tests in your project, -you can declare the `junit.jupiter.params.displayname.default` configuration parameter in -the `junit-platform.properties` file as demonstrated in the following example (see +If you'd like to set a default name pattern for all parameterized classes and tests in +your project, you can declare the `junit.jupiter.params.displayname.default` configuration +parameter in the `junit-platform.properties` file as demonstrated in the following example (see <> for other options). [source,properties,indent=0] @@ -2422,16 +2587,20 @@ the `junit-platform.properties` file as demonstrated in the following example (s junit.jupiter.params.displayname.default = {index} ---- -The display name for a parameterized test is determined according to the following -precedence rules: +The display name for a parameterized class or test is determined according to the +following precedence rules: -1. `name` attribute in `@ParameterizedTest`, if present +1. `name` attribute in `@ParameterizedClass` or `@ParameterizedTest`, if present 2. value of the `junit.jupiter.params.displayname.default` configuration parameter, if present -3. `DEFAULT_DISPLAY_NAME` constant defined in `@ParameterizedTest` +3. `DEFAULT_DISPLAY_NAME` constant defined in + `org.junit.jupiter.params.ParameterizedInvocationConstants` [[writing-tests-parameterized-tests-lifecycle-interop]] ==== Lifecycle and Interoperability +[[writing-tests-parameterized-tests-lifecycle-interop-methods]] +===== Parameterized Tests + Each invocation of a parameterized test has the same lifecycle as a regular `@Test` method. For example, `@BeforeEach` methods will be executed before each invocation. Similar to <>, invocations will appear one by one in the @@ -2440,7 +2609,7 @@ methods within the same test class. You may use `ParameterResolver` extensions with `@ParameterizedTest` methods. However, method parameters that are resolved by argument sources need to come first in the -argument list. Since a test class may contain regular tests as well as parameterized +parameter list. Since a test class may contain regular tests as well as parameterized tests with different parameter lists, values from argument sources are not resolved for lifecycle methods (e.g. `@BeforeEach`) and test class constructors. @@ -2449,6 +2618,20 @@ lifecycle methods (e.g. `@BeforeEach`) and test class constructors. include::{testDir}/example/ParameterizedTestDemo.java[tags=ParameterResolver_example] ---- +[[writing-tests-parameterized-tests-lifecycle-interop-classes]] +===== Parameterized Classes + +Each invocation of a parameterized class has the same lifecycle as a regular test class. +For example, `@BeforeAll` methods will be executed _once_ before all invocations and +`@BeforeEach` methods will be executed before each _test method_ invocation. Similar to +<>, invocations will appear one by one in the test tree of an +IDE. + +You may use `ParameterResolver` extensions with `@ParameterizedClass` constructors. +However, if constructor injection is used, constructor parameters that are resolved by +argument sources need to come first in the parameter list. Values from argument sources +are not resolved for lifecycle methods (e.g. `@BeforeEach`). + [[writing-tests-container-templates]] === Container Templates @@ -2460,6 +2643,9 @@ Each invocation of a container template class behaves like the execution of a re class with full support for the same lifecycle callbacks and extensions. Please refer to <> for usage examples. +NOTE: <> are a built-in +specialization of container templates. + [[writing-tests-test-templates]] === Test Templates @@ -2471,8 +2657,9 @@ invocation of a test template method behaves like the execution of a regular `@T method with full support for the same lifecycle callbacks and extensions. Please refer to <> for usage examples. -NOTE: <> and <> are -built-in specializations of test templates. +NOTE: <> and +<> are built-in specializations of +test templates. [[writing-tests-dynamic-tests]] === Dynamic Tests diff --git a/documentation/src/test/java/example/ParameterizedClassDemo.java b/documentation/src/test/java/example/ParameterizedClassDemo.java new file mode 100644 index 000000000000..544a55ab6a48 --- /dev/null +++ b/documentation/src/test/java/example/ParameterizedClassDemo.java @@ -0,0 +1,150 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package example; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD; + +import java.time.Duration; +import java.util.Arrays; + +import example.util.StringUtils; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.params.Parameter; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +public class ParameterizedClassDemo { + + @Nested + // tag::first_example[] + @ParameterizedClass + @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" }) + class PalindromeTests { + + @Parameter + String candidate; + + @Test + void palindrome() { + assertTrue(StringUtils.isPalindrome(candidate)); + } + + @Test + void reversePalindrome() { + String reverseCandidate = new StringBuilder(candidate).reverse().toString(); + assertTrue(StringUtils.isPalindrome(reverseCandidate)); + } + } + // end::first_example[] + + @Nested + class ConstructorInjection { + @Nested + // tag::constructor_injection[] + @ParameterizedClass + @CsvSource({ "apple, 23", "banana, 42" }) + class FruitTests { + + final String fruit; + final int quantity; + + FruitTests(String fruit, int quantity) { + this.fruit = fruit; + this.quantity = quantity; + } + + @Test + void test() { + assertFruit(fruit); + assertQuantity(quantity); + } + + @Test + void anotherTest() { + // ... + } + } + // end::constructor_injection[] + } + + @Nested + class FieldInjection { + @Nested + // tag::field_injection[] + @ParameterizedClass + @CsvSource({ "apple, 23", "banana, 42" }) + class FruitTests { + + @Parameter(0) + String fruit; + + @Parameter(1) + int quantity; + + @Test + void test() { + assertFruit(fruit); + assertQuantity(quantity); + } + + @Test + void anotherTest() { + // ... + } + } + // end::field_injection[] + } + + @Nested + // tag::nested[] + @Execution(SAME_THREAD) + @ParameterizedClass + @ValueSource(strings = { "apple", "banana" }) + class FruitTests { + + @Parameter + String fruit; + + @Nested + @ParameterizedClass + @ValueSource(ints = { 23, 42 }) + class QuantityTests { + + @Parameter + int quantity; + + @ParameterizedTest + @ValueSource(strings = { "PT1H", "PT2H" }) + void test(Duration duration) { + assertFruit(fruit); + assertQuantity(quantity); + assertFalse(duration.isNegative()); + } + } + } + // end::nested[] + + static void assertFruit(String fruit) { + assertTrue(Arrays.asList("apple", "banana", "cherry", "dewberry").contains(fruit), + () -> "not a fruit: " + fruit); + } + + static void assertQuantity(int quantity) { + assertTrue(quantity > 0); + } +} diff --git a/documentation/src/test/java/example/ParameterizedTestDemo.java b/documentation/src/test/java/example/ParameterizedTestDemo.java index 2a9d0b77fb78..50bf1ff882fa 100644 --- a/documentation/src/test/java/example/ParameterizedTestDemo.java +++ b/documentation/src/test/java/example/ParameterizedTestDemo.java @@ -44,18 +44,19 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestReporter; +import org.junit.jupiter.api.extension.AnnotatedElementContext; import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.params.ArgumentCountValidationMode; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.aggregator.AggregateWith; import org.junit.jupiter.params.aggregator.ArgumentsAccessor; -import org.junit.jupiter.params.aggregator.ArgumentsAggregator; +import org.junit.jupiter.params.aggregator.SimpleArgumentsAggregator; import org.junit.jupiter.params.converter.ConvertWith; import org.junit.jupiter.params.converter.JavaTimeConversionPattern; import org.junit.jupiter.params.converter.SimpleArgumentConverter; @@ -72,6 +73,7 @@ import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.params.support.ParameterDeclarations; @Execution(SAME_THREAD) class ParameterizedTestDemo { @@ -360,7 +362,8 @@ void testWithArgumentsSource(String argument) { public class MyArgumentsProvider implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { return Stream.of("apple", "banana").map(Arguments::of); } } @@ -383,7 +386,8 @@ public MyArgumentsProviderWithConstructorInjection(TestInfo testInfo) { } @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { return Stream.of(Arguments.of(testInfo.getDisplayName())); } } @@ -536,9 +540,10 @@ void testWithArgumentsAggregator(@AggregateWith(PersonAggregator.class) Person p // end::ArgumentsAggregator_example[] static // tag::ArgumentsAggregator_example_PersonAggregator[] - public class PersonAggregator implements ArgumentsAggregator { + public class PersonAggregator extends SimpleArgumentsAggregator { @Override - public Person aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) { + protected Person aggregateArguments(ArgumentsAccessor arguments, Class targetType, + AnnotatedElementContext context, int parameterIndex) { return new Person( arguments.getString(0), arguments.getString(1), @@ -628,7 +633,7 @@ static Stream otherProvider() { } // end::repeatable_annotations[] - @extensions.ExpectToFail + @Disabled("Fails prior to invoking the test method") // tag::argument_count_validation[] @ParameterizedTest(argumentCountValidation = ArgumentCountValidationMode.STRICT) @CsvSource({ "42, -666" }) diff --git a/documentation/src/test/java21/example/ParameterizedRecordDemo.java b/documentation/src/test/java21/example/ParameterizedRecordDemo.java new file mode 100644 index 000000000000..beb62b6bc81e --- /dev/null +++ b/documentation/src/test/java21/example/ParameterizedRecordDemo.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package example; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.provider.CsvSource; + +public class ParameterizedRecordDemo { + + @SuppressWarnings("JUnitMalformedDeclaration") + @Nested + // tag::example[] + @ParameterizedClass + @CsvSource({ "apple, 23", "banana, 42" }) + record FruitTests(String fruit, int quantity) { + + @Test + void test() { + assertFruit(fruit); + assertQuantity(quantity); + } + + @Test + void anotherTest() { + // ... + } + } + // end::example[] + + static void assertFruit(String fruit) { + assertTrue(Arrays.asList("apple", "banana", "cherry", "dewberry").contains(fruit)); + } + + static void assertQuantity(int quantity) { + assertTrue(quantity >= 0); + } +} diff --git a/documentation/src/test/resources/junit-platform.properties b/documentation/src/test/resources/junit-platform.properties index 6f2ed6e735fa..0f0255f62dbb 100644 --- a/documentation/src/test/resources/junit-platform.properties +++ b/documentation/src/test/resources/junit-platform.properties @@ -2,3 +2,5 @@ junit.jupiter.execution.parallel.enabled=true junit.jupiter.execution.parallel.mode.default=concurrent junit.jupiter.execution.parallel.config.strategy=fixed junit.jupiter.execution.parallel.config.fixed.parallelism=6 + +junit.platform.stacktrace.pruning.enabled=false diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.kotlin-library-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.kotlin-library-conventions.gradle.kts index 046cf094dfad..73680fcf6e6d 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.kotlin-library-conventions.gradle.kts +++ b/gradle/plugins/common/src/main/kotlin/junitbuild.kotlin-library-conventions.gradle.kts @@ -27,6 +27,7 @@ afterEvaluate { tasks { withType().configureEach { compilerOptions.jvmTarget = JvmTarget.fromTarget(extension.mainJavaVersion.toString()) + compilerOptions.javaParameters = true } named("compileTestKotlin") { compilerOptions.jvmTarget = JvmTarget.fromTarget(extension.testJavaVersion.toString()) diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java index 0077d5d7f512..ea4f217a24d5 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java @@ -325,8 +325,8 @@ protected final TestInstances instantiateTestClass(Optional outer Object instance = this.testInstanceFactory != null // ? invokeTestInstanceFactory(outerInstance, extensionContext) // : invokeTestClassConstructor(outerInstance, registry, extensionContext); - return outerInstances.map(instances -> DefaultTestInstances.of(instances, instance)).orElse( - DefaultTestInstances.of(instance)); + return outerInstances.map(instances -> DefaultTestInstances.of(instances, instance)) // + .orElse(DefaultTestInstances.of(instance)); } private Object invokeTestInstanceFactory(Optional outerInstance, diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateInvocationTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateInvocationTestDescriptor.java index e24fa1b53129..99cfdf2658d8 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateInvocationTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ContainerTemplateInvocationTestDescriptor.java @@ -12,6 +12,7 @@ import static org.apiguardian.api.API.Status.INTERNAL; import static org.junit.jupiter.engine.extension.MutableExtensionRegistry.createRegistryFrom; +import static org.junit.jupiter.engine.support.JupiterThrowableCollectorFactory.createThrowableCollector; import java.util.List; import java.util.Set; @@ -29,6 +30,7 @@ import org.junit.jupiter.engine.extension.MutableExtensionRegistry; import org.junit.platform.engine.TestSource; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.hierarchical.ThrowableCollector; /** * @since 5.13 @@ -102,11 +104,6 @@ public Function> getResou // --- Node ---------------------------------------------------------------- - @Override - public SkipResult shouldBeSkipped(JupiterEngineExecutionContext context) { - return SkipResult.doNotSkip(); - } - @Override public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext context) { MutableExtensionRegistry registry = context.getExtensionRegistry(); @@ -119,13 +116,21 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte } ExtensionContext extensionContext = new ContainerTemplateInvocationExtensionContext( context.getExtensionContext(), context.getExecutionListener(), this, context.getConfiguration(), registry); - this.invocationContext.prepareInvocation(extensionContext); + ThrowableCollector throwableCollector = createThrowableCollector(); + throwableCollector.execute(() -> this.invocationContext.prepareInvocation(extensionContext)); return context.extend() // - .withExtensionContext(extensionContext) // .withExtensionRegistry(registry) // + .withExtensionContext(extensionContext) // + .withThrowableCollector(throwableCollector) // .build(); } + @Override + public SkipResult shouldBeSkipped(JupiterEngineExecutionContext context) { + context.getThrowableCollector().assertEmpty(); + return SkipResult.doNotSkip(); + } + @Override public JupiterEngineExecutionContext execute(JupiterEngineExecutionContext context, DynamicTestExecutor dynamicTestExecutor) throws Exception { diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java index 27344fc499f9..01fb5c24be18 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java @@ -189,7 +189,7 @@ public Set getExclusiveResources() { } @Override - public SkipResult shouldBeSkipped(JupiterEngineExecutionContext context) throws Exception { + public SkipResult shouldBeSkipped(JupiterEngineExecutionContext context) { context.getThrowableCollector().assertEmpty(); ConditionEvaluationResult evaluationResult = conditionEvaluator.evaluate(context.getExtensionRegistry(), context.getConfiguration(), context.getExtensionContext()); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java index 59cf610de724..d266e33e9cf6 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java @@ -117,7 +117,6 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte ThrowableCollector throwableCollector = createThrowableCollector(); MethodExtensionContext extensionContext = new MethodExtensionContext(context.getExtensionContext(), context.getExecutionListener(), this, context.getConfiguration(), registry, throwableCollector); - throwableCollector.execute(() -> prepareExtensionContext(extensionContext)); // @formatter:off JupiterEngineExecutionContext newContext = context.extend() .withExtensionRegistry(registry) @@ -128,6 +127,7 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte throwableCollector.execute(() -> { TestInstances testInstances = newContext.getTestInstancesProvider().getTestInstances(newContext); extensionContext.setTestInstances(testInstances); + prepareExtensionContext(extensionContext); }); return newContext; } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ParameterResolutionUtils.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ParameterResolutionUtils.java index 65a4ddc0d38d..b2b846949637 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ParameterResolutionUtils.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/ParameterResolutionUtils.java @@ -99,6 +99,7 @@ public static Object[] resolveParameters(Executable executable, Optional // Ensure that the outer instance is resolved as the first parameter if // the executable is a constructor for an inner class. if (outerInstance.isPresent()) { + Preconditions.condition(parameters[0].isImplicit(), "First parameter must be implicit"); values[0] = outerInstance.get(); start = 1; } @@ -114,6 +115,9 @@ public static Object[] resolveParameters(Executable executable, Optional private static Object resolveParameter(ParameterContext parameterContext, Executable executable, ExtensionContextSupplier extensionContext, ExtensionRegistry extensionRegistry) { + Preconditions.condition(!parameterContext.getParameter().isImplicit(), + () -> String.format("Parameter at index %d must not be implicit", parameterContext.getIndex())); + try { // @formatter:off List matchingResolvers = extensionRegistry.stream(ParameterResolver.class) diff --git a/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedTestNameFormatterBenchmarks.java b/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterBenchmarks.java similarity index 78% rename from junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedTestNameFormatterBenchmarks.java rename to junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterBenchmarks.java index d6c62b3fcc20..38bd5694b3d6 100644 --- a/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedTestNameFormatterBenchmarks.java +++ b/junit-jupiter-params/src/jmh/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterBenchmarks.java @@ -10,6 +10,9 @@ package org.junit.jupiter.params; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.DEFAULT_DISPLAY_NAME; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.DISPLAY_NAME_PLACEHOLDER; + import java.util.List; import java.util.stream.IntStream; @@ -28,7 +31,7 @@ @Fork(1) @Warmup(iterations = 1, time = 2) @Measurement(iterations = 3, time = 2) -public class ParameterizedTestNameFormatterBenchmarks { +public class ParameterizedInvocationNameFormatterBenchmarks { @Param({ "1", "2", "4", "10", "100", "1000" }) private int numberOfParameters; @@ -45,10 +48,9 @@ public void setUp() { @Benchmark public void formatTestNames(Blackhole blackhole) throws Exception { var method = TestCase.class.getDeclaredMethod("parameterizedTest", int.class); - var formatter = new ParameterizedTestNameFormatter( - ParameterizedTest.DISPLAY_NAME_PLACEHOLDER + " " + ParameterizedTest.DEFAULT_DISPLAY_NAME + " ({0})", - "displayName", new ParameterizedTestMethodContext(method, method.getAnnotation(ParameterizedTest.class)), - 512); + var formatter = new ParameterizedInvocationNameFormatter( + DISPLAY_NAME_PLACEHOLDER + " " + DEFAULT_DISPLAY_NAME + " ({0})", "displayName", + new ParameterizedTestContext(method, method.getAnnotation(ParameterizedTest.class)), 512); for (int i = 0; i < argumentsList.size(); i++) { Arguments arguments = argumentsList.get(i); blackhole.consume(formatter.format(i, EvaluatedArgumentSet.allOf(arguments))); diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidationMode.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidationMode.java index fe50db276cdf..2188d8a170b1 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidationMode.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidationMode.java @@ -14,39 +14,46 @@ import org.junit.jupiter.params.provider.ArgumentsSource; /** - * Enumeration of argument count validation modes for {@link ParameterizedTest @ParameterizedTest}. + * Enumeration of argument count validation modes for + * {@link ParameterizedClass @ParameterizedClass} and + * {@link ParameterizedTest @ParameterizedTest}. * - *

When an {@link ArgumentsSource} provides more arguments than declared by the test method, - * there might be a bug in the test method or the {@link ArgumentsSource}. - * By default, the additional arguments are ignored. - * {@link ArgumentCountValidationMode} allows you to control how additional arguments are handled. + *

When an {@link ArgumentsSource} provides more arguments than declared by + * the parameterized class or method, there might be a bug in the class/method + * or the {@link ArgumentsSource}. By default, the additional arguments are + * ignored. {@link ArgumentCountValidationMode} allows you to control how + * additional arguments are handled. * * @since 5.12 - * @see ParameterizedTest + * @see ParameterizedClass#argumentCountValidation() + * @see ParameterizedTest#argumentCountValidation() */ @API(status = API.Status.EXPERIMENTAL, since = "5.12") public enum ArgumentCountValidationMode { + /** * Use the default validation mode. * *

The default validation mode may be changed via the - * {@value ArgumentCountValidator#ARGUMENT_COUNT_VALIDATION_KEY} configuration parameter - * (see the User Guide for details on configuration parameters). + * {@value ArgumentCountValidator#ARGUMENT_COUNT_VALIDATION_KEY} + * configuration parameter (see the User Guide for details on configuration + * parameters). */ DEFAULT, /** * Use the "none" argument count validation mode. * - *

When there are more arguments provided than declared by the test method, - * these additional arguments are ignored. + *

When there are more arguments provided than declared by the + * parameterized class or method, these additional arguments are ignored. */ NONE, /** * Use the strict argument count validation mode. * - *

When there are more arguments provided than declared by the test method, this raises an error. + *

When there are more arguments provided than declared by the + * parameterized class or method, this raises an error. */ STRICT, } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java index 556543b35319..322aa34d557d 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ArgumentCountValidator.java @@ -10,56 +10,47 @@ package org.junit.jupiter.params; -import java.lang.reflect.Method; import java.util.Arrays; import java.util.Optional; import org.junit.jupiter.api.extension.ExtensionConfigurationException; import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.InvocationInterceptor; -import org.junit.jupiter.api.extension.ReflectiveInvocationContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; import org.junit.platform.commons.logging.Logger; import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.util.Preconditions; -class ArgumentCountValidator implements InvocationInterceptor { +class ArgumentCountValidator { + private static final Logger logger = LoggerFactory.getLogger(ArgumentCountValidator.class); static final String ARGUMENT_COUNT_VALIDATION_KEY = "junit.jupiter.params.argumentCountValidation"; - private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create( - ArgumentCountValidator.class); + private static final Namespace NAMESPACE = Namespace.create(ArgumentCountValidator.class); - private final ParameterizedTestMethodContext methodContext; + private final ParameterizedDeclarationContext declarationContext; private final EvaluatedArgumentSet arguments; - ArgumentCountValidator(ParameterizedTestMethodContext methodContext, EvaluatedArgumentSet arguments) { - this.methodContext = methodContext; + ArgumentCountValidator(ParameterizedDeclarationContext declarationContext, EvaluatedArgumentSet arguments) { + this.declarationContext = declarationContext; this.arguments = arguments; } - @Override - public void interceptTestTemplateMethod(InvocationInterceptor.Invocation invocation, - ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { - validateArgumentCount(extensionContext); - invocation.proceed(); - } - - private ExtensionContext.Store getStore(ExtensionContext context) { - return context.getRoot().getStore(NAMESPACE); - } - - private void validateArgumentCount(ExtensionContext extensionContext) { + void validate(ExtensionContext extensionContext) { ArgumentCountValidationMode argumentCountValidationMode = getArgumentCountValidationMode(extensionContext); switch (argumentCountValidationMode) { case DEFAULT: case NONE: return; case STRICT: - int testParamCount = extensionContext.getRequiredTestMethod().getParameterCount(); - int argumentsCount = arguments.getTotalLength(); - Preconditions.condition(testParamCount == argumentsCount, () -> String.format( - "Configuration error: the @ParameterizedTest has %s argument(s) but there were %s argument(s) provided.%nNote: the provided arguments are %s", - testParamCount, argumentsCount, Arrays.toString(arguments.getAllPayloads()))); + int consumedCount = this.declarationContext.getResolverFacade().determineConsumedArgumentCount( + this.arguments); + int totalCount = this.arguments.getTotalLength(); + Preconditions.condition(consumedCount == totalCount, () -> String.format( + "Configuration error: @%s consumes %s %s but there %s %s %s provided.%nNote: the provided arguments were %s", + this.declarationContext.getAnnotationName(), consumedCount, + pluralize(consumedCount, "parameter", "parameters"), pluralize(totalCount, "was", "were"), + totalCount, pluralize(totalCount, "argument", "arguments"), + Arrays.toString(this.arguments.getAllPayloads()))); break; default: throw new ExtensionConfigurationException( @@ -68,9 +59,9 @@ private void validateArgumentCount(ExtensionContext extensionContext) { } private ArgumentCountValidationMode getArgumentCountValidationMode(ExtensionContext extensionContext) { - ParameterizedTest parameterizedTest = methodContext.annotation; - if (parameterizedTest.argumentCountValidation() != ArgumentCountValidationMode.DEFAULT) { - return parameterizedTest.argumentCountValidation(); + ArgumentCountValidationMode mode = declarationContext.getArgumentCountValidationMode(); + if (mode != ArgumentCountValidationMode.DEFAULT) { + return mode; } else { return getArgumentCountValidationModeConfiguration(extensionContext); @@ -107,4 +98,12 @@ private ArgumentCountValidationMode getArgumentCountValidationModeConfiguration( } }, ArgumentCountValidationMode.class); } + + private static String pluralize(int count, String singular, String plural) { + return count == 1 ? singular : plural; + } + + private ExtensionContext.Store getStore(ExtensionContext context) { + return context.getRoot().getStore(NAMESPACE); + } } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ContainerTemplateConstructorParameterResolver.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ContainerTemplateConstructorParameterResolver.java new file mode 100644 index 000000000000..753003cb8ff8 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ContainerTemplateConstructorParameterResolver.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; + +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * @since 5.13 + */ +class ContainerTemplateConstructorParameterResolver extends ParameterizedInvocationParameterResolver { + + private final Class containerTemplateClass; + + ContainerTemplateConstructorParameterResolver(ParameterizedClassContext classContext, + EvaluatedArgumentSet arguments, int invocationIndex, ResolutionCache resolutionCache) { + super(classContext.getResolverFacade(), arguments, invocationIndex, resolutionCache); + this.containerTemplateClass = classContext.getAnnotatedElement(); + } + + @Override + protected boolean isSupportedOnConstructorOrMethod(Executable declaringExecutable, + ExtensionContext extensionContext) { + return declaringExecutable instanceof Constructor // + && this.containerTemplateClass.equals(declaringExecutable.getDeclaringClass()); + } + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ContainerTemplateInstanceFieldInjectingBeforeEachCallback.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ContainerTemplateInstanceFieldInjectingBeforeEachCallback.java new file mode 100644 index 000000000000..7b65a9db3b44 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ContainerTemplateInstanceFieldInjectingBeforeEachCallback.java @@ -0,0 +1,39 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +class ContainerTemplateInstanceFieldInjectingBeforeEachCallback implements BeforeEachCallback { + + private final ResolverFacade resolverFacade; + private final EvaluatedArgumentSet arguments; + private final int invocationIndex; + private final ResolutionCache resolutionCache; + + ContainerTemplateInstanceFieldInjectingBeforeEachCallback(ResolverFacade resolverFacade, + EvaluatedArgumentSet arguments, int invocationIndex, ResolutionCache resolutionCache) { + this.resolverFacade = resolverFacade; + this.arguments = arguments; + this.invocationIndex = invocationIndex; + this.resolutionCache = resolutionCache; + } + + @Override + public void beforeEach(ExtensionContext extensionContext) throws Exception { + extensionContext.getTestInstance() // + .ifPresent(testInstance -> this.resolverFacade // + .resolveAndInjectFields(testInstance, extensionContext, this.arguments, this.invocationIndex, + this.resolutionCache)); + } + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ContainerTemplateInstanceFieldInjectingPostProcessor.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ContainerTemplateInstanceFieldInjectingPostProcessor.java new file mode 100644 index 000000000000..9a2735d7bf59 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ContainerTemplateInstanceFieldInjectingPostProcessor.java @@ -0,0 +1,41 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestInstancePostProcessor; + +class ContainerTemplateInstanceFieldInjectingPostProcessor implements TestInstancePostProcessor { + + private final ResolverFacade resolverFacade; + private final EvaluatedArgumentSet arguments; + private final int invocationIndex; + private final ResolutionCache resolutionCache; + + ContainerTemplateInstanceFieldInjectingPostProcessor(ResolverFacade resolverFacade, EvaluatedArgumentSet arguments, + int invocationIndex, ResolutionCache resolutionCache) { + this.resolverFacade = resolverFacade; + this.arguments = arguments; + this.invocationIndex = invocationIndex; + this.resolutionCache = resolutionCache; + } + + @Override + public ExtensionContextScope getTestInstantiationExtensionContextScope(ExtensionContext rootContext) { + return ExtensionContextScope.TEST_METHOD; + } + + @Override + public void postProcessTestInstance(Object testInstance, ExtensionContext extensionContext) { + this.resolverFacade.resolveAndInjectFields(testInstance, extensionContext, this.arguments, this.invocationIndex, + this.resolutionCache); + } +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/EvaluatedArgumentSet.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/EvaluatedArgumentSet.java index ae9f0a4dc18e..623f262ddc91 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/EvaluatedArgumentSet.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/EvaluatedArgumentSet.java @@ -76,6 +76,10 @@ Object[] getConsumedPayloads() { return extractFromNamed(this.consumed, Named::getPayload); } + Object getConsumedPayload(int index) { + return extractFromNamed(this.consumed[index], Named::getPayload); + } + Optional getName() { return this.name; } @@ -96,8 +100,12 @@ private static Optional determineName(Arguments arguments) { private static Object[] extractFromNamed(Object[] arguments, Function, Object> mapper) { return Arrays.stream(arguments) // - .map(argument -> argument instanceof Named ? mapper.apply((Named) argument) : argument) // + .map(argument -> extractFromNamed(argument, mapper)) // .toArray(); } + private static Object extractFromNamed(Object argument, Function, Object> mapper) { + return argument instanceof Named ? mapper.apply((Named) argument) : argument; + } + } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ExecutableParameterDeclaration.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ExecutableParameterDeclaration.java new file mode 100644 index 000000000000..c9a175653bd3 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ExecutableParameterDeclaration.java @@ -0,0 +1,49 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import java.util.Optional; + +import org.junit.jupiter.params.support.ParameterDeclaration; + +/** + * @since 5.13 + */ +class ExecutableParameterDeclaration implements ParameterDeclaration { + + private final java.lang.reflect.Parameter parameter; + private final int index; + + ExecutableParameterDeclaration(java.lang.reflect.Parameter parameter, int index) { + this.parameter = parameter; + this.index = index; + } + + @Override + public java.lang.reflect.Parameter getAnnotatedElement() { + return this.parameter; + } + + @Override + public Class getParameterType() { + return this.parameter.getType(); + } + + @Override + public int getParameterIndex() { + return this.index; + } + + @Override + public Optional getParameterName() { + return this.parameter.isNamePresent() ? Optional.of(this.parameter.getName()) : Optional.empty(); + } +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/FieldParameterDeclaration.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/FieldParameterDeclaration.java new file mode 100644 index 000000000000..f493a0996e4c --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/FieldParameterDeclaration.java @@ -0,0 +1,57 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import java.lang.reflect.Field; +import java.util.Optional; + +import org.junit.jupiter.params.support.FieldContext; +import org.junit.jupiter.params.support.ParameterDeclaration; + +/** + * @since 5.13 + */ +class FieldParameterDeclaration implements ParameterDeclaration, FieldContext { + + private final Field field; + private final int index; + + FieldParameterDeclaration(Field field, int index) { + this.field = field; + this.index = index; + } + + @Override + public Field getField() { + return this.field; + } + + @Override + public Field getAnnotatedElement() { + return this.field; + } + + @Override + public Class getParameterType() { + return this.field.getType(); + } + + @Override + public int getParameterIndex() { + return index; + } + + @Override + public Optional getParameterName() { + return Optional.of(this.field.getName()); + } + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/Parameter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/Parameter.java new file mode 100644 index 000000000000..70325ff71746 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/Parameter.java @@ -0,0 +1,65 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; +import org.junit.jupiter.params.aggregator.AggregateWith; +import org.junit.jupiter.params.aggregator.ArgumentsAccessor; + +/** + * {@code @Parameter} is used to signal that a field in a + * {@code @ParameterizedClass} constitutes a parameter and marks it for + * field injection. + * + *

{@code @Parameter} may also be used as a meta-annotation in order to + * create a custom composed annotation that inherits the semantics of + * {@code @Parameter}. + * + * @since 5.13 + * @see ParameterizedClass + * @see ArgumentsAccessor + * @see AggregateWith + * @see org.junit.jupiter.params.converter.ArgumentConverter + * @see org.junit.jupiter.params.converter.ConvertWith + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD }) +@Documented +@API(status = EXPERIMENTAL, since = "5.13") +public @interface Parameter { + + /** + * Constant that indicates that the index of the parameter is unset. + */ + int UNSET_INDEX = -1; + + /** + * {@return the index of the parameter in the list of parameters} + * + *

Must be {@value #UNSET_INDEX} (the default) for aggregators, + * that is any field of type {@link ArgumentsAccessor} or any field + * annotated with {@link AggregateWith @AggregateWith}. + * + *

May be omitted if there's a single indexed parameter. + * Otherwise, must be unique among all indexed parameters of the + * parameterized class and its superclasses. + */ + int value() default UNSET_INDEX; + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClass.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClass.java new file mode 100644 index 000000000000..57ca9bc84045 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClass.java @@ -0,0 +1,239 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.ContainerTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.provider.ArgumentsSource; + +/** + * {@code @ParameterizedClass} is used to signal that the annotated class is + * a parameterized test class. + * + *

Arguments Providers and Sources

+ * + *

A {@code @ParameterizedClass} must specify at least one + * {@link org.junit.jupiter.params.provider.ArgumentsProvider ArgumentsProvider} + * via {@link org.junit.jupiter.params.provider.ArgumentsSource @ArgumentsSource} + * or a corresponding composed annotation (e.g., {@code @ValueSource}, + * {@code @CsvSource}, etc.). The provider is responsible for providing a + * {@link java.util.stream.Stream Stream} of + * {@link org.junit.jupiter.params.provider.Arguments Arguments} that will be + * used to invoke the parameterized class. + * + *

Field or Constructor Injection

+ * + *

The provided arguments can either be injected into fields annotated with + * {@link Parameter @Parameter} or passed to the unique constructor of the + * parameterized class. If a {@code @Parameter}-annotated field is declared in + * the parameterized class or one of its superclasses, field injection will be + * used. Otherwise, constructor injection will be used. + * + *

Constructor Injection

+ * + *

A {@code @ParameterizedClass} constructor may declare additional + * parameters at the end of its parameter list to be resolved by other + * {@link org.junit.jupiter.api.extension.ParameterResolver ParameterResolvers} + * (e.g., {@code TestInfo}, {@code TestReporter}, etc.). Specifically, such a + * constructor must declare formal parameters according to the following rules. + * + *

    + *
  1. Zero or more indexed parameters must be declared first.
  2. + *
  3. Zero or more aggregators must be declared next.
  4. + *
  5. Zero or more parameters supplied by other {@code ParameterResolver} + * implementations must be declared last.
  6. + *
+ * + *

In this context, an indexed parameter is an argument for a given + * index in the {@code Arguments} provided by an {@code ArgumentsProvider} that + * is passed as an argument to the parameterized class at the same index in + * the constructor's formal parameter list. An aggregator is any + * parameter of type + * {@link org.junit.jupiter.params.aggregator.ArgumentsAccessor ArgumentsAccessor} + * or any parameter annotated with + * {@link org.junit.jupiter.params.aggregator.AggregateWith @AggregateWith}. + * + *

Field injection

+ * + *

Fields annotated with {@code @Parameter} must be declared according to the + * following rules. + * + *

    + *
  1. Zero or more indexed parameters may be declared; each must have + * a unique index specified in its {@code @Parameter(index)} annotation. The + * index may be omitted if there is only one indexed parameter. If there are at + * least two indexed parameter declarations, there must be declarations for all + * indexes from 0 to the largest declared index.
  2. + *
  3. Zero or more aggregators may be declared; each without + * specifying an index in its {@code @Parameter} annotation.
  4. + *
  5. Zero or more other fields may be declared as usual as long as they're not + * annotated with {@code @Parameter}.
  6. + *
+ * + *

In this context, an indexed parameter is an argument for a given + * index in the {@code Arguments} provided by an {@code ArgumentsProvider} that + * is injected into a field annotated with {@code @Parameter(index)}. An + * aggregator is any {@code @Parameter}-annotated field of type + * {@link org.junit.jupiter.params.aggregator.ArgumentsAccessor ArgumentsAccessor} + * or any field annotated with + * {@link org.junit.jupiter.params.aggregator.AggregateWith @AggregateWith}. + * + *

Argument Conversion

+ * + *

{@code @Parameter}-annotated fields or constructor parameters may be + * annotated with + * {@link org.junit.jupiter.params.converter.ConvertWith @ConvertWith} + * or a corresponding composed annotation to specify an explicit + * {@link org.junit.jupiter.params.converter.ArgumentConverter ArgumentConverter}. + * Otherwise, JUnit Jupiter will attempt to perform an implicit + * conversion to the target type automatically (see the User Guide for further + * details). + * + *

Composed Annotations

+ * + *

{@code @ParameterizedClass} may also be used as a meta-annotation in + * order to create a custom composed annotation that inherits the + * semantics of {@code @ParameterizedClass}. + * + *

Inheritance

+ * + *

The {@code @ParameterizedClass} annotation is not inherited + * from superclasses but may be (re-)declared on a concrete parameterized + * class. {@code Parameter}-annotated fields from superclasses are detected and + * used for field injection as if they were declared on the concrete + * parameterized class. + * + * @since 5.13 + * @see Parameter + * @see ParameterizedTest + * @see org.junit.jupiter.params.provider.Arguments + * @see org.junit.jupiter.params.provider.ArgumentsProvider + * @see org.junit.jupiter.params.provider.ArgumentsSource + * @see org.junit.jupiter.params.provider.CsvFileSource + * @see org.junit.jupiter.params.provider.CsvSource + * @see org.junit.jupiter.params.provider.EnumSource + * @see org.junit.jupiter.params.provider.MethodSource + * @see org.junit.jupiter.params.provider.ValueSource + * @see org.junit.jupiter.params.aggregator.ArgumentsAccessor + * @see org.junit.jupiter.params.aggregator.AggregateWith + * @see org.junit.jupiter.params.converter.ArgumentConverter + * @see org.junit.jupiter.params.converter.ConvertWith + */ +@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@API(status = EXPERIMENTAL, since = "5.13") +@ContainerTemplate +@ExtendWith(ParameterizedClassExtension.class) +@SuppressWarnings("exports") +public @interface ParameterizedClass { + + /** + * The display name to be used for individual invocations of the + * parameterized class; never blank or consisting solely of whitespace. + * + *

Defaults to {@value ParameterizedInvocationNameFormatter#DEFAULT_DISPLAY_NAME}. + * + *

If the default display name flag + * ({@value ParameterizedInvocationNameFormatter#DEFAULT_DISPLAY_NAME}) + * is not overridden, JUnit will: + *

    + *
  • Look up the {@value ParameterizedInvocationNameFormatter#DISPLAY_NAME_PATTERN_KEY} + * configuration parameter and use it if available. The configuration + * parameter can be supplied via the {@code Launcher} API, build tools (e.g., + * Gradle and Maven), a JVM system property, or the JUnit Platform configuration + * file (i.e., a file named {@code junit-platform.properties} in the root of + * the class path). Consult the User Guide for further information.
  • + *
  • Otherwise, {@value ParameterizedInvocationConstants#DEFAULT_DISPLAY_NAME} will be used.
  • + *
+ * + *

Supported placeholders

+ *
    + *
  • {@value ParameterizedInvocationConstants#DISPLAY_NAME_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#INDEX_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#ARGUMENT_SET_NAME_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#ARGUMENTS_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#ARGUMENTS_WITH_NAMES_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER}
  • + *
  • "{0}", "{1}", etc.: an individual argument (0-based)
  • + *
+ * + *

For the latter, you may use {@link java.text.MessageFormat} patterns + * to customize formatting (for example, {@code {0,number,#.###}}). Please + * note that the original arguments are passed when formatting, regardless + * of any implicit or explicit argument conversions. + * + *

Note that + * {@value ParameterizedInvocationNameFormatter#DEFAULT_DISPLAY_NAME} is + * a flag rather than a placeholder. + * + * @see java.text.MessageFormat + */ + String name() default ParameterizedInvocationNameFormatter.DEFAULT_DISPLAY_NAME; + + /** + * Configure whether all arguments of the parameterized class that implement + * {@link AutoCloseable} will be closed after their corresponding + * invocation. + * + *

Defaults to {@code true}. + * + *

WARNING: if an argument that implements + * {@code AutoCloseable} is reused for multiple invocations of the same + * parameterized class, you must set {@code autoCloseArguments} to + * {@code false} to ensure that the argument is not closed between + * invocations. + * + * @see java.lang.AutoCloseable + */ + boolean autoCloseArguments() default true; + + /** + * Configure whether zero invocations are allowed for this + * parameterized class. + * + *

Set this attribute to {@code true} if the absence of invocations is + * expected in some cases and should not cause a test failure. + * + *

Defaults to {@code false}. + */ + boolean allowZeroInvocations() default false; + + /** + * Configure how the number of arguments provided by an + * {@link ArgumentsSource} are validated. + * + *

Defaults to {@link ArgumentCountValidationMode#DEFAULT}. + * + *

When an {@link ArgumentsSource} provides more arguments than declared + * by the parameterized class constructor or {@link Parameter}-annotated + * fields, there might be a bug in the parameterized class or the + * {@link ArgumentsSource}. By default, the additional arguments are + * ignored. {@code argumentCountValidation} allows you to control how + * additional arguments are handled. The default can be configured via the + * {@value ArgumentCountValidator#ARGUMENT_COUNT_VALIDATION_KEY} + * configuration parameter (see the User Guide for details on configuration + * parameters). + * + * @see ArgumentCountValidationMode + */ + ArgumentCountValidationMode argumentCountValidation() default ArgumentCountValidationMode.DEFAULT; + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java new file mode 100644 index 000000000000..bbac39648d4b --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassContext.java @@ -0,0 +1,111 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import static java.util.Collections.emptyList; +import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated; +import static org.junit.platform.commons.support.HierarchyTraversalMode.BOTTOM_UP; +import static org.junit.platform.commons.support.ReflectionSupport.findFields; +import static org.junit.platform.commons.util.ReflectionUtils.isRecordClass; + +import java.lang.reflect.Field; +import java.util.List; + +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ContainerTemplateInvocationContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.platform.commons.util.ReflectionUtils; + +class ParameterizedClassContext implements ParameterizedDeclarationContext { + + private final Class clazz; + private final ParameterizedClass annotation; + private final TestInstance.Lifecycle testInstanceLifecycle; + private final ResolverFacade resolverFacade; + private final InjectionType injectionType; + + ParameterizedClassContext(Class clazz, ParameterizedClass annotation, + TestInstance.Lifecycle testInstanceLifecycle) { + this.clazz = clazz; + this.annotation = annotation; + this.testInstanceLifecycle = testInstanceLifecycle; + + List fields = findParameterAnnotatedFields(clazz); + if (fields.isEmpty()) { + this.resolverFacade = ResolverFacade.create(ReflectionUtils.getDeclaredConstructor(clazz), annotation); + this.injectionType = InjectionType.CONSTRUCTOR; + } + else { + this.resolverFacade = ResolverFacade.create(clazz, fields); + this.injectionType = InjectionType.FIELDS; + } + } + + private static List findParameterAnnotatedFields(Class clazz) { + if (isRecordClass(clazz)) { + return emptyList(); + } + return findFields(clazz, it -> isAnnotated(it, Parameter.class), BOTTOM_UP); + } + + @Override + public ParameterizedClass getAnnotation() { + return this.annotation; + } + + @Override + public Class getAnnotatedElement() { + return this.clazz; + } + + @Override + public String getDisplayNamePattern() { + return this.annotation.name(); + } + + @Override + public boolean isAutoClosingArguments() { + return this.annotation.autoCloseArguments(); + } + + @Override + public boolean isAllowingZeroInvocations() { + return this.annotation.allowZeroInvocations(); + } + + @Override + public ArgumentCountValidationMode getArgumentCountValidationMode() { + return this.annotation.argumentCountValidation(); + } + + @Override + public ResolverFacade getResolverFacade() { + return this.resolverFacade; + } + + @Override + public ContainerTemplateInvocationContext createInvocationContext(ParameterizedInvocationNameFormatter formatter, + Arguments arguments, int invocationIndex) { + return new ParameterizedClassInvocationContext(this, formatter, arguments, invocationIndex); + } + + TestInstance.Lifecycle getTestInstanceLifecycle() { + return testInstanceLifecycle; + } + + InjectionType getInjectionType() { + return injectionType; + } + + enum InjectionType { + CONSTRUCTOR, FIELDS + } +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassExtension.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassExtension.java new file mode 100644 index 000000000000..b87e4d37d8d0 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassExtension.java @@ -0,0 +1,135 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.junit.jupiter.params.ParameterizedClassContext.InjectionType.CONSTRUCTOR; +import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.util.Optional; +import java.util.stream.Stream; + +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ContainerTemplateInvocationContext; +import org.junit.jupiter.api.extension.ContainerTemplateInvocationContextProvider; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ExtensionContext.Store; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.platform.commons.JUnitException; +import org.junit.platform.commons.PreconditionViolationException; + +/** + * @since 5.13 + */ +class ParameterizedClassExtension extends ParameterizedInvocationContextProvider + implements ContainerTemplateInvocationContextProvider, ParameterResolver { + + private static final String DECLARATION_CONTEXT_KEY = "context"; + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + + // This method always returns `false` because it is not intended to be used as a parameter resolver. + // Instead, it is used to provide a better error message when `TestInstance.Lifecycle.PER_CLASS` is + // attempted to be combined with constructor injection of parameters. + + if (isDeclaredOnTestClassConstructor(parameterContext, extensionContext)) { + validateAndStoreClassContext(extensionContext); + } + + return false; + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + + // Should never be called (see comment above). + + throw new JUnitException("Unexpected call to resolveParameter"); + } + + @Override + public boolean supportsContainerTemplate(ExtensionContext extensionContext) { + return validateAndStoreClassContext(extensionContext); + } + + @Override + public Stream provideContainerTemplateInvocationContexts( + ExtensionContext extensionContext) { + + return provideInvocationContexts(extensionContext, getDeclarationContext(extensionContext)); + } + + @Override + public boolean mayReturnZeroContainerTemplateInvocationContexts(ExtensionContext extensionContext) { + return getDeclarationContext(extensionContext).isAllowingZeroInvocations(); + } + + private static boolean isDeclaredOnTestClassConstructor(ParameterContext parameterContext, + ExtensionContext extensionContext) { + + Executable declaringExecutable = parameterContext.getDeclaringExecutable(); + return declaringExecutable instanceof Constructor // + && declaringExecutable.getDeclaringClass().equals(extensionContext.getTestClass().orElse(null)); + } + + private boolean validateAndStoreClassContext(ExtensionContext extensionContext) { + + Store store = getStore(extensionContext); + if (store.get(DECLARATION_CONTEXT_KEY) != null) { + return true; + } + + Optional annotation = findAnnotation(extensionContext.getTestClass(), + ParameterizedClass.class); + if (!annotation.isPresent()) { + return false; + } + + store.put(DECLARATION_CONTEXT_KEY, + createClassContext(extensionContext, extensionContext.getRequiredTestClass(), annotation.get())); + + return true; + } + + private static ParameterizedClassContext createClassContext(ExtensionContext extensionContext, Class testClass, + ParameterizedClass annotation) { + + TestInstance.Lifecycle lifecycle = extensionContext.getTestInstanceLifecycle() // + .orElseThrow(() -> new PreconditionViolationException("TestInstance.Lifecycle not present")); + + ParameterizedClassContext classContext = new ParameterizedClassContext(testClass, annotation, lifecycle); + + if (lifecycle == PER_CLASS && classContext.getInjectionType() == CONSTRUCTOR) { + throw new PreconditionViolationException( + "Constructor injection is not supported for @ParameterizedClass classes with @TestInstance(Lifecycle.PER_CLASS)"); + } + + return classContext; + } + + private ParameterizedClassContext getDeclarationContext(ExtensionContext extensionContext) { + return getStore(extensionContext)// + .get(DECLARATION_CONTEXT_KEY, ParameterizedClassContext.class); + } + + private Store getStore(ExtensionContext context) { + return context.getStore(Namespace.create(ParameterizedClassExtension.class, context.getRequiredTestClass())); + } + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java new file mode 100644 index 000000000000..5177aead98ee --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedClassInvocationContext.java @@ -0,0 +1,80 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD; + +import java.util.List; + +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ContainerTemplateInvocationContext; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedClassContext.InjectionType; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.platform.commons.JUnitException; +import org.junit.platform.commons.util.Preconditions; + +class ParameterizedClassInvocationContext extends ParameterizedInvocationContext + implements ContainerTemplateInvocationContext { + + private final ResolutionCache resolutionCache = ResolutionCache.enabled(); + + ParameterizedClassInvocationContext(ParameterizedClassContext classContext, + ParameterizedInvocationNameFormatter formatter, Arguments arguments, int invocationIndex) { + super(classContext, formatter, arguments, invocationIndex); + } + + @Override + public String getDisplayName(int invocationIndex) { + return super.getDisplayName(invocationIndex); + } + + @Override + public List getAdditionalExtensions() { + InjectionType injectionType = this.declarationContext.getInjectionType(); + switch (injectionType) { + case CONSTRUCTOR: + return singletonList(createExtensionForConstructorInjection()); + case FIELDS: + return singletonList(createExtensionForFieldInjection()); + } + throw new JUnitException("Unsupported injection type: " + injectionType); + } + + private ContainerTemplateConstructorParameterResolver createExtensionForConstructorInjection() { + Preconditions.condition(this.declarationContext.getTestInstanceLifecycle() == PER_METHOD, + "Constructor injection is only supported for lifecycle PER_METHOD"); + return new ContainerTemplateConstructorParameterResolver(this.declarationContext, this.arguments, + this.invocationIndex, this.resolutionCache); + } + + private Extension createExtensionForFieldInjection() { + ResolverFacade resolverFacade = this.declarationContext.getResolverFacade(); + TestInstance.Lifecycle lifecycle = this.declarationContext.getTestInstanceLifecycle(); + switch (lifecycle) { + case PER_CLASS: + return new ContainerTemplateInstanceFieldInjectingBeforeEachCallback(resolverFacade, this.arguments, + this.invocationIndex, this.resolutionCache); + case PER_METHOD: + return new ContainerTemplateInstanceFieldInjectingPostProcessor(resolverFacade, this.arguments, + this.invocationIndex, this.resolutionCache); + } + throw new JUnitException("Unsupported lifecycle: " + lifecycle); + } + + @Override + public void prepareInvocation(ExtensionContext context) { + super.prepareInvocation(context); + } + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedDeclarationContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedDeclarationContext.java new file mode 100644 index 000000000000..aa532646db6f --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedDeclarationContext.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; + +import org.junit.jupiter.params.provider.Arguments; + +/** + * @since 5.13 + */ +interface ParameterizedDeclarationContext { + + Annotation getAnnotation(); + + AnnotatedElement getAnnotatedElement(); + + String getDisplayNamePattern(); + + boolean isAutoClosingArguments(); + + boolean isAllowingZeroInvocations(); + + ArgumentCountValidationMode getArgumentCountValidationMode(); + + default String getAnnotationName() { + return getAnnotation().annotationType().getSimpleName(); + } + + ResolverFacade getResolverFacade(); + + C createInvocationContext(ParameterizedInvocationNameFormatter formatter, Arguments arguments, int invocationIndex); + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationConstants.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationConstants.java new file mode 100644 index 000000000000..04eff295b1a2 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationConstants.java @@ -0,0 +1,131 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import static org.apiguardian.api.API.Status.MAINTAINED; + +import org.apiguardian.api.API; + +/** + * Constants for the use with the + * {@link ParameterizedClass @ParameterizedClass} and + * {@link ParameterizedTest @ParameterizedTest} annotations. + * + * @since 5.13 + */ +@API(status = MAINTAINED, since = "5.13") +public class ParameterizedInvocationConstants { + + /** + * Placeholder for the {@linkplain org.junit.jupiter.api.TestInfo#getDisplayName + * display name} of a {@code @ParameterizedTest} method: {displayName} + * + * @since 5.3 + * @see ParameterizedClass#name() + * @see ParameterizedTest#name() + */ + public static final String DISPLAY_NAME_PLACEHOLDER = "{displayName}"; + + /** + * Placeholder for the current invocation index of a {@code @ParameterizedTest} + * method (1-based): {index} + * + * @since 5.3 + * @see ParameterizedClass#name() + * @see ParameterizedTest#name() + * @see #DEFAULT_DISPLAY_NAME + */ + public static final String INDEX_PLACEHOLDER = "{index}"; + + /** + * Placeholder for the complete, comma-separated arguments list of the + * current invocation of a {@code @ParameterizedTest} method: + * {arguments} + * + * @since 5.3 + * @see ParameterizedClass#name() + * @see ParameterizedTest#name() + */ + public static final String ARGUMENTS_PLACEHOLDER = "{arguments}"; + + /** + * Placeholder for the complete, comma-separated named arguments list + * of the current invocation of a {@code @ParameterizedTest} method: + * {argumentsWithNames} + * + *

Argument names will be retrieved via the {@link java.lang.reflect.Parameter#getName()} + * API if the byte code contains parameter names — for example, if + * the code was compiled with the {@code -parameters} command line argument + * for {@code javac}. + * + * @since 5.6 + * @see ParameterizedClass#name() + * @see ParameterizedTest#name() + * @see #ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER + */ + public static final String ARGUMENTS_WITH_NAMES_PLACEHOLDER = "{argumentsWithNames}"; + + /** + * Placeholder for the name of the argument set for the current invocation + * of a {@code @ParameterizedTest} method: {argumentSetName}. + * + *

This placeholder can be used when the current set of arguments was created via + * {@link org.junit.jupiter.params.provider.Arguments#argumentSet(String, Object...) + * argumentSet()}. + * + * @since 5.11 + * @see ParameterizedClass#name() + * @see ParameterizedTest#name() + * @see #ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER + * @see org.junit.jupiter.params.provider.Arguments#argumentSet(String, Object...) + */ + @API(status = EXPERIMENTAL, since = "5.11") + public static final String ARGUMENT_SET_NAME_PLACEHOLDER = "{argumentSetName}"; + + /** + * Placeholder for either {@link #ARGUMENT_SET_NAME_PLACEHOLDER} or + * {@link #ARGUMENTS_WITH_NAMES_PLACEHOLDER}, depending on whether the + * current set of arguments was created via + * {@link org.junit.jupiter.params.provider.Arguments#argumentSet(String, Object...) + * argumentSet()}: {argumentSetNameOrArgumentsWithNames}. + * + * @since 5.11 + * @see ParameterizedClass#name() + * @see ParameterizedTest#name() + * @see #ARGUMENT_SET_NAME_PLACEHOLDER + * @see #ARGUMENTS_WITH_NAMES_PLACEHOLDER + * @see #DEFAULT_DISPLAY_NAME + * @see org.junit.jupiter.params.provider.Arguments#argumentSet(String, Object...) + */ + @API(status = EXPERIMENTAL, since = "5.11") + public static final String ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER = "{argumentSetNameOrArgumentsWithNames}"; + + /** + * Default display name pattern for the current invocation of a + * {@code @ParameterizedTest} method: {@value} + * + *

Note that the default pattern does not include the + * {@linkplain #DISPLAY_NAME_PLACEHOLDER display name} of the + * {@code @ParameterizedTest} method. + * + * @since 5.3 + * @see ParameterizedClass#name() + * @see ParameterizedTest#name() + * @see #DISPLAY_NAME_PLACEHOLDER + * @see #INDEX_PLACEHOLDER + * @see #ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER + */ + public static final String DEFAULT_DISPLAY_NAME = ParameterizedInvocationNameFormatter.DEFAULT_DISPLAY_NAME_PATTERN; + + private ParameterizedInvocationConstants() { + } +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java new file mode 100644 index 000000000000..88285f29f9f6 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.params.provider.Arguments; + +class ParameterizedInvocationContext> { + + private static final Namespace NAMESPACE = Namespace.create(ParameterizedTestInvocationContext.class); + + protected final T declarationContext; + private final ParameterizedInvocationNameFormatter formatter; + protected final EvaluatedArgumentSet arguments; + protected final int invocationIndex; + + ParameterizedInvocationContext(T declarationContext, ParameterizedInvocationNameFormatter formatter, + Arguments arguments, int invocationIndex) { + + this.declarationContext = declarationContext; + this.formatter = formatter; + ResolverFacade resolverFacade = this.declarationContext.getResolverFacade(); + this.arguments = EvaluatedArgumentSet.of(arguments, resolverFacade::determineConsumedArgumentLength); + this.invocationIndex = invocationIndex; + } + + public String getDisplayName(int invocationIndex) { + return this.formatter.format(invocationIndex, this.arguments); + } + + public void prepareInvocation(ExtensionContext context) { + if (this.declarationContext.isAutoClosingArguments()) { + registerAutoCloseableArgumentsInStoreForClosing(context); + } + new ArgumentCountValidator(this.declarationContext, this.arguments).validate(context); + } + + private void registerAutoCloseableArgumentsInStoreForClosing(ExtensionContext context) { + ExtensionContext.Store store = context.getStore(NAMESPACE); + AtomicInteger argumentIndex = new AtomicInteger(); + + Arrays.stream(this.arguments.getAllPayloads()) // + .filter(AutoCloseable.class::isInstance) // + .map(AutoCloseable.class::cast) // + .map(CloseableArgument::new) // + .forEach(closeable -> store.put(argumentIndex.incrementAndGet(), closeable)); + } + + private static class CloseableArgument implements ExtensionContext.Store.CloseableResource { + + private final AutoCloseable autoCloseable; + + CloseableArgument(AutoCloseable autoCloseable) { + this.autoCloseable = autoCloseable; + } + + @Override + public void close() throws Throwable { + this.autoCloseable.close(); + } + + } +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContextProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContextProvider.java new file mode 100644 index 000000000000..ffa324c13df6 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContextProvider.java @@ -0,0 +1,77 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import static org.junit.platform.commons.support.AnnotationSupport.findRepeatableAnnotations; + +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.support.AnnotationConsumerInitializer; +import org.junit.jupiter.params.support.ParameterDeclarations; +import org.junit.platform.commons.util.ExceptionUtils; +import org.junit.platform.commons.util.Preconditions; + +class ParameterizedInvocationContextProvider { + + protected Stream provideInvocationContexts(ExtensionContext extensionContext, + ParameterizedDeclarationContext declarationContext) { + + List argumentsSources = collectArgumentSources(declarationContext); + ParameterDeclarations parameters = declarationContext.getResolverFacade().getIndexedParameterDeclarations(); + ParameterizedInvocationNameFormatter formatter = ParameterizedInvocationNameFormatter.create(extensionContext, + declarationContext); + AtomicLong invocationCount = new AtomicLong(0); + + // @formatter:off + return argumentsSources + .stream() + .map(ArgumentsSource::value) + .map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentsProvider.class, clazz, extensionContext)) + .map(provider -> AnnotationConsumerInitializer.initialize(declarationContext.getAnnotatedElement(), provider)) + .flatMap(provider -> arguments(provider, parameters, extensionContext)) + .map(arguments -> { + invocationCount.incrementAndGet(); + return declarationContext.createInvocationContext(formatter, arguments, invocationCount.intValue()); + }) + .onClose(() -> + Preconditions.condition(invocationCount.get() > 0 || declarationContext.isAllowingZeroInvocations(), + () -> String.format("Configuration error: You must configure at least one set of arguments for this @%s", declarationContext.getAnnotationName()))); + // @formatter:on + } + + private static List collectArgumentSources(ParameterizedDeclarationContext declarationContext) { + List argumentsSources = findRepeatableAnnotations(declarationContext.getAnnotatedElement(), + ArgumentsSource.class); + + Preconditions.notEmpty(argumentsSources, + () -> String.format("Configuration error: You must configure at least one arguments source for this @%s", + declarationContext.getAnnotationName())); + + return argumentsSources; + } + + protected static Stream arguments(ArgumentsProvider provider, ParameterDeclarations parameters, + ExtensionContext context) { + try { + return provider.provideArguments(parameters, context); + } + catch (Exception e) { + throw ExceptionUtils.throwAsUncheckedException(e); + } + } + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestNameFormatter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatter.java similarity index 70% rename from junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestNameFormatter.java rename to junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatter.java index 754b1ad2ec66..1f1e9f103e65 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestNameFormatter.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatter.java @@ -11,12 +11,12 @@ package org.junit.jupiter.params; import static java.util.stream.Collectors.joining; -import static org.junit.jupiter.params.ParameterizedTest.ARGUMENTS_PLACEHOLDER; -import static org.junit.jupiter.params.ParameterizedTest.ARGUMENTS_WITH_NAMES_PLACEHOLDER; -import static org.junit.jupiter.params.ParameterizedTest.ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER; -import static org.junit.jupiter.params.ParameterizedTest.ARGUMENT_SET_NAME_PLACEHOLDER; -import static org.junit.jupiter.params.ParameterizedTest.DISPLAY_NAME_PLACEHOLDER; -import static org.junit.jupiter.params.ParameterizedTest.INDEX_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.ARGUMENTS_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.ARGUMENTS_WITH_NAMES_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.ARGUMENT_SET_NAME_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.DISPLAY_NAME_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.INDEX_PLACEHOLDER; import static org.junit.platform.commons.util.StringUtils.isNotBlank; import java.text.FieldPosition; @@ -35,20 +35,48 @@ import java.util.stream.IntStream; import org.junit.jupiter.api.extension.ExtensionConfigurationException; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.platform.commons.JUnitException; +import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.StringUtils; /** * @since 5.0 */ -class ParameterizedTestNameFormatter { +class ParameterizedInvocationNameFormatter { + + static final String DEFAULT_DISPLAY_NAME = "{default_display_name}"; + static final String DEFAULT_DISPLAY_NAME_PATTERN = "[" + INDEX_PLACEHOLDER + "] " + + ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER; + static final String DISPLAY_NAME_PATTERN_KEY = "junit.jupiter.params.displayname.default"; + static final String ARGUMENT_MAX_LENGTH_KEY = "junit.jupiter.params.displayname.argument.maxlength"; + + static ParameterizedInvocationNameFormatter create(ExtensionContext extensionContext, + ParameterizedDeclarationContext declarationContext) { + + String name = declarationContext.getDisplayNamePattern(); + String pattern = name.equals(DEFAULT_DISPLAY_NAME) + ? extensionContext.getConfigurationParameter(DISPLAY_NAME_PATTERN_KEY) // + .orElse(DEFAULT_DISPLAY_NAME_PATTERN) + : name; + pattern = Preconditions.notBlank(pattern.trim(), () -> String.format( + "Configuration error: @%s on %s must be declared with a non-empty name.", + declarationContext.getAnnotationName(), + declarationContext.getResolverFacade().getIndexedParameterDeclarations().getSourceElementDescription())); + + int argumentMaxLength = extensionContext.getConfigurationParameter(ARGUMENT_MAX_LENGTH_KEY, Integer::parseInt) // + .orElse(512); + + return new ParameterizedInvocationNameFormatter(pattern, extensionContext.getDisplayName(), declarationContext, + argumentMaxLength); + } private final PartialFormatter[] partialFormatters; - ParameterizedTestNameFormatter(String pattern, String displayName, ParameterizedTestMethodContext methodContext, - int argumentMaxLength) { + ParameterizedInvocationNameFormatter(String pattern, String displayName, + ParameterizedDeclarationContext declarationContext, int argumentMaxLength) { try { - this.partialFormatters = parse(pattern, displayName, methodContext, argumentMaxLength); + this.partialFormatters = parse(pattern, displayName, declarationContext, argumentMaxLength); } catch (Exception ex) { String message = "The display name pattern defined for the parameterized test is invalid. " @@ -78,11 +106,11 @@ private String formatSafely(int invocationIndex, EvaluatedArgumentSet arguments) return result.toString(); } - private PartialFormatter[] parse(String pattern, String displayName, ParameterizedTestMethodContext methodContext, - int argumentMaxLength) { + private PartialFormatter[] parse(String pattern, String displayName, + ParameterizedDeclarationContext declarationContext, int argumentMaxLength) { List result = new ArrayList<>(); - PartialFormatters formatters = createPartialFormatters(displayName, methodContext, argumentMaxLength); + PartialFormatters formatters = createPartialFormatters(displayName, declarationContext, argumentMaxLength); String unparsedSegment = pattern; while (isNotBlank(unparsedSegment)) { @@ -127,32 +155,36 @@ private static PartialFormatter determineNonPlaceholderFormatter(String segment, : (context, result) -> result.append(segment); } - private PartialFormatters createPartialFormatters(String displayName, ParameterizedTestMethodContext methodContext, - int argumentMaxLength) { + private PartialFormatters createPartialFormatters(String displayName, + ParameterizedDeclarationContext declarationContext, int argumentMaxLength) { PartialFormatter argumentsWithNamesFormatter = new CachingByArgumentsLengthPartialFormatter( - length -> new MessageFormatPartialFormatter(argumentsWithNamesPattern(length, methodContext), + length -> new MessageFormatPartialFormatter(argumentsWithNamesPattern(length, declarationContext), argumentMaxLength)); + PartialFormatter argumentSetNameFormatter = new ArgumentSetNameFormatter( + declarationContext.getAnnotationName()); + PartialFormatters formatters = new PartialFormatters(); formatters.put(INDEX_PLACEHOLDER, PartialFormatter.INDEX); formatters.put(DISPLAY_NAME_PLACEHOLDER, (context, result) -> result.append(displayName)); - formatters.put(ARGUMENT_SET_NAME_PLACEHOLDER, PartialFormatter.ARGUMENT_SET_NAME); + formatters.put(ARGUMENT_SET_NAME_PLACEHOLDER, argumentSetNameFormatter); formatters.put(ARGUMENTS_WITH_NAMES_PLACEHOLDER, argumentsWithNamesFormatter); formatters.put(ARGUMENTS_PLACEHOLDER, new CachingByArgumentsLengthPartialFormatter( length -> new MessageFormatPartialFormatter(argumentsPattern(length), argumentMaxLength))); formatters.put(ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER, (context, result) -> { PartialFormatter formatterToUse = context.argumentSetName.isPresent() // - ? PartialFormatter.ARGUMENT_SET_NAME // + ? argumentSetNameFormatter // : argumentsWithNamesFormatter; formatterToUse.append(context, result); }); return formatters; } - private static String argumentsWithNamesPattern(int length, ParameterizedTestMethodContext methodContext) { + private static String argumentsWithNamesPattern(int length, ParameterizedDeclarationContext declarationContext) { + ResolverFacade resolverFacade = declarationContext.getResolverFacade(); return IntStream.range(0, length) // - .mapToObj(index -> methodContext.getParameterName(index).map(name -> name + "=").orElse("") + "{" + .mapToObj(index -> resolverFacade.getParameterName(index).map(name -> name + "=").orElse("") + "{" + index + "}") // .collect(joining(", ")); } @@ -193,18 +225,28 @@ private interface PartialFormatter { PartialFormatter INDEX = (context, result) -> result.append(context.invocationIndex); - PartialFormatter ARGUMENT_SET_NAME = (context, result) -> { + void append(ArgumentsContext context, StringBuffer result); + + } + + private static class ArgumentSetNameFormatter implements PartialFormatter { + + private final String annotationName; + + ArgumentSetNameFormatter(String annotationName) { + this.annotationName = annotationName; + } + + @Override + public void append(ArgumentsContext context, StringBuffer result) { if (context.argumentSetName.isPresent()) { result.append(context.argumentSetName.get()); return; } - throw new ExtensionConfigurationException( - String.format("When the display name pattern for a @ParameterizedTest contains %s, " - + "the arguments must be supplied as an ArgumentSet.", - ARGUMENT_SET_NAME_PLACEHOLDER)); - }; - - void append(ArgumentsContext context, StringBuffer result); + throw new ExtensionConfigurationException(String.format( + "When the display name pattern for a @%s contains %s, the arguments must be supplied as an ArgumentSet.", + this.annotationName, ARGUMENT_SET_NAME_PLACEHOLDER)); + } } private static class MessageFormatPartialFormatter implements PartialFormatter { diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationParameterResolver.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationParameterResolver.java new file mode 100644 index 000000000000..84faf1971d23 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationParameterResolver.java @@ -0,0 +1,63 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import java.lang.reflect.Executable; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * @since 5.13 + */ +abstract class ParameterizedInvocationParameterResolver implements ParameterResolver { + + private final ResolverFacade resolverFacade; + private final EvaluatedArgumentSet arguments; + private final int invocationIndex; + private final ResolutionCache resolutionCache; + + ParameterizedInvocationParameterResolver(ResolverFacade resolverFacade, EvaluatedArgumentSet arguments, + int invocationIndex, ResolutionCache resolutionCache) { + + this.resolverFacade = resolverFacade; + this.arguments = arguments; + this.invocationIndex = invocationIndex; + this.resolutionCache = resolutionCache; + } + + @Override + public final ExtensionContextScope getTestInstantiationExtensionContextScope(ExtensionContext rootContext) { + return ExtensionContextScope.TEST_METHOD; + } + + @Override + public final boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + + return isSupportedOnConstructorOrMethod(parameterContext.getDeclaringExecutable(), extensionContext) // + && this.resolverFacade.isSupportedParameter(parameterContext, this.arguments); + + } + + @Override + public final Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + + return this.resolverFacade.resolve(parameterContext, extensionContext, this.arguments, this.invocationIndex, + this.resolutionCache); + } + + protected abstract boolean isSupportedOnConstructorOrMethod(Executable declaringExecutable, + ExtensionContext extensionContext); + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTest.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTest.java index ce16337b45d2..eb841a9bdc5b 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTest.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTest.java @@ -10,6 +10,7 @@ package org.junit.jupiter.params; +import static org.apiguardian.api.API.Status.DEPRECATED; import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; @@ -46,18 +47,18 @@ *

A {@code @ParameterizedTest} method may declare additional parameters at * the end of the method's parameter list to be resolved by other * {@link org.junit.jupiter.api.extension.ParameterResolver ParameterResolvers} - * (e.g., {@code TestInfo}, {@code TestReporter}, etc). Specifically, a + * (e.g., {@code TestInfo}, {@code TestReporter}, etc.). Specifically, a * parameterized test method must declare formal parameters according to the * following rules. * *

    - *
  1. Zero or more indexed arguments must be declared first.
  2. + *
  3. Zero or more indexed parameters must be declared first.
  4. *
  5. Zero or more aggregators must be declared next.
  6. - *
  7. Zero or more arguments supplied by other {@code ParameterResolver} + *
  8. Zero or more parameters supplied by other {@code ParameterResolver} * implementations must be declared last.
  9. *
* - *

In this context, an indexed argument is an argument for a given + *

In this context, an indexed parameter is an argument for a given * index in the {@code Arguments} provided by an {@code ArgumentsProvider} that * is passed as an argument to the parameterized method at the same index in the * method's formal parameter list. An aggregator is any parameter of type @@ -113,6 +114,7 @@ * implementation. * * @since 5.0 + * @see ParameterizedClass * @see org.junit.jupiter.params.provider.Arguments * @see org.junit.jupiter.params.provider.ArgumentsProvider * @see org.junit.jupiter.params.provider.ArgumentsSource @@ -136,127 +138,135 @@ public @interface ParameterizedTest { /** - * Placeholder for the {@linkplain org.junit.jupiter.api.TestInfo#getDisplayName - * display name} of a {@code @ParameterizedTest} method: {displayName} + * See {@link ParameterizedInvocationConstants#DISPLAY_NAME_PLACEHOLDER}. * * @since 5.3 * @see #name + * @deprecated Please reference + * {@link ParameterizedInvocationConstants#DISPLAY_NAME_PLACEHOLDER} + * instead. */ - String DISPLAY_NAME_PLACEHOLDER = "{displayName}"; + @API(status = DEPRECATED, since = "5.13") + @Deprecated + String DISPLAY_NAME_PLACEHOLDER = ParameterizedInvocationConstants.DISPLAY_NAME_PLACEHOLDER; /** - * Placeholder for the current invocation index of a {@code @ParameterizedTest} - * method (1-based): {index} + * See {@link ParameterizedInvocationConstants#INDEX_PLACEHOLDER}. * * @since 5.3 * @see #name - * @see #DEFAULT_DISPLAY_NAME + * @see ParameterizedInvocationConstants#DEFAULT_DISPLAY_NAME + * @deprecated Please reference + * {@link ParameterizedInvocationConstants#INDEX_PLACEHOLDER} instead. */ + @API(status = DEPRECATED, since = "5.13") + @Deprecated String INDEX_PLACEHOLDER = "{index}"; /** - * Placeholder for the complete, comma-separated arguments list of the - * current invocation of a {@code @ParameterizedTest} method: - * {arguments} + * See {@link ParameterizedInvocationConstants#ARGUMENTS_PLACEHOLDER}. * * @since 5.3 * @see #name + * @deprecated Please reference + * {@link ParameterizedInvocationConstants#ARGUMENTS_PLACEHOLDER} instead. */ + @API(status = DEPRECATED, since = "5.13") + @Deprecated String ARGUMENTS_PLACEHOLDER = "{arguments}"; /** - * Placeholder for the complete, comma-separated named arguments list - * of the current invocation of a {@code @ParameterizedTest} method: - * {argumentsWithNames} - * - *

Argument names will be retrieved via the {@link java.lang.reflect.Parameter#getName()} - * API if the byte code contains parameter names — for example, if - * the code was compiled with the {@code -parameters} command line argument - * for {@code javac}. + * See + * {@link ParameterizedInvocationConstants#ARGUMENTS_WITH_NAMES_PLACEHOLDER}. * * @since 5.6 * @see #name - * @see #ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER + * @see ParameterizedInvocationConstants#ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER + * @deprecated Please reference + * {@link ParameterizedInvocationConstants#ARGUMENTS_WITH_NAMES_PLACEHOLDER} + * instead. */ + @API(status = DEPRECATED, since = "5.13") + @Deprecated String ARGUMENTS_WITH_NAMES_PLACEHOLDER = "{argumentsWithNames}"; /** - * Placeholder for the name of the argument set for the current invocation - * of a {@code @ParameterizedTest} method: {argumentSetName}. - * - *

This placeholder can be used when the current set of arguments was created via - * {@link org.junit.jupiter.params.provider.Arguments#argumentSet(String, Object...) - * argumentSet()}. + * See + * {@link ParameterizedInvocationConstants#ARGUMENT_SET_NAME_PLACEHOLDER}. * * @since 5.11 * @see #name - * @see #ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER + * @see ParameterizedInvocationConstants#ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER * @see org.junit.jupiter.params.provider.Arguments#argumentSet(String, Object...) + * @deprecated Please reference + * {@link ParameterizedInvocationConstants#ARGUMENT_SET_NAME_PLACEHOLDER} + * instead. */ - @API(status = EXPERIMENTAL, since = "5.11") + @API(status = DEPRECATED, since = "5.13") + @Deprecated String ARGUMENT_SET_NAME_PLACEHOLDER = "{argumentSetName}"; /** - * Placeholder for either {@link #ARGUMENT_SET_NAME_PLACEHOLDER} or - * {@link #ARGUMENTS_WITH_NAMES_PLACEHOLDER}, depending on whether the - * current set of arguments was created via - * {@link org.junit.jupiter.params.provider.Arguments#argumentSet(String, Object...) - * argumentSet()}: {argumentSetNameOrArgumentsWithNames}. + * See + * {@link ParameterizedInvocationConstants#ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER}. * * @since 5.11 * @see #name - * @see #ARGUMENT_SET_NAME_PLACEHOLDER - * @see #ARGUMENTS_WITH_NAMES_PLACEHOLDER - * @see #DEFAULT_DISPLAY_NAME + * @see ParameterizedInvocationConstants#ARGUMENT_SET_NAME_PLACEHOLDER + * @see ParameterizedInvocationConstants#ARGUMENTS_WITH_NAMES_PLACEHOLDER + * @see ParameterizedInvocationConstants#DEFAULT_DISPLAY_NAME * @see org.junit.jupiter.params.provider.Arguments#argumentSet(String, Object...) + * @deprecated Please reference + * {@link ParameterizedInvocationConstants#ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER} + * instead. */ - @API(status = EXPERIMENTAL, since = "5.11") + @API(status = DEPRECATED, since = "5.13") + @Deprecated String ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER = "{argumentSetNameOrArgumentsWithNames}"; /** - * Default display name pattern for the current invocation of a - * {@code @ParameterizedTest} method: {@value} - * - *

Note that the default pattern does not include the - * {@linkplain #DISPLAY_NAME_PLACEHOLDER display name} of the - * {@code @ParameterizedTest} method. + * See + * {@link ParameterizedInvocationConstants#DEFAULT_DISPLAY_NAME}. * * @since 5.3 * @see #name - * @see #DISPLAY_NAME_PLACEHOLDER - * @see #INDEX_PLACEHOLDER - * @see #ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER + * @see ParameterizedInvocationConstants#DISPLAY_NAME_PLACEHOLDER + * @see ParameterizedInvocationConstants#INDEX_PLACEHOLDER + * @see ParameterizedInvocationConstants#ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER + * @deprecated Please reference + * {@link ParameterizedInvocationConstants#DEFAULT_DISPLAY_NAME} instead. */ - String DEFAULT_DISPLAY_NAME = "[" + INDEX_PLACEHOLDER + "] " - + ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER; + @API(status = DEPRECATED, since = "5.13") + @Deprecated + String DEFAULT_DISPLAY_NAME = ParameterizedInvocationConstants.DEFAULT_DISPLAY_NAME; /** * The display name to be used for individual invocations of the * parameterized test; never blank or consisting solely of whitespace. * - *

Defaults to {@value ParameterizedTestExtension#DEFAULT_DISPLAY_NAME}. + *

Defaults to {@value ParameterizedInvocationNameFormatter#DEFAULT_DISPLAY_NAME}. * *

If the default display name flag - * ({@value ParameterizedTestExtension#DEFAULT_DISPLAY_NAME}) + * ({@value ParameterizedInvocationNameFormatter#DEFAULT_DISPLAY_NAME}) * is not overridden, JUnit will: *

    - *
  • Look up the {@value ParameterizedTestExtension#DISPLAY_NAME_PATTERN_KEY} + *
  • Look up the {@value ParameterizedInvocationNameFormatter#DISPLAY_NAME_PATTERN_KEY} * configuration parameter and use it if available. The configuration * parameter can be supplied via the {@code Launcher} API, build tools (e.g., * Gradle and Maven), a JVM system property, or the JUnit Platform configuration * file (i.e., a file named {@code junit-platform.properties} in the root of * the class path). Consult the User Guide for further information.
  • - *
  • Otherwise, {@value #DEFAULT_DISPLAY_NAME} will be used.
  • + *
  • Otherwise, {@value ParameterizedInvocationConstants#DEFAULT_DISPLAY_NAME} will be used.
  • *
* *

Supported placeholders

*
    - *
  • {@value #DISPLAY_NAME_PLACEHOLDER}
  • - *
  • {@value #INDEX_PLACEHOLDER}
  • - *
  • {@value #ARGUMENT_SET_NAME_PLACEHOLDER}
  • - *
  • {@value #ARGUMENTS_PLACEHOLDER}
  • - *
  • {@value #ARGUMENTS_WITH_NAMES_PLACEHOLDER}
  • - *
  • {@value #ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#DISPLAY_NAME_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#INDEX_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#ARGUMENT_SET_NAME_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#ARGUMENTS_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#ARGUMENTS_WITH_NAMES_PLACEHOLDER}
  • + *
  • {@value ParameterizedInvocationConstants#ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER}
  • *
  • "{0}", "{1}", etc.: an individual argument (0-based)
  • *
* @@ -266,25 +276,25 @@ * of any implicit or explicit argument conversions. * *

Note that - * {@value ParameterizedTestExtension#DEFAULT_DISPLAY_NAME} is + * {@value ParameterizedInvocationNameFormatter#DEFAULT_DISPLAY_NAME} is * a flag rather than a placeholder. * * @see java.text.MessageFormat */ - String name() default ParameterizedTestExtension.DEFAULT_DISPLAY_NAME; + String name() default ParameterizedInvocationNameFormatter.DEFAULT_DISPLAY_NAME; /** - * Configure whether all arguments of the parameterized test that implement {@link AutoCloseable} - * will be closed after {@link org.junit.jupiter.api.AfterEach @AfterEach} methods - * and {@link org.junit.jupiter.api.extension.AfterEachCallback AfterEachCallback} - * extensions have been called for the current parameterized test invocation. + * Configure whether all arguments of the parameterized test that implement + * {@link AutoCloseable} will be closed after their corresponding + * invocation. * *

Defaults to {@code true}. * - *

WARNING: if an argument that implements {@code AutoCloseable} - * is reused for multiple invocations of the same parameterized test method, - * you must set {@code autoCloseArguments} to {@code false} to ensure that - * the argument is not closed between invocations. + *

WARNING: if an argument that implements + * {@code AutoCloseable} is reused for multiple invocations of the same + * parameterized test method, you must set {@code autoCloseArguments} to + * {@code false} to ensure that the argument is not closed between + * invocations. * * @since 5.8 * @see java.lang.AutoCloseable @@ -307,20 +317,24 @@ boolean allowZeroInvocations() default false; /** - * Configure how the number of arguments provided by an {@link ArgumentsSource} are validated. + * Configure how the number of arguments provided by an + * {@link ArgumentsSource} are validated. * *

Defaults to {@link ArgumentCountValidationMode#DEFAULT}. * - *

When an {@link ArgumentsSource} provides more arguments than declared by the test method, - * there might be a bug in the test method or the {@link ArgumentsSource}. - * By default, the additional arguments are ignored. - * {@code argumentCountValidation} allows you to control how additional arguments are handled. - * The default can be configured via the {@value ArgumentCountValidator#ARGUMENT_COUNT_VALIDATION_KEY} - * configuration parameter (see the User Guide for details on configuration parameters). + *

When an {@link ArgumentsSource} provides more arguments than declared + * by the parameterized test method, there might be a bug in the method or + * the {@link ArgumentsSource}. By default, the additional arguments are + * ignored. {@code argumentCountValidation} allows you to control how + * additional arguments are handled. The default can be configured via the + * {@value ArgumentCountValidator#ARGUMENT_COUNT_VALIDATION_KEY} + * configuration parameter (see the User Guide for details on configuration + * parameters). * * @since 5.12 * @see ArgumentCountValidationMode */ @API(status = EXPERIMENTAL, since = "5.12") ArgumentCountValidationMode argumentCountValidation() default ArgumentCountValidationMode.DEFAULT; + } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestContext.java new file mode 100644 index 000000000000..b716d84543a9 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestContext.java @@ -0,0 +1,78 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.extension.TestTemplateInvocationContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.platform.commons.util.Preconditions; + +/** + * Encapsulates access to the parameters of a parameterized test method and + * caches the converters and aggregators used to resolve them. + * + * @since 5.3 + */ +class ParameterizedTestContext implements ParameterizedDeclarationContext { + + private final Method method; + private final ParameterizedTest annotation; + private final ResolverFacade resolverFacade; + + ParameterizedTestContext(Method method, ParameterizedTest annotation) { + this.method = Preconditions.notNull(method, "method must not be null"); + this.annotation = Preconditions.notNull(annotation, "annotation must not be null"); + this.resolverFacade = ResolverFacade.create(method, annotation); + } + + @Override + public ParameterizedTest getAnnotation() { + return this.annotation; + } + + @Override + public Method getAnnotatedElement() { + return this.method; + } + + @Override + public String getDisplayNamePattern() { + return this.annotation.name(); + } + + @Override + public boolean isAutoClosingArguments() { + return this.annotation.autoCloseArguments(); + } + + @Override + public boolean isAllowingZeroInvocations() { + return this.annotation.allowZeroInvocations(); + } + + @Override + public ArgumentCountValidationMode getArgumentCountValidationMode() { + return this.annotation.argumentCountValidation(); + } + + @Override + public ResolverFacade getResolverFacade() { + return this.resolverFacade; + } + + @Override + public TestTemplateInvocationContext createInvocationContext(ParameterizedInvocationNameFormatter formatter, + Arguments arguments, int invocationIndex) { + return new ParameterizedTestInvocationContext(this, formatter, arguments, invocationIndex); + } + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java index 9390c1fd3827..56b29245dec0 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java @@ -11,58 +11,34 @@ package org.junit.jupiter.params; import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation; -import static org.junit.platform.commons.support.AnnotationSupport.findRepeatableAnnotations; -import java.lang.reflect.Method; -import java.util.List; import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Stream; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.ArgumentsProvider; -import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.support.AnnotationConsumerInitializer; -import org.junit.platform.commons.util.ExceptionUtils; -import org.junit.platform.commons.util.Preconditions; /** * @since 5.0 */ -class ParameterizedTestExtension implements TestTemplateInvocationContextProvider { +class ParameterizedTestExtension extends ParameterizedInvocationContextProvider + implements TestTemplateInvocationContextProvider { - static final String METHOD_CONTEXT_KEY = "context"; - static final String ARGUMENT_MAX_LENGTH_KEY = "junit.jupiter.params.displayname.argument.maxlength"; - static final String DEFAULT_DISPLAY_NAME = "{default_display_name}"; - static final String DISPLAY_NAME_PATTERN_KEY = "junit.jupiter.params.displayname.default"; + static final String DECLARATION_CONTEXT_KEY = "context"; @Override public boolean supportsTestTemplate(ExtensionContext context) { - if (!context.getTestMethod().isPresent()) { - return false; - } - - Method templateMethod = context.getTestMethod().get(); - Optional annotation = findAnnotation(templateMethod, ParameterizedTest.class); + Optional annotation = findAnnotation(context.getTestMethod(), ParameterizedTest.class); if (!annotation.isPresent()) { return false; } - ParameterizedTestMethodContext methodContext = new ParameterizedTestMethodContext(templateMethod, + ParameterizedTestContext methodContext = new ParameterizedTestContext(context.getRequiredTestMethod(), annotation.get()); - Preconditions.condition(methodContext.hasPotentiallyValidSignature(), - () -> String.format( - "@ParameterizedTest method [%s] declares formal parameters in an invalid order: " - + "argument aggregators must be declared after any indexed arguments " - + "and before any arguments resolved by another ParameterResolver.", - templateMethod.toGenericString())); - - getStore(context).put(METHOD_CONTEXT_KEY, methodContext); + getStore(context).put(DECLARATION_CONTEXT_KEY, methodContext); return true; } @@ -71,80 +47,21 @@ public boolean supportsTestTemplate(ExtensionContext context) { public Stream provideTestTemplateInvocationContexts( ExtensionContext extensionContext) { - ParameterizedTestMethodContext methodContext = getMethodContext(extensionContext); - ParameterizedTestNameFormatter formatter = createNameFormatter(extensionContext, methodContext); - AtomicLong invocationCount = new AtomicLong(0); - - List argumentsSources = findRepeatableAnnotations(methodContext.method, ArgumentsSource.class); - - Preconditions.notEmpty(argumentsSources, - "Configuration error: You must configure at least one arguments source for this @ParameterizedTest"); - - // @formatter:off - return argumentsSources - .stream() - .map(ArgumentsSource::value) - .map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentsProvider.class, clazz, extensionContext)) - .map(provider -> AnnotationConsumerInitializer.initialize(methodContext.method, provider)) - .flatMap(provider -> arguments(provider, extensionContext)) - .map(arguments -> { - invocationCount.incrementAndGet(); - return createInvocationContext(formatter, methodContext, arguments, invocationCount.intValue()); - }) - .onClose(() -> - Preconditions.condition(invocationCount.get() > 0 || methodContext.annotation.allowZeroInvocations(), - "Configuration error: You must configure at least one set of arguments for this @ParameterizedTest")); - // @formatter:on + return provideInvocationContexts(extensionContext, getDeclarationContext(extensionContext)); } @Override public boolean mayReturnZeroTestTemplateInvocationContexts(ExtensionContext extensionContext) { - ParameterizedTestMethodContext methodContext = getMethodContext(extensionContext); - return methodContext.annotation.allowZeroInvocations(); + return getDeclarationContext(extensionContext).isAllowingZeroInvocations(); } - private ParameterizedTestMethodContext getMethodContext(ExtensionContext extensionContext) { + private ParameterizedTestContext getDeclarationContext(ExtensionContext extensionContext) { return getStore(extensionContext)// - .get(METHOD_CONTEXT_KEY, ParameterizedTestMethodContext.class); + .get(DECLARATION_CONTEXT_KEY, ParameterizedTestContext.class); } private ExtensionContext.Store getStore(ExtensionContext context) { return context.getStore(Namespace.create(ParameterizedTestExtension.class, context.getRequiredTestMethod())); } - private TestTemplateInvocationContext createInvocationContext(ParameterizedTestNameFormatter formatter, - ParameterizedTestMethodContext methodContext, Arguments arguments, int invocationIndex) { - - return new ParameterizedTestInvocationContext(formatter, methodContext, arguments, invocationIndex); - } - - private ParameterizedTestNameFormatter createNameFormatter(ExtensionContext extensionContext, - ParameterizedTestMethodContext methodContext) { - - String name = methodContext.annotation.name(); - String pattern = name.equals(DEFAULT_DISPLAY_NAME) - ? extensionContext.getConfigurationParameter(DISPLAY_NAME_PATTERN_KEY) // - .orElse(ParameterizedTest.DEFAULT_DISPLAY_NAME) - : name; - pattern = Preconditions.notBlank(pattern.trim(), - () -> String.format( - "Configuration error: @ParameterizedTest on method [%s] must be declared with a non-empty name.", - methodContext.method)); - - int argumentMaxLength = extensionContext.getConfigurationParameter(ARGUMENT_MAX_LENGTH_KEY, Integer::parseInt) // - .orElse(512); - - return new ParameterizedTestNameFormatter(pattern, extensionContext.getDisplayName(), methodContext, - argumentMaxLength); - } - - protected static Stream arguments(ArgumentsProvider provider, ExtensionContext context) { - try { - return provider.provideArguments(context); - } - catch (Exception e) { - throw ExceptionUtils.throwAsUncheckedException(e); - } - } - } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestInvocationContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestInvocationContext.java index 8e94ec5ed5bb..6fc9c9aa4286 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestInvocationContext.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestInvocationContext.java @@ -10,83 +10,41 @@ package org.junit.jupiter.params; -import java.util.Arrays; +import static java.util.Collections.singletonList; + import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.ExtensionContext.Namespace; -import org.junit.jupiter.api.extension.ExtensionContext.Store; import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import org.junit.jupiter.params.provider.Arguments; /** * @since 5.0 */ -class ParameterizedTestInvocationContext implements TestTemplateInvocationContext { - - private static final Namespace NAMESPACE = Namespace.create(ParameterizedTestInvocationContext.class); - - private final ParameterizedTestNameFormatter formatter; - private final ParameterizedTestMethodContext methodContext; - private final EvaluatedArgumentSet arguments; - private final int invocationIndex; - - ParameterizedTestInvocationContext(ParameterizedTestNameFormatter formatter, - ParameterizedTestMethodContext methodContext, Arguments arguments, int invocationIndex) { +class ParameterizedTestInvocationContext extends ParameterizedInvocationContext + implements TestTemplateInvocationContext { - this.formatter = formatter; - this.methodContext = methodContext; - this.arguments = EvaluatedArgumentSet.of(arguments, this::determineConsumedArgumentCount); - this.invocationIndex = invocationIndex; + ParameterizedTestInvocationContext(ParameterizedTestContext methodContext, + ParameterizedInvocationNameFormatter formatter, Arguments arguments, int invocationIndex) { + super(methodContext, formatter, arguments, invocationIndex); } @Override public String getDisplayName(int invocationIndex) { - return this.formatter.format(invocationIndex, this.arguments); + return super.getDisplayName(invocationIndex); } @Override public List getAdditionalExtensions() { - return Arrays.asList( - new ParameterizedTestParameterResolver(this.methodContext, this.arguments, this.invocationIndex), - new ArgumentCountValidator(this.methodContext, this.arguments)); + return singletonList( // + new ParameterizedTestMethodParameterResolver(this.declarationContext, this.arguments, this.invocationIndex) // + ); } @Override public void prepareInvocation(ExtensionContext context) { - if (this.methodContext.annotation.autoCloseArguments()) { - Store store = context.getStore(NAMESPACE); - AtomicInteger argumentIndex = new AtomicInteger(); - - Arrays.stream(this.arguments.getAllPayloads()) // - .filter(AutoCloseable.class::isInstance) // - .map(AutoCloseable.class::cast) // - .map(CloseableArgument::new) // - .forEach(closeable -> store.put(argumentIndex.incrementAndGet(), closeable)); - } - } - - private int determineConsumedArgumentCount(int totalLength) { - return methodContext.hasAggregator() // - ? totalLength // - : Math.min(totalLength, methodContext.getParameterCount()); - } - - private static class CloseableArgument implements Store.CloseableResource { - - private final AutoCloseable autoCloseable; - - CloseableArgument(AutoCloseable autoCloseable) { - this.autoCloseable = autoCloseable; - } - - @Override - public void close() throws Throwable { - this.autoCloseable.close(); - } - + super.prepareInvocation(context); } } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodContext.java deleted file mode 100644 index 074b32a1b9cb..000000000000 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodContext.java +++ /dev/null @@ -1,282 +0,0 @@ -/* - * Copyright 2015-2025 the original author or authors. - * - * All rights reserved. This program and the accompanying materials are - * made available under the terms of the Eclipse Public License v2.0 which - * accompanies this distribution and is available at - * - * https://www.eclipse.org/legal/epl-v20.html - */ - -package org.junit.jupiter.params; - -import static org.junit.jupiter.params.ParameterizedTestMethodContext.ResolverType.AGGREGATOR; -import static org.junit.jupiter.params.ParameterizedTestMethodContext.ResolverType.CONVERTER; -import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated; - -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.ParameterContext; -import org.junit.jupiter.api.extension.ParameterResolutionException; -import org.junit.jupiter.params.aggregator.AggregateWith; -import org.junit.jupiter.params.aggregator.ArgumentsAccessor; -import org.junit.jupiter.params.aggregator.ArgumentsAggregator; -import org.junit.jupiter.params.aggregator.DefaultArgumentsAccessor; -import org.junit.jupiter.params.converter.ArgumentConverter; -import org.junit.jupiter.params.converter.ConvertWith; -import org.junit.jupiter.params.converter.DefaultArgumentConverter; -import org.junit.jupiter.params.support.AnnotationConsumerInitializer; -import org.junit.platform.commons.support.AnnotationSupport; -import org.junit.platform.commons.util.Preconditions; -import org.junit.platform.commons.util.StringUtils; - -/** - * Encapsulates access to the parameters of a parameterized test method and - * caches the converters and aggregators used to resolve them. - * - * @since 5.3 - */ -class ParameterizedTestMethodContext { - - final Method method; - final ParameterizedTest annotation; - - private final Parameter[] parameters; - private final Resolver[] resolvers; - private final List resolverTypes; - - ParameterizedTestMethodContext(Method method, ParameterizedTest annotation) { - this.method = Preconditions.notNull(method, "method must not be null"); - this.annotation = Preconditions.notNull(annotation, "annotation must not be null"); - this.parameters = method.getParameters(); - this.resolvers = new Resolver[this.parameters.length]; - this.resolverTypes = new ArrayList<>(this.parameters.length); - for (Parameter parameter : this.parameters) { - this.resolverTypes.add(isAggregator(parameter) ? AGGREGATOR : CONVERTER); - } - } - - /** - * Determine if the supplied {@link Parameter} is an aggregator (i.e., of - * type {@link ArgumentsAccessor} or annotated with {@link AggregateWith}). - * - * @return {@code true} if the parameter is an aggregator - */ - private static boolean isAggregator(Parameter parameter) { - return ArgumentsAccessor.class.isAssignableFrom(parameter.getType()) - || isAnnotated(parameter, AggregateWith.class); - } - - /** - * Determine if the {@link Method} represented by this context has a - * potentially valid signature (i.e., formal parameter - * declarations) with regard to aggregators. - * - *

This method takes a best-effort approach at enforcing the following - * policy for parameterized test methods that accept aggregators as arguments. - * - *

    - *
  1. zero or more indexed arguments come first.
  2. - *
  3. zero or more aggregators come next.
  4. - *
  5. zero or more arguments supplied by other {@code ParameterResolver} - * implementations come last.
  6. - *
- * - * @return {@code true} if the method has a potentially valid signature - */ - boolean hasPotentiallyValidSignature() { - int indexOfPreviousAggregator = -1; - for (int i = 0; i < getParameterCount(); i++) { - if (isAggregator(i)) { - if ((indexOfPreviousAggregator != -1) && (i != indexOfPreviousAggregator + 1)) { - return false; - } - indexOfPreviousAggregator = i; - } - } - return true; - } - - /** - * Get the number of parameters of the {@link Method} represented by this - * context. - */ - int getParameterCount() { - return parameters.length; - } - - /** - * Get the name of the {@link Parameter} with the supplied index, if - * it is present and declared before the aggregators. - * - * @return an {@code Optional} containing the name of the parameter - */ - Optional getParameterName(int parameterIndex) { - if (parameterIndex >= getParameterCount()) { - return Optional.empty(); - } - Parameter parameter = this.parameters[parameterIndex]; - if (!parameter.isNamePresent()) { - return Optional.empty(); - } - if (hasAggregator() && parameterIndex >= indexOfFirstAggregator()) { - return Optional.empty(); - } - return Optional.of(parameter.getName()); - } - - /** - * Determine if the {@link Method} represented by this context declares at - * least one {@link Parameter} that is an - * {@linkplain #isAggregator aggregator}. - * - * @return {@code true} if the method has an aggregator - */ - boolean hasAggregator() { - return resolverTypes.contains(AGGREGATOR); - } - - /** - * Determine if the {@link Parameter} with the supplied index is an - * aggregator (i.e., of type {@link ArgumentsAccessor} or annotated with - * {@link AggregateWith}). - * - * @return {@code true} if the parameter is an aggregator - */ - boolean isAggregator(int parameterIndex) { - return resolverTypes.get(parameterIndex) == AGGREGATOR; - } - - /** - * Find the index of the first {@linkplain #isAggregator aggregator} - * {@link Parameter} in the {@link Method} represented by this context. - * - * @return the index of the first aggregator, or {@code -1} if not found - */ - int indexOfFirstAggregator() { - return resolverTypes.indexOf(AGGREGATOR); - } - - /** - * Resolve the parameter for the supplied context using the supplied - * arguments. - */ - Object resolve(ParameterContext parameterContext, ExtensionContext extensionContext, Object[] arguments, - int invocationIndex) { - return getResolver(parameterContext, extensionContext).resolve(parameterContext, arguments, invocationIndex); - } - - private Resolver getResolver(ParameterContext parameterContext, ExtensionContext extensionContext) { - int index = parameterContext.getIndex(); - if (resolvers[index] == null) { - resolvers[index] = resolverTypes.get(index).createResolver(parameterContext, extensionContext); - } - return resolvers[index]; - } - - enum ResolverType { - - CONVERTER { - @Override - Resolver createResolver(ParameterContext parameterContext, ExtensionContext extensionContext) { - try { // @formatter:off - return AnnotationSupport.findAnnotation(parameterContext.getParameter(), ConvertWith.class) - .map(ConvertWith::value) - .map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentConverter.class, clazz, extensionContext)) - .map(converter -> AnnotationConsumerInitializer.initialize(parameterContext.getParameter(), converter)) - .map(Converter::new) - .orElse(Converter.DEFAULT); - } // @formatter:on - catch (Exception ex) { - throw parameterResolutionException("Error creating ArgumentConverter", ex, parameterContext); - } - } - }, - - AGGREGATOR { - @Override - Resolver createResolver(ParameterContext parameterContext, ExtensionContext extensionContext) { - try { // @formatter:off - return AnnotationSupport.findAnnotation(parameterContext.getParameter(), AggregateWith.class) - .map(AggregateWith::value) - .map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentsAggregator.class, clazz, extensionContext)) - .map(Aggregator::new) - .orElse(Aggregator.DEFAULT); - } // @formatter:on - catch (Exception ex) { - throw parameterResolutionException("Error creating ArgumentsAggregator", ex, parameterContext); - } - } - }; - - abstract Resolver createResolver(ParameterContext parameterContext, ExtensionContext extensionContext); - - } - - interface Resolver { - - Object resolve(ParameterContext parameterContext, Object[] arguments, int invocationIndex); - - } - - static class Converter implements Resolver { - - private static final Converter DEFAULT = new Converter(DefaultArgumentConverter.INSTANCE); - - private final ArgumentConverter argumentConverter; - - Converter(ArgumentConverter argumentConverter) { - this.argumentConverter = argumentConverter; - } - - @Override - public Object resolve(ParameterContext parameterContext, Object[] arguments, int invocationIndex) { - Object argument = arguments[parameterContext.getIndex()]; - try { - return this.argumentConverter.convert(argument, parameterContext); - } - catch (Exception ex) { - throw parameterResolutionException("Error converting parameter", ex, parameterContext); - } - } - - } - - static class Aggregator implements Resolver { - - private static final Aggregator DEFAULT = new Aggregator((accessor, context) -> accessor); - - private final ArgumentsAggregator argumentsAggregator; - - Aggregator(ArgumentsAggregator argumentsAggregator) { - this.argumentsAggregator = argumentsAggregator; - } - - @Override - public Object resolve(ParameterContext parameterContext, Object[] arguments, int invocationIndex) { - ArgumentsAccessor accessor = new DefaultArgumentsAccessor(parameterContext, invocationIndex, arguments); - try { - return this.argumentsAggregator.aggregateArguments(accessor, parameterContext); - } - catch (Exception ex) { - throw parameterResolutionException("Error aggregating arguments for parameter", ex, parameterContext); - } - } - - } - - private static ParameterResolutionException parameterResolutionException(String message, Exception cause, - ParameterContext parameterContext) { - String fullMessage = message + " at index " + parameterContext.getIndex(); - if (StringUtils.isNotBlank(cause.getMessage())) { - fullMessage += ": " + cause.getMessage(); - } - return new ParameterResolutionException(fullMessage, cause); - } - -} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodParameterResolver.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodParameterResolver.java new file mode 100644 index 000000000000..be3d75322e35 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestMethodParameterResolver.java @@ -0,0 +1,37 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import java.lang.reflect.Executable; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * @since 5.0 + */ +class ParameterizedTestMethodParameterResolver extends ParameterizedInvocationParameterResolver { + + private final Method testTemplateMethod; + + ParameterizedTestMethodParameterResolver(ParameterizedTestContext methodContext, EvaluatedArgumentSet arguments, + int invocationIndex) { + super(methodContext.getResolverFacade(), arguments, invocationIndex, ResolutionCache.DISABLED); + this.testTemplateMethod = methodContext.getAnnotatedElement(); + } + + @Override + protected boolean isSupportedOnConstructorOrMethod(Executable declaringExecutable, + ExtensionContext extensionContext) { + return this.testTemplateMethod.equals(declaringExecutable); + } + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestParameterResolver.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestParameterResolver.java deleted file mode 100644 index cf1196bac37b..000000000000 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestParameterResolver.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2015-2025 the original author or authors. - * - * All rights reserved. This program and the accompanying materials are - * made available under the terms of the Eclipse Public License v2.0 which - * accompanies this distribution and is available at - * - * https://www.eclipse.org/legal/epl-v20.html - */ - -package org.junit.jupiter.params; - -import java.lang.reflect.Executable; -import java.lang.reflect.Method; - -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.ParameterContext; -import org.junit.jupiter.api.extension.ParameterResolutionException; -import org.junit.jupiter.api.extension.ParameterResolver; - -/** - * @since 5.0 - */ -class ParameterizedTestParameterResolver implements ParameterResolver { - - private final ParameterizedTestMethodContext methodContext; - private final EvaluatedArgumentSet arguments; - private final int invocationIndex; - - ParameterizedTestParameterResolver(ParameterizedTestMethodContext methodContext, EvaluatedArgumentSet arguments, - int invocationIndex) { - - this.methodContext = methodContext; - this.arguments = arguments; - this.invocationIndex = invocationIndex; - } - - @Override - public ExtensionContextScope getTestInstantiationExtensionContextScope(ExtensionContext rootContext) { - return ExtensionContextScope.TEST_METHOD; - } - - @Override - public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { - Executable declaringExecutable = parameterContext.getDeclaringExecutable(); - Method testMethod = extensionContext.getTestMethod().orElse(null); - int parameterIndex = parameterContext.getIndex(); - - // Not a @ParameterizedTest method? - if (!declaringExecutable.equals(testMethod)) { - return false; - } - - // Current parameter is an aggregator? - if (this.methodContext.isAggregator(parameterIndex)) { - return true; - } - - // Ensure that the current parameter is declared before aggregators. - // Otherwise, a different ParameterResolver should handle it. - if (this.methodContext.hasAggregator()) { - return parameterIndex < this.methodContext.indexOfFirstAggregator(); - } - - // Else fallback to behavior for parameterized test methods without aggregators. - return parameterIndex < this.arguments.getConsumedLength(); - } - - @Override - public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) - throws ParameterResolutionException { - return this.methodContext.resolve(parameterContext, extensionContext, this.arguments.getConsumedPayloads(), - this.invocationIndex); - } - -} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolutionCache.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolutionCache.java new file mode 100644 index 000000000000..eeec256dccd0 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolutionCache.java @@ -0,0 +1,41 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +import org.junit.jupiter.params.support.ParameterDeclaration; + +/** + * @since 5.13 + */ +interface ResolutionCache { + + static ResolutionCache enabled() { + return new Concurrent(); + } + + ResolutionCache DISABLED = (__, resolver) -> resolver.get(); + + Object resolve(ParameterDeclaration declaration, Supplier resolver); + + class Concurrent implements ResolutionCache { + + private final Map cache = new ConcurrentHashMap<>(); + + @Override + public Object resolve(ParameterDeclaration declaration, Supplier resolver) { + return cache.computeIfAbsent(declaration, __ -> resolver.get()); + } + } +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java new file mode 100644 index 000000000000..55a775621b0b --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ResolverFacade.java @@ -0,0 +1,541 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params; + +import static java.lang.System.lineSeparator; +import static java.util.Collections.unmodifiableList; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; +import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation; +import static org.junit.platform.commons.support.AnnotationSupport.isAnnotated; +import static org.junit.platform.commons.support.ReflectionSupport.makeAccessible; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.AnnotatedElementContext; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.params.aggregator.AggregateWith; +import org.junit.jupiter.params.aggregator.ArgumentsAccessor; +import org.junit.jupiter.params.aggregator.ArgumentsAggregationException; +import org.junit.jupiter.params.aggregator.ArgumentsAggregator; +import org.junit.jupiter.params.aggregator.DefaultArgumentsAccessor; +import org.junit.jupiter.params.aggregator.SimpleArgumentsAggregator; +import org.junit.jupiter.params.converter.ArgumentConverter; +import org.junit.jupiter.params.converter.ConvertWith; +import org.junit.jupiter.params.converter.DefaultArgumentConverter; +import org.junit.jupiter.params.support.AnnotationConsumerInitializer; +import org.junit.jupiter.params.support.FieldContext; +import org.junit.jupiter.params.support.ParameterDeclaration; +import org.junit.jupiter.params.support.ParameterDeclarations; +import org.junit.platform.commons.JUnitException; +import org.junit.platform.commons.PreconditionViolationException; +import org.junit.platform.commons.support.ModifierSupport; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.commons.util.StringUtils; + +class ResolverFacade { + + static ResolverFacade create(Class clazz, List fields) { + Preconditions.notEmpty(fields, "Fields must not be empty"); + + NavigableMap> allIndexedParameters = new TreeMap<>(); + Set aggregatorParameters = new LinkedHashSet<>(); + + for (Field field : fields) { + Parameter annotation = findAnnotation(field, Parameter.class) // + .orElseThrow(() -> new JUnitException("No @Parameter annotation present")); + int index = annotation.value(); + + FieldParameterDeclaration declaration = new FieldParameterDeclaration(field, annotation.value()); + if (isAggregator(declaration)) { + aggregatorParameters.add(declaration); + } + else { + if (fields.size() == 1 && index == Parameter.UNSET_INDEX) { + index = 0; + declaration = new FieldParameterDeclaration(field, 0); + } + allIndexedParameters.computeIfAbsent(index, __ -> new ArrayList<>()) // + .add(declaration); + } + } + + NavigableMap uniqueIndexedParameters = validateFieldDeclarations( + allIndexedParameters, aggregatorParameters); + + Stream.concat(uniqueIndexedParameters.values().stream(), aggregatorParameters.stream()) // + .forEach(declaration -> makeAccessible(declaration.getField())); + + return new ResolverFacade(clazz, uniqueIndexedParameters, aggregatorParameters, 0); + } + + static ResolverFacade create(Constructor constructor, ParameterizedClass annotation) { + java.lang.reflect.Parameter[] parameters = constructor.getParameters(); + // Inner classes get the outer instance as first parameter + int implicitParameters = parameters.length > 0 && parameters[0].isImplicit() ? 1 : 0; + return create(constructor, annotation, implicitParameters); + } + + static ResolverFacade create(Method method, ParameterizedTest annotation) { + return create(method, annotation, 0); + } + + /** + * Create a new {@link ResolverFacade} for the supplied {@link Executable}. + * + *

This method takes a best-effort approach at enforcing the following + * policy for parameterized class constructors and parameterized test + * methods that accept aggregators as arguments. + *

    + *
  1. zero or more indexed arguments come first.
  2. + *
  3. zero or more aggregators come next.
  4. + *
  5. zero or more arguments supplied by other {@code ParameterResolver} + * implementations come last.
  6. + *
+ */ + private static ResolverFacade create(Executable executable, Annotation annotation, int indexOffset) { + NavigableMap indexedParameters = new TreeMap<>(); + NavigableMap aggregatorParameters = new TreeMap<>(); + java.lang.reflect.Parameter[] parameters = executable.getParameters(); + for (int index = indexOffset; index < parameters.length; index++) { + ParameterDeclaration declaration = new ExecutableParameterDeclaration(parameters[index], + index - indexOffset); + if (isAggregator(declaration)) { + Preconditions.condition( + aggregatorParameters.isEmpty() + || aggregatorParameters.lastKey() == declaration.getParameterIndex() - 1, + () -> String.format( + "@%s %s declares formal parameters in an invalid order: " + + "argument aggregators must be declared after any indexed arguments " + + "and before any arguments resolved by another ParameterResolver.", + annotation.annotationType().getSimpleName(), + DefaultParameterDeclarations.describe(executable))); + aggregatorParameters.put(declaration.getParameterIndex(), declaration); + } + else if (aggregatorParameters.isEmpty()) { + indexedParameters.put(declaration.getParameterIndex(), declaration); + } + } + return new ResolverFacade(executable, indexedParameters, new LinkedHashSet<>(aggregatorParameters.values()), + indexOffset); + } + + private final int parameterIndexOffset; + private final Map resolvers; + private final DefaultParameterDeclarations indexedParameterDeclarations; + private final Set aggregatorParameters; + + private ResolverFacade(AnnotatedElement sourceElement, + NavigableMap indexedParameters, + Set aggregatorParameters, int parameterIndexOffset) { + this.aggregatorParameters = aggregatorParameters; + this.parameterIndexOffset = parameterIndexOffset; + this.resolvers = new ConcurrentHashMap<>(indexedParameters.size() + aggregatorParameters.size()); + this.indexedParameterDeclarations = new DefaultParameterDeclarations(sourceElement, indexedParameters); + } + + ParameterDeclarations getIndexedParameterDeclarations() { + return this.indexedParameterDeclarations; + } + + boolean isSupportedParameter(ParameterContext parameterContext, EvaluatedArgumentSet arguments) { + int index = toLogicalIndex(parameterContext); + if (this.indexedParameterDeclarations.get(index).isPresent()) { + return index < arguments.getConsumedLength(); + } + return !this.aggregatorParameters.isEmpty() + && this.aggregatorParameters.stream().anyMatch(it -> it.getParameterIndex() == index); + } + + /** + * Get the name of the parameter with the supplied index, if it is present + * and declared before the aggregators. + * + * @return an {@code Optional} containing the name of the parameter + */ + Optional getParameterName(int parameterIndex) { + return this.indexedParameterDeclarations.get(parameterIndex) // + .flatMap(ParameterDeclaration::getParameterName); + } + + /** + * Determine the length of the arguments array that is considered consumed + * by the parameter declarations in this resolver. + * + *

If an aggregator is present, all arguments are considered consumed. + * Otherwise, the consumed argument length is the minimum of the total + * length and the number of indexed parameter declarations. + */ + int determineConsumedArgumentLength(int totalLength) { + NavigableMap declarationsByIndex = this.indexedParameterDeclarations.declarationsByIndex; + return this.aggregatorParameters.isEmpty() // + ? Math.min(totalLength, declarationsByIndex.isEmpty() ? 0 : declarationsByIndex.lastKey() + 1) // + : totalLength; + } + + /** + * Determine the number of arguments that are considered consumed by the + * parameter declarations in this resolver. + * + *

If an aggregator is present, all arguments are considered consumed. + * Otherwise, the consumed argument count, is the number of indexes that + * correspond to indexed parameter declarations. + */ + int determineConsumedArgumentCount(EvaluatedArgumentSet arguments) { + if (this.aggregatorParameters.isEmpty()) { + return this.indexedParameterDeclarations.declarationsByIndex.subMap(0, + arguments.getConsumedLength()).size(); + } + return arguments.getTotalLength(); + } + + /** + * Resolve the parameter for the supplied context using the supplied + * arguments. + */ + Object resolve(ParameterContext parameterContext, ExtensionContext extensionContext, EvaluatedArgumentSet arguments, + int invocationIndex, ResolutionCache resolutionCache) { + + int parameterIndex = toLogicalIndex(parameterContext); + ParameterDeclaration declaration = this.indexedParameterDeclarations.get(parameterIndex) // + .orElseGet(() -> this.aggregatorParameters.stream().filter( + it -> it.getParameterIndex() == parameterIndex).findFirst() // + .orElseThrow(() -> new ParameterResolutionException( + "Parameter index out of bounds: " + parameterIndex))); + return resolutionCache.resolve(declaration, + () -> getResolver(extensionContext, declaration, parameterContext.getParameter()) // + .resolve(parameterContext, parameterIndex, arguments, invocationIndex)); + } + + void resolveAndInjectFields(Object testInstance, ExtensionContext extensionContext, EvaluatedArgumentSet arguments, + int invocationIndex, ResolutionCache resolutionCache) { + + if (this.indexedParameterDeclarations.sourceElement.equals(testInstance.getClass())) { + getAllParameterDeclarations() // + .filter(FieldParameterDeclaration.class::isInstance) // + .map(FieldParameterDeclaration.class::cast) // + .forEach(declaration -> setField(testInstance, declaration, extensionContext, arguments, + invocationIndex, resolutionCache)); + } + } + + private Stream getAllParameterDeclarations() { + return Stream.concat(this.indexedParameterDeclarations.declarationsByIndex.values().stream(), + aggregatorParameters.stream()); + } + + private void setField(Object testInstance, FieldParameterDeclaration declaration, ExtensionContext extensionContext, + EvaluatedArgumentSet arguments, int invocationIndex, ResolutionCache resolutionCache) { + + Object argument = resolutionCache.resolve(declaration, + () -> resolve(declaration, extensionContext, arguments, invocationIndex)); + try { + declaration.getField().set(testInstance, argument); + } + catch (Exception e) { + throw new JUnitException("Failed to inject parameter value into field: " + declaration.getField(), e); + } + } + + private Object resolve(FieldParameterDeclaration parameterDeclaration, ExtensionContext extensionContext, + EvaluatedArgumentSet arguments, int invocationIndex) { + return getResolver(extensionContext, parameterDeclaration, parameterDeclaration.getField()) // + .resolve(parameterDeclaration, arguments, invocationIndex); + } + + private Resolver getResolver(ExtensionContext extensionContext, ParameterDeclaration declaration, + AnnotatedElement annotatedElement) { + return this.resolvers.computeIfAbsent(declaration, __ -> this.aggregatorParameters.contains(declaration) // + ? createAggregator(declaration.getParameterIndex(), annotatedElement, extensionContext) // + : createConverter(declaration.getParameterIndex(), annotatedElement, extensionContext)); + } + + private int toLogicalIndex(ParameterContext parameterContext) { + int index = parameterContext.getIndex() - this.parameterIndexOffset; + Preconditions.condition(index >= 0, () -> "Parameter index must be greater than or equal to zero"); + return index; + } + + private static NavigableMap validateFieldDeclarations( + NavigableMap> indexedParameters, + Set aggregatorParameters) { + + List errors = new ArrayList<>(); + validateIndexedParameters(indexedParameters, errors); + validateAggregatorParameters(aggregatorParameters, errors); + + if (errors.isEmpty()) { + return indexedParameters.entrySet().stream() // + .collect(toMap(Map.Entry::getKey, entry -> entry.getValue().get(0), (d, __) -> d, TreeMap::new)); + } + else if (errors.size() == 1) { + throw new PreconditionViolationException("Configuration error: " + errors.get(0) + "."); + } + else { + throw new PreconditionViolationException(String.format("%d configuration errors:%n%s", errors.size(), + errors.stream().collect(joining(lineSeparator() + "- ", "- ", "")))); + } + } + + private static void validateIndexedParameters( + NavigableMap> indexedParameters, List errors) { + + if (indexedParameters.isEmpty()) { + return; + } + + indexedParameters.forEach( + (index, declarations) -> validateIndexedParameterDeclarations(index, declarations, errors)); + + for (int index = 0; index <= indexedParameters.lastKey(); index++) { + if (!indexedParameters.containsKey(index)) { + errors.add(String.format("no field annotated with @Parameter(%d) declared", index)); + } + } + } + + private static void validateIndexedParameterDeclarations(int index, List declarations, + List errors) { + List fields = declarations.stream().map(FieldParameterDeclaration::getField).collect(toList()); + if (index < 0) { + declarations.stream() // + .map(declaration -> String.format( + "index must be greater than or equal to zero in @Parameter(%d) annotation on field [%s]", index, + declaration.getField())) // + .forEach(errors::add); + } + else if (declarations.size() > 1) { + errors.add( + String.format("duplicate index declared in @Parameter(%d) annotation on fields %s", index, fields)); + } + fields.stream() // + .filter(ModifierSupport::isFinal) // + .map(field -> String.format("@Parameter field [%s] must not be declared as final", field)) // + .forEach(errors::add); + } + + private static void validateAggregatorParameters(Set aggregatorParameters, + List errors) { + aggregatorParameters.stream() // + .filter(declaration -> declaration.getParameterIndex() != Parameter.UNSET_INDEX) // + .map(declaration -> String.format( + "no index may be declared in @Parameter(%d) annotation on aggregator field [%s]", + declaration.getParameterIndex(), declaration.getField())) // + .forEach(errors::add); + } + + /** + * Determine if the supplied {@link Parameter} is an aggregator (i.e., of + * type {@link ArgumentsAccessor} or annotated with {@link AggregateWith}). + * + * @return {@code true} if the parameter is an aggregator + */ + private static boolean isAggregator(ParameterDeclaration declaration) { + return ArgumentsAccessor.class.isAssignableFrom(declaration.getParameterType()) + || isAnnotated(declaration.getAnnotatedElement(), AggregateWith.class); + } + + private static Converter createConverter(int index, AnnotatedElement annotatedElement, + ExtensionContext extensionContext) { + try { // @formatter:off + return findAnnotation(annotatedElement, ConvertWith.class) + .map(ConvertWith::value) + .map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentConverter.class, clazz, extensionContext)) + .map(converter -> AnnotationConsumerInitializer.initialize(annotatedElement, converter)) + .map(Converter::new) + .orElse(Converter.DEFAULT); + } // @formatter:on + catch (Exception ex) { + throw parameterResolutionException("Error creating ArgumentConverter", ex, index); + } + } + + private static Aggregator createAggregator(int index, AnnotatedElement annotatedElement, + ExtensionContext extensionContext) { + try { // @formatter:off + return findAnnotation(annotatedElement, AggregateWith.class) + .map(AggregateWith::value) + .map(clazz -> ParameterizedTestSpiInstantiator.instantiate(ArgumentsAggregator.class, clazz, extensionContext)) + .map(Aggregator::new) + .orElse(Aggregator.DEFAULT); + } // @formatter:on + catch (Exception ex) { + throw parameterResolutionException("Error creating ArgumentsAggregator", ex, index); + } + } + + private static ParameterResolutionException parameterResolutionException(String message, Exception cause, + int index) { + String fullMessage = message + " at index " + index; + if (StringUtils.isNotBlank(cause.getMessage())) { + fullMessage += ": " + cause.getMessage(); + } + return new ParameterResolutionException(fullMessage, cause); + } + + private interface Resolver { + + Object resolve(ParameterContext parameterContext, int parameterIndex, EvaluatedArgumentSet arguments, + int invocationIndex); + + Object resolve(FieldContext fieldContext, EvaluatedArgumentSet arguments, int invocationIndex); + + } + + private static class Converter implements Resolver { + + private static final Converter DEFAULT = new Converter(DefaultArgumentConverter.INSTANCE); + + private final ArgumentConverter argumentConverter; + + Converter(ArgumentConverter argumentConverter) { + this.argumentConverter = argumentConverter; + } + + @Override + public Object resolve(ParameterContext parameterContext, int parameterIndex, EvaluatedArgumentSet arguments, + int invocationIndex) { + Object argument = arguments.getConsumedPayload(parameterIndex); + try { + return this.argumentConverter.convert(argument, parameterContext); + } + catch (Exception ex) { + throw parameterResolutionException("Error converting parameter", ex, parameterContext.getIndex()); + } + } + + @Override + public Object resolve(FieldContext fieldContext, EvaluatedArgumentSet arguments, int invocationIndex) { + Object argument = arguments.getConsumedPayload(fieldContext.getParameterIndex()); + try { + return this.argumentConverter.convert(argument, fieldContext); + } + catch (Exception ex) { + throw parameterResolutionException("Error converting parameter", ex, fieldContext.getParameterIndex()); + } + } + } + + private static class Aggregator implements Resolver { + + private static final Aggregator DEFAULT = new Aggregator(new SimpleArgumentsAggregator() { + @Override + protected Object aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) throws ArgumentsAggregationException { + return accessor; + } + }); + + private final ArgumentsAggregator argumentsAggregator; + + Aggregator(ArgumentsAggregator argumentsAggregator) { + this.argumentsAggregator = argumentsAggregator; + } + + @Override + public Object resolve(ParameterContext parameterContext, int parameterIndex, EvaluatedArgumentSet arguments, + int invocationIndex) { + ArgumentsAccessor accessor = DefaultArgumentsAccessor.create(parameterContext, invocationIndex, + arguments.getConsumedPayloads()); + try { + return this.argumentsAggregator.aggregateArguments(accessor, parameterContext); + } + catch (Exception ex) { + throw parameterResolutionException("Error aggregating arguments for parameter", ex, + parameterContext.getIndex()); + } + } + + @Override + public Object resolve(FieldContext fieldContext, EvaluatedArgumentSet arguments, int invocationIndex) { + ArgumentsAccessor accessor = DefaultArgumentsAccessor.create(fieldContext, invocationIndex, + arguments.getConsumedPayloads()); + try { + return this.argumentsAggregator.aggregateArguments(accessor, fieldContext); + } + catch (Exception ex) { + throw parameterResolutionException("Error aggregating arguments for parameter", ex, + fieldContext.getParameterIndex()); + } + } + } + + private static class DefaultParameterDeclarations implements ParameterDeclarations { + + private final AnnotatedElement sourceElement; + private final NavigableMap declarationsByIndex; + + DefaultParameterDeclarations(AnnotatedElement sourceElement, + NavigableMap declarationsByIndex) { + this.sourceElement = sourceElement; + this.declarationsByIndex = declarationsByIndex; + } + + @Override + public AnnotatedElement getSourceElement() { + return this.sourceElement; + } + + @Override + public Optional getFirst() { + return this.declarationsByIndex.isEmpty() // + ? Optional.empty() // + : Optional.of(this.declarationsByIndex.firstEntry().getValue()); + } + + @Override + public List getAll() { + return unmodifiableList(new ArrayList<>(this.declarationsByIndex.values())); + } + + @Override + public Optional get(int parameterIndex) { + return Optional.ofNullable(this.declarationsByIndex.get(parameterIndex)); + } + + @Override + public String getSourceElementDescription() { + return describe(this.sourceElement); + } + + static String describe(AnnotatedElement sourceElement) { + if (sourceElement instanceof Method) { + return String.format("method [%s]", ((Method) sourceElement).toGenericString()); + } + if (sourceElement instanceof Constructor) { + return String.format("constructor [%s]", ((Constructor) sourceElement).toGenericString()); + } + if (sourceElement instanceof Class) { + return String.format("class [%s]", ((Class) sourceElement).getName()); + } + return sourceElement.toString(); + } + } + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/AggregateWith.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/AggregateWith.java index e7fcca21eb50..0b30acecbccd 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/AggregateWith.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/AggregateWith.java @@ -24,10 +24,14 @@ * {@code @AggregateWith} is an annotation that allows one to specify an * {@link ArgumentsAggregator}. * - *

This annotation may be applied to a parameter of a + *

This annotation may be applied to parameters of a + * {@link org.junit.jupiter.params.ParameterizedClass @ParameterizedClass} + * constructor or its + * {@link org.junit.jupiter.params.Parameter @Parameter}-annotated fields, or to + * parameters of a * {@link org.junit.jupiter.params.ParameterizedTest @ParameterizedTest} method * in order for an aggregated value to be resolved for the annotated parameter - * when the test method is invoked. + * when the parameterized class or method is invoked. * *

{@code @AggregateWith} may also be used as a meta-annotation in order to * create a custom composed annotation that inherits the semantics @@ -38,7 +42,7 @@ * @see org.junit.jupiter.params.ParameterizedTest */ @Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.PARAMETER, ElementType.FIELD }) @Documented @API(status = STABLE, since = "5.7") public @interface AggregateWith { diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/ArgumentsAggregator.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/ArgumentsAggregator.java index 90ce75ddd048..905b69e8fe33 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/ArgumentsAggregator.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/ArgumentsAggregator.java @@ -10,11 +10,14 @@ package org.junit.jupiter.params.aggregator; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.params.support.FieldContext; +import org.junit.platform.commons.JUnitException; /** * {@code ArgumentsAggregator} is an abstraction for the aggregation of arguments @@ -43,6 +46,7 @@ * @since 5.2 * @see AggregateWith * @see ArgumentsAccessor + * @see SimpleArgumentsAggregator * @see org.junit.jupiter.params.ParameterizedTest */ @API(status = STABLE, since = "5.7") @@ -64,4 +68,27 @@ public interface ArgumentsAggregator { Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) throws ArgumentsAggregationException; + /** + * Aggregate the arguments contained in the supplied {@code accessor} into a + * single object. + * + * @param accessor an {@link ArgumentsAccessor} containing the arguments to be + * aggregated; never {@code null} + * @param context the field context where the aggregated result is to be + * injected; never {@code null} + * @return the aggregated result; may be {@code null} but only if the target + * type is a reference type + * @throws ArgumentsAggregationException if an error occurs during the + * aggregation + * @since 5.13 + */ + @API(status = EXPERIMENTAL, since = "5.13") + default Object aggregateArguments(ArgumentsAccessor accessor, FieldContext context) + throws ArgumentsAggregationException { + throw new JUnitException( + String.format("ArgumentsAggregator does not override the convert(ArgumentsAccessor, FieldContext) method. " + + "Please report this issue to the maintainers of %s.", + getClass().getName())); + } + } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java index 79e1f37999c1..cdb5c9d0806e 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessor.java @@ -16,10 +16,12 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.function.BiFunction; import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.converter.DefaultArgumentConverter; +import org.junit.jupiter.params.support.FieldContext; import org.junit.platform.commons.util.ClassUtils; import org.junit.platform.commons.util.Preconditions; @@ -36,17 +38,33 @@ @API(status = INTERNAL, since = "5.2") public class DefaultArgumentsAccessor implements ArgumentsAccessor { - private final ParameterContext parameterContext; private final int invocationIndex; private final Object[] arguments; + private final BiFunction, Object> converter; + + public static DefaultArgumentsAccessor create(ParameterContext parameterContext, int invocationIndex, + Object... arguments) { - public DefaultArgumentsAccessor(ParameterContext parameterContext, int invocationIndex, Object... arguments) { Preconditions.notNull(parameterContext, "ParameterContext must not be null"); - Preconditions.condition(invocationIndex >= 1, () -> "invocation index must be >= 1"); - Preconditions.notNull(arguments, "Arguments array must not be null"); - this.parameterContext = parameterContext; + BiFunction, Object> converter = (source, targetType) -> DefaultArgumentConverter.INSTANCE // + .convert(source, targetType, parameterContext); + return new DefaultArgumentsAccessor(converter, invocationIndex, arguments); + } + + public static DefaultArgumentsAccessor create(FieldContext fieldContext, int invocationIndex, Object... arguments) { + + Preconditions.notNull(fieldContext, "FieldContext must not be null"); + BiFunction, Object> converter = (source, targetType) -> DefaultArgumentConverter.INSTANCE // + .convert(source, targetType, fieldContext); + return new DefaultArgumentsAccessor(converter, invocationIndex, arguments); + } + + private DefaultArgumentsAccessor(BiFunction, Object> converter, int invocationIndex, + Object... arguments) { + Preconditions.condition(invocationIndex >= 1, () -> "Invocation index must be >= 1"); + this.converter = Preconditions.notNull(converter, "Converter must not be null"); this.invocationIndex = invocationIndex; - this.arguments = arguments; + this.arguments = Preconditions.notNull(arguments, "Arguments array must not be null"); } @Override @@ -61,8 +79,7 @@ public T get(int index, Class requiredType) { Preconditions.notNull(requiredType, "requiredType must not be null"); Object value = get(index); try { - Object convertedValue = DefaultArgumentConverter.INSTANCE.convert(value, requiredType, - this.parameterContext); + Object convertedValue = converter.apply(value, requiredType); return requiredType.cast(convertedValue); } catch (Exception ex) { diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/SimpleArgumentsAggregator.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/SimpleArgumentsAggregator.java new file mode 100644 index 000000000000..25373b624f75 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/aggregator/SimpleArgumentsAggregator.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.params.aggregator; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.AnnotatedElementContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.params.support.FieldContext; + +/** + * {@code SimpleArgumentsAggregator} is an abstract base class for + * {@link ArgumentsAggregator} implementations that do not need to distinguish + * between fields and method/constructor parameters. + * + * @since 5.0 + * @see ArgumentsAggregator + */ +@API(status = EXPERIMENTAL, since = "5.13") +public abstract class SimpleArgumentsAggregator implements ArgumentsAggregator { + + public SimpleArgumentsAggregator() { + } + + @Override + public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) + throws ArgumentsAggregationException { + return aggregateArguments(accessor, context.getParameter().getType(), context, context.getIndex()); + } + + @Override + public Object aggregateArguments(ArgumentsAccessor accessor, FieldContext context) + throws ArgumentsAggregationException { + return aggregateArguments(accessor, null, context, context.getParameterIndex()); + } + + protected abstract Object aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) throws ArgumentsAggregationException; +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/AnnotationBasedArgumentConverter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/AnnotationBasedArgumentConverter.java index b100f3ad4854..40dc578f40b4 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/AnnotationBasedArgumentConverter.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/AnnotationBasedArgumentConverter.java @@ -17,6 +17,7 @@ import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.support.AnnotationConsumer; +import org.junit.jupiter.params.support.FieldContext; import org.junit.platform.commons.util.Preconditions; /** @@ -49,6 +50,11 @@ public final Object convert(Object source, ParameterContext context) throws Argu return convert(source, context.getParameter().getType(), this.annotation); } + @Override + public final Object convert(Object source, FieldContext context) throws ArgumentConversionException { + return convert(source, context.getField().getType(), this.annotation); + } + /** * Convert the supplied {@code source} object into the supplied {@code targetType}, * based on metadata in the provided annotation. diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ArgumentConverter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ArgumentConverter.java index 2e5495eae0da..eae935d66e75 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ArgumentConverter.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ArgumentConverter.java @@ -10,11 +10,14 @@ package org.junit.jupiter.params.converter; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.params.support.FieldContext; +import org.junit.platform.commons.JUnitException; /** * {@code ArgumentConverter} is an abstraction that allows an input object to @@ -55,7 +58,7 @@ public interface ArgumentConverter { * * @param source the source object to convert; may be {@code null} * @param context the parameter context where the converted object will be - * used; never {@code null} + * supplied; never {@code null} * @return the converted object; may be {@code null} but only if the target * type is a reference type * @throws ArgumentConversionException if an error occurs during the @@ -63,4 +66,24 @@ public interface ArgumentConverter { */ Object convert(Object source, ParameterContext context) throws ArgumentConversionException; + /** + * Convert the supplied {@code source} object according to the supplied + * {@code context}. + * + * @param source the source object to convert; may be {@code null} + * @param context the field context where the converted object will be + * injected; never {@code null} + * @return the converted object; may be {@code null} but only if the target + * type is a reference type + * @throws ArgumentConversionException if an error occurs during the + * conversion + * @since 5.13 + */ + @API(status = EXPERIMENTAL, since = "5.13") + default Object convert(Object source, FieldContext context) throws ArgumentConversionException { + throw new JUnitException( + String.format("ArgumentConverter does not override the convert(Object, FieldContext) method. " + + "Please report this issue to the maintainers of %s.", + getClass().getName())); + } } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ConvertWith.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ConvertWith.java index d9e7e4fb907d..66bea68bea1d 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ConvertWith.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/ConvertWith.java @@ -23,16 +23,20 @@ /** * {@code @ConvertWith} is an annotation that allows one to specify an explicit * {@link ArgumentConverter}. - - *

This annotation may be applied to parameters of - * {@link org.junit.jupiter.params.ParameterizedTest @ParameterizedTest} methods + * + *

This annotation may be applied to parameters of a + * {@link org.junit.jupiter.params.ParameterizedClass @ParameterizedClass} + * constructor or its + * {@link org.junit.jupiter.params.Parameter @Parameter}-annotated fields, or to + * parameters of a + * {@link org.junit.jupiter.params.ParameterizedTest @ParameterizedTest} method * which need to have their {@code Arguments} converted before consuming them. * * @since 5.0 * @see org.junit.jupiter.params.ParameterizedTest * @see org.junit.jupiter.params.converter.ArgumentConverter */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.PARAMETER }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.PARAMETER, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) @Documented @API(status = STABLE, since = "5.7") diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java index 1de98dcd494d..af84d5da36f2 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/DefaultArgumentConverter.java @@ -13,6 +13,7 @@ import static org.apiguardian.api.API.Status.INTERNAL; import java.io.File; +import java.lang.reflect.Member; import java.math.BigDecimal; import java.math.BigInteger; import java.net.URI; @@ -23,6 +24,7 @@ import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.params.support.FieldContext; import org.junit.platform.commons.support.conversion.ConversionException; import org.junit.platform.commons.support.conversion.ConversionSupport; import org.junit.platform.commons.util.ClassLoaderUtils; @@ -61,7 +63,21 @@ public final Object convert(Object source, ParameterContext context) { return convert(source, targetType, context); } + @Override + public final Object convert(Object source, FieldContext context) throws ArgumentConversionException { + Class targetType = context.getField().getType(); + return convert(source, targetType, context); + } + public final Object convert(Object source, Class targetType, ParameterContext context) { + return convert(source, targetType, context.getDeclaringExecutable()); + } + + public final Object convert(Object source, Class targetType, FieldContext context) { + return convert(source, targetType, context.getField()); + } + + private Object convert(Object source, Class targetType, Member member) { if (source == null) { if (targetType.isPrimitive()) { throw new ArgumentConversionException( @@ -75,7 +91,7 @@ public final Object convert(Object source, Class targetType, ParameterContext } if (source instanceof String) { - Class declaringClass = context.getDeclaringExecutable().getDeclaringClass(); + Class declaringClass = member.getDeclaringClass(); ClassLoader classLoader = ClassLoaderUtils.getClassLoader(declaringClass); try { return convert((String) source, targetType, classLoader); diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/JavaTimeConversionPattern.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/JavaTimeConversionPattern.java index d4ab3110e629..a3d466b1df08 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/JavaTimeConversionPattern.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/JavaTimeConversionPattern.java @@ -20,14 +20,17 @@ import java.lang.annotation.Target; import org.apiguardian.api.API; -import org.junit.jupiter.params.ParameterizedTest; /** * {@code @JavaTimeConversionPattern} is an annotation that allows a date/time * conversion pattern to be specified on a parameter of a - * {@link ParameterizedTest @ParameterizedTest} method. + * {@link org.junit.jupiter.params.ParameterizedClass @ParameterizedClass} + * or + * {@link org.junit.jupiter.params.ParameterizedTest @ParameterizedTest}. * * @since 5.0 + * @see ConvertWith + * @see org.junit.jupiter.params.ParameterizedClass * @see org.junit.jupiter.params.ParameterizedTest * @see java.time.format.DateTimeFormatterBuilder#appendPattern(String) */ diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/SimpleArgumentConverter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/SimpleArgumentConverter.java index dcf714f5cb84..2cb0f3f922a6 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/SimpleArgumentConverter.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/SimpleArgumentConverter.java @@ -14,6 +14,7 @@ import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.params.support.FieldContext; /** * {@code SimpleArgumentConverter} is an abstract base class for @@ -36,6 +37,11 @@ public final Object convert(Object source, ParameterContext context) throws Argu return convert(source, context.getParameter().getType()); } + @Override + public final Object convert(Object source, FieldContext context) throws ArgumentConversionException { + return convert(source, context.getField().getType()); + } + /** * Convert the supplied {@code source} object into the supplied * {@code targetType}. diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/TypedArgumentConverter.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/TypedArgumentConverter.java index f229572a2a75..949cba18590c 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/TypedArgumentConverter.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/converter/TypedArgumentConverter.java @@ -14,6 +14,7 @@ import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.params.support.FieldContext; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.ReflectionUtils; @@ -48,6 +49,15 @@ protected TypedArgumentConverter(Class sourceType, Class targetType) { @Override public final Object convert(Object source, ParameterContext context) throws ArgumentConversionException { + return convert(source, context.getParameter().getType()); + } + + @Override + public final Object convert(Object source, FieldContext context) throws ArgumentConversionException { + return convert(source, context.getField().getType()); + } + + private T convert(Object source, Class actualTargetType) { if (source == null) { return convert(null); } @@ -57,9 +67,9 @@ public final Object convert(Object source, ParameterContext context) throws Argu getClass().getSimpleName(), source.getClass().getName(), this.sourceType.getName()); throw new ArgumentConversionException(message); } - if (!ReflectionUtils.isAssignableTo(this.targetType, context.getParameter().getType())) { + if (!ReflectionUtils.isAssignableTo(this.targetType, actualTargetType)) { String message = String.format("%s cannot convert to type [%s]. Only target type [%s] is supported.", - getClass().getSimpleName(), context.getParameter().getType().getName(), this.targetType.getName()); + getClass().getSimpleName(), actualTargetType.getName(), this.targetType.getName()); throw new ArgumentConversionException(message); } return convert(this.sourceType.cast(source)); diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProvider.java index b8ecb2f374dc..f1d94eca244a 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProvider.java @@ -20,6 +20,8 @@ import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.support.AnnotationConsumer; +import org.junit.jupiter.params.support.ParameterDeclarations; +import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.util.Preconditions; /** @@ -50,8 +52,8 @@ public final void accept(A annotation) { } @Override - public final Stream provideArguments(ExtensionContext context) { - return annotations.stream().flatMap(annotation -> provideArguments(context, annotation)); + public Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context) { + return annotations.stream().flatMap(annotation -> provideArguments(parameters, context, annotation)); } /** @@ -61,7 +63,20 @@ public final Stream provideArguments(ExtensionContext conte * @param context the current extension context; never {@code null} * @param annotation the annotation to process; never {@code null} * @return a stream of arguments; never {@code null} + * @deprecated Please implement + * {@link #provideArguments(ParameterDeclarations, ExtensionContext, Annotation)} + * instead. */ - protected abstract Stream provideArguments(ExtensionContext context, A annotation); + @Deprecated + protected Stream provideArguments(ExtensionContext context, A annotation) { + throw new JUnitException(String.format( + "AnnotationBasedArgumentsProvider does not override the provideArguments(ParameterDeclarations, ExtensionContext, Annotation) method. " + + "Please report this issue to the maintainers of %s.", + getClass().getName())); + } + protected Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context, + A annotation) { + return provideArguments(context, annotation); + } } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsProvider.java index 253e99cbb149..993b67cb1e23 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsProvider.java @@ -10,6 +10,8 @@ package org.junit.jupiter.params.provider; +import static org.apiguardian.api.API.Status.DEPRECATED; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import java.util.stream.Stream; @@ -17,11 +19,14 @@ import org.apiguardian.api.API; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.params.support.ParameterDeclarations; +import org.junit.platform.commons.JUnitException; /** - * An {@code ArgumentsProvider} is responsible for {@linkplain #provideArguments - * providing} a stream of arguments to be passed to a {@code @ParameterizedTest} - * method. + * An {@code ArgumentsProvider} is responsible for + * {@linkplain #provideArguments(ParameterDeclarations, ExtensionContext) providing} + * a stream of arguments to be passed to a {@code @ParameterizedClass} or + * {@code @ParameterizedTest}. * *

An {@code ArgumentsProvider} can be registered via the * {@link ArgumentsSource @ArgumentsSource} annotation. @@ -30,6 +35,7 @@ * constructor to use {@linkplain ParameterResolver parameter resolution}. * * @since 5.0 + * @see org.junit.jupiter.params.ParameterizedClass * @see org.junit.jupiter.params.ParameterizedTest * @see org.junit.jupiter.params.provider.ArgumentsSource * @see org.junit.jupiter.params.provider.Arguments @@ -44,7 +50,39 @@ public interface ArgumentsProvider { * * @param context the current extension context; never {@code null} * @return a stream of arguments; never {@code null} + * @deprecated Please implement + * {@link #provideArguments(ParameterDeclarations, ExtensionContext)} instead. */ - Stream provideArguments(ExtensionContext context) throws Exception; + @Deprecated + @API(status = DEPRECATED, since = "5.13") + default Stream provideArguments(@SuppressWarnings("unused") ExtensionContext context) + throws Exception { + throw new UnsupportedOperationException( + "Please implement provideArguments(ParameterDeclarations, ExtensionContext) instead."); + } + + /** + * Provide a {@link Stream} of {@link Arguments} to be passed to a + * {@code @ParameterizedClass} or {@code @ParameterizedTest}. + * + * @param parameters the parameter declarations for the parameterized + * class or test; never {@code null} + * @param context the current extension context; never {@code null} + * @return a stream of arguments; never {@code null} + * @since 5.13 + */ + @API(status = EXPERIMENTAL, since = "5.13") + default Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context) + throws Exception { + try { + return provideArguments(context); + } + catch (Exception e) { + throw new JUnitException(String.format( + "ArgumentsProvider does not override the provideArguments(ParameterDeclarations, ExtensionContext) method. " + + "Please report this issue to the maintainers of %s.", + getClass().getName()), e); + } + } } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsSource.java index 7180cf80ea5a..9dca8797cc72 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsSource.java @@ -20,11 +20,12 @@ import java.lang.annotation.Target; import org.apiguardian.api.API; +import org.junit.jupiter.params.ParameterizedClass; /** * {@code @ArgumentsSource} is a {@linkplain Repeatable repeatable} annotation * that is used to register {@linkplain ArgumentsProvider arguments providers} - * for the annotated test method. + * for the annotated class or method. * *

{@code @ArgumentsSource} may also be used as a meta-annotation in order to * create a custom composed annotation that inherits the semantics @@ -32,8 +33,10 @@ * * @since 5.0 * @see org.junit.jupiter.params.provider.ArgumentsProvider + * @see ParameterizedClass + * @see org.junit.jupiter.params.ParameterizedTest */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented @Repeatable(ArgumentsSources.class) diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsSources.java index d40ff40ef6fc..fa4e898c6582 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsSources.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ArgumentsSources.java @@ -31,7 +31,7 @@ * @since 5.0 * @see org.junit.jupiter.params.provider.ArgumentsSource */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented @API(status = STABLE, since = "5.7") diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvArgumentsProvider.java index d248b2dd1cec..f5d25363c379 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvArgumentsProvider.java @@ -26,6 +26,7 @@ import org.junit.jupiter.api.Named; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.support.ParameterDeclarations; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.UnrecoverableExceptions; @@ -41,7 +42,8 @@ class CsvArgumentsProvider extends AnnotationBasedArgumentsProvider { private CsvParser csvParser; @Override - protected Stream provideArguments(ExtensionContext context, CsvSource csvSource) { + protected Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context, + CsvSource csvSource) { this.nullValues = toSet(csvSource.nullValues()); this.csvParser = createParserFor(csvSource); final boolean textBlockDeclared = !csvSource.textBlock().isEmpty(); diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileArgumentsProvider.java index acc13160d544..f514bb74bed7 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileArgumentsProvider.java @@ -34,6 +34,7 @@ import com.univocity.parsers.csv.CsvParser; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.support.ParameterDeclarations; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.util.Preconditions; @@ -58,7 +59,8 @@ class CsvFileArgumentsProvider extends AnnotationBasedArgumentsProvider provideArguments(ExtensionContext context, CsvFileSource csvFileSource) { + protected Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context, + CsvFileSource csvFileSource) { this.charset = getCharsetFrom(csvFileSource); this.numLinesToSkip = csvFileSource.numLinesToSkip(); this.csvParser = createParserFor(csvFileSource); @@ -90,7 +92,7 @@ private CsvParser beginParsing(InputStream inputStream, CsvFileSource csvFileSou this.csvParser.beginParsing(inputStream, this.charset); } catch (Throwable throwable) { - handleCsvException(throwable, csvFileSource); + throw handleCsvException(throwable, csvFileSource); } return this.csvParser; } @@ -104,7 +106,7 @@ private Stream toStream(CsvParser csvParser, CsvFileSource csvFileSou csvParser.stopParsing(); } catch (Throwable throwable) { - handleCsvException(throwable, csvFileSource); + throw handleCsvException(throwable, csvFileSource); } }); } @@ -154,14 +156,14 @@ private void advance() { } } catch (Throwable throwable) { - handleCsvException(throwable, this.csvFileSource); + throw handleCsvException(throwable, this.csvFileSource); } } } @FunctionalInterface - private interface Source { + interface Source { InputStream open(ExtensionContext context); @@ -178,7 +180,7 @@ default Source classpathResource(String path) { } default Source file(String path) { - return context -> openFile(path); + return __ -> openFile(path); } } @@ -190,6 +192,7 @@ private static class DefaultInputStreamProvider implements InputStreamProvider { @Override public InputStream openClasspathResource(Class baseClass, String path) { Preconditions.notBlank(path, () -> "Classpath resource [" + path + "] must not be null or blank"); + //noinspection resource (closed elsewhere) InputStream inputStream = baseClass.getResourceAsStream(path); return Preconditions.notNull(inputStream, () -> "Classpath resource [" + path + "] does not exist"); } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java index 3c2c8c14a3f3..f3b75d69de84 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java @@ -20,6 +20,7 @@ import java.lang.annotation.Target; import org.apiguardian.api.API; +import org.junit.jupiter.params.ParameterizedInvocationConstants; /** * {@code @CsvFileSource} is a {@linkplain Repeatable repeatable} @@ -27,9 +28,9 @@ * files from one or more classpath {@link #resources} or {@link #files}. * *

The CSV records parsed from these resources and files will be provided as - * arguments to the annotated {@code @ParameterizedTest} method. Note that the - * first record may optionally be used to supply CSV headers (see - * {@link #useHeadersInDisplayName}). + * arguments to the annotated {@code @ParameterizedClass} or + * {@code @ParameterizedTest}. Note that the first record may optionally + * be used to supply CSV headers (see {@link #useHeadersInDisplayName}). * *

Any line beginning with a {@code #} symbol will be interpreted as a comment * and will be ignored. @@ -59,9 +60,10 @@ * @since 5.0 * @see CsvSource * @see org.junit.jupiter.params.provider.ArgumentsSource + * @see org.junit.jupiter.params.ParameterizedClass * @see org.junit.jupiter.params.ParameterizedTest */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented @Repeatable(CsvFileSources.class) @@ -104,16 +106,16 @@ * for columns. * *

When set to {@code true}, the header names will be used in the - * generated display name for each {@code @ParameterizedTest} method - * invocation. When using this feature, you must ensure that the display name - * pattern for {@code @ParameterizedTest} includes - * {@value org.junit.jupiter.params.ParameterizedTest#ARGUMENTS_PLACEHOLDER} instead of - * {@value org.junit.jupiter.params.ParameterizedTest#ARGUMENTS_WITH_NAMES_PLACEHOLDER} + * generated display name for each {@code @ParameterizedClass} or + * {@code @ParameterizedTest} invocation. When using this feature, you must + * ensure that the display name pattern for {@code @ParameterizedClass} or + * {@code @ParameterizedTest} includes + * {@value ParameterizedInvocationConstants#ARGUMENTS_PLACEHOLDER} instead of + * {@value ParameterizedInvocationConstants#ARGUMENTS_WITH_NAMES_PLACEHOLDER} * as demonstrated in the example below. * *

Defaults to {@code false}. * - * *

Example

*
 	 * {@literal @}ParameterizedTest(name = "[{index}] {arguments}")
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSources.java
index c246d1000020..7509ae660384 100644
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSources.java
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSources.java
@@ -32,7 +32,7 @@
  * @see CsvFileSource
  * @see java.lang.annotation.Repeatable
  */
-@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD })
+@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE })
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 @API(status = STABLE, since = "5.11")
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSource.java
index 09732e2101f1..f89c4d91862b 100644
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSource.java
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSource.java
@@ -20,6 +20,8 @@
 import java.lang.annotation.Target;
 
 import org.apiguardian.api.API;
+import org.junit.jupiter.params.ParameterizedClass;
+import org.junit.jupiter.params.ParameterizedInvocationConstants;
 
 /**
  * {@code @CsvSource} is a {@linkplain Repeatable repeatable}
@@ -28,7 +30,7 @@
  * {@link #textBlock} attribute.
  *
  * 

The supplied values will be provided as arguments to the annotated - * {@code @ParameterizedTest} method. + * {@code @ParameterizedClass} or {@code @ParameterizedTest}. * *

The column delimiter (which defaults to a comma ({@code ,})) can be customized * via either {@link #delimiter} or {@link #delimiterString}. @@ -62,9 +64,10 @@ * @since 5.0 * @see CsvFileSource * @see org.junit.jupiter.params.provider.ArgumentsSource + * @see ParameterizedClass * @see org.junit.jupiter.params.ParameterizedTest */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Repeatable(CsvSources.class) @Documented @@ -163,11 +166,12 @@ * for columns. * *

When set to {@code true}, the header names will be used in the - * generated display name for each {@code @ParameterizedTest} method - * invocation. When using this feature, you must ensure that the display name - * pattern for {@code @ParameterizedTest} includes - * {@value org.junit.jupiter.params.ParameterizedTest#ARGUMENTS_PLACEHOLDER} instead of - * {@value org.junit.jupiter.params.ParameterizedTest#ARGUMENTS_WITH_NAMES_PLACEHOLDER} + * generated display name for each {@code @ParameterizedClass} or + * {@code @ParameterizedTest} invocation. When using this feature, you must + * ensure that the display name pattern for {@code @ParameterizedClass} or + * {@code @ParameterizedTest} includes + * {@value ParameterizedInvocationConstants#ARGUMENTS_PLACEHOLDER} instead of + * {@value ParameterizedInvocationConstants#ARGUMENTS_WITH_NAMES_PLACEHOLDER} * as demonstrated in the example below. * *

Defaults to {@code false}. diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSources.java index b5e48ab5de00..3efeb23199e0 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSources.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSources.java @@ -32,7 +32,7 @@ * @see CsvSource * @see java.lang.annotation.Repeatable */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented @API(status = STABLE, since = "5.11") diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EmptyArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EmptyArgumentsProvider.java index 18e9d7d6c7b1..65657724932e 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EmptyArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EmptyArgumentsProvider.java @@ -15,7 +15,6 @@ import java.lang.reflect.Array; import java.lang.reflect.Constructor; -import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -29,6 +28,8 @@ import java.util.stream.Stream; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.support.ParameterDeclaration; +import org.junit.jupiter.params.support.ParameterDeclarations; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.util.Preconditions; @@ -39,15 +40,15 @@ class EmptyArgumentsProvider implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) { - Method testMethod = context.getRequiredTestMethod(); - Class[] parameterTypes = testMethod.getParameterTypes(); + public Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context) { - Preconditions.condition(parameterTypes.length > 0, () -> String.format( - "@EmptySource cannot provide an empty argument to method [%s]: the method does not declare any formal parameters.", - testMethod.toGenericString())); + Optional firstParameter = parameters.getFirst(); - Class parameterType = parameterTypes[0]; + Preconditions.condition(firstParameter.isPresent(), + () -> String.format("@EmptySource cannot provide an empty argument to %s: no formal parameters declared.", + parameters.getSourceElementDescription())); + + Class parameterType = firstParameter.get().getParameterType(); if (String.class.equals(parameterType)) { return Stream.of(arguments("")); @@ -88,8 +89,8 @@ public Stream provideArguments(ExtensionContext context) { } // else throw new PreconditionViolationException( - String.format("@EmptySource cannot provide an empty argument to method [%s]: [%s] is not a supported type.", - testMethod.toGenericString(), parameterType.getName())); + String.format("@EmptySource cannot provide an empty argument to %s: [%s] is not a supported type.", + parameters.getSourceElementDescription(), parameterType.getName())); } private static Optional> getDefaultConstructor(Class clazz) { diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EmptySource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EmptySource.java index fef989fc810d..3e0a9386f65c 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EmptySource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EmptySource.java @@ -22,12 +22,13 @@ /** * {@code @EmptySource} is an {@link ArgumentsSource} which provides a single - * empty argument to the annotated {@code @ParameterizedTest} method. + * empty argument to the annotated {@code @ParameterizedClass} + * or {@code @ParameterizedTest}. * *

Supported Parameter Types

* *

This argument source will only provide an empty argument for the following - * method parameter types. + * parameter types. * *