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. * *

    *
  • {@link java.lang.String}
  • @@ -45,11 +46,12 @@ * * @since 5.4 * @see org.junit.jupiter.params.provider.ArgumentsSource + * @see org.junit.jupiter.params.ParameterizedClass * @see org.junit.jupiter.params.ParameterizedTest * @see NullSource * @see NullAndEmptySource */ -@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/EnumArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumArgumentsProvider.java index 27e2d3a57fc8..e234e3a7f722 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumArgumentsProvider.java @@ -13,12 +13,14 @@ import static java.util.Arrays.stream; import static java.util.stream.Collectors.toSet; -import java.lang.reflect.Method; import java.util.EnumSet; import java.util.Set; 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; /** @@ -27,8 +29,9 @@ class EnumArgumentsProvider extends AnnotationBasedArgumentsProvider { @Override - protected Stream provideArguments(ExtensionContext context, EnumSource enumSource) { - Set> constants = getEnumConstants(context, enumSource); + protected Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context, + EnumSource enumSource) { + Set> constants = getEnumConstants(parameters, enumSource); EnumSource.Mode mode = enumSource.mode(); String[] declaredConstantNames = enumSource.names(); if (declaredConstantNames.length > 0) { @@ -41,8 +44,9 @@ protected Stream provideArguments(ExtensionContext context, return constants.stream().map(Arguments::of); } - private > Set getEnumConstants(ExtensionContext context, EnumSource enumSource) { - Class enumClass = determineEnumClass(context, enumSource); + private > Set getEnumConstants(ParameterDeclarations parameters, + EnumSource enumSource) { + Class enumClass = determineEnumClass(parameters, enumSource); E[] constants = enumClass.getEnumConstants(); if (constants.length == 0) { Preconditions.condition(enumSource.from().isEmpty() && enumSource.to().isEmpty(), @@ -59,17 +63,18 @@ private > Set getEnumConstants(ExtensionContext c } @SuppressWarnings({ "unchecked", "rawtypes" }) - private > Class determineEnumClass(ExtensionContext context, EnumSource enumSource) { + private > Class determineEnumClass(ParameterDeclarations parameters, EnumSource enumSource) { Class enumClass = enumSource.value(); if (enumClass.equals(NullEnum.class)) { - Method method = context.getRequiredTestMethod(); - Class[] parameterTypes = method.getParameterTypes(); - Preconditions.condition(parameterTypes.length > 0, - () -> "Test method must declare at least one parameter: " + method.toGenericString()); - Preconditions.condition(Enum.class.isAssignableFrom(parameterTypes[0]), - () -> "First parameter must reference an Enum type (alternatively, use the annotation's 'value' attribute to specify the type explicitly): " - + method.toGenericString()); - enumClass = parameterTypes[0]; + enumClass = parameters.getFirst() // + .map(ParameterDeclaration::getParameterType).map(parameterType -> { + Preconditions.condition(Enum.class.isAssignableFrom(parameterType), + () -> "First parameter must reference an Enum type (alternatively, use the annotation's 'value' attribute to specify the type explicitly): " + + parameters.getSourceElementDescription()); + return (Class) parameterType; + }).orElseThrow( + () -> new PreconditionViolationException("There must be at least one declared parameter for " + + parameters.getSourceElementDescription())); } return enumClass; } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSource.java index 20eb707d638d..ac7032234f5d 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSource.java @@ -34,20 +34,21 @@ * {@link ArgumentsSource} for constants of an {@link Enum}. * *

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

    The enum type can be specified explicitly using the {@link #value} * attribute. Otherwise, the declared type of the first parameter of the - * {@code @ParameterizedTest} method is used. + * {@code @ParameterizedClass} or {@code @ParameterizedTest} is used. * *

    The set of enum constants can be restricted via the {@link #names}, * {@link #from}, {@link #to} and {@link #mode} attributes. * * @since 5.0 * @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(EnumSources.class) diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSources.java index 610589378783..0d48d47f162a 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSources.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSources.java @@ -32,7 +32,7 @@ * @see EnumSource * @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/FieldArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldArgumentsProvider.java index 8d02863a9e34..6062b7d1bc8b 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldArgumentsProvider.java @@ -14,15 +14,18 @@ import static java.util.Arrays.stream; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Iterator; +import java.util.Optional; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.BaseStream; import java.util.stream.Stream; 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.support.ModifierSupport; import org.junit.platform.commons.support.ReflectionSupport; @@ -40,12 +43,16 @@ class FieldArgumentsProvider extends AnnotationBasedArgumentsProvider { @Override - protected Stream provideArguments(ExtensionContext context, FieldSource fieldSource) { + protected Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context, + FieldSource fieldSource) { Class testClass = context.getRequiredTestClass(); Object testInstance = context.getTestInstance().orElse(null); String[] fieldNames = fieldSource.value(); if (fieldNames.length == 0) { - fieldNames = new String[] { context.getRequiredTestMethod().getName() }; + Optional testMethod = context.getTestMethod(); + Preconditions.condition(testMethod.isPresent(), + "You must specify a field name when using @FieldSource with @ContainerTemplate"); + fieldNames = new String[] { testMethod.get().getName() }; } // @formatter:off return stream(fieldNames) diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java index 8c2db1a90fb1..65a376b6203a 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java @@ -20,6 +20,7 @@ import java.lang.annotation.Target; import org.apiguardian.api.API; +import org.junit.jupiter.params.ParameterizedClass; import org.junit.jupiter.params.ParameterizedTest; /** @@ -32,7 +33,8 @@ *

    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 - * {@link ParameterizedTest @ParameterizedTest} method. + * {@link ParameterizedClass @ParameterizedClass} or + * {@link ParameterizedTest @ParameterizedTest}. * *

    In this context, a "stream" is anything that JUnit can reliably convert to * a {@link java.util.stream.Stream Stream}; however, the actual concrete field @@ -46,8 +48,8 @@ * {@link java.util.Iterator Iterator}, an array of objects, or an array of * primitives. Each set of "arguments" within the "stream" can be supplied as an * instance of {@link Arguments}, an array of objects (for example, {@code Object[]}, - * {@code String[]}, etc.), or a single value if the parameterized test - * method accepts a single argument. + * {@code String[]}, etc.), or a single value if the parameterized + * class or test accepts a single argument. * *

    In contrast to the supported return types for {@link MethodSource @MethodSource} * factory methods, the value of a {@code @FieldSource} field cannot be an instance of @@ -104,14 +106,19 @@ * test instance lifecycle mode is used; whereas, fields in external classes must * always be {@code static}. * + *

    This behavior and the above examples also apply to parameters of a + * {@link ParameterizedClass @ParameterizedClass}, regardless whether field or + * constructor injection is used. + * * @since 5.11 * @see MethodSource * @see Arguments * @see ArgumentsSource + * @see ParameterizedClass * @see ParameterizedTest * @see org.junit.jupiter.api.TestInstance */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented @Repeatable(FieldSources.class) @@ -131,7 +138,10 @@ * static nested class. * *

    If no field names are declared, a field within the test class that has - * the same name as the test method will be used as the field by default. + * the same name as the test method will be used as the field by default in + * case this annotation is applied to a {@code @ParameterizedTest} method. + * For a {@code @ParameterizedClass}, at least one field name must be + * declared explicitly. * *

    For further information, see the {@linkplain FieldSource class-level Javadoc}. */ diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSources.java index f0ca8ad87940..c820347c0601 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSources.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSources.java @@ -32,7 +32,7 @@ * @see FieldSource * @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 = EXPERIMENTAL, since = "5.11") diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodArgumentsProvider.java index 3bfced72e817..2598534c0bdc 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodArgumentsProvider.java @@ -18,6 +18,7 @@ import java.lang.reflect.Method; import java.util.List; +import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Stream; @@ -25,6 +26,7 @@ import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.TestTemplate; 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.ClassLoaderUtils; import org.junit.platform.commons.util.CollectionUtils; @@ -41,9 +43,10 @@ class MethodArgumentsProvider extends AnnotationBasedArgumentsProvider isConvertibleToStream(method.getReturnType()) && !isTestMethod(method); @Override - protected Stream provideArguments(ExtensionContext context, MethodSource methodSource) { + protected Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context, + MethodSource methodSource) { Class testClass = context.getRequiredTestClass(); - Method testMethod = context.getRequiredTestMethod(); + Optional testMethod = context.getTestMethod(); Object testInstance = context.getTestInstance().orElse(null); String[] methodNames = methodSource.value(); // @formatter:off @@ -56,13 +59,15 @@ protected Stream provideArguments(ExtensionContext context, // @formatter:on } - private static Method findFactoryMethod(Class testClass, Method testMethod, String factoryMethodName) { + private static Method findFactoryMethod(Class testClass, Optional testMethod, String factoryMethodName) { String originalFactoryMethodName = factoryMethodName; // If the user did not provide a factory method name, find a "default" local // factory method with the same name as the parameterized test method. if (StringUtils.isBlank(factoryMethodName)) { - factoryMethodName = testMethod.getName(); + Preconditions.condition(testMethod.isPresent(), + "You must specify a method name when using @MethodSource with @ContainerTemplate"); + factoryMethodName = testMethod.get().getName(); return findFactoryMethodBySimpleName(testClass, testMethod, factoryMethodName); } @@ -103,7 +108,7 @@ private static boolean looksLikeAFullyQualifiedMethodName(String factoryMethodNa } // package-private for testing - static Method findFactoryMethodByFullyQualifiedName(Class testClass, Method testMethod, + static Method findFactoryMethodByFullyQualifiedName(Class testClass, Optional testMethod, String fullyQualifiedMethodName) { String[] methodParts = ReflectionUtils.parseFullyQualifiedMethodName(fullyQualifiedMethodName); String className = methodParts[0]; @@ -142,24 +147,25 @@ static Method findFactoryMethodByFullyQualifiedName(Class testClass, Method t * @throws PreconditionViolationException if the factory method was not found or * multiple competing factory methods with the same name were found */ - private static Method findFactoryMethodBySimpleName(Class clazz, Method testMethod, String factoryMethodName) { + private static Method findFactoryMethodBySimpleName(Class clazz, Optional testMethod, + String factoryMethodName) { Predicate isCandidate = candidate -> factoryMethodName.equals(candidate.getName()) - && !testMethod.equals(candidate); + && !candidate.equals(testMethod.orElse(null)); List candidates = ReflectionUtils.findMethods(clazz, isCandidate); List factoryMethods = candidates.stream().filter(isFactoryMethod).collect(toList()); - Preconditions.condition(factoryMethods.size() > 0, () -> { + Preconditions.notEmpty(factoryMethods, () -> { + if (candidates.isEmpty()) { + // Report that we didn't find anything. + return format("Could not find factory method [%s] in class [%s]", factoryMethodName, clazz.getName()); + } // If we didn't find the factory method using the isFactoryMethod Predicate, perhaps // the specified factory method has an invalid return type or is a test method. // In that case, we report the invalid candidates that were found. - if (candidates.size() > 0) { - return format( - "Could not find valid factory method [%s] in class [%s] but found the following invalid candidates: %s", - factoryMethodName, clazz.getName(), candidates); - } - // Otherwise, report that we didn't find anything. - return format("Could not find factory method [%s] in class [%s]", factoryMethodName, clazz.getName()); + return format( + "Could not find valid factory method [%s] in class [%s] but found the following invalid candidates: %s", + factoryMethodName, clazz.getName(), candidates); }); Preconditions.condition(factoryMethods.size() == 1, () -> format("%d factory methods named [%s] were found in class [%s]: %s", factoryMethods.size(), diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java index 2ea6da4da72f..a5cc3542db38 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java @@ -20,6 +20,7 @@ import java.lang.annotation.Target; import org.apiguardian.api.API; +import org.junit.jupiter.params.ParameterizedClass; import org.junit.jupiter.params.ParameterizedTest; /** @@ -30,9 +31,10 @@ * by fully qualified method name. * *

    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 - * {@link ParameterizedTest @ParameterizedTest} method. Generally speaking this + * and each set of "arguments" within the "stream" will be provided as the + * physical arguments for individual invocations of the annotated + * {@code ParameterizedClass @ParameterizedClass} or + * {@link ParameterizedTest @ParameterizedTest}. Generally speaking this * translates to a {@link java.util.stream.Stream Stream} of {@link Arguments} * (i.e., {@code Stream}); however, the actual concrete return type * can take on many forms. In this context, a "stream" is anything that JUnit @@ -92,6 +94,10 @@ * test instance lifecycle mode is used; whereas, factory methods in external * classes must always be {@code static}. * + *

    This behavior and the above examples also apply to parameters of a + * {@link ParameterizedClass @ParameterizedClass}, regardless whether field or + * constructor injection is used. + * *

    Factory methods can declare parameters, which will be provided by registered * implementations of {@link org.junit.jupiter.api.extension.ParameterResolver}. * @@ -99,10 +105,11 @@ * @see FieldSource * @see Arguments * @see ArgumentsSource + * @see ParameterizedClass * @see ParameterizedTest * @see org.junit.jupiter.api.TestInstance */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented @Repeatable(MethodSources.class) @@ -131,7 +138,9 @@ * *

    If no factory method names are declared, a method within the test class * that has the same name as the test method will be used as the factory - * method by default. + * method by default in case this annotation is applied to a + * {@code @ParameterizedTest} method. For a {@code @ParameterizedClass}, at + * least one method name must be declared explicitly. * *

    For further information, see the {@linkplain MethodSource class-level Javadoc}. */ diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSources.java index 605702827d2f..84bdcdda56b7 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSources.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSources.java @@ -32,7 +32,7 @@ * @see MethodSource * @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/NullAndEmptySource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullAndEmptySource.java index d38b2dff4ee4..6b9daf925a15 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullAndEmptySource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullAndEmptySource.java @@ -25,16 +25,17 @@ * the functionality of {@link NullSource @NullSource} and * {@link EmptySource @EmptySource}. * - *

    Annotating a {@code @ParameterizedTest} method with - * {@code @NullAndEmptySource} is equivalent to annotating the method with - * {@code @NullSource} and {@code @EmptySource}. + *

    Annotating a {@code @ParameterizedClass} or {@code @ParameterizedTest} + * with {@code @NullAndEmptySource} is equivalent to annotating the method with + * both {@code @NullSource} and {@code @EmptySource}. * * @since 5.4 + * @see org.junit.jupiter.params.ParameterizedClass * @see org.junit.jupiter.params.ParameterizedTest * @see NullSource * @see EmptySource */ -@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/NullArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullArgumentsProvider.java index eef9d19990c6..8cafbb51e3cf 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullArgumentsProvider.java @@ -12,10 +12,10 @@ import static org.junit.jupiter.params.provider.Arguments.arguments; -import java.lang.reflect.Method; import java.util.stream.Stream; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.support.ParameterDeclarations; import org.junit.platform.commons.util.Preconditions; /** @@ -27,11 +27,10 @@ class NullArgumentsProvider implements ArgumentsProvider { private static final Arguments nullArguments = arguments(new Object[] { null }); @Override - public Stream provideArguments(ExtensionContext context) { - Method testMethod = context.getRequiredTestMethod(); - Preconditions.condition(testMethod.getParameterCount() > 0, () -> String.format( - "@NullSource cannot provide a null argument to method [%s]: the method does not declare any formal parameters.", - testMethod.toGenericString())); + public Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context) { + Preconditions.condition(parameters.getFirst().isPresent(), + () -> String.format("@NullSource cannot provide a null argument to %s: no formal parameters declared.", + parameters.getSourceElementDescription())); return Stream.of(nullArguments); } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullSource.java index 3dce2cb097ad..dac89bb2a7d8 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/NullSource.java @@ -22,7 +22,8 @@ /** * {@code @NullSource} is an {@link ArgumentsSource} which provides a single - * {@code null} argument to the annotated {@code @ParameterizedTest} method. + * {@code null} argument to the annotated {@code @ParameterizedClass} or + * {@code @ParameterizedTest}. * *

    Note that {@code @NullSource} cannot be used for an argument that has * a primitive type, unless the argument is converted to a corresponding wrapper @@ -30,11 +31,12 @@ * * @since 5.4 * @see org.junit.jupiter.params.provider.ArgumentsSource + * @see org.junit.jupiter.params.ParameterizedClass * @see org.junit.jupiter.params.ParameterizedTest * @see EmptySource * @see NullAndEmptySource */ -@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/ValueArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueArgumentsProvider.java index 39bc714671da..a3a929dfa2ba 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueArgumentsProvider.java @@ -19,6 +19,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.support.ParameterDeclarations; import org.junit.platform.commons.util.Preconditions; /** @@ -27,7 +28,8 @@ class ValueArgumentsProvider extends AnnotationBasedArgumentsProvider { @Override - protected Stream provideArguments(ExtensionContext context, ValueSource valueSource) { + protected Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context, + ValueSource valueSource) { Object[] arguments = getArgumentsFromSource(valueSource); return Arrays.stream(arguments).map(Arguments::of); } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSource.java index 55d8c50aaa2c..080a8e1c934c 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSource.java @@ -20,6 +20,7 @@ import java.lang.annotation.Target; import org.apiguardian.api.API; +import org.junit.jupiter.params.ParameterizedClass; /** * {@code @ValueSource} is a {@linkplain Repeatable repeatable} @@ -32,13 +33,14 @@ * {@code @ValueSource} declaration. * *

    The supplied literal values will be provided as arguments to the - * annotated {@code @ParameterizedTest} method. + * annotated {@code @ParameterizedClass} or {@code @ParameterizedTest}. * * @since 5.0 * @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) @Documented @Repeatable(ValueSources.class) diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSources.java index 6d52255d9713..09528228d06a 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSources.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSources.java @@ -32,7 +32,7 @@ * @see ValueSource * @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/support/AnnotationConsumerInitializer.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/AnnotationConsumerInitializer.java index 27f6b52853d7..785c9e571fc4 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/AnnotationConsumerInitializer.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/AnnotationConsumerInitializer.java @@ -40,7 +40,7 @@ public final class AnnotationConsumerInitializer { private static final List annotationConsumingMethodSignatures = asList( // new AnnotationConsumingMethodSignature("accept", 1, 0), // - new AnnotationConsumingMethodSignature("provideArguments", 2, 1), // + new AnnotationConsumingMethodSignature("provideArguments", 3, 2), // new AnnotationConsumingMethodSignature("convert", 3, 2)); private AnnotationConsumerInitializer() { diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/FieldContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/FieldContext.java new file mode 100644 index 000000000000..355da13d126e --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/FieldContext.java @@ -0,0 +1,50 @@ +/* + * 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.support; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.reflect.Field; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.AnnotatedElementContext; +import org.junit.jupiter.params.Parameter; +import org.junit.jupiter.params.ParameterizedClass; + +/** + * {@code FieldContext} encapsulates the context in which an + * {@link Parameter @Parameter}-annotated {@link Field} is declared in a + * {@link ParameterizedClass @ParameterizedClass}. + * + * @since 5.13 + * @see ParameterizedClass + * @see Parameter + */ +@API(status = EXPERIMENTAL, since = "5.13") +public interface FieldContext extends AnnotatedElementContext { + + /** + * {@return the field for this context; never {@code null}} + */ + Field getField(); + + /** + * {@return the index of the parameter} + * + *

    This method returns {@value Parameter#UNSET_INDEX} for aggregator + * fields and a value greater than or equal to zero for indexed + * parameters. + * + * @see Parameter#value() + */ + int getParameterIndex(); + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterDeclaration.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterDeclaration.java new file mode 100644 index 000000000000..fca970de9df6 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterDeclaration.java @@ -0,0 +1,55 @@ +/* + * 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.support; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.reflect.AnnotatedElement; +import java.util.Optional; + +import org.apiguardian.api.API; + +/** + * {@code ParameterDeclaration} encapsulates the declaration of an + * indexed {@code @ParameterizedClass} or {@code @ParameterizedTest} parameter. + * + * @since 5.13 + * @see ParameterDeclarations + */ +@API(status = EXPERIMENTAL, since = "5.13") +public interface ParameterDeclaration { + + /** + * {@return the {@link AnnotatedElement} that declares the parameter; never + * {@code null}} + * + *

    This is either a {@link java.lang.reflect.Parameter} or a + * {@link java.lang.reflect.Field}. + */ + AnnotatedElement getAnnotatedElement(); + + /** + * {@return the type of the parameter; never {@code null}} + */ + Class getParameterType(); + + /** + * {@return the index of the parameter} + */ + int getParameterIndex(); + + /** + * {@return the name of the parameter, if available; never {@code null} but + * potentially empty} + */ + Optional getParameterName(); + +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterDeclarations.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterDeclarations.java new file mode 100644 index 000000000000..bc30402ad63f --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/ParameterDeclarations.java @@ -0,0 +1,83 @@ +/* + * 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.support; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.reflect.AnnotatedElement; +import java.util.List; +import java.util.Optional; + +import org.apiguardian.api.API; +import org.junit.jupiter.params.aggregator.ArgumentsAccessor; + +/** + * {@code ParameterDeclarations} encapsulates the combined declarations + * of all indexed {@code @ParameterizedClass} or + * {@code @ParameterizedTest} parameters. + * + *

    For a {@code @ParameterizedTest}, the parameter declarations are derived + * from the method signature. For a {@code @ParameterizedClass}, they may be + * derived from the constructor or + * {@link java.lang.reflect.Parameter @Parameter}-annotated fields. + * + *

    Aggregators, that is parameters of type + * {@link ArgumentsAccessor ArgumentsAccessor} or parameters annotated with + * {@link org.junit.jupiter.params.aggregator.AggregateWith @AggregateWith}, are + * not indexed and thus not included in the list of parameter + * declarations. + * + * @since 5.13 + * @see ParameterDeclaration + * @see org.junit.jupiter.params.ParameterizedClass + * @see org.junit.jupiter.params.ParameterizedTest + */ +@API(status = EXPERIMENTAL, since = "5.13") +public interface ParameterDeclarations { + + /** + * {@return all indexed parameter declarations; never {@code null}, + * sorted by index} + */ + List getAll(); + + /** + * {@return the first indexed parameter declaration, if available; + * never {@code null}} + */ + Optional getFirst(); + + /** + * {@return the indexed parameter declaration for the supplied + * index, if available; never {@code null}} + */ + Optional get(int parameterIndex); + + /** + * {@return the source element of all parameter declarations} + * + *

    For {@code @ParameterizedTest}, this always corresponds to the + * parameterized test method. For {@code @ParameterizedClass}, this + * corresponds to the parameterized test class constructor, if constructor + * injection is used; or the test class itself, if field injection is used. + */ + AnnotatedElement getSourceElement(); + + /** + * {@return a human-readable description of the source element} + * + *

    This may, for example, be used in error messages. + * + * @see #getSourceElement() + */ + String getSourceElementDescription(); + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java new file mode 100644 index 000000000000..d4cf4e98e376 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedClassIntegrationTests.java @@ -0,0 +1,1384 @@ +/* + * 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.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +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.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.junit.jupiter.params.ArgumentCountValidationMode.NONE; +import static org.junit.jupiter.params.ArgumentCountValidationMode.STRICT; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.ARGUMENTS_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.DISPLAY_NAME_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.INDEX_PLACEHOLDER; +import static org.junit.jupiter.params.provider.Arguments.argumentSet; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +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.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.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestReporter; +import org.junit.jupiter.api.extension.AnnotatedElementContext; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; +import org.junit.jupiter.engine.Constants; +import org.junit.jupiter.engine.descriptor.ContainerTemplateInvocationTestDescriptor; +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.SimpleArgumentsAggregator; +import org.junit.jupiter.params.converter.ArgumentConversionException; +import org.junit.jupiter.params.converter.ConvertWith; +import org.junit.jupiter.params.converter.SimpleArgumentConverter; +import org.junit.jupiter.params.converter.TypedArgumentConverter; +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.provider.CsvFileSource; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.FieldSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.params.support.ParameterDeclarations; +import org.junit.platform.commons.util.StringUtils; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.reporting.ReportEntry; +import org.junit.platform.testkit.engine.EngineExecutionResults; +import org.junit.platform.testkit.engine.Event; + +@SuppressWarnings("JUnitMalformedDeclaration") +public class ParameterizedClassIntegrationTests extends AbstractJupiterTestEngineTests { + + @ParameterizedTest + @ValueSource(classes = { ConstructorInjectionTestCase.class, RecordTestCase.class, + RecordWithParameterAnnotationOnComponentTestCase.class, ParameterizedDataClassTestCase.class, + FieldInjectionTestCase.class, RecordWithBuiltInConverterTestCase.class, + RecordWithRegisteredConversionTestCase.class, FieldInjectionWithRegisteredConversionTestCase.class, + RecordWithBuiltInAggregatorTestCase.class, FieldInjectionWithBuiltInAggregatorTestCase.class, + RecordWithCustomAggregatorTestCase.class, FieldInjectionWithCustomAggregatorTestCase.class }) + void injectsParametersIntoContainerTemplate(Class containerTemplateClass) { + + var results = executeTestsForClass(containerTemplateClass); + + String parameterNamePrefix = containerTemplateClass.getSimpleName().contains("Aggregator") ? "" : "value="; + + results.allEvents().assertEventsMatchExactly( // + event(engine(), started()), // + event(container(containerTemplateClass), started()), // + + event(dynamicTestRegistered("#1"), displayName("[1] %s-1".formatted(parameterNamePrefix))), // + event(container("#1"), started()), // + event(dynamicTestRegistered("test1")), // + event(dynamicTestRegistered("test2")), // + event(test("test1"), started()), // + event(test("test1"), finishedSuccessfully()), // + event(test("test2"), started()), // + event(test("test2"), finishedSuccessfully()), // + event(container("#1"), finishedSuccessfully()), // + + event(dynamicTestRegistered("#2"), displayName("[2] %s1".formatted(parameterNamePrefix))), // + event(container("#2"), started()), // + event(dynamicTestRegistered("test1")), // + event(dynamicTestRegistered("test2")), // + event(test("test1"), started()), // + event(test("test1"), finishedWithFailure(message(it -> it.contains("negative")))), // + event(test("test2"), started()), // + event(test("test2"), finishedWithFailure(message(it -> it.contains("negative")))), // + event(container("#2"), finishedSuccessfully()), // + + event(container(containerTemplateClass), finishedSuccessfully()), // + event(engine(), finishedSuccessfully())); + } + + @ParameterizedTest + @ValueSource(classes = { //NullAndEmptySourceConstructorInjectionTestCase.class, + NullAndEmptySourceConstructorFieldInjectionTestCase.class }) + void supportsNullAndEmptySource(Class containerTemplateClass) { + + var results = executeTestsForClass(containerTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6)); + assertThat(invocationDisplayNames(results)) // + .containsExactly("[1] value=null", "[2] value="); + } + + @ParameterizedTest + @ValueSource(classes = { CsvFileSourceConstructorInjectionTestCase.class, + CsvFileSourceFieldInjectionTestCase.class }) + void supportsCsvFileSource(Class containerTemplateClass) { + + var results = executeTestsForClass(containerTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(10).succeeded(10)); + assertThat(invocationDisplayNames(results)) // + .containsExactly("[1] name=foo, value=1", "[2] name=bar, value=2", "[3] name=baz, value=3", + "[4] name=qux, value=4"); + } + + @ParameterizedTest + @ValueSource(classes = { SingleEnumSourceConstructorInjectionTestCase.class, + SingleEnumSourceFieldInjectionTestCase.class }) + void supportsSingleEnumSource(Class containerTemplateClass) { + + var results = executeTestsForClass(containerTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + assertThat(invocationDisplayNames(results)) // + .containsExactly("[1] value=FOO"); + } + + @ParameterizedTest + @ValueSource(classes = { RepeatedEnumSourceConstructorInjectionTestCase.class, + RepeatedEnumSourceFieldInjectionTestCase.class }) + void supportsRepeatedEnumSource(Class containerTemplateClass) { + + var results = executeTestsForClass(containerTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6)); + assertThat(invocationDisplayNames(results)) // + .containsExactly("[1] value=FOO", "[2] value=BAR"); + } + + @ParameterizedTest + @ValueSource(classes = { MethodSourceConstructorInjectionTestCase.class, MethodSourceFieldInjectionTestCase.class }) + void supportsMethodSource(Class containerTemplateClass) { + + var results = executeTestsForClass(containerTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6)); + assertThat(invocationDisplayNames(results)) // + .containsExactly("[1] value=foo", "[2] value=bar"); + } + + @Test + void doesNotSupportDerivingMethodName() { + + var results = executeTestsForClass(MethodSourceWithoutMethodNameTestCase.class); + + results.allEvents().failed() // + .assertEventsMatchExactly(finishedWithFailure( + message("You must specify a method name when using @MethodSource with @ContainerTemplate"))); + } + + @ParameterizedTest + @ValueSource(classes = { FieldSourceConstructorInjectionTestCase.class, FieldSourceFieldInjectionTestCase.class }) + void supportsFieldSource(Class containerTemplateClass) { + + var results = executeTestsForClass(containerTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6)); + assertThat(invocationDisplayNames(results)) // + .containsExactly("[1] value=foo", "[2] value=bar"); + } + + @Test + void doesNotSupportDerivingFieldName() { + + var results = executeTestsForClass(FieldSourceWithoutFieldNameTestCase.class); + + results.allEvents().failed() // + .assertEventsMatchExactly(finishedWithFailure( + message("You must specify a field name when using @FieldSource with @ContainerTemplate"))); + } + + @ParameterizedTest + @ValueSource(classes = { ArgumentsSourceConstructorInjectionTestCase.class, + ArgumentsSourceFieldInjectionTestCase.class }) + void supportsArgumentsSource(Class containerTemplateClass) { + + var results = executeTestsForClass(containerTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6)); + assertThat(invocationDisplayNames(results)) // + .containsExactly("[1] value=foo", "[2] value=bar"); + } + + @Test + void supportsCustomNamePatterns() { + + var results = executeTestsForClass(CustomNamePatternTestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6)); + assertThat(invocationDisplayNames(results)) // + .containsExactly("1 | TesT | 1, foo | set", "2 | TesT | 2, bar | number=2, name=bar"); + } + + @Test + void closesAutoCloseableArguments() { + AutoCloseableArgument.closeCounter = 0; + + var results = executeTestsForClass(AutoCloseableArgumentTestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + assertThat(AutoCloseableArgument.closeCounter).isEqualTo(2); + } + + @Test + void doesNotCloseAutoCloseableArgumentsWhenDisabled() { + AutoCloseableArgument.closeCounter = 0; + + var results = executeTestsForClass(AutoCloseableArgumentWithDisabledCleanupTestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + assertThat(AutoCloseableArgument.closeCounter).isEqualTo(0); + } + + @Test + void failsOnStrictArgumentCountValidationMode() { + var results = executeTestsForClass(StrictArgumentCountValidationModeTestCase.class); + + results.allEvents().assertThatEvents() // + .haveExactly(1, event(finishedWithFailure(message( + "Configuration error: @ParameterizedClass consumes 1 parameter but there were 2 arguments provided.%nNote: the provided arguments were [foo, unused]".formatted())))); + } + + @ParameterizedTest + @ValueSource(classes = { NoneArgumentCountValidationModeTestCase.class, + DefaultArgumentCountValidationModeTestCase.class }) + void doesNotFailOnNoneOrDefaultArgumentCountValidationMode(Class containerTemplateClass) { + + var results = executeTestsForClass(containerTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(4).succeeded(4)); + } + + @Test + void failsOnStrictArgumentCountValidationModeSetViaConfigurationParameter() { + var results = executeTests(request -> request // + .selectors(selectClass(DefaultArgumentCountValidationModeTestCase.class)).configurationParameter( + ArgumentCountValidator.ARGUMENT_COUNT_VALIDATION_KEY, STRICT.name())); + + results.allEvents().assertThatEvents() // + .haveExactly(1, event(finishedWithFailure(message( + "Configuration error: @ParameterizedClass consumes 1 parameter but there were 2 arguments provided.%nNote: the provided arguments were [foo, unused]".formatted())))); + } + + @Test + void failsForSkippedParameters() { + var results = executeTestsForClass(InvalidUnusedParameterIndexesTestCase.class); + + results.allEvents().assertThatEvents() // + .haveExactly(1, event(finishedWithFailure(message( + "2 configuration errors:%n- no field annotated with @Parameter(0) declared%n- no field annotated with @Parameter(2) declared".formatted())))); + } + + @Test + void failsWhenInvocationIsRequiredButNoArgumentSetsAreProvided() { + var results = executeTestsForClass(ForbiddenZeroInvocationsTestCase.class); + + results.containerEvents().assertThatEvents() // + .haveExactly(1, event(finishedWithFailure(message( + "Configuration error: You must configure at least one set of arguments for this @ParameterizedClass")))); + } + + @Test + void doesNotFailWhenInvocationIsNotRequiredAndNoArgumentSetsAreProvided() { + var results = executeTestsForClass(AllowedZeroInvocationsTestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(2).succeeded(2)); + } + + @Test + void failsWhenNoArgumentsSourceIsDeclared() { + var results = executeTestsForClass(NoArgumentSourceTestCase.class); + + results.containerEvents().assertThatEvents() // + .haveExactly(1, event(finishedWithFailure(message( + "Configuration error: You must configure at least one arguments source for this @ParameterizedClass")))); + } + + @ParameterizedTest + @ValueSource(classes = { NestedFieldInjectionTestCase.class, NestedConstructorInjectionTestCase.class }) + void supportsNestedParameterizedClasss(Class containerTemplateClass) { + + var results = executeTestsForClass(containerTemplateClass); + + results.containerEvents().assertStatistics(stats -> stats.started(14).succeeded(14)); + results.testEvents().assertStatistics(stats -> stats.started(8).succeeded(8)); + assertThat(invocationDisplayNames(results)) // + .containsExactly( // + "[1] number=1", "[1] text=foo", "[2] text=bar", // + "[2] number=2", "[1] text=foo", "[2] text=bar" // + ); + } + + @ParameterizedTest + @ValueSource(classes = { ConstructorInjectionWithRegularNestedTestCase.class, + FieldInjectionWithRegularNestedTestCase.class }) + void supportsRegularNestedTestClassesInsideParameterizedClass(Class containerTemplateClass) { + + var results = executeTestsForClass(containerTemplateClass); + + results.containerEvents().assertStatistics(stats -> stats.started(6).succeeded(6)); + results.testEvents().assertStatistics(stats -> stats.started(2).succeeded(2)); + } + + @Test + void supportsMultipleAggregatorFields() { + + var results = executeTestsForClass(MultiAggregatorFieldInjectionTestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6)); + } + + @Test + void supportsFieldInjectionForTestInstanceLifecyclePerClass() { + + var results = executeTestsForClass(FieldInjectionWithPerClassTestInstanceLifecycleTestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(8).succeeded(8)); + + assertThat(allReportEntries(results).map(it -> it.get("value"))) // + .containsExactly("foo", "foo", "bar", "bar"); + assertThat(allReportEntries(results).map(it -> it.get("instanceHashCode")).distinct()) // + .hasSize(1); + } + + @Test + void doesNotSupportConstructorInjectionForTestInstanceLifecyclePerClass() { + + var results = executeTests(request -> request // + .selectors(selectClass(ConstructorInjectionTestCase.class)) // + .configurationParameter(Constants.DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME, PER_CLASS.name())); + + results.allEvents().assertThatEvents() // + .haveExactly(1, finishedWithFailure(message(it -> it.contains( + "Constructor injection is not supported for @ParameterizedClass classes with @TestInstance(Lifecycle.PER_CLASS)")))); + } + + @Test + void supportsInjectionOfInheritedFields() { + + var results = executeTestsForClass(InheritedHiddenParameterFieldTestCase.class); + + results.allEvents().assertStatistics(stats -> stats.started(6).succeeded(6)); + + assertThat(allReportEntries(results)) // + .extracting(it -> tuple(it.get("super.value"), it.get("this.value"))) // + .containsExactly(tuple("foo", "1"), tuple("bar", "2")); + } + + @Test + void doesNotSupportInjectionForFinalFields() { + + var containerTemplateClass = InvalidFinalFieldTestCase.class; + + var results = executeTestsForClass(containerTemplateClass); + + results.allEvents().assertThatEvents() // + .haveExactly(1, finishedWithFailure(message( + "Configuration error: @Parameter field [final int %s.i] must not be declared as final.".formatted( + containerTemplateClass.getName())))); + } + + @Test + void aggregatorFieldsMustNotDeclareIndex() { + + var containerTemplateClass = InvalidAggregatorFieldWithIndexTestCase.class; + + var results = executeTestsForClass(containerTemplateClass); + + results.allEvents().assertThatEvents() // + .haveExactly(1, finishedWithFailure(message( + "Configuration error: no index may be declared in @Parameter(0) annotation on aggregator field [%s %s.accessor].".formatted( + ArgumentsAccessor.class.getName(), containerTemplateClass.getName())))); + } + + @Test + void declaredIndexMustNotBeNegative() { + + var containerTemplateClass = InvalidParameterIndexTestCase.class; + + var results = executeTestsForClass(containerTemplateClass); + + results.allEvents().assertThatEvents() // + .haveExactly(1, finishedWithFailure(message( + "Configuration error: index must be greater than or equal to zero in @Parameter(-42) annotation on field [int %s.i].".formatted( + containerTemplateClass.getName())))); + } + + @Test + void declaredIndexMustBeUnique() { + + var containerTemplateClass = InvalidDuplicateParameterDeclarationTestCase.class; + + var results = executeTestsForClass(containerTemplateClass); + + results.allEvents().assertThatEvents() // + .haveExactly(1, finishedWithFailure(message( + "Configuration error: duplicate index declared in @Parameter(0) annotation on fields [int %s.i, long %s.l].".formatted( + containerTemplateClass.getName(), containerTemplateClass.getName())))); + } + + @ParameterizedTest + @ValueSource(classes = { ArgumentConversionPerInvocationConstructorInjectionTestCase.class, + ArgumentConversionPerInvocationFieldInjectionTestCase.class }) + void argumentConverterIsOnlyCalledOncePerInvocation(Class containerTemplateClass) { + + var results = executeTestsForClass(containerTemplateClass); + + results.allEvents().assertStatistics(stats -> stats.started(5).succeeded(5)); + } + + // ------------------------------------------------------------------- + + private static Stream invocationDisplayNames(EngineExecutionResults results) { + return results.containerEvents() // + .started() // + .filter(uniqueId(lastSegmentType(ContainerTemplateInvocationTestDescriptor.SEGMENT_TYPE))::matches) // + .map(Event::getTestDescriptor) // + .map(TestDescriptor::getDisplayName); + } + + private static Stream> allReportEntries(EngineExecutionResults results) { + return results.allEvents().reportingEntryPublished() // + .map(e -> e.getRequiredPayload(ReportEntry.class)) // + .map(ReportEntry::getKeyValuePairs); + } + + private static Condition lastSegmentType(@SuppressWarnings("SameParameterValue") String segmentType) { + return new Condition<>(it -> segmentType.equals(it.getLastSegment().getType()), "last segment type is '%s'", + segmentType); + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClassWithNegativeAndPositiveValue + static class ConstructorInjectionTestCase { + + private int value; + private final TestInfo testInfo; + + public ConstructorInjectionTestCase(int value, TestInfo testInfo) { + this.value = value; + this.testInfo = testInfo; + } + + @Test + void test1() { + assertEquals("test1()", testInfo.getDisplayName()); + assertTrue(value < 0, "negative"); + value *= -1; + } + + @Test + void test2() { + assertEquals("test2()", testInfo.getDisplayName()); + assertTrue(value < 0, "negative"); + value *= -1; + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClassWithNegativeAndPositiveValue + record RecordTestCase(int value, TestInfo testInfo) { + + @Test + void test1() { + assertEquals("test1()", testInfo.getDisplayName()); + assertTrue(value < 0, "negative"); + } + + @Test + void test2() { + assertEquals("test2()", testInfo.getDisplayName()); + assertTrue(value < 0, "negative"); + } + } + + @ParameterizedClass + @ValueSource(ints = { -1, 1 }) + record RecordWithParameterAnnotationOnComponentTestCase(@Parameter int value) { + + @Test + void test1() { + assertTrue(value < 0, "negative"); + } + + @Test + void test2() { + assertTrue(value < 0, "negative"); + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @ValueSource(ints = { -1, 1 }) + static class FieldInjectionTestCase { + + @Parameter + private int value; + + @Test + void test1() { + assertTrue(value < 0, "negative"); + value *= -1; + } + + @Test + void test2() { + assertTrue(value < 0, "negative"); + value *= -1; + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @CsvSource({ "-1", "1" }) + record RecordWithBuiltInConverterTestCase(int value) { + + @Test + void test1() { + assertTrue(value < 0, "negative"); + } + + @Test + void test2() { + assertTrue(value < 0, "negative"); + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @ValueSource(ints = { -1, 1 }) + record RecordWithRegisteredConversionTestCase(@ConvertWith(CustomIntegerToStringConverter.class) String value) { + + @Test + void test1() { + assertTrue(value.startsWith("minus"), "negative"); + } + + @Test + void test2() { + assertTrue(value.startsWith("minus"), "negative"); + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @ValueSource(ints = { -1, 1 }) + static class FieldInjectionWithRegisteredConversionTestCase { + + @Parameter + @ConvertWith(CustomIntegerToStringConverter.class) + private String value; + + @Test + void test1() { + assertTrue(value.startsWith("minus"), "negative"); + } + + @Test + void test2() { + assertTrue(value.startsWith("minus"), "negative"); + } + } + + private static class CustomIntegerToStringConverter extends TypedArgumentConverter { + + CustomIntegerToStringConverter() { + super(Integer.class, String.class); + } + + @Override + protected String convert(Integer source) throws ArgumentConversionException { + return switch (source) { + case -1 -> "minus one"; + case +1 -> "plus one"; + default -> throw new IllegalArgumentException("Unsupported value: " + source); + }; + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @ValueSource(ints = { -1, 1 }) + record RecordWithBuiltInAggregatorTestCase(ArgumentsAccessor accessor) { + + @Test + void test1() { + assertTrue(accessor.getInteger(0) < 0, "negative"); + } + + @Test + void test2() { + assertTrue(accessor.getInteger(0) < 0, "negative"); + } + + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @ValueSource(ints = { -1, 1 }) + static class FieldInjectionWithBuiltInAggregatorTestCase { + + @Parameter + private ArgumentsAccessor accessor; + + @Test + void test1() { + assertTrue(accessor.getInteger(0) < 0, "negative"); + } + + @Test + void test2() { + assertTrue(accessor.getInteger(0) < 0, "negative"); + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @ValueSource(ints = { -1, 1 }) + record RecordWithCustomAggregatorTestCase(@AggregateWith(TimesTwoAggregator.class) int value) { + + @Test + void test1() { + assertTrue(value <= -2, "negative"); + } + + @Test + void test2() { + assertTrue(value <= -2, "negative"); + } + + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @ValueSource(ints = { -1, 1 }) + static class FieldInjectionWithCustomAggregatorTestCase { + + @TimesTwo + private int value; + + @Test + void test1() { + assertTrue(value <= -2, "negative"); + } + + @Test + void test2() { + assertTrue(value <= -2, "negative"); + } + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @ParameterizedClass + @ValueSource(ints = { -1, 1 }) + @interface ParameterizedClassWithNegativeAndPositiveValue { + } + + private static class TimesTwoAggregator extends SimpleArgumentsAggregator { + + @Override + protected Object aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) throws ArgumentsAggregationException { + + return accessor.getInteger(0) * 2; + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @NullAndEmptySource + record NullAndEmptySourceConstructorInjectionTestCase(String value) { + @Test + void test() { + assertTrue(StringUtils.isBlank(value)); + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @NullAndEmptySource + static class NullAndEmptySourceConstructorFieldInjectionTestCase { + + @Parameter + String value; + + @Test + void test() { + assertTrue(StringUtils.isBlank(value)); + } + } + + @ParameterizedClass + @CsvFileSource(resources = "two-column.csv") + record CsvFileSourceConstructorInjectionTestCase(String name, int value) { + @Test + void test() { + assertNotNull(name); + assertTrue(value > 0 && value < 5); + } + } + + @ParameterizedClass + @CsvFileSource(resources = "two-column.csv") + static class CsvFileSourceFieldInjectionTestCase { + + @Parameter(0) + String name; + + @Parameter(1) + int value; + + @Test + void test() { + assertNotNull(name); + assertTrue(value > 0 && value < 5); + } + } + + @ParameterizedClass + @EnumSource + record SingleEnumSourceConstructorInjectionTestCase(EnumOne value) { + @Test + void test() { + assertEquals(EnumOne.FOO, value); + } + } + + @ParameterizedClass + @EnumSource + static class SingleEnumSourceFieldInjectionTestCase { + + @Parameter + EnumOne value; + + @Test + void test() { + assertEquals(EnumOne.FOO, value); + } + } + + @ParameterizedClass + @EnumSource(EnumOne.class) + @EnumSource(EnumTwo.class) + record RepeatedEnumSourceConstructorInjectionTestCase(Object value) { + @Test + void test() { + assertTrue(value == EnumOne.FOO || value == EnumTwo.BAR); + } + } + + @ParameterizedClass + @EnumSource(EnumOne.class) + @EnumSource(EnumTwo.class) + static class RepeatedEnumSourceFieldInjectionTestCase { + + @Parameter + Object value; + + @Test + void test() { + assertTrue(value == EnumOne.FOO || value == EnumTwo.BAR); + } + } + + private enum EnumOne { + FOO + } + + private enum EnumTwo { + BAR + } + + @ParameterizedClass + @MethodSource("parameters") + record MethodSourceConstructorInjectionTestCase(String value) { + + static Stream parameters() { + return Stream.of("foo", "bar"); + } + + @Test + void test() { + assertTrue(value.equals("foo") || value.equals("bar")); + } + } + + @ParameterizedClass + @MethodSource("parameters") + static class MethodSourceFieldInjectionTestCase { + + static Stream parameters() { + return Stream.of("foo", "bar"); + } + + @Parameter + String value; + + @Test + void test() { + assertTrue(value.equals("foo") || value.equals("bar")); + } + } + + @ParameterizedClass + @MethodSource + record MethodSourceWithoutMethodNameTestCase(String value) { + + @Test + void test() { + fail("should not be executed"); + } + } + + @ParameterizedClass + @FieldSource("parameters") + record FieldSourceConstructorInjectionTestCase(String value) { + + static final List parameters = List.of("foo", "bar"); + + @Test + void test() { + assertTrue(value.equals("foo") || value.equals("bar")); + } + } + + @ParameterizedClass + @FieldSource("parameters") + static class FieldSourceFieldInjectionTestCase { + + static final List parameters = List.of("foo", "bar"); + + @Parameter + String value; + + @Test + void test() { + assertTrue(value.equals("foo") || value.equals("bar")); + } + } + + @ParameterizedClass + @FieldSource + record FieldSourceWithoutFieldNameTestCase(String value) { + + @Test + void test() { + fail("should not be executed"); + } + } + + @ParameterizedClass + @ArgumentsSource(CustomArgumentsProvider.class) + record ArgumentsSourceConstructorInjectionTestCase(String value) { + @Test + void test() { + assertTrue(value.equals("foo") || value.equals("bar")); + } + } + + @ParameterizedClass + @ArgumentsSource(CustomArgumentsProvider.class) + static class ArgumentsSourceFieldInjectionTestCase { + + @Parameter + String value; + + @Test + void test() { + assertTrue(value.equals("foo") || value.equals("bar")); + } + } + + static class CustomArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ParameterDeclarations parameters, ExtensionContext context) + throws Exception { + return Stream.of("foo", "bar").map(Arguments::of); + } + } + + @ParameterizedClass(name = INDEX_PLACEHOLDER + " | " // + + DISPLAY_NAME_PLACEHOLDER + " | " // + + ARGUMENTS_PLACEHOLDER + " | " // + + ARGUMENT_SET_NAME_OR_ARGUMENTS_WITH_NAMES_PLACEHOLDER) + @MethodSource("arguments") + @DisplayName("TesT") + record CustomNamePatternTestCase(int number, String name) { + + static Stream arguments() { + return Stream.of(argumentSet("set", 1, "foo"), Arguments.of(2, "bar")); + } + + @Test + void test() { + assertTrue(number > 0); + assertFalse(name.isBlank()); + } + } + + @ParameterizedClass + @ArgumentsSource(AutoCloseableArgumentProvider.class) + record AutoCloseableArgumentTestCase(AutoCloseableArgument argument) { + @Test + void test() { + assertNotNull(argument); + assertEquals(0, AutoCloseableArgument.closeCounter); + } + } + + @ParameterizedClass(autoCloseArguments = false) + @ArgumentsSource(AutoCloseableArgumentProvider.class) + record AutoCloseableArgumentWithDisabledCleanupTestCase(AutoCloseableArgument argument) { + @Test + void test() { + assertNotNull(argument); + assertEquals(0, AutoCloseableArgument.closeCounter); + } + } + + private static class AutoCloseableArgumentProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { + return Stream.of(arguments(new AutoCloseableArgument(), Named.of("unused", new AutoCloseableArgument()))); + } + } + + static class AutoCloseableArgument implements AutoCloseable { + + static int closeCounter = 0; + + @Override + public void close() { + closeCounter++; + } + } + + @ParameterizedClass(argumentCountValidation = STRICT) + @CsvSource("foo, unused") + record StrictArgumentCountValidationModeTestCase(String value) { + @Test + void test() { + fail("should not be called"); + } + } + + @ParameterizedClass(argumentCountValidation = NONE) + @CsvSource("foo, unused") + record NoneArgumentCountValidationModeTestCase(String value) { + @Test + void test() { + assertEquals("foo", value); + } + } + + @ParameterizedClass + @CsvSource("foo, unused") + record DefaultArgumentCountValidationModeTestCase(String value) { + @Test + void test() { + assertEquals("foo", value); + } + } + + @ParameterizedClass + @MethodSource("org.junit.jupiter.params.ParameterizedClassIntegrationTests#zeroArguments") + record ForbiddenZeroInvocationsTestCase(String value) { + @Test + void test() { + fail("should not be called"); + } + } + + @ParameterizedClass(allowZeroInvocations = true) + @MethodSource("org.junit.jupiter.params.ParameterizedClassIntegrationTests#zeroArguments") + record AllowedZeroInvocationsTestCase(String value) { + @Test + void test() { + fail("should not be called"); + } + } + + static Stream zeroArguments() { + return Stream.empty(); + } + + @ParameterizedClass + record NoArgumentSourceTestCase(String value) { + @Test + void test() { + fail("should not be called"); + } + } + + @ParameterizedClass + @ValueSource(ints = { 1, 2 }) + static class NestedFieldInjectionTestCase { + + @Parameter + int number; + + @Nested + @ParameterizedClass + @ValueSource(strings = { "foo", "bar" }) + class InnerTestCase { + + @Parameter + String text; + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void test(boolean flag) { + assertTrue(number > 0); + assertTrue(List.of("foo", "bar").contains(text)); + } + } + } + + @ParameterizedClass + @ValueSource(ints = { 1, 2 }) + record NestedConstructorInjectionTestCase(int number) { + + @Nested + @ParameterizedClass + @ValueSource(strings = { "foo", "bar" }) + class InnerTestCase { + + final String text; + + InnerTestCase(String text) { + this.text = text; + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void test(boolean flag) { + assertTrue(number > 0); + assertTrue(List.of("foo", "bar").contains(text)); + } + } + } + + @ParameterizedClass + @ValueSource(ints = { 1, 2 }) + record ConstructorInjectionWithRegularNestedTestCase(int number) { + + @Nested + @TestInstance(PER_CLASS) + class InnerTestCase { + + InnerTestCase(TestInfo testInfo) { + assertThat(testInfo.getTestClass()).contains(InnerTestCase.class); + assertThat(testInfo.getTestMethod()).isEmpty(); + } + + @Test + void test() { + assertTrue(number >= 0); + } + } + } + + @ParameterizedClass + @ValueSource(ints = { 1, 2 }) + static class FieldInjectionWithRegularNestedTestCase { + + @Parameter + int number; + + @Nested + @TestInstance(PER_CLASS) + class InnerTestCase { + + InnerTestCase(TestInfo testInfo) { + assertThat(testInfo.getTestClass()).contains(InnerTestCase.class); + assertThat(testInfo.getTestMethod()).isEmpty(); + } + + @Test + void test() { + assertTrue(number >= 0); + } + } + } + + @ParameterizedClass + @CsvSource({ "1, foo", "2, bar" }) + static class MultiAggregatorFieldInjectionTestCase { + + @Parameter + ArgumentsAccessor accessor; + + @TimesTwo + int numberTimesTwo; + + @Parameter(0) + int number; + + @Parameter(1) + String text; + + @Test + void test() { + assertEquals(2, accessor.size()); + assertEquals(number, accessor.getInteger(0)); + assertEquals(number * 2, numberTimesTwo); + assertEquals(text, accessor.getString(1)); + } + + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @Parameter + @AggregateWith(TimesTwoAggregator.class) + @interface TimesTwo { + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ParameterizedClass + @MethodSource("methodSource") + @FieldSource("fieldSource") + @TestInstance(PER_CLASS) + static class FieldInjectionWithPerClassTestInstanceLifecycleTestCase { + + List methodSource() { + return List.of("foo"); + } + + final List fieldSource = List.of("bar"); + + @Parameter + private String value; + + @Test + void test1(TestReporter reporter) { + publishReportEntry(reporter); + } + + @Test + void test2(TestReporter reporter) { + publishReportEntry(reporter); + } + + private void publishReportEntry(TestReporter reporter) { + assertNotNull(value); + reporter.publishEntry(Map.of( // + "instanceHashCode", Integer.toHexString(hashCode()), // + "value", value // + )); + } + } + + abstract static class BaseTestCase { + @Parameter(0) + String value; + } + + @ParameterizedClass + @CsvSource({ "foo, 1", "bar, 2" }) + static class InheritedHiddenParameterFieldTestCase extends BaseTestCase { + @Parameter(1) + String value; + + @Test + void test(TestReporter reporter) { + reporter.publishEntry(Map.of( // + "super.value", super.value, // + "this.value", this.value // + )); + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + static class InvalidFinalFieldTestCase { + + @Parameter + final int i = -1; + + @Test + void test() { + fail("should not be called"); + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + static class InvalidAggregatorFieldWithIndexTestCase { + + @Parameter(0) + ArgumentsAccessor accessor; + + @Test + void test() { + fail("should not be called"); + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + static class InvalidParameterIndexTestCase { + + @Parameter(-42) + int i; + + @Test + void test() { + fail("should not be called"); + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + static class InvalidDuplicateParameterDeclarationTestCase { + + @Parameter(0) + int i; + + @Parameter(0) + long l; + + @Test + void test() { + fail("should not be called"); + } + } + + @ParameterizedClass + @CsvSource({ "unused1, foo, unused2, bar", "unused4, baz, unused5, qux" }) + static class InvalidUnusedParameterIndexesTestCase { + + @Parameter(1) + String second; + + @Parameter(3) + String fourth; + + @Test + void test(TestReporter reporter) { + reporter.publishEntry(Map.of( // + "second", second, // + "fourth", fourth // + )); + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + record ArgumentConversionPerInvocationConstructorInjectionTestCase( + @ConvertWith(Wrapper.Converter.class) Wrapper wrapper) { + + static Wrapper instance; + + @BeforeAll + @AfterAll + static void clearWrapper() { + instance = null; + } + + @Test + void test1() { + setOrCheckWrapper(); + } + + @Test + void test2() { + setOrCheckWrapper(); + } + + private void setOrCheckWrapper() { + if (instance == null) { + instance = wrapper; + } + else { + assertSame(instance, wrapper); + } + } + } + + @ParameterizedClass + @ValueSource(ints = 1) + static class ArgumentConversionPerInvocationFieldInjectionTestCase { + + static Wrapper instance; + + @BeforeAll + @AfterAll + static void clearWrapper() { + instance = null; + } + + @Parameter + @ConvertWith(Wrapper.Converter.class) + Wrapper wrapper; + + @Test + void test1() { + setOrCheckWrapper(); + } + + @Test + void test2() { + setOrCheckWrapper(); + } + + private void setOrCheckWrapper() { + if (instance == null) { + instance = wrapper; + } + else { + assertSame(instance, wrapper); + } + } + } + + record Wrapper(int value) { + static class Converter extends SimpleArgumentConverter { + @Override + protected Object convert(Object source, Class targetType) { + return new Wrapper((Integer) source); + } + } + } +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestNameFormatterTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterTests.java similarity index 84% rename from jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestNameFormatterTests.java rename to jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterTests.java index 749ed94bced4..5214e774ec39 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestNameFormatterTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedInvocationNameFormatterTests.java @@ -16,16 +16,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; -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.DEFAULT_DISPLAY_NAME; -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.DEFAULT_DISPLAY_NAME; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.DISPLAY_NAME_PLACEHOLDER; +import static org.junit.jupiter.params.ParameterizedInvocationConstants.INDEX_PLACEHOLDER; import static org.junit.jupiter.params.provider.Arguments.argumentSet; import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.lang.reflect.Method; import java.math.BigDecimal; @@ -39,11 +40,11 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.AnnotatedElementContext; import org.junit.jupiter.api.extension.ExtensionConfigurationException; -import org.junit.jupiter.api.extension.ParameterContext; 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.provider.Arguments; import org.junit.jupiter.params.provider.CsvSource; import org.junit.platform.commons.JUnitException; @@ -53,7 +54,7 @@ * @since 5.0 */ @SuppressWarnings("ALL") -class ParameterizedTestNameFormatterTests { +class ParameterizedInvocationNameFormatterTests { private final Locale originalLocale = Locale.getDefault(); @@ -322,20 +323,25 @@ void mixedTypesOfArgumentsImplementationsAndCustomDisplayNamePattern() { // ------------------------------------------------------------------------- - private static ParameterizedTestNameFormatter formatter(String pattern, String displayName) { + private static ParameterizedInvocationNameFormatter formatter(String pattern, String displayName) { return formatter(pattern, displayName, 512); } - private static ParameterizedTestNameFormatter formatter(String pattern, String displayName, int argumentMaxLength) { - return new ParameterizedTestNameFormatter(pattern, displayName, mock(), argumentMaxLength); + private static ParameterizedInvocationNameFormatter formatter(String pattern, String displayName, + int argumentMaxLength) { + ParameterizedDeclarationContext context = mock(); + when(context.getResolverFacade()).thenReturn(mock()); + when(context.getAnnotationName()).thenReturn(ParameterizedTest.class.getSimpleName()); + return new ParameterizedInvocationNameFormatter(pattern, displayName, context, argumentMaxLength); } - private static ParameterizedTestNameFormatter formatter(String pattern, String displayName, Method method) { - var context = new ParameterizedTestMethodContext(method, method.getAnnotation(ParameterizedTest.class)); - return new ParameterizedTestNameFormatter(pattern, displayName, context, 512); + private static ParameterizedInvocationNameFormatter formatter(String pattern, String displayName, Method method) { + var context = new ParameterizedTestContext(method, method.getAnnotation(ParameterizedTest.class)); + return new ParameterizedInvocationNameFormatter(pattern, displayName, context, 512); } - private static String format(ParameterizedTestNameFormatter formatter, int invocationIndex, Arguments arguments) { + private static String format(ParameterizedInvocationNameFormatter formatter, int invocationIndex, + Arguments arguments) { return formatter.format(invocationIndex, EvaluatedArgumentSet.allOf(arguments)); } @@ -377,9 +383,10 @@ void parameterizedTestWithAggregator(int someNumber, void processFruits(String fruit1, String fruit2) { } - private static class CustomAggregator implements ArgumentsAggregator { + private static class CustomAggregator extends SimpleArgumentsAggregator { @Override - public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) { + protected Object aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) { return accessor.get(0); } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestMethodContextTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestContextTests.java similarity index 77% rename from jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestMethodContextTests.java rename to jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestContextTests.java index 7272b084a52d..36f29fb2e55b 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestMethodContextTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestContextTests.java @@ -10,39 +10,41 @@ package org.junit.jupiter.params; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.params.aggregator.AggregatorIntegrationTests.CsvToPerson; import org.junit.jupiter.params.aggregator.AggregatorIntegrationTests.Person; import org.junit.jupiter.params.aggregator.ArgumentsAccessor; import org.junit.jupiter.params.provider.ValueSource; +import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.util.ReflectionUtils; /** - * Unit tests for {@link ParameterizedTestMethodContext}. + * Unit tests for {@link ParameterizedTestContext}. * * @since 5.2 */ -class ParameterizedTestMethodContextTests { +class ParameterizedTestContextTests { @ParameterizedTest @ValueSource(strings = { "onePrimitive", "twoPrimitives", "twoAggregators", "twoAggregatorsWithTestInfoAtTheEnd", "mixedMode" }) void validSignatures(String methodName) { - assertTrue(createMethodContext(ValidTestCase.class, methodName).hasPotentiallyValidSignature()); + assertDoesNotThrow(() -> createMethodContext(ValidTestCase.class, methodName)); } @ParameterizedTest @ValueSource(strings = { "twoAggregatorsWithPrimitiveInTheMiddle", "twoAggregatorsWithTestInfoInTheMiddle" }) void invalidSignatures(String methodName) { - assertFalse(createMethodContext(InvalidTestCase.class, methodName).hasPotentiallyValidSignature()); + assertThrows(PreconditionViolationException.class, + () -> createMethodContext(InvalidTestCase.class, methodName)); } - private ParameterizedTestMethodContext createMethodContext(Class testClass, String methodName) { + private ParameterizedTestContext createMethodContext(Class testClass, String methodName) { var method = ReflectionUtils.findMethods(testClass, m -> m.getName().equals(methodName)).getFirst(); - return new ParameterizedTestMethodContext(method, method.getAnnotation(ParameterizedTest.class)); + return new ParameterizedTestContext(method, method.getAnnotation(ParameterizedTest.class)); } @SuppressWarnings("JUnitMalformedDeclaration") diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java index aa36f907aa25..5550270a5300 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java @@ -15,8 +15,8 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.params.ParameterizedTestExtension.METHOD_CONTEXT_KEY; -import static org.junit.jupiter.params.ParameterizedTestExtension.arguments; +import static org.junit.jupiter.params.ParameterizedInvocationContextProvider.arguments; +import static org.junit.jupiter.params.ParameterizedTestExtension.DECLARATION_CONTEXT_KEY; import java.io.FileNotFoundException; import java.lang.reflect.AnnotatedElement; @@ -42,6 +42,7 @@ 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.ParameterDeclarations; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.util.ReflectionUtils; @@ -105,7 +106,7 @@ void emptyDisplayNameIsIllegal() { void defaultDisplayNameWithEmptyStringInConfigurationIsIllegal() { AtomicInteger invocations = new AtomicInteger(); Function> configurationSupplier = key -> { - if (key.equals(ParameterizedTestExtension.DISPLAY_NAME_PATTERN_KEY)) { + if (key.equals(ParameterizedInvocationNameFormatter.DISPLAY_NAME_PATTERN_KEY)) { invocations.incrementAndGet(); return Optional.of(""); } @@ -122,11 +123,15 @@ void defaultDisplayNameWithEmptyStringInConfigurationIsIllegal() { @Test void argumentsRethrowsOriginalExceptionFromProviderAsUncheckedException() { - ArgumentsProvider failingProvider = (context) -> { - throw new FileNotFoundException("a message"); + ArgumentsProvider failingProvider = new ArgumentsProvider() { + @Override + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) throws Exception { + throw new FileNotFoundException("a message"); + } }; - var exception = assertThrows(FileNotFoundException.class, () -> arguments(failingProvider, null)); + var exception = assertThrows(FileNotFoundException.class, () -> arguments(failingProvider, null, null)); assertEquals("a message", exception.getMessage()); } @@ -297,8 +302,8 @@ public void publishDirectory(String name, ThrowingConsumer action) { public Store getStore(Namespace namespace) { var store = new NamespaceAwareStore(this.store, namespace); method // - .map(it -> new ParameterizedTestMethodContext(it, it.getAnnotation(ParameterizedTest.class))) // - .ifPresent(ctx -> store.put(METHOD_CONTEXT_KEY, ctx)); + .map(it -> new ParameterizedTestContext(it, it.getAnnotation(ParameterizedTest.class))) // + .ifPresent(ctx -> store.put(DECLARATION_CONTEXT_KEY, ctx)); return store; } @@ -360,7 +365,8 @@ void method() { static class ZeroArgumentsProvider implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { return Stream.empty(); } } @@ -376,7 +382,8 @@ void method(String parameter) { static class ArgumentsProviderWithCloseHandler implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { var argumentsStream = Stream.of("foo", "bar").map(Arguments::of); return argumentsStream.onClose(() -> streamWasClosed = true); } @@ -393,7 +400,8 @@ void method() { class NonStaticArgumentsProvider implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { return null; } } @@ -431,7 +439,8 @@ static class AmbiguousConstructorArgumentsProvider implements ArgumentsProvider } @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { return null; } } 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 e8afc956fbff..0c0c44842427 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 @@ -30,7 +30,6 @@ import static org.junit.platform.testkit.engine.EventConditions.abortedWithReason; 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.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; @@ -82,6 +81,7 @@ import org.junit.jupiter.api.TestInstance.Lifecycle; import org.junit.jupiter.api.TestMethodOrder; 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.extension.ParameterResolutionException; @@ -92,7 +92,7 @@ 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.SimpleArgumentsAggregator; import org.junit.jupiter.params.converter.ArgumentConversionException; import org.junit.jupiter.params.converter.ArgumentConverter; import org.junit.jupiter.params.converter.ConvertWith; @@ -108,6 +108,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; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.util.ClassUtils; import org.junit.platform.engine.DiscoverySelector; @@ -430,7 +431,7 @@ void executesLifecycleMethods() { @Test void truncatesArgumentsThatExceedMaxLength() { var results = EngineTestKit.engine(new JupiterTestEngine()) // - .configurationParameter(ParameterizedTestExtension.ARGUMENT_MAX_LENGTH_KEY, "2") // + .configurationParameter(ParameterizedInvocationNameFormatter.ARGUMENT_MAX_LENGTH_KEY, "2") // .selectors(selectMethod(TestCase.class, "testWithCsvSource", String.class.getName())) // .execute(); results.testEvents().assertThatEvents() // @@ -441,7 +442,7 @@ void truncatesArgumentsThatExceedMaxLength() { @Test void displayNamePatternFromConfiguration() { var results = EngineTestKit.engine(new JupiterTestEngine()) // - .configurationParameter(ParameterizedTestExtension.DISPLAY_NAME_PATTERN_KEY, "{index}") // + .configurationParameter(ParameterizedInvocationNameFormatter.DISPLAY_NAME_PATTERN_KEY, "{index}") // .selectors(selectMethod(TestCase.class, "testWithCsvSource", String.class.getName())) // .execute(); results.testEvents().assertThatEvents() // @@ -450,27 +451,26 @@ void displayNamePatternFromConfiguration() { } @Test - void failsWhenArgumentsRequiredButNoneProvided() { - var result = execute(ZeroArgumentsTestCase.class, "testThatRequiresArguments", String.class); - result.containerEvents().assertThatEvents().haveExactly(1, event(finishedWithFailure(message( - "Configuration error: You must configure at least one set of arguments for this @ParameterizedTest")))); + void failsWhenInvocationIsRequiredButNoArgumentSetsAreProvided() { + var results = execute(ZeroInvocationsTestCase.class, "testThatRequiresInvocations", String.class); + + results.containerEvents().assertThatEvents() // + .haveExactly(1, event(finishedWithFailure(message( + "Configuration error: You must configure at least one set of arguments for this @ParameterizedTest")))); } @Test - void doesNotFailWhenArgumentsAreNotRequiredAndNoneProvided() { - var result = execute(ZeroArgumentsTestCase.class, "testThatDoesNotRequireArguments", String.class); - result.allEvents().assertEventsMatchExactly( // - event(engine(), started()), event(container(ZeroArgumentsTestCase.class), started()), - event(container("testThatDoesNotRequireArguments"), started()), - event(container("testThatDoesNotRequireArguments"), finishedSuccessfully()), - event(container(ZeroArgumentsTestCase.class), finishedSuccessfully()), - event(engine(), finishedSuccessfully())); + void doesNotFailWhenInvocationIsNotRequiredAndNoArgumentSetsAreProvided() { + var results = execute(ZeroInvocationsTestCase.class, "testThatDoesNotRequireInvocations", String.class); + + results.allEvents().assertStatistics(stats -> stats.started(3).succeeded(3)); } @Test void failsWhenNoArgumentsSourceIsDeclared() { - var result = execute(ZeroArgumentsTestCase.class, "testThatHasNoArgumentsSource", String.class); - result.containerEvents().assertThatEvents() // + var results = execute(ZeroInvocationsTestCase.class, "testThatHasNoArgumentsSource", String.class); + + results.containerEvents().assertThatEvents() // .haveExactly(1, // event(displayName("testThatHasNoArgumentsSource(String)"), finishedWithFailure(message( "Configuration error: You must configure at least one arguments source for this @ParameterizedTest")))); @@ -523,7 +523,7 @@ void failsWithNullSourceWithZeroFormalParameters() { finishedWithFailure(// instanceOf(PreconditionViolationException.class), // message(msg -> msg.matches( - "@NullSource cannot provide a null argument to method .+: the method does not declare any formal parameters."))))); + "@NullSource cannot provide a null argument to method .+: no formal parameters declared."))))); } @Test @@ -663,7 +663,7 @@ void failsWithEmptySourceWithZeroFormalParameters() { finishedWithFailure(// instanceOf(PreconditionViolationException.class), // message(msg -> msg.matches( - "@EmptySource cannot provide an empty argument to method .+: the method does not declare any formal parameters."))))); + "@EmptySource cannot provide an empty argument to method .+: no formal parameters declared."))))); } @ParameterizedTest(name = "{1}") @@ -1130,7 +1130,7 @@ void failsWithArgumentsSourceProvidingUnusedArguments() { "testWithTwoUnusedStringArgumentsProvider", 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, unused1]"))))); + "Configuration error: @ParameterizedTest consumes 1 parameter but there were 2 arguments provided.%nNote: the provided arguments were [foo, unused1]"))))); } @Test @@ -1139,7 +1139,7 @@ void failsWithMethodSourceProvidingUnusedArguments() { "testWithMethodSourceProvidingUnusedArguments", 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, unused1]"))))); + "Configuration error: @ParameterizedTest consumes 1 parameter but there were 2 arguments provided.%nNote: the provided arguments were [foo, unused1]"))))); } @Test @@ -1148,7 +1148,7 @@ void failsWithCsvSourceUnusedArgumentsAndStrictArgumentCountValidationAnnotation "testWithStrictArgumentCountValidation", 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, unused1]"))))); + "Configuration error: @ParameterizedTest consumes 1 parameter but there were 2 arguments provided.%nNote: the provided arguments were [foo, unused1]"))))); } @Test @@ -1157,7 +1157,7 @@ void failsWithCsvSourceUnusedArgumentsButExecutesRemainingArgumentsWhereThereIsN "testWithCsvSourceContainingDifferentNumbersOfArguments", 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, unused1]"))))) // + "Configuration error: @ParameterizedTest consumes 1 parameter but there were 2 arguments provided.%nNote: the provided arguments were [foo, unused1]"))))) // .haveExactly(1, event(test(), displayName("[2] argument=bar"), finishedWithFailure(message("bar")))); } @@ -1186,7 +1186,7 @@ void evaluatesArgumentsAtMostOnce() { "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]"))))); + "Configuration error: @ParameterizedTest consumes 1 parameter but there were 2 arguments provided.%nNote: the provided arguments were [foo, unused]"))))); results.allEvents().reportingEntryPublished().assertThatEvents() // .haveExactly(1, event(EventConditions.reportEntry(Map.of("evaluated", "true")))); } @@ -1313,6 +1313,15 @@ void closeAutoCloseableArgumentsAfterTest() { assertEquals(2, AutoCloseableArgument.closeCounter); } + @Test + void doNotCloseAutoCloseableArgumentsAfterTestWhenDisabled() { + var results = execute("testWithAutoCloseableArgumentButDisabledCleanup", AutoCloseableArgument.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, event(test(), finishedSuccessfully())); + + assertEquals(0, AutoCloseableArgument.closeCounter); + } + @Test void closeAutoCloseableArgumentsAfterTestDespiteEarlyFailure() { var results = execute(FailureInBeforeEachTestCase.class, "test", AutoCloseableArgument.class); @@ -1453,6 +1462,12 @@ void testWithAutoCloseableArgument(AutoCloseableArgument autoCloseable) { assertEquals(0, AutoCloseableArgument.closeCounter); } + @ParameterizedTest(autoCloseArguments = false) + @ArgumentsSource(AutoCloseableArgumentProvider.class) + void testWithAutoCloseableArgumentButDisabledCleanup(AutoCloseableArgument autoCloseable) { + assertEquals(0, AutoCloseableArgument.closeCounter); + } + @ParameterizedTest @ValueSource(ints = { 2, 3, 5 }) void testWithThreeIterations(int argument) { @@ -2153,7 +2168,8 @@ void testWithEvaluationReportingArgumentsProvider(String argument) { private static class EvaluationReportingArgumentsProvider implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { return Stream.of(() -> { context.publishReportEntry("evaluated", "true"); return List.of("foo", "unused").toArray(); @@ -2437,7 +2453,8 @@ void argumentsAggregatorWithConstructorParameter( record ArgumentsProviderWithConstructorParameter(String value) implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { return Stream.of(arguments(value)); } } @@ -2450,27 +2467,33 @@ public Object convert(Object source, ParameterContext context) throws ArgumentCo } } - record ArgumentsAggregatorWithConstructorParameter(String value) implements ArgumentsAggregator { + static class ArgumentsAggregatorWithConstructorParameter extends SimpleArgumentsAggregator { + + private final String value; + + public ArgumentsAggregatorWithConstructorParameter(String value) { + this.value = value; + } @Override - public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) - throws ArgumentsAggregationException { - return value; + protected Object aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) throws ArgumentsAggregationException { + return this.value; } } } - static class ZeroArgumentsTestCase { + static class ZeroInvocationsTestCase { @ParameterizedTest @MethodSource("zeroArgumentsProvider") - void testThatRequiresArguments(String argument) { + void testThatRequiresInvocations(String argument) { fail("This test should not be executed, because no arguments are provided."); } @ParameterizedTest(allowZeroInvocations = true) @MethodSource("zeroArgumentsProvider") - void testThatDoesNotRequireArguments(String argument) { + void testThatDoesNotRequireInvocations(String argument) { fail("This test should not be executed, because no arguments are provided."); } @@ -2488,7 +2511,8 @@ public static Stream zeroArgumentsProvider() { private static class TwoSingleStringArgumentsProvider implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { return Stream.of(arguments("foo"), arguments("bar")); } } @@ -2496,7 +2520,8 @@ public Stream provideArguments(ExtensionContext context) { private static class TwoUnusedStringArgumentsProvider implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { return Stream.of(arguments("foo", "unused1"), arguments("bar", "unused2")); } } @@ -2509,11 +2534,11 @@ public Object convert(Object source, ParameterContext context) throws ArgumentCo } } - private static class StringAggregator implements ArgumentsAggregator { + private static class StringAggregator extends SimpleArgumentsAggregator { @Override - public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) - throws ArgumentsAggregationException { + protected Object aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) throws ArgumentsAggregationException { return accessor.getString(0) + accessor.getString(1); } } @@ -2529,7 +2554,8 @@ public Object convert(Object source, ParameterContext context) throws ArgumentCo private static class AutoCloseableArgumentProvider implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext context) { + public Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context) { return Stream.of(arguments(new AutoCloseableArgument(), Named.of("unused", new AutoCloseableArgument()))); } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/AggregatorIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/AggregatorIntegrationTests.java index 90bfcb7b367b..999a101abd21 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/AggregatorIntegrationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/AggregatorIntegrationTests.java @@ -37,6 +37,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.AnnotatedElementContext; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.parallel.ResourceLock; @@ -278,27 +279,29 @@ static class Address { @interface CsvToAddress { } - static class PersonAggregator implements ArgumentsAggregator { + static class PersonAggregator extends SimpleArgumentsAggregator { @Override - public Person aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) { + protected Person aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) { int startIndex = context.findAnnotation(StartIndex.class).map(StartIndex::value).orElse(0); // @formatter:off return new Person( - arguments.getString(startIndex + 0), - arguments.getString(startIndex + 1), - arguments.get(startIndex + 2, LocalDate.class), - arguments.get(startIndex + 3, Gender.class) + accessor.getString(startIndex + 0), + accessor.getString(startIndex + 1), + accessor.get(startIndex + 2, LocalDate.class), + accessor.get(startIndex + 3, Gender.class) ); // @formatter:on } } - static class AddressAggregator implements ArgumentsAggregator { + static class AddressAggregator extends SimpleArgumentsAggregator { @Override - public Address aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) { + public Address aggregateArguments(ArgumentsAccessor arguments, Class targetType, + AnnotatedElementContext context, int parameterIndex) { int startIndex = context.findAnnotation(StartIndex.class).map(StartIndex::value).orElse(0); // @formatter:off @@ -314,10 +317,11 @@ public Address aggregateArguments(ArgumentsAccessor arguments, ParameterContext /** * Maps from String to length of String. */ - static class MapAggregator implements ArgumentsAggregator { + static class MapAggregator extends SimpleArgumentsAggregator { @Override - public Map aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) { + protected Map aggregateArguments(ArgumentsAccessor arguments, Class targetType, + AnnotatedElementContext context, int parameterIndex) { // @formatter:off return IntStream.range(0, arguments.size()) .mapToObj(arguments::getString) @@ -326,19 +330,19 @@ public Map aggregateArguments(ArgumentsAccessor arguments, Para } } - static class NullAggregator implements ArgumentsAggregator { + static class NullAggregator extends SimpleArgumentsAggregator { @Override - public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) { - Preconditions.condition(!context.getParameter().getType().isPrimitive(), - () -> "only supports reference types"); + protected Object aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) { + Preconditions.condition(!targetType.isPrimitive(), () -> "only supports reference types"); return null; } } - static class ErroneousAggregator implements ArgumentsAggregator { + static class ErroneousAggregator extends SimpleArgumentsAggregator { @Override - public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) - throws ArgumentsAggregationException { + protected Object aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) throws ArgumentsAggregationException { throw new ArgumentsAggregationException("something went horribly wrong"); } } @@ -392,7 +396,7 @@ public Object convert(Object source, ParameterContext context) throws ArgumentCo } } - static class InstanceCountingAggregator implements ArgumentsAggregator { + static class InstanceCountingAggregator extends SimpleArgumentsAggregator { static int instanceCount; InstanceCountingAggregator() { @@ -400,8 +404,8 @@ static class InstanceCountingAggregator implements ArgumentsAggregator { } @Override - public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) - throws ArgumentsAggregationException { + protected Object aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) throws ArgumentsAggregationException { return "enigma"; } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java index 22d1ce685a67..09359a7b3769 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/aggregator/DefaultArgumentsAccessorTests.java @@ -169,7 +169,7 @@ void size() { } private static DefaultArgumentsAccessor defaultArgumentsAccessor(int invocationIndex, Object... arguments) { - return new DefaultArgumentsAccessor(parameterContext(), invocationIndex, arguments); + return DefaultArgumentsAccessor.create(parameterContext(), invocationIndex, arguments); } private static ParameterContext parameterContext() { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProviderTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProviderTests.java index 0cbbb252204c..885ba7a77591 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProviderTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProviderTests.java @@ -24,13 +24,16 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.support.ParameterDeclarations; @DisplayName("AnnotationBasedArgumentsProvider") class AnnotationBasedArgumentsProviderTests { private final AnnotationBasedArgumentsProvider annotationBasedArgumentsProvider = new AnnotationBasedArgumentsProvider<>() { @Override - protected Stream provideArguments(ExtensionContext context, CsvSource annotation) { + protected Stream provideArguments( + org.junit.jupiter.params.support.ParameterDeclarations parameters, ExtensionContext context, + CsvSource annotation) { return Stream.of(Arguments.of(annotation)); } }; @@ -46,18 +49,20 @@ void shouldThrowExceptionWhenNullAnnotationIsProvidedToAccept() { @DisplayName("should invoke the provideArguments template method with the accepted annotation") void shouldInvokeTemplateMethodWithTheAnnotationProvidedToAccept() { var spiedProvider = spy(annotationBasedArgumentsProvider); + var parameters = mock(org.junit.jupiter.params.support.ParameterDeclarations.class); var extensionContext = mock(ExtensionContext.class); var annotation = csvSource("0", "1", "2"); annotationBasedArgumentsProvider.accept(annotation); - annotationBasedArgumentsProvider.provideArguments(extensionContext); + annotationBasedArgumentsProvider.provideArguments(parameters, extensionContext); - verify(spiedProvider, atMostOnce()).provideArguments(eq(extensionContext), eq(annotation)); + verify(spiedProvider, atMostOnce()).provideArguments(eq(parameters), eq(extensionContext), eq(annotation)); } @Test @DisplayName("should invoke the provideArguments template method for every accepted annotation") void shouldInvokeTemplateMethodForEachAnnotationProvided() { + var parameters = mock(ParameterDeclarations.class); var extensionContext = mock(ExtensionContext.class); var foo = csvSource("foo"); var bar = csvSource("bar"); @@ -65,7 +70,7 @@ void shouldInvokeTemplateMethodForEachAnnotationProvided() { annotationBasedArgumentsProvider.accept(foo); annotationBasedArgumentsProvider.accept(bar); - var arguments = annotationBasedArgumentsProvider.provideArguments(extensionContext).toList(); + var arguments = annotationBasedArgumentsProvider.provideArguments(parameters, extensionContext).toList(); assertThat(arguments).hasSize(2); assertThat(arguments.getFirst().get()[0]).isEqualTo(foo); diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java index 4a1e9722e0a9..9beac70e9616 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java @@ -18,6 +18,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.PreconditionViolationException; @@ -382,7 +383,7 @@ void throwsExceptionIfColumnCountExceedsHeaderCount() { private Stream provideArguments(CsvSource annotation) { var provider = new CsvArgumentsProvider(); provider.accept(annotation); - return provider.provideArguments(mock()).map(Arguments::get); + return provider.provideArguments(mock(), mock(ExtensionContext.class)).map(Arguments::get); } @SuppressWarnings("unchecked") diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/CsvFileArgumentsProviderTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/CsvFileArgumentsProviderTests.java index 63f5a7aa3a76..3a7269f4ee0f 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/CsvFileArgumentsProviderTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/CsvFileArgumentsProviderTests.java @@ -544,7 +544,7 @@ private Stream provideArguments(CsvFileArgumentsProvider provider, Csv var context = mock(ExtensionContext.class); when(context.getTestClass()).thenReturn(Optional.of(CsvFileArgumentsProviderTests.class)); doCallRealMethod().when(context).getRequiredTestClass(); - return provider.provideArguments(context).map(Arguments::get); + return provider.provideArguments(mock(), context).map(Arguments::get); } @SuppressWarnings("unchecked") diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/EnumArgumentsProviderTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/EnumArgumentsProviderTests.java index 8d2d5cfbd170..e15bc98e7308 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/EnumArgumentsProviderTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/EnumArgumentsProviderTests.java @@ -20,11 +20,14 @@ import static org.mockito.Mockito.when; import java.util.Arrays; +import java.util.Optional; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.provider.EnumSource.Mode; +import org.junit.jupiter.params.support.ParameterDeclaration; +import org.junit.jupiter.params.support.ParameterDeclarations; import org.junit.platform.commons.PreconditionViolationException; /** @@ -32,7 +35,8 @@ */ class EnumArgumentsProviderTests { - private ExtensionContext extensionContext = mock(); + final ParameterDeclarations parameters = mock(); + final ExtensionContext extensionContext = mock(); @Test void providesAllEnumConstants() { @@ -78,9 +82,10 @@ void invalidPatternIsDetected() { } @Test - void providesEnumConstantsBasedOnTestMethod() throws Exception { - when(extensionContext.getRequiredTestMethod()).thenReturn( - TestCase.class.getDeclaredMethod("methodWithCorrectParameter", EnumWithFourConstants.class)); + void providesEnumConstantsBasedOnTestMethod() { + org.junit.jupiter.params.support.ParameterDeclaration firstParameterDeclaration = mock(); + when(firstParameterDeclaration.getParameterType()).thenAnswer(__ -> EnumWithFourConstants.class); + when(parameters.getFirst()).thenReturn(Optional.of(firstParameterDeclaration)); var arguments = provideArguments(NullEnum.class); @@ -89,9 +94,10 @@ void providesEnumConstantsBasedOnTestMethod() throws Exception { } @Test - void incorrectParameterTypeIsDetected() throws Exception { - when(extensionContext.getRequiredTestMethod()).thenReturn( - TestCase.class.getDeclaredMethod("methodWithIncorrectParameter", Object.class)); + void incorrectParameterTypeIsDetected() { + ParameterDeclaration firstParameterDeclaration = mock(); + when(firstParameterDeclaration.getParameterType()).thenAnswer(__ -> Object.class); + when(parameters.getFirst()).thenReturn(Optional.of(firstParameterDeclaration)); var exception = assertThrows(PreconditionViolationException.class, () -> provideArguments(NullEnum.class).findAny()); @@ -99,13 +105,12 @@ void incorrectParameterTypeIsDetected() throws Exception { } @Test - void methodsWithoutParametersAreDetected() throws Exception { - when(extensionContext.getRequiredTestMethod()).thenReturn( - TestCase.class.getDeclaredMethod("methodWithoutParameters")); + void methodsWithoutParametersAreDetected() { + when(parameters.getSourceElementDescription()).thenReturn("method"); var exception = assertThrows(PreconditionViolationException.class, () -> provideArguments(NullEnum.class).findAny()); - assertThat(exception).hasMessageStartingWith("Test method must declare at least one parameter"); + assertThat(exception).hasMessageStartingWith("There must be at least one declared parameter for method"); } @Test @@ -179,12 +184,6 @@ void invalidRangeIsDetectedWhenEnumWithNoConstantIsProvided() { } static class TestCase { - void methodWithCorrectParameter(EnumWithFourConstants parameter) { - } - - void methodWithIncorrectParameter(Object parameter) { - } - void methodWithoutParameters() { } } @@ -218,7 +217,7 @@ private > Stream provideArguments(Class enumClass var provider = new EnumArgumentsProvider(); provider.accept(annotation); - return provider.provideArguments(extensionContext).map(Arguments::get); + return provider.provideArguments(parameters, extensionContext).map(Arguments::get); } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/FieldArgumentsProviderTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/FieldArgumentsProviderTests.java index a8a395cea0ac..f8a9a9c6c299 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/FieldArgumentsProviderTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/FieldArgumentsProviderTests.java @@ -34,6 +34,7 @@ import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.engine.execution.DefaultExecutableInvoker; import org.junit.jupiter.params.ParameterizedTest; +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.ReflectionSupport; @@ -478,6 +479,7 @@ private static Stream provideArguments(Class testClass, Method test when(fieldSource.value()).thenReturn(fieldNames); + var parameters = mock(ParameterDeclarations.class); var extensionContext = mock(ExtensionContext.class); when(extensionContext.getTestClass()).thenReturn(Optional.of(testClass)); when(extensionContext.getTestMethod()).thenReturn(Optional.of(testMethod)); @@ -495,7 +497,7 @@ private static Stream provideArguments(Class testClass, Method test var provider = new FieldArgumentsProvider(); provider.accept(fieldSource); - return provider.provideArguments(extensionContext).map(Arguments::get); + return provider.provideArguments(parameters, extensionContext).map(Arguments::get); } // ------------------------------------------------------------------------- diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/MethodArgumentsProviderTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/MethodArgumentsProviderTests.java index 50e8bcc83ee2..da3a2994bb8e 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/MethodArgumentsProviderTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/MethodArgumentsProviderTests.java @@ -250,8 +250,8 @@ void providesArgumentsUsingExternalFactoryMethodInTypeFromDifferentClassLoader() var arguments = provideArguments(testClass, false, fullyQualifiedMethodName); assertThat(arguments).containsExactly(array("string1"), array("string2")); - var factoryMethod = MethodArgumentsProvider.findFactoryMethodByFullyQualifiedName(testClass, testMethod, - fullyQualifiedMethodName); + var factoryMethod = MethodArgumentsProvider.findFactoryMethodByFullyQualifiedName(testClass, + Optional.of(testMethod), fullyQualifiedMethodName); assertThat(factoryMethod).isNotNull(); assertThat(factoryMethod.getName()).isEqualTo("stringsProvider"); assertThat(factoryMethod.getParameterTypes()).isEmpty(); @@ -759,7 +759,6 @@ private Stream provideArguments(Class testClass, Method testMethod, when(extensionContext.getExecutableInvoker()).thenReturn( new DefaultExecutableInvoker(extensionContext, extensionRegistry)); - doCallRealMethod().when(extensionContext).getRequiredTestMethod(); doCallRealMethod().when(extensionContext).getRequiredTestClass(); var testInstance = allowNonStaticMethod ? ReflectionUtils.newInstance(testClass) : null; @@ -770,7 +769,7 @@ private Stream provideArguments(Class testClass, Method testMethod, var provider = new MethodArgumentsProvider(); provider.accept(methodSource); - return provider.provideArguments(extensionContext).map(Arguments::get); + return provider.provideArguments(mock(), extensionContext).map(Arguments::get); } // ------------------------------------------------------------------------- diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/ValueArgumentsProviderTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/ValueArgumentsProviderTests.java index bbfaf94b69f0..a601714565e8 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/ValueArgumentsProviderTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/ValueArgumentsProviderTests.java @@ -18,6 +18,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.platform.commons.PreconditionViolationException; /** @@ -160,7 +161,7 @@ private static Stream provideArguments(short[] shorts, byte[] bytes, i var provider = new ValueArgumentsProvider(); provider.accept(annotation); - return provider.provideArguments(mock()).map(Arguments::get); + return provider.provideArguments(mock(), mock(ExtensionContext.class)).map(Arguments::get); } private static Object[] array(Object... objects) { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/support/AnnotationConsumerInitializerTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/support/AnnotationConsumerInitializerTests.java index b304ec8f4baa..16306451bca4 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/support/AnnotationConsumerInitializerTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/support/AnnotationConsumerInitializerTests.java @@ -58,7 +58,7 @@ void shouldInitializeAnnotationBasedArgumentsProvider() throws NoSuchMethodExcep var method = SubjectClass.class.getDeclaredMethod("foo"); var initialisedAnnotationConsumer = initialize(method, instance); - initialisedAnnotationConsumer.provideArguments(mock()).findAny(); + initialisedAnnotationConsumer.provideArguments(mock(), mock(ExtensionContext.class)).findAny(); assertThat(initialisedAnnotationConsumer.annotations) // .hasSize(1) // @@ -116,7 +116,8 @@ private static class SomeAnnotationBasedArgumentsProvider extends AnnotationBase List annotations = new ArrayList<>(); @Override - protected Stream provideArguments(ExtensionContext context, CsvSource annotation) { + protected Stream provideArguments(ParameterDeclarations parameters, + ExtensionContext context, CsvSource annotation) { annotations.add(annotation); return Stream.empty(); } diff --git a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedDataClassTestCase.kt b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedDataClassTestCase.kt new file mode 100644 index 000000000000..b663295ca755 --- /dev/null +++ b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedDataClassTestCase.kt @@ -0,0 +1,35 @@ +/* + * 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.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInfo +import org.junit.jupiter.params.provider.ValueSource + +@ParameterizedClass +@ValueSource(ints = [-1, 1]) +data class ParameterizedDataClassTestCase( + val value: Int, + val testInfo: TestInfo +) { + @Test + fun test1() { + assertEquals("test1()", testInfo.displayName) + assertTrue(value < 0, "negative") + } + + @Test + fun test2() { + assertEquals("test2()", testInfo.displayName) + assertTrue(value < 0, "negative") + } +} diff --git a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedTestNameFormatterIntegrationTests.kt b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedInvocationNameFormatterIntegrationTests.kt similarity index 92% rename from jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedTestNameFormatterIntegrationTests.kt rename to jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedInvocationNameFormatterIntegrationTests.kt index 5ce7d3fabdae..7da01fb28742 100644 --- a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedTestNameFormatterIntegrationTests.kt +++ b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/ParameterizedInvocationNameFormatterIntegrationTests.kt @@ -13,7 +13,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.TestInfo import org.junit.jupiter.params.provider.ValueSource -class ParameterizedTestNameFormatterIntegrationTests { +class ParameterizedInvocationNameFormatterIntegrationTests { @ValueSource(strings = ["foo", "bar"]) @ParameterizedTest fun defaultDisplayName( @@ -21,9 +21,9 @@ class ParameterizedTestNameFormatterIntegrationTests { info: TestInfo ) { if (param.equals("foo")) { - assertEquals("[1] foo", info.displayName) + assertEquals("[1] param=foo", info.displayName) } else { - assertEquals("[2] bar", info.displayName) + assertEquals("[2] param=bar", info.displayName) } } diff --git a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt index 479a625775cc..a83881e4753a 100644 --- a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt +++ b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/ArgumentsAccessorKotlinTests.kt @@ -56,7 +56,7 @@ class ArgumentsAccessorKotlinTests { fun defaultArgumentsAccessor( invocationIndex: Int, vararg arguments: Any - ): DefaultArgumentsAccessor = DefaultArgumentsAccessor(parameterContext(), invocationIndex, *arguments) + ): DefaultArgumentsAccessor = DefaultArgumentsAccessor.create(parameterContext(), invocationIndex, *arguments) fun parameterContext(): ParameterContext { val declaringExecutable: Method = ReflectionUtils.findMethod(DefaultArgumentsAccessorTests::class.java, "foo").get() diff --git a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/DisplayNameTests.kt b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/DisplayNameTests.kt index 12d9b336860f..6a1f91cbb19f 100644 --- a/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/DisplayNameTests.kt +++ b/jupiter-tests/src/test/kotlin/org/junit/jupiter/params/aggregator/DisplayNameTests.kt @@ -35,6 +35,6 @@ object DisplayNameTests { number: Int, info: TestInfo ) { - assertEquals("[$number] $char, $number", info.displayName) + assertEquals("[$number] char=$char, number=$number", info.displayName) } } diff --git a/platform-tests/src/test/java/org/junit/platform/engine/discovery/IterationSelectorTests.java b/platform-tests/src/test/java/org/junit/platform/engine/discovery/IterationSelectorTests.java index cba42d58f00b..e6b25b11cb09 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/discovery/IterationSelectorTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/discovery/IterationSelectorTests.java @@ -19,12 +19,12 @@ import java.util.Optional; import java.util.stream.IntStream; -import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.AnnotatedElementContext; 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.ArgumentsAggregationException; -import org.junit.jupiter.params.aggregator.ArgumentsAggregator; +import org.junit.jupiter.params.aggregator.SimpleArgumentsAggregator; import org.junit.jupiter.params.provider.CsvSource; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.engine.DiscoverySelector; @@ -61,14 +61,14 @@ private static DiscoverySelector selectorWithIdentifier(String identifier) { return parent; } - private static class VarargsAggregator implements ArgumentsAggregator { + private static class VarargsAggregator extends SimpleArgumentsAggregator { @Override - public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) - throws ArgumentsAggregationException { - Class parameterType = context.getParameter().getType(); - Preconditions.condition(parameterType.isArray(), () -> "must be an array type, but was " + parameterType); - Class componentType = parameterType.getComponentType(); - IntStream indices = IntStream.range(context.getIndex(), accessor.size()); + protected Object aggregateArguments(ArgumentsAccessor accessor, Class targetType, + AnnotatedElementContext context, int parameterIndex) throws ArgumentsAggregationException { + + Preconditions.condition(targetType.isArray(), () -> "must be an array type, but was " + targetType); + Class componentType = targetType.getComponentType(); + IntStream indices = IntStream.range(parameterIndex, accessor.size()); if (componentType == int.class) { return indices.map(accessor::getInteger).toArray(); }