aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Duarte <licorne@google.com>2024-04-05 18:19:46 +0000
committerDavid Duarte <licorne@google.com>2024-04-05 18:24:08 +0000
commit273cae1c01676b8ec59a0271aeeee5304b645ae6 (patch)
treeea6745de613747a454d5dbc59882551899915abd
parent4a1b074a53356ed6f81836343058859652f3ca4f (diff)
parent12066d29df68922d8c4a1a0c2c6128abc487340f (diff)
downloadTestParameterInjector-273cae1c01676b8ec59a0271aeeee5304b645ae6.tar.gz
Merge remote-tracking branch 'aosp/upstream-main' into main
Bug: 333088631 Test: m TestParameterInjector Change-Id: Ie6334c3fc67bcf8213b450942c5bcab1e71feac4
-rw-r--r--.github/dependabot.yaml21
-rw-r--r--.github/workflows/build.yaml48
-rw-r--r--.github/workflows/release.yaml43
-rw-r--r--.gitignore1
-rw-r--r--CHANGELOG.md135
-rw-r--r--METADATA4
-rw-r--r--README.md149
-rw-r--r--junit4/pom.xml41
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java90
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/ByteStringReflection.java98
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java72
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/GenericParameterContext.java191
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java303
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java441
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java325
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java66
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java146
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java260
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java244
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java1369
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java33
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorUtils.java47
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java68
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValue.java57
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java94
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java27
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java162
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java276
-rw-r--r--junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java468
-rw-r--r--junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java206
-rw-r--r--junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java213
-rw-r--r--junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java153
-rw-r--r--junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java278
-rw-r--r--junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java866
-rw-r--r--junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt278
-rw-r--r--junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java254
-rw-r--r--junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java554
-rw-r--r--junit5/pom.xml60
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/BaseTestParameterValidator.java90
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ByteStringReflection.java98
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ExecutableValidationResult.java72
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/GenericParameterContext.java191
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java303
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java325
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessor.java66
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessorList.java146
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java260
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotation.java244
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java1369
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorExtension.java140
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorTest.java50
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorUtils.java47
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValidator.java68
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValue.java57
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java94
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValues.java27
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java162
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameters.java276
-rw-r--r--junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParametersMethodProcessor.java468
-rw-r--r--junit5/src/test/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorJUnit5Test.java608
-rw-r--r--pom.xml43
61 files changed, 13307 insertions, 38 deletions
diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml
new file mode 100644
index 0000000..f262838
--- /dev/null
+++ b/.github/dependabot.yaml
@@ -0,0 +1,21 @@
+# Copyright 2021 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+version: 2
+
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "daily"
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
new file mode 100644
index 0000000..3452265
--- /dev/null
+++ b/.github/workflows/build.yaml
@@ -0,0 +1,48 @@
+# Copyright 2021 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+name: build
+
+on:
+ pull_request: {}
+ push:
+ branches:
+ - '**'
+ tags-ignore:
+ - '**'
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-java@v3
+ with:
+ distribution: 'zulu'
+ java-version: 11
+ cache: maven
+
+ - run: mvn --update-snapshots -B verify javadoc:javadoc
+
+ - name: Deploy docs to website
+ if: ${{ github.ref == 'refs/heads/main' }}
+ uses: JamesIves/github-pages-deploy-action@releases/v3
+ with:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ BRANCH: site
+ FOLDER: junit4/target/site/apidocs
+ TARGET_FOLDER: docs/latest/
+ CLEAN: true
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
new file mode 100644
index 0000000..034eff7
--- /dev/null
+++ b/.github/workflows/release.yaml
@@ -0,0 +1,43 @@
+# Copyright 2021 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+name: release
+
+on:
+ push:
+ tags:
+ - '**'
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-java@v3
+ with:
+ distribution: 'zulu'
+ java-version: 11
+
+ - run: mvn javadoc:javadoc
+
+ - name: Deploy docs to website
+ uses: JamesIves/github-pages-deploy-action@releases/v3
+ with:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ BRANCH: site
+ FOLDER: junit4/target/site/apidocs
+ TARGET_FOLDER: docs/1.x/
+ CLEAN: true
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2f7896d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+target/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ebe26a6..5b80fa1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,138 @@
+## 1.16
+
+- Deprecated [`TestParameter.TestParameterValuesProvider`](
+ https://google.github.io/TestParameterInjector/docs/latest/com/google/testing/junit/testparameterinjector/TestParameter.TestParameterValuesProvider.html)
+ in favor of its newer version [`TestParameterValuesProvider`](
+ https://google.github.io/TestParameterInjector/docs/latest/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.html).
+
+## 1.15
+
+- Add context aware version of [`TestParameterValuesProvider`](
+ https://google.github.io/TestParameterInjector/docs/latest/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.html).
+ It is the same as the old [`TestParameter.TestParameterValuesProvider`](
+ https://google.github.io/TestParameterInjector/docs/latest/com/google/testing/junit/testparameterinjector/TestParameter.TestParameterValuesProvider.html),
+ except that `provideValues()` was changed to `provideValues(Context)` where
+ [`Context`](
+ https://google.github.io/TestParameterInjector/docs/latest/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.Context.html)
+ contains the test class and the other annotations. This allows for more generic
+ providers that take into account custom annotations with extra data, or the
+ implementation of abstract methods on a base test class.
+
+ Example usage:
+
+```java
+import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider;
+
+private static final class MyProvider extends TestParameterValuesProvider {
+ @Override
+ public List<?> provideValues(Context context) throws Exception {
+ var testInstance = context.testClass().getDeclaredConstructor().newInstance();
+ var fooList = ((MyBaseTestClass) testInstance).getFooList();
+ // ...
+
+ // OR
+
+ var fooList = context.getOtherAnnotation(MyCustomAnnotation.class).fooList();
+ // ...
+ }
+}
+```
+
+- Fixed some theoretical non-determinism that could arise from Java reflection
+ methods
+
+## 1.14
+
+- Fixed multiple constructors error when this library is used with Powermock.
+ See https://github.com/google/TestParameterInjector/issues/40.
+
+## 1.13
+
+- Add support for setting a custom name for a `@TestParameter` value given via a provider:
+
+```java
+private static final class FruitProvider implements TestParameterValuesProvider {
+ @Override
+ public List<?> provideValues() {
+ return ImmutableList.of(
+ value(new Apple()).withName("apple"),
+ value(new Banana()).withName("banana"));
+ }
+}
+```
+
+- Add support for `BigInteger` and `UnsignedLong`
+- JUnit4: Fix for interrupted test cases causing random failures with thread
+ reuse (porting [the earlier fix in
+ JUnit4](https://github.com/junit-team/junit4/issues/1365))
+
+## 1.12
+
+- Tweak to the test name generation: Show the parameter name if its value is potentially
+ ambiguous (e.g. null, "" or "123").
+- Made `TestParametersValues.name()` optional. If missing, a name will be generated.
+
+## 1.11
+
+- Replaced deprecated call to org.yaml.snakeyaml.constructor.SafeConstructor
+
+## 1.10
+
+- Removed dependency on `protobuf-javalite` (see
+ [issue #24](https://github.com/google/TestParameterInjector/issues/24))
+
+## 1.9
+
+- Bugfix: Support explicit ordering by the JUnit4 `@Rule`. For example: `@Rule(ordering=3)`.
+- Potential test name change: Test names are no longer dependent on the locale of the machine
+ running it (e.g. doubles with integer values are always formatted with a trailing `.0`)
+
+## 1.8
+
+- Add support for JUnit5 (Jupiter)
+
+## 1.7
+
+- Remove `TestParameterInjector` support for `org.junit.runners.Parameterized`,
+ which was undocumented and thus unlikely to be used.
+
+## 1.6
+
+- Bugfixes
+- Better documentation
+
+## 1.5
+
+- `@TestParameters` can now also be used as a repeated annotation:
+
+```java
+// Newly added and recommended for new code
+@Test
+@TestParameters("{age: 17, expectIsAdult: false}")
+@TestParameters("{age: 22, expectIsAdult: true}")
+public void withRepeatedAnnotation(int age, boolean expectIsAdult){...}
+
+// The old way of using @TestParameters is still supported
+@Test
+@TestParameters({
+ "{age: 17, expectIsAdult: false}",
+ "{age: 22, expectIsAdult: true}",
+})
+public void withSingleAnnotation(int age, boolean expectIsAdult){...}
+```
+
+- `@TestParameters` supports setting a custom test name:
+
+```java
+@Test
+@TestParameters(customName = "teenager", value = "{age: 17, expectIsAdult: false}")
+@TestParameters(customName = "young adult", value = "{age: 22, expectIsAdult: true}")
+public void personIsAdult(int age, boolean expectIsAdult){...}
+```
+
+- Test names with very long parameter strings are abbreviated differentily: In
+ some cases, more characters are allowed.
+
## 1.4
- Bugfix: Run test methods declared in a base class (instead of throwing an
diff --git a/METADATA b/METADATA
index 0a617ab..f2d2416 100644
--- a/METADATA
+++ b/METADATA
@@ -11,7 +11,7 @@ third_party {
type: GIT
value: "https://github.com/google/TestParameterInjector"
}
- version: "e65d6bebdba9df211b258fae996fe34b6eadb787"
- last_upgrade_date { year: 2021 month: 7 day: 26 }
+ version: "12066d29df68922d8c4a1a0c2c6128abc487340f"
+ last_upgrade_date { year: 2024 month: 4 day: 5 }
license_type: NOTICE
}
diff --git a/README.md b/README.md
index 7af215b..d0178a6 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@ TestParameterInjector
## Introduction
-`TestParameterInjector` is a JUnit4 test runner that runs its test methods for
+`TestParameterInjector` is a JUnit4 and JUnit5 test runner that runs its test methods for
different combinations of field/parameter values.
Parameterized tests are a great way to avoid code duplication between tests and
@@ -23,6 +23,8 @@ frameworks used at Google.
## Getting started
+### JUnit4
+
To start using `TestParameterInjector` right away, copy the following snippet:
```java
@@ -52,7 +54,8 @@ And add the following dependency to your `.pom` file:
<dependency>
<groupId>com.google.testparameterinjector</groupId>
<artifactId>test-parameter-injector</artifactId>
- <version>1.4</version>
+ <version>1.15</version>
+ <scope>test</scope>
</dependency>
```
@@ -60,9 +63,57 @@ or see [this maven.org
page](https://search.maven.org/artifact/com.google.testparameterinjector/test-parameter-injector)
for instructions for other build tools.
+### JUnit5 (Jupiter)
+<details>
+<summary>Click to expand</summary>
+
+To start using `TestParameterInjector` right away, copy the following snippet:
+
+```java
+import com.google.testing.junit.testparameterinjector.junit5.TestParameterInjectorTest;
+import com.google.testing.junit.testparameterinjector.junit5.TestParameter;
+
+class MyTest {
+
+ @TestParameter boolean isDryRun;
+
+ @TestParameterInjectorTest
+ void test1(@TestParameter boolean enableFlag) {
+ // ...
+ }
+
+ @TestParameterInjectorTest
+ void test2(@TestParameter MyEnum myEnum) {
+ // ...
+ }
+
+ enum MyEnum { VALUE_A, VALUE_B, VALUE_C }
+}
+```
+
+And add the following dependency to your `.pom` file:
+
+```xml
+<dependency>
+ <groupId>com.google.testparameterinjector</groupId>
+ <artifactId>test-parameter-injector-junit5</artifactId>
+ <version>1.15</version>
+ <scope>test</scope>
+</dependency>
+```
+
+or see [this maven.org
+page](https://search.maven.org/artifact/com.google.testparameterinjector/test-parameter-injector-junit5)
+for instructions for other build tools.
+
+</details>
## Basics
+**Note about JUnit4 vs JUnit5:**<br />
+The code below assumes you're using JUnit4. For JUnit5 users, simply remove the
+`@RunWith` annotation and replace `@Test` by `@TestParameterInjectorTest`.
+
### `@TestParameter` for testing all combinations
#### Parameterizing a single test method
@@ -108,6 +159,10 @@ public class MyTest {
In this example, both `test1` and `test2` will be run twice (once for each
parameter value).
+The test runner will set these fields before calling any methods, so it is safe
+to use such `@TestParameter`-annotated fields for setting up other test values
+and behavior in `@Before` methods.
+
#### Supported types
The following examples show most of the supported types. See the `@TestParameter` javadoc for more details.
@@ -207,44 +262,116 @@ mappings:
```java
@Test
-@TestParameters({
- "{age: 17, expectIsAdult: false}",
- "{age: 22, expectIsAdult: true}",
-})
+@TestParameters("{age: 17, expectIsAdult: false}")
+@TestParameters("{age: 22, expectIsAdult: true}")
public void personIsAdult(int age, boolean expectIsAdult) { ... }
```
+which would generate the following tests:
+
+```
+MyTest#personIsAdult[{age: 17, expectIsAdult: false}]
+MyTest#personIsAdult[{age: 22, expectIsAdult: true}]
+```
+
The string format supports the same types as `@TestParameter` (e.g. enums). See
the `@TestParameters` javadoc for more info.
`@TestParameters` works in the same way on the constructor, in which case all
tests will be run for the given parameter sets.
+> Tip: Consider setting a custom name if the YAML string is large:
+>
+> ```java
+> @Test
+> @TestParameters(customName = "teenager", value = "{age: 17, expectIsAdult: false}")
+> @TestParameters(customName = "young adult", value = "{age: 22, expectIsAdult: true}")
+> public void personIsAdult(int age, boolean expectIsAdult) { ... }
+> ```
+>
+> This will generate the following test names:
+>
+> ```
+> MyTest#personIsAdult[teenager]
+> MyTest#personIsAdult[young adult]
+> ```
+
+### Filtering unwanted parameters
+
+Sometimes, you want to exclude a parameter or a combination of parameters. We
+recommend doing this via JUnit assumptions which is also supported by
+[Truth](https://truth.dev/):
+
+```java
+import static com.google.common.truth.TruthJUnit.assume;
+
+@Test
+public void myTest(@TestParameter Fruit fruit) {
+ assume().that(fruit).isNotEqualTo(Fruit.BANANA);
+
+ // At this point, the test will only run for APPLE and CHERRY.
+ // The BANANA case will silently be ignored.
+}
+
+enum Fruit { APPLE, BANANA, CHERRY }
+```
+
+Note that the above works regardless of what parameterization framework you
+choose.
+
## Advanced usage
+**Note about JUnit4 vs JUnit5:**<br />
+The code below assumes you're using JUnit4. For JUnit5 users, simply remove the
+`@RunWith` annotation and replace `@Test` by `@TestParameterInjectorTest`.
+
### Dynamic parameter generation for `@TestParameter`
Instead of providing a list of parsable strings, you can implement your own
`TestParameterValuesProvider` as follows:
```java
+import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider;
+
@Test
public void matchesAllOf_throwsOnNull(
@TestParameter(valuesProvider = CharMatcherProvider.class) CharMatcher charMatcher) {
assertThrows(NullPointerException.class, () -> charMatcher.matchesAllOf(null));
}
-private static final class CharMatcherProvider implements TestParameterValuesProvider {
+private static final class CharMatcherProvider extends TestParameterValuesProvider {
@Override
- public List<CharMatcher> provideValues() {
+ public List<CharMatcher> provideValues(Context context) {
return ImmutableList.of(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace());
}
}
```
-Note that `provideValues()` dynamically construct the returned list, e.g. by
-reading a file. There are no restrictions on the object types returned, but note
-that `toString()` will be used for the test names.
+Notes:
+
+- The `provideValues()` method can dynamically construct the returned list,
+ e.g. by reading a file.
+- There are no restrictions on the object types returned.
+- The `provideValues()` method is called before `@BeforeClass`, so don't rely
+ on any static state initialized in there.
+- The returned objects' `toString()` will be used for the test names. If you
+ want to customize the value names, you can do that as follows:
+
+ ```
+ private static final class FruitProvider extends TestParameterValuesProvider {
+ @Override
+ public List<?> provideValues(Context context) {
+ return ImmutableList.of(
+ value(new Apple()).withName("apple"),
+ value(new Banana()).withName("banana"));
+ }
+ }
+ ```
+
+- The given `Context` contains the test class and other annotations on the
+ `@TestParameter`-annotated parameter/field. This allows more generic
+ providers that take into account custom annotations with extra data, or the
+ implementation of abstract methods on a base test class.
### Dynamic parameter generation for `@TestParameters`
diff --git a/junit4/pom.xml b/junit4/pom.xml
new file mode 100644
index 0000000..789f648
--- /dev/null
+++ b/junit4/pom.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright 2021 Google Inc.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>com.google.testparameterinjector</groupId>
+ <artifactId>test-parameter-injector-parent</artifactId>
+ <version>HEAD-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>test-parameter-injector</artifactId>
+
+ <name>TestParameterInjector for JUnit4</name>
+
+ <dependencies>
+ <!-- Compile-time dependencies -->
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>4.13.2</version>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java
new file mode 100644
index 0000000..6c23efa
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static java.lang.Math.min;
+
+import com.google.common.collect.FluentIterable;
+import java.lang.annotation.Annotation;
+import java.util.List;
+
+/**
+ * Default base class for {@link TestParameterValidator}, simplifying how validators can exclude
+ * variable independent test parameters annotations.
+ */
+abstract class BaseTestParameterValidator implements TestParameterValidator {
+
+ @Override
+ public boolean shouldSkip(Context context) {
+ for (List<Class<? extends Annotation>> parameters : getIndependentParameters(context)) {
+ checkArgument(!parameters.isEmpty());
+ // For independent test parameters, the only allowed tests will be those that use the same
+ // Nth specified parameter, except for parameter values that have less specified values than
+ // others.
+
+ // For example, if parameter A has values a1 and a2, parameter B has values b1 and b2, and
+ // parameter C has values c1, c2 and c3, given that A, B and C are independent, the only
+ // tests that will not be skipped will be {(a1, b1, c1), (a2, b2, c2), (a2, b2, c3)},
+ // instead of 12 tests that would constitute their cartesian product.
+
+ // First, find the largest specified value count (parameter C in the example above),
+ // so that we can easily determine which parameter value should be used for validating the
+ // other parameters (e.g. should this test be for (a1, b1, c1), (a2, b2, c2), or
+ // (a2, b2, c3). The test parameter 'C' will be the 'leadingParameter'.
+
+ Class<? extends Annotation> leadingParameter =
+ FluentIterable.from(parameters)
+ .toSortedList(
+ (o1, o2) ->
+ Integer.compare(
+ context.getSpecifiedValues(o1).size(),
+ context.getSpecifiedValues(o2).size()))
+ .reverse()
+ .get(0);
+
+ // Second, determine which index is the current value in the specified value list of
+ // the leading parameter. In the example above, the index of the current value 'c2' of the
+ // leading parameter 'C' would be '1', given the specified values (c1, c2, c3).
+ int leadingParameterValueIndex =
+ getValueIndex(context, leadingParameter, context.getValue(leadingParameter).get());
+ checkState(leadingParameterValueIndex >= 0);
+ // Each independent test parameter should be the same index, or the last available index.
+ // For example, if the parameter is A, and the leading parameter (C) index is 2, the A's index
+ // should be 1, since a2 is the only available value.
+ for (Class<? extends Annotation> parameter : parameters) {
+ List<Object> specifiedValues = context.getSpecifiedValues(parameter);
+ int valueIndex = specifiedValues.indexOf(context.getValue(parameter).get());
+ int requiredValueIndex = min(leadingParameterValueIndex, specifiedValues.size() - 1);
+ if (valueIndex != requiredValueIndex) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private int getValueIndex(Context context, Class<? extends Annotation> annotation, Object value) {
+ return context.getSpecifiedValues(annotation).indexOf(value);
+ }
+
+ /**
+ * Returns a list of TestParameterAnnotation annotated annotation types that are mutually
+ * independent, and therefore the combinations of their values do not need to be tested.
+ */
+ protected abstract List<List<Class<? extends Annotation>>> getIndependentParameters(
+ Context context);
+}
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ByteStringReflection.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ByteStringReflection.java
new file mode 100644
index 0000000..ca94a39
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ByteStringReflection.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2022 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableMap;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Utility methods to interact with com.google.protobuf.ByteString via reflection.
+ *
+ * <p>This is a hack to avoid the open source project to depend on protobuf-lite/javalite, which is
+ * causing conflicts for users (see https://github.com/google/TestParameterInjector/issues/24).
+ */
+final class ByteStringReflection {
+
+ static final Optional<Class<?>> MAYBE_BYTE_STRING_CLASS = maybeGetByteStringClass();
+
+ /** Equivalent of {@code object instanceof ByteString} */
+ static boolean isInstanceOfByteString(Object object) {
+ if (MAYBE_BYTE_STRING_CLASS.isPresent()) {
+ return MAYBE_BYTE_STRING_CLASS.get().isInstance(object);
+ } else {
+ return false;
+ }
+ }
+
+ /** Eqvuivalent of {@code ((ByteString) byteString).toByteArray()} */
+ static byte[] byteStringToByteArray(Object byteString) {
+ return (byte[])
+ invokeByteStringMethod("toByteArray", /* obj= */ byteString, /* args= */ ImmutableMap.of());
+ }
+
+ /**
+ * Eqvuivalent of {@code ByteString.copyFromUtf8(text)}.
+ *
+ * <p>Encodes {@code text} into a sequence of UTF-8 bytes and returns the result as a {@code
+ * ByteString}.
+ */
+ static Object copyFromUtf8(String text) {
+ return invokeByteStringMethod(
+ "copyFromUtf8", /* obj= */ null, /* args= */ ImmutableMap.of(String.class, text));
+ }
+
+ /**
+ * Eqvuivalent of {@code ByteString.copyFrom(bytes)}.
+ *
+ * <p>Copies the given bytes into a {@code ByteString}.
+ */
+ static Object copyFrom(byte[] bytes) {
+ return invokeByteStringMethod(
+ "copyFrom", /* obj= */ null, /* args= */ ImmutableMap.of(byte[].class, bytes));
+ }
+
+ @SuppressWarnings("UseMultiCatch")
+ private static Object invokeByteStringMethod(
+ String methodName, Object obj, ImmutableMap<Class<?>, ?> args) {
+ try {
+ return MAYBE_BYTE_STRING_CLASS
+ .get()
+ .getMethod(methodName, args.keySet().toArray(new Class<?>[0]))
+ .invoke(obj, args.values().toArray());
+ /*
+ * Do not merge the 3 catch blocks below. javac would infer a type of
+ * ReflectiveOperationException, which Animal Sniffer would reject. (Old versions of
+ * Android don't *seem* to mind, but there might be edge cases of which we're unaware.)
+ */
+ } catch (IllegalAccessException e) {
+ throw new LinkageError(String.format("Accessing %s()", methodName), e);
+ } catch (InvocationTargetException e) {
+ throw new LinkageError(String.format("Calling %s()", methodName), e);
+ } catch (NoSuchMethodException e) {
+ throw new LinkageError(String.format("Calling %s()", methodName), e);
+ }
+ }
+
+ private static Optional<Class<?>> maybeGetByteStringClass() {
+ try {
+ return Optional.of(Class.forName("com.google.protobuf.ByteString"));
+ } catch (ClassNotFoundException | LinkageError unused) {
+ return Optional.absent();
+ }
+ }
+
+ private ByteStringReflection() {} // Inhibit instantiation
+}
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java
new file mode 100644
index 0000000..47b445b
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.Iterables.getOnlyElement;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+
+/**
+ * Value class that captures the result of a validating a single constructor or test method.
+ *
+ * <p>If the validation is not validated by any processor, it will be validated using the default
+ * validator. If a processor validates a constructor/test method, the remaining processors will
+ * *not* be called.
+ */
+@AutoValue
+abstract class ExecutableValidationResult {
+
+ /** Returns true if the properties of the given constructor/test method were validated. */
+ public abstract boolean wasValidated();
+
+ /** Returns the validation errors, if any. */
+ public abstract ImmutableList<Throwable> validationErrors();
+
+ static ExecutableValidationResult notValidated() {
+ return of(/* wasValidated= */ false, /* validationErrors= */ ImmutableList.of());
+ }
+
+ static ExecutableValidationResult validated(Collection<Throwable> errors) {
+ return of(/* wasValidated= */ true, /* validationErrors= */ errors);
+ }
+
+ static ExecutableValidationResult validated(Throwable error) {
+ return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of(error));
+ }
+
+ static ExecutableValidationResult valid() {
+ return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of());
+ }
+
+ private static ExecutableValidationResult of(
+ boolean wasValidated, Collection<Throwable> validationErrors) {
+ checkArgument(wasValidated || validationErrors.isEmpty());
+ return new AutoValue_ExecutableValidationResult(
+ wasValidated, ImmutableList.copyOf(validationErrors));
+ }
+
+ void assertValid() {
+ if (wasValidated() && !validationErrors().isEmpty()) {
+ if (validationErrors().size() == 1) {
+ throw new AssertionError(getOnlyElement(validationErrors()));
+ } else {
+ throw new AssertionError(String.format("Found validation errors: %s", validationErrors()));
+ }
+ }
+ }
+}
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/GenericParameterContext.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/GenericParameterContext.java
new file mode 100644
index 0000000..5586d7b
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/GenericParameterContext.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2024 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.collect.Iterables.getOnlyElement;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Ordering;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Repeatable;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.lang.reflect.Proxy;
+import java.util.NoSuchElementException;
+
+/** A value class that contains extra information about the context of a field or parameter. */
+final class GenericParameterContext {
+
+ private final ImmutableList<Annotation> annotationsOnParameter;
+
+ /** Same contract as #getAnnotations */
+ private final Function<Class<? extends Annotation>, ImmutableList<? extends Annotation>>
+ getAnnotationsFunction;
+
+ private final Class<?> testClass;
+
+ private GenericParameterContext(
+ ImmutableList<Annotation> annotationsOnParameter,
+ Function<Class<? extends Annotation>, ImmutableList<? extends Annotation>>
+ getAnnotationsFunction,
+ Class<?> testClass) {
+ this.annotationsOnParameter = annotationsOnParameter;
+ this.getAnnotationsFunction = getAnnotationsFunction;
+ this.testClass = testClass;
+ }
+
+ // Field.getAnnotationsByType() is not available on old Android SDKs. There is a fallback in that
+ // case in this method.
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ static GenericParameterContext create(Field field, Class<?> testClass) {
+ return new GenericParameterContext(
+ ImmutableList.copyOf(field.getAnnotations()),
+ /* getAnnotationsFunction= */ annotationType -> {
+ try {
+ return ImmutableList.copyOf(field.getAnnotationsByType(annotationType));
+ } catch (NoSuchMethodError ignored) {
+ return getAnnotationsFallback(
+ ImmutableList.copyOf(field.getAnnotations()), annotationType);
+ }
+ },
+ testClass);
+ }
+
+ // Parameter is not available on old Android SDKs, and isn't desugared. That's why this method
+ // should only be called with a fallback.
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ static GenericParameterContext create(Parameter parameter, Class<?> testClass) {
+ return new GenericParameterContext(
+ ImmutableList.copyOf(parameter.getAnnotations()),
+ /* getAnnotationsFunction= */ annotationType ->
+ ImmutableList.copyOf(parameter.getAnnotationsByType(annotationType)),
+ testClass);
+ }
+
+ static GenericParameterContext createWithRepeatableAnnotationsFallback(
+ Annotation[] annotationsOnParameter, Class<?> testClass) {
+ return new GenericParameterContext(
+ ImmutableList.copyOf(annotationsOnParameter),
+ /* getAnnotationsFunction= */ annotationType ->
+ getAnnotationsFallback(ImmutableList.copyOf(annotationsOnParameter), annotationType),
+ testClass);
+ }
+
+ static GenericParameterContext createWithoutParameterAnnotations(Class<?> testClass) {
+ return new GenericParameterContext(
+ /* annotationsOnParameter= */ ImmutableList.of(),
+ /* getAnnotationsFunction= */ annotationType ->
+ getAnnotationsFallback(ImmutableList.of(), annotationType),
+ testClass);
+ }
+
+ /**
+ * Returns the only annotation with the given type on the field or parameter.
+ *
+ * @throws NoSuchElementException if this there is no annotation with the given type
+ * @throws IllegalArgumentException if there are multiple annotations with the given type
+ */
+ @SuppressWarnings("unchecked") // Safe because of the filter operation
+ <A extends Annotation> A getAnnotation(Class<A> annotationType) {
+ return (A)
+ getOnlyElement(
+ FluentIterable.from(annotationsOnParameter)
+ .filter(annotation -> annotation.annotationType().equals(annotationType))
+ .toList());
+ }
+
+ /**
+ * Returns the annotations with the given type on the field or parameter.
+ *
+ * <p>Returns an empty list if this there is no annotation with the given type.
+ */
+ @SuppressWarnings("unchecked") // Safe because of the getAnnotationsFunction contract
+ <A extends Annotation> ImmutableList<A> getAnnotations(Class<A> annotationType) {
+ return (ImmutableList<A>) getAnnotationsFunction.apply(annotationType);
+ }
+
+ /** The class that contains the test that is currently being run. */
+ Class<?> testClass() {
+ return testClass;
+ }
+
+ /** A list of all annotations on the field or parameter. */
+ ImmutableList<Annotation> annotationsOnParameter() {
+ return annotationsOnParameter;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "context(annotationsOnParameter=[%s],testClass=%s)",
+ FluentIterable.from(
+ ImmutableList.sortedCopyOf(
+ Ordering.natural().onResultOf(Annotation::toString), annotationsOnParameter))
+ .transform(
+ annotation -> String.format("@%s", annotation.annotationType().getSimpleName()))
+ .join(Joiner.on(',')),
+ testClass().getSimpleName());
+ }
+
+ private static ImmutableList<Annotation> getAnnotationsFallback(
+ ImmutableList<Annotation> annotationsOnParameter,
+ Class<? extends Annotation> annotationType) {
+ ImmutableList<Annotation> candidates =
+ FluentIterable.from(annotationsOnParameter)
+ .filter(annotation -> annotation.annotationType().equals(annotationType))
+ .toList();
+ if (candidates.isEmpty() && getContainerType(annotationType).isPresent()) {
+ ImmutableList<Annotation> containerAnnotations =
+ getAnnotationsFallback(annotationsOnParameter, getContainerType(annotationType).get());
+ if (containerAnnotations.size() == 1) {
+ Annotation containerAnnotation = getOnlyElement(containerAnnotations);
+ try {
+ Method annotationValueMethod =
+ containerAnnotation.annotationType().getDeclaredMethod("value");
+ annotationValueMethod.setAccessible(true);
+ return ImmutableList.copyOf(
+ (Annotation[])
+ Proxy.getInvocationHandler(containerAnnotation)
+ .invoke(containerAnnotation, annotationValueMethod, null));
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return ImmutableList.of();
+ } else {
+ return candidates;
+ }
+ }
+
+ private static Optional<Class<? extends Annotation>> getContainerType(
+ Class<? extends Annotation> annotationType) {
+ try {
+ Repeatable repeatable = annotationType.getAnnotation(Repeatable.class);
+ if (repeatable == null) {
+ return Optional.absent();
+ } else {
+ return Optional.of(repeatable.value());
+ }
+ } catch (NoClassDefFoundError ignored) {
+ // If @Repeatable does not exist, then there is no container type by definition
+ return Optional.absent();
+ }
+ }
+}
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java
new file mode 100644
index 0000000..e09c1d9
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.collect.Lists;
+import com.google.common.primitives.Primitives;
+import com.google.common.primitives.UnsignedLong;
+import com.google.common.reflect.TypeToken;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.lang.reflect.Array;
+import java.lang.reflect.ParameterizedType;
+import java.math.BigInteger;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.annotation.Nullable;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
+
+/** A helper class for parsing parameter values from strings. */
+final class ParameterValueParsing {
+
+ @SuppressWarnings("unchecked")
+ static <E extends Enum<E>> Enum<?> parseEnum(String str, Class<?> enumType) {
+ return Enum.valueOf((Class<E>) enumType, str);
+ }
+
+ static boolean isValidYamlString(String yamlString) {
+ try {
+ new Yaml(new SafeConstructor(new LoaderOptions())).load(yamlString);
+ return true;
+ } catch (RuntimeException e) {
+ return false;
+ }
+ }
+
+ static Object parseYamlStringToJavaType(String yamlString, Class<?> javaType) {
+ return parseYamlObjectToJavaType(parseYamlStringToObject(yamlString), TypeToken.of(javaType));
+ }
+
+ static Object parseYamlStringToObject(String yamlString) {
+ return new Yaml(new SafeConstructor(new LoaderOptions())).load(yamlString);
+ }
+
+ private static UnsignedLong parseYamlSignedLongToUnsignedLong(long number) {
+ checkState(number >= 0, "%s should be greater than or equal to zero", number);
+ return UnsignedLong.fromLongBits(number);
+ }
+
+ @SuppressWarnings({"unchecked"})
+ static Object parseYamlObjectToJavaType(Object parsedYaml, TypeToken<?> javaType) {
+ // Pass along null so we don't have to worry about it below
+ if (parsedYaml == null) {
+ return null;
+ }
+
+ YamlValueTransformer yamlValueTransformer =
+ new YamlValueTransformer(parsedYaml, javaType.getRawType());
+
+ yamlValueTransformer
+ .ifJavaType(String.class)
+ .supportParsedType(String.class, self -> self)
+ // Also support other primitives because it's easy to accidentally write e.g. a number when
+ // a string was intended in YAML
+ .supportParsedType(Boolean.class, Object::toString)
+ .supportParsedType(Integer.class, Object::toString)
+ .supportParsedType(Long.class, Object::toString)
+ .supportParsedType(Double.class, Object::toString);
+
+ yamlValueTransformer.ifJavaType(Boolean.class).supportParsedType(Boolean.class, self -> self);
+
+ yamlValueTransformer.ifJavaType(Integer.class).supportParsedType(Integer.class, self -> self);
+
+ yamlValueTransformer
+ .ifJavaType(Long.class)
+ .supportParsedType(Long.class, self -> self)
+ .supportParsedType(Integer.class, Integer::longValue);
+
+ yamlValueTransformer
+ .ifJavaType(UnsignedLong.class)
+ .supportParsedType(Long.class, self -> parseYamlSignedLongToUnsignedLong(self.longValue()))
+ .supportParsedType(
+ Integer.class, self -> parseYamlSignedLongToUnsignedLong(self.longValue()))
+ // UnsignedLong::valueOf(BigInteger) will validate that BigInteger is in the valid range and
+ // throws otherwise.
+ .supportParsedType(BigInteger.class, UnsignedLong::valueOf);
+
+ yamlValueTransformer
+ .ifJavaType(BigInteger.class)
+ .supportParsedType(Long.class, self -> BigInteger.valueOf(self.longValue()))
+ .supportParsedType(Integer.class, self -> BigInteger.valueOf(self.longValue()))
+ .supportParsedType(BigInteger.class, self -> self);
+
+ yamlValueTransformer
+ .ifJavaType(Float.class)
+ .supportParsedType(Float.class, self -> self)
+ .supportParsedType(Double.class, Double::floatValue)
+ .supportParsedType(Integer.class, Integer::floatValue)
+ .supportParsedType(String.class, Float::valueOf);
+
+ yamlValueTransformer
+ .ifJavaType(Double.class)
+ .supportParsedType(Double.class, self -> self)
+ .supportParsedType(Integer.class, Integer::doubleValue)
+ .supportParsedType(Long.class, Long::doubleValue)
+ .supportParsedType(String.class, Double::valueOf);
+
+ yamlValueTransformer
+ .ifJavaType(Enum.class)
+ .supportParsedType(
+ String.class, str -> ParameterValueParsing.parseEnum(str, javaType.getRawType()));
+
+ yamlValueTransformer
+ .ifJavaType(byte[].class)
+ .supportParsedType(byte[].class, self -> self)
+ // Uses String based charset because StandardCharsets was not introduced until later
+ // versions of Android
+ // See https://developer.android.com/reference/java/nio/charset/StandardCharsets.
+ .supportParsedType(String.class, s -> s.getBytes(Charset.forName("UTF-8")));
+
+ if (ByteStringReflection.MAYBE_BYTE_STRING_CLASS.isPresent()) {
+ yamlValueTransformer
+ .ifJavaType((Class<Object>) ByteStringReflection.MAYBE_BYTE_STRING_CLASS.get())
+ .supportParsedType(String.class, ByteStringReflection::copyFromUtf8)
+ .supportParsedType(byte[].class, ByteStringReflection::copyFrom);
+ }
+
+ // Added mainly for protocol buffer parsing
+ yamlValueTransformer
+ .ifJavaType(List.class)
+ .supportParsedType(
+ List.class,
+ list ->
+ Lists.transform(
+ list,
+ e ->
+ parseYamlObjectToJavaType(
+ e, getGenericParameterType(javaType, /* parameterIndex= */ 0))));
+ yamlValueTransformer
+ .ifJavaType(Map.class)
+ .supportParsedType(Map.class, map -> parseYamlMapToJavaMap(map, javaType));
+
+ return yamlValueTransformer.transformedJavaValue();
+ }
+
+ private static Map<?, ?> parseYamlMapToJavaMap(Map<?, ?> map, TypeToken<?> javaType) {
+ Map<Object, Object> returnedMap = new LinkedHashMap<>();
+ for (Entry<?, ?> entry : map.entrySet()) {
+ returnedMap.put(
+ parseYamlObjectToJavaType(
+ entry.getKey(), getGenericParameterType(javaType, /* parameterIndex= */ 0)),
+ parseYamlObjectToJavaType(
+ entry.getValue(), getGenericParameterType(javaType, /* parameterIndex= */ 1)));
+ }
+ return returnedMap;
+ }
+
+ private static TypeToken<?> getGenericParameterType(TypeToken<?> typeToken, int parameterIndex) {
+ checkArgument(
+ typeToken.getType() instanceof ParameterizedType,
+ "Could not parse the generic parameter of type %s",
+ typeToken);
+
+ ParameterizedType parameterizedType = (ParameterizedType) typeToken.getType();
+ return TypeToken.of(parameterizedType.getActualTypeArguments()[parameterIndex]);
+ }
+
+ private static final class YamlValueTransformer {
+ private final Object parsedYaml;
+ private final Class<?> javaType;
+ @Nullable private Object transformedJavaValue;
+
+ YamlValueTransformer(Object parsedYaml, Class<?> javaType) {
+ this.parsedYaml = parsedYaml;
+ this.javaType = javaType;
+ }
+
+ <JavaT> SupportedJavaType<JavaT> ifJavaType(Class<JavaT> supportedJavaType) {
+ return new SupportedJavaType<>(supportedJavaType);
+ }
+
+ Object transformedJavaValue() {
+ checkArgument(
+ transformedJavaValue != null,
+ "Could not map YAML value %s (class = %s) to java class %s",
+ parsedYaml,
+ parsedYaml.getClass(),
+ javaType);
+ return transformedJavaValue;
+ }
+
+ final class SupportedJavaType<JavaT> {
+
+ private final Class<JavaT> supportedJavaType;
+
+ private SupportedJavaType(Class<JavaT> supportedJavaType) {
+ this.supportedJavaType = supportedJavaType;
+ }
+
+ @SuppressWarnings("unchecked")
+ @CanIgnoreReturnValue
+ <ParsedYamlT> SupportedJavaType<JavaT> supportParsedType(
+ Class<ParsedYamlT> parsedYamlType, Function<ParsedYamlT, JavaT> transformation) {
+ if (Primitives.wrap(supportedJavaType).isAssignableFrom(Primitives.wrap(javaType))) {
+ if (Primitives.wrap(parsedYamlType).isInstance(parsedYaml)) {
+ checkState(
+ transformedJavaValue == null,
+ "This case is already handled. This is a bug in"
+ + " testparameterinjector.TestParametersMethodProcessor.");
+ try {
+ transformedJavaValue = checkNotNull(transformation.apply((ParsedYamlT) parsedYaml));
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Could not map YAML value %s (class = %s) to java class %s",
+ parsedYaml, parsedYaml.getClass(), javaType),
+ e);
+ }
+ }
+ }
+
+ return this;
+ }
+ }
+ }
+
+ static String formatTestNameString(Optional<String> parameterName, @Nullable Object value) {
+ Object unwrappedValue;
+ Optional<String> customName;
+
+ if (value instanceof TestParameterValue) {
+ TestParameterValue tpValue = (TestParameterValue) value;
+ unwrappedValue = tpValue.getWrappedValue();
+ customName = tpValue.getCustomName();
+ } else {
+ unwrappedValue = value;
+ customName = Optional.absent();
+ }
+
+ String result = customName.or(() -> valueAsString(unwrappedValue));
+ if (parameterName.isPresent() && !customName.isPresent()) {
+ if (unwrappedValue == null
+ ||
+ // Primitives are often ambiguous
+ Primitives.unwrap(unwrappedValue.getClass()).isPrimitive()
+ // Ambiguous String cases
+ || unwrappedValue.equals("null")
+ || (unwrappedValue instanceof CharSequence
+ && CharMatcher.anyOf("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
+ .matchesNoneOf((CharSequence) unwrappedValue))) {
+ // Prefix the parameter value with its field name. This is to avoid test names
+ // such as myMethod_success[true,false,2]. Instead, it'll be
+ // myMethod_success[dryRun=true,experimentFlag=false,retries=2].
+ result = String.format("%s=%s", parameterName.get(), valueAsString(unwrappedValue));
+ }
+ }
+ return result.trim().replaceAll("\\s+", " ");
+ }
+
+ private static String valueAsString(Object value) {
+ if (value != null && value.getClass().isArray()) {
+ StringBuilder resultBuider = new StringBuilder();
+ resultBuider.append("[");
+ for (int i = 0; i < Array.getLength(value); i++) {
+ if (i > 0) {
+ resultBuider.append(", ");
+ }
+ resultBuider.append(Array.get(value, i));
+ }
+ resultBuider.append("]");
+ return resultBuider.toString();
+ } else if (ByteStringReflection.isInstanceOfByteString(value)) {
+ return Arrays.toString(ByteStringReflection.byteStringToByteArray(value));
+ } else {
+ return String.valueOf(value);
+ }
+ }
+
+ private ParameterValueParsing() {}
+}
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java
new file mode 100644
index 0000000..b2a0ad8
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java
@@ -0,0 +1,441 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Lists;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.internal.runners.model.ReflectiveCallable;
+import org.junit.internal.runners.statements.Fail;
+import org.junit.internal.runners.statements.FailOnTimeout;
+import org.junit.rules.MethodRule;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.MemberValueConsumer;
+import org.junit.runners.model.Statement;
+import org.junit.runners.model.TestClass;
+
+/**
+ * Class to substitute JUnit4 runner in JUnit4 tests, adding additional functionality.
+ *
+ * <p>See {@link TestParameterInjector} for an example implementation.
+ */
+abstract class PluggableTestRunner extends BlockJUnit4ClassRunner {
+
+ /**
+ * A {@link ThreadLocal} is used to handle cases where multiple tests are executing in the same
+ * java process in different threads.
+ *
+ * <p>A null value indicates that the TestInfo hasn't been set yet, which would typically happen
+ * if the test hasn't yet started, or the {@link PluggableTestRunner} is not the test runner.
+ */
+ private static final ThreadLocal<TestInfo> currentTestInfo = new ThreadLocal<>();
+
+ private TestMethodProcessorList testMethodProcessors;
+
+ protected PluggableTestRunner(Class<?> klass) throws InitializationError {
+ super(klass);
+ }
+
+ /** Returns the TestMethodProcessorList to use. This is meant to be overridden by subclasses. */
+ protected abstract TestMethodProcessorList createTestMethodProcessorList();
+
+ /**
+ * This method is run to perform optional additional operations on the test instance, right after
+ * it was created.
+ */
+ protected void finalizeCreatedTestInstance(Object testInstance) {
+ // Do nothing by default
+ }
+
+ /**
+ * If true, all test methods (across different TestMethodProcessors) will be sorted in a
+ * deterministic way.
+ *
+ * <p>Deterministic means that the order will not change, even when tests are added/removed or
+ * between releases.
+ *
+ * @deprecated Override {@link #sortTestMethods} with preferred sorting strategy.
+ */
+ @Deprecated
+ protected boolean shouldSortTestMethodsDeterministically() {
+ return false; // Don't sort methods by default
+ }
+
+ /**
+ * Sort test methods (across different TestMethodProcessors).
+ *
+ * <p>This should be deterministic. The order should not change, even when tests are added/removed
+ * or between releases.
+ */
+ protected ImmutableList<FrameworkMethod> sortTestMethods(ImmutableList<FrameworkMethod> methods) {
+ if (!shouldSortTestMethodsDeterministically()) {
+ return methods;
+ }
+ return FluentIterable.from(methods)
+ .toSortedList(
+ (o1, o2) ->
+ ComparisonChain.start()
+ .compare(o1.getName().hashCode(), o2.getName().hashCode())
+ .compare(o1.getName(), o2.getName())
+ .result());
+ }
+
+ /**
+ * Returns classes used as annotations to indicate test methods.
+ *
+ * <p>Defaults to {@link Test}.
+ */
+ protected ImmutableList<Class<? extends Annotation>> getSupportedTestAnnotations() {
+ return ImmutableList.of(Test.class);
+ }
+
+ /**
+ * {@link TestRule}s that will be executed before the ones defined in the test class. This is
+ * meant to be overridden by subclasses.
+ */
+ protected List<TestRule> getExtraTestRules() {
+ return ImmutableList.of();
+ }
+
+ @Override
+ protected final ImmutableList<FrameworkMethod> computeTestMethods() {
+ return sortTestMethods(
+ FluentIterable.from(getSupportedTestAnnotations())
+ .transformAndConcat(annotation -> getTestClass().getAnnotatedMethods(annotation))
+ .transformAndConcat(this::processMethod)
+ .toList());
+ }
+
+ /** Implementation of a JUnit FrameworkMethod where the name and annotation list is overridden. */
+ private static class OverriddenFrameworkMethod extends FrameworkMethod {
+
+ private final TestInfo testInfo;
+
+ public OverriddenFrameworkMethod(Method method, TestInfo testInfo) {
+ super(method);
+ this.testInfo = testInfo;
+ }
+
+ public TestInfo getTestInfo() {
+ return testInfo;
+ }
+
+ @Override
+ public String getName() {
+ return testInfo.getName();
+ }
+
+ @Override
+ public Annotation[] getAnnotations() {
+ List<Annotation> annotations = testInfo.getAnnotations();
+ return annotations.toArray(new Annotation[0]);
+ }
+
+ @Override
+ public <T extends Annotation> T getAnnotation(final Class<T> annotationClass) {
+ return testInfo.getAnnotation(annotationClass);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof PluggableTestRunner.OverriddenFrameworkMethod)) {
+ return false;
+ }
+
+ OverriddenFrameworkMethod other = (OverriddenFrameworkMethod) obj;
+ return super.equals(other) && other.testInfo.equals(testInfo);
+ }
+
+ @Override
+ public int hashCode() {
+ return super.hashCode() * 37 + testInfo.hashCode();
+ }
+ }
+
+ private ImmutableList<FrameworkMethod> processMethod(FrameworkMethod initialMethod) {
+ return FluentIterable.from(
+ getTestMethodProcessors()
+ .calculateTestInfos(initialMethod.getMethod(), getTestClass().getJavaClass()))
+ .transform(
+ testInfo ->
+ (FrameworkMethod) new OverriddenFrameworkMethod(testInfo.getMethod(), testInfo))
+ .toList();
+ }
+
+ // Note: This is a copy of the parent implementation, except that instead of calling
+ // #createTest(), this method calls #createTestForMethod(method).
+ @Override
+ protected final Statement methodBlock(final FrameworkMethod method) {
+ Object testObject;
+ try {
+ testObject =
+ new ReflectiveCallable() {
+ @Override
+ protected Object runReflectiveCall() throws Throwable {
+ return createTestForMethod(method);
+ }
+ }.run();
+ } catch (Throwable e) {
+ return new Fail(e);
+ }
+
+ Statement statement = methodInvoker(method, testObject);
+ statement = possiblyExpectingExceptions(method, testObject, statement);
+ statement = withPotentialTimeoutInternal(method, testObject, statement);
+ statement = withBefores(method, testObject, statement);
+ statement = withAfters(method, testObject, statement);
+ statement = withRules(method, testObject, statement);
+ statement = withInterruptIsolation(statement);
+ return statement;
+ }
+
+ // Note: This does the same as BlockJUnit4ClassRunner.withPotentialTimeout(), which is deprecated
+ // and will soon be private.
+ private Statement withPotentialTimeoutInternal(
+ FrameworkMethod method, Object test, Statement next) {
+ Test testAnnotation = method.getAnnotation(Test.class);
+ if (testAnnotation == null) {
+ return next;
+ } else if (testAnnotation.timeout() <= 0) {
+ return next;
+ } else {
+ return FailOnTimeout.builder()
+ .withTimeout(testAnnotation.timeout(), TimeUnit.MILLISECONDS)
+ .build(next);
+ }
+ }
+
+ @Override
+ protected final Statement methodInvoker(FrameworkMethod frameworkMethod, Object testObject) {
+ TestInfo testInfo = ((OverriddenFrameworkMethod) frameworkMethod).getTestInfo();
+
+ if (testInfo.getMethod().getParameterTypes().length == 0) {
+ return super.methodInvoker(frameworkMethod, testObject);
+ } else {
+ List<Object> parameters = getTestMethodProcessors().getTestMethodParameters(testInfo);
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ frameworkMethod.invokeExplosively(testObject, parameters.toArray());
+ }
+ };
+ }
+ }
+
+ /** Modifies the statement with each {@link MethodRule} and {@link TestRule} */
+ private Statement withRules(FrameworkMethod method, Object target, Statement statement) {
+ Description testDescription = describeChild(method);
+ TestClass testClass = getTestClass();
+
+ LinkedListMultimap<Integer, Object> orderToRulesMultimap = LinkedListMultimap.create();
+ MemberValueConsumer<Object> collector =
+ (frameworkMember, rule) -> {
+ Rule ruleAnnotation = frameworkMember.getAnnotation(Rule.class);
+ int order = ruleAnnotation == null ? Rule.DEFAULT_ORDER : ruleAnnotation.order();
+ if (orderToRulesMultimap.containsValue(rule)
+ && rule instanceof MethodRule
+ && rule instanceof TestRule) {
+ // This rule was already added because it is both a MethodRule and a TestRule.
+ // For legacy reasons, we need to put the new rule at the end of the list.
+ orderToRulesMultimap.remove(order, rule);
+ }
+ orderToRulesMultimap.put(order, rule);
+ };
+
+ testClass.collectAnnotatedMethodValues(target, Rule.class, MethodRule.class, collector::accept);
+ testClass.collectAnnotatedFieldValues(target, Rule.class, MethodRule.class, collector::accept);
+ testClass.collectAnnotatedMethodValues(target, Rule.class, TestRule.class, collector::accept);
+ testClass.collectAnnotatedFieldValues(target, Rule.class, TestRule.class, collector::accept);
+
+ ArrayList<Integer> keys = new ArrayList<>(orderToRulesMultimap.keySet());
+ Collections.sort(keys);
+ ImmutableList<Object> orderedRules =
+ FluentIterable.from(keys)
+ .transformAndConcat(
+ // Execute the rules in the reverse order of when the fields occurred. This may look
+ // counter-intuitive, but that is what the default JUnit4 runner does, and there is
+ // no reason to deviate from that here.
+ key -> Lists.reverse(orderToRulesMultimap.get(key)))
+ .toList();
+
+ // Note: The perceived order* is the reverse of the order in which the code below applies the
+ // rules to the statements because each subsequent rule wraps the previous statement.
+ //
+ // [*] The rule implementation can add its logic both before or after the base statement, so the
+ // order depends on the rule implementation. If all rules put their logic before the base
+ // statement, the order matches that of `orderedRules`.
+
+ for (Object rule : Lists.reverse(orderedRules)) {
+ if (rule instanceof TestRule) {
+ statement = ((TestRule) rule).apply(statement, testDescription);
+ } else if (rule instanceof MethodRule) {
+ statement = ((MethodRule) rule).apply(statement, method, target);
+ } else {
+ throw new AssertionError(rule);
+ }
+ }
+
+ // Apply extra rules
+ for (TestRule testRule : getExtraTestRules()) {
+ statement = testRule.apply(statement, testDescription);
+ }
+ statement = new ContextMethodRule().apply(statement, method, target);
+
+ return statement;
+ }
+
+ private Object createTestForMethod(FrameworkMethod method) throws Exception {
+ TestInfo testInfo = ((OverriddenFrameworkMethod) method).getTestInfo();
+ Constructor<?> constructor =
+ TestParameterInjectorUtils.getOnlyConstructor(getTestClass().getJavaClass());
+
+ // Construct a test instance
+ Object testInstance;
+ if (constructor.getParameterTypes().length == 0) {
+ testInstance = createTest();
+ } else {
+ List<Object> constructorParameters =
+ getTestMethodProcessors().getConstructorParameters(constructor, testInfo);
+ try {
+ testInstance = constructor.newInstance(constructorParameters.toArray());
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ // Run all post processors on the newly created instance
+ getTestMethodProcessors().postProcessTestInstance(testInstance, testInfo);
+
+ finalizeCreatedTestInstance(testInstance);
+
+ return testInstance;
+ }
+
+ @Override
+ protected final void validateZeroArgConstructor(List<Throwable> errorsReturned) {
+ ExecutableValidationResult validationResult =
+ getTestMethodProcessors()
+ .validateConstructor(
+ TestParameterInjectorUtils.getOnlyConstructor(getTestClass().getJavaClass()));
+
+ if (validationResult.wasValidated()) {
+ errorsReturned.addAll(validationResult.validationErrors());
+ } else {
+ super.validateZeroArgConstructor(errorsReturned);
+ }
+ }
+
+ @Override
+ protected final void validateTestMethods(List<Throwable> errorsReturned) {
+ List<FrameworkMethod> testMethods =
+ FluentIterable.from(getSupportedTestAnnotations())
+ .transformAndConcat(annotation -> getTestClass().getAnnotatedMethods(annotation))
+ .toList();
+ for (FrameworkMethod testMethod : testMethods) {
+ ExecutableValidationResult validationResult =
+ getTestMethodProcessors()
+ .validateTestMethod(testMethod.getMethod(), getTestClass().getJavaClass());
+
+ if (Modifier.isStatic(testMethod.getMethod().getModifiers())) {
+ errorsReturned.add(
+ new Exception(String.format("Method %s() should not be static", testMethod.getName())));
+ }
+ if (!Modifier.isPublic(testMethod.getMethod().getModifiers())) {
+ errorsReturned.add(
+ new Exception(String.format("Method %s() should be public", testMethod.getName())));
+ }
+
+ if (validationResult.wasValidated()) {
+ errorsReturned.addAll(validationResult.validationErrors());
+ } else {
+ testMethod.validatePublicVoidNoArg(/* isStatic= */ false, errorsReturned);
+ }
+ }
+ }
+
+ // Fix for ParentRunner bug:
+ // Overriding this method because a superclass (ParentRunner) is calling this in its constructor
+ // and then throwing an InitializationError that doesn't have any of the causes in the exception
+ // message.
+ @Override
+ protected final void collectInitializationErrors(List<Throwable> errors) {
+ super.collectInitializationErrors(errors);
+ if (!errors.isEmpty()) {
+ throw new RuntimeException(
+ String.format(
+ "Found %s issues while initializing the test runner:\n\n - %s\n\n\n",
+ errors.size(),
+ FluentIterable.from(errors)
+ .transform(Throwables::getStackTraceAsString)
+ .join(Joiner.on("\n\n\n - "))));
+ }
+ }
+
+ // Override this test as final because it is not (always) invoked
+ @Override
+ protected final Object createTest() throws Exception {
+ return super.createTest();
+ }
+
+ // Override this test as final because it is not (always) invoked
+ @Override
+ protected final void validatePublicVoidNoArgMethods(
+ Class<? extends Annotation> annotation, boolean isStatic, List<Throwable> errors) {
+ super.validatePublicVoidNoArgMethods(annotation, isStatic, errors);
+ }
+
+ private synchronized TestMethodProcessorList getTestMethodProcessors() {
+ if (testMethodProcessors == null) {
+ testMethodProcessors = createTestMethodProcessorList();
+ }
+ return testMethodProcessors;
+ }
+
+ /** {@link MethodRule} that sets up the Context for each test. */
+ private static class ContextMethodRule implements MethodRule {
+ @Override
+ public Statement apply(Statement statement, FrameworkMethod method, Object o) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ currentTestInfo.set(((OverriddenFrameworkMethod) method).getTestInfo());
+ try {
+ statement.evaluate();
+ } finally {
+ currentTestInfo.set(null);
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java
new file mode 100644
index 0000000..965d41a
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ContiguousSet;
+import com.google.common.collect.DiscreteDomain;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.Range;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.Nullable;
+
+/** A POJO containing information about a test (name and anotations). */
+@AutoValue
+abstract class TestInfo {
+
+ /**
+ * The maximum amount of characters that {@link #getName()} can have.
+ *
+ * <p>See b/168325767 for the reason behind this. tl;dr the name is put into a Unix file with max
+ * 255 characters. The surrounding constant characters take up 31 characters. The max is reduced
+ * by an additional 24 characters to account for future changes.
+ */
+ static final int MAX_TEST_NAME_LENGTH = 200;
+
+ public abstract Method getMethod();
+
+ /**
+ * The test class that is being run.
+ *
+ * <p>Note that this is not always the same as the class that declares {@link #getMethod()}
+ * because test methods can be inherited.
+ */
+ public abstract Class<?> getTestClass();
+
+ public final String getName() {
+ if (getParameters().isEmpty()) {
+ return getMethod().getName();
+ } else {
+ return String.format(
+ "%s[%s]",
+ getMethod().getName(),
+ FluentIterable.from(getParameters())
+ .transform(TestInfoParameter::getValueInTestName)
+ .join(Joiner.on(",")));
+ }
+ }
+
+ abstract ImmutableList<TestInfoParameter> getParameters();
+
+ public abstract ImmutableList<Annotation> getAnnotations();
+
+ @Nullable
+ public final <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
+ for (Annotation annotation : getAnnotations()) {
+ if (annotationClass.isInstance(annotation)) {
+ return annotationClass.cast(annotation);
+ }
+ }
+ return null;
+ }
+
+ final TestInfo withExtraParameters(List<TestInfoParameter> parameters) {
+ return new AutoValue_TestInfo(
+ getMethod(),
+ getTestClass(),
+ ImmutableList.<TestInfoParameter>builder()
+ .addAll(this.getParameters())
+ .addAll(parameters)
+ .build(),
+ getAnnotations());
+ }
+
+ final TestInfo withExtraAnnotation(Annotation annotation) {
+ ImmutableList<Annotation> newAnnotations =
+ ImmutableList.<Annotation>builder().addAll(this.getAnnotations()).add(annotation).build();
+ return new AutoValue_TestInfo(getMethod(), getTestClass(), getParameters(), newAnnotations);
+ }
+
+ /**
+ * Returns a new TestInfo instance with updated parameter names.
+ *
+ * @param parameterWithIndexToNewName A function of the parameter and its index in the {@link
+ * #getParameters()} list to the new name.
+ */
+ private TestInfo withUpdatedParameterNames(
+ Java8BiFunction<TestInfoParameter, Integer, String> parameterWithIndexToNewName) {
+ return new AutoValue_TestInfo(
+ getMethod(),
+ getTestClass(),
+ FluentIterable.from(
+ ContiguousSet.create(
+ Range.closedOpen(0, getParameters().size()), DiscreteDomain.integers()))
+ .transform(
+ parameterIndex -> {
+ TestInfoParameter parameter = getParameters().get(parameterIndex);
+ return parameter.withValueInTestName(
+ parameterWithIndexToNewName.apply(parameter, parameterIndex));
+ })
+ .toList(),
+ getAnnotations());
+ }
+
+ public static TestInfo legacyCreate(
+ Method method, Class<?> testClass, String name, List<Annotation> annotations) {
+ return new AutoValue_TestInfo(
+ method, testClass, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations));
+ }
+
+ static TestInfo createWithoutParameters(
+ Method method, Class<?> testClass, List<Annotation> annotations) {
+ return new AutoValue_TestInfo(
+ method, testClass, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations));
+ }
+
+ static ImmutableList<TestInfo> shortenNamesIfNecessary(List<TestInfo> testInfos) {
+ if (FluentIterable.from(testInfos)
+ .anyMatch(info -> info.getName().length() > MAX_TEST_NAME_LENGTH)) {
+ int numberOfParameters = testInfos.get(0).getParameters().size();
+
+ if (numberOfParameters == 0) {
+ return ImmutableList.copyOf(testInfos);
+ } else {
+ Set<Integer> parameterIndicesThatNeedUpdate =
+ FluentIterable.from(
+ ContiguousSet.create(
+ Range.closedOpen(0, numberOfParameters), DiscreteDomain.integers()))
+ .filter(
+ parameterIndex ->
+ FluentIterable.from(testInfos)
+ .anyMatch(
+ info ->
+ info.getParameters()
+ .get(parameterIndex)
+ .getValueInTestName()
+ .length()
+ > getMaxCharactersPerParameter(info, numberOfParameters)))
+ .toSet();
+
+ return FluentIterable.from(testInfos)
+ .transform(
+ info ->
+ info.withUpdatedParameterNames(
+ (parameter, parameterIndex) ->
+ parameterIndicesThatNeedUpdate.contains(parameterIndex)
+ ? getShortenedName(
+ parameter,
+ getMaxCharactersPerParameter(info, numberOfParameters))
+ : info.getParameters().get(parameterIndex).getValueInTestName()))
+ .toList();
+ }
+ } else {
+ return ImmutableList.copyOf(testInfos);
+ }
+ }
+
+ private static int getMaxCharactersPerParameter(TestInfo testInfo, int numberOfParameters) {
+ int maxLengthOfAllParameters =
+ // Subtract 2 characters for square brackets
+ MAX_TEST_NAME_LENGTH - testInfo.getMethod().getName().length() - 2;
+
+ // Subtract 4 characters to leave place for joining commas and the parameter index.
+ return maxLengthOfAllParameters / numberOfParameters - 4;
+ }
+
+ static ImmutableList<TestInfo> deduplicateTestNames(List<TestInfo> testInfos) {
+ long uniqueTestNameCount =
+ FluentIterable.from(testInfos).transform(TestInfo::getName).toSet().size();
+ if (testInfos.size() == uniqueTestNameCount) {
+ // Return early if there are no duplicates
+ return ImmutableList.copyOf(testInfos);
+ } else {
+ return deduplicateWithNumberPrefixes(maybeAddTypesIfDuplicate(testInfos));
+ }
+ }
+
+ private static String getShortenedName(
+ TestInfoParameter parameter, int maxCharactersPerParameter) {
+ if (maxCharactersPerParameter < 4) {
+ // Not enough characters for "..." suffix
+ return String.valueOf(parameter.getIndexInValueSource() + 1);
+ } else {
+ String shortenedName =
+ parameter.getValueInTestName().length() > maxCharactersPerParameter
+ ? parameter.getValueInTestName().substring(0, maxCharactersPerParameter - 3) + "..."
+ : parameter.getValueInTestName();
+ return String.format("%s.%s", parameter.getIndexInValueSource() + 1, shortenedName);
+ }
+ }
+
+ private static ImmutableList<TestInfo> maybeAddTypesIfDuplicate(List<TestInfo> testInfos) {
+ Multimap<String, TestInfo> testNameToInfo =
+ MultimapBuilder.linkedHashKeys().arrayListValues().build();
+ for (TestInfo testInfo : testInfos) {
+ testNameToInfo.put(testInfo.getName(), testInfo);
+ }
+
+ return FluentIterable.from(testNameToInfo.keySet())
+ .transformAndConcat(
+ testName -> {
+ Collection<TestInfo> matchedInfos = testNameToInfo.get(testName);
+ if (matchedInfos.size() == 1) {
+ // There was only one method with this name, so no deduplication is necessary
+ return matchedInfos;
+ } else {
+ // Found tests with duplicate test names
+ int numParameters = matchedInfos.iterator().next().getParameters().size();
+ Set<Integer> indicesThatShouldGetSuffix =
+ // Find parameter indices for which a suffix would allow the reader to
+ // differentiate
+ FluentIterable.from(
+ ContiguousSet.create(
+ Range.closedOpen(0, numParameters), DiscreteDomain.integers()))
+ .filter(
+ parameterIndex ->
+ FluentIterable.from(matchedInfos)
+ .transform(
+ info ->
+ getTypeSuffix(
+ info.getParameters()
+ .get(parameterIndex)
+ .getValue()))
+ .toSet()
+ .size()
+ > 1)
+ .toSet();
+
+ return FluentIterable.from(matchedInfos)
+ .transform(
+ testInfo ->
+ testInfo.withUpdatedParameterNames(
+ (parameter, parameterIndex) ->
+ indicesThatShouldGetSuffix.contains(parameterIndex)
+ ? parameter.getValueInTestName()
+ + getTypeSuffix(parameter.getValue())
+ : parameter.getValueInTestName()));
+ }
+ })
+ .toList();
+ }
+
+ private static String getTypeSuffix(@Nullable Object value) {
+ if (value == null) {
+ return " (null reference)";
+ } else {
+ return String.format(" (%s)", value.getClass().getSimpleName());
+ }
+ }
+
+ private static ImmutableList<TestInfo> deduplicateWithNumberPrefixes(
+ ImmutableList<TestInfo> testInfos) {
+ long uniqueTestNameCount =
+ FluentIterable.from(testInfos).transform(TestInfo::getName).toSet().size();
+ if (testInfos.size() == uniqueTestNameCount) {
+ return ImmutableList.copyOf(testInfos);
+ } else {
+ // There are still duplicates, even after adding type suffixes. As a last resort: add a
+ // counter to all parameters to guarantee that each case is unique.
+ return FluentIterable.from(testInfos)
+ .transform(
+ testInfo ->
+ testInfo.withUpdatedParameterNames(
+ (parameter, parameterIndex) ->
+ String.format(
+ "%s.%s",
+ parameter.getIndexInValueSource() + 1,
+ parameter.getValueInTestName())))
+ .toList();
+ }
+ }
+
+ @AutoValue
+ abstract static class TestInfoParameter {
+
+ abstract String getValueInTestName();
+
+ @Nullable
+ abstract Object getValue();
+
+ /**
+ * The index of this parameter value in the list of all values provided by the provider that
+ * returned this value.
+ */
+ abstract int getIndexInValueSource();
+
+ final TestInfoParameter withValueInTestName(String newValueInTestName) {
+ return create(newValueInTestName, getValue(), getIndexInValueSource());
+ }
+
+ static TestInfoParameter create(
+ String valueInTestName, @Nullable Object value, int indexInValueSource) {
+ checkArgument(indexInValueSource >= 0);
+ return new AutoValue_TestInfo_TestInfoParameter(
+ checkNotNull(valueInTestName), value, indexInValueSource);
+ }
+ }
+
+ /** Copy of Java8's java.util.BiFunction which is not available in older versions of the JDK */
+ interface Java8BiFunction<I, J, K> {
+ K apply(I a, J b);
+ }
+}
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java
new file mode 100644
index 0000000..60a01bc
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import com.google.common.base.Optional;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.List;
+
+/**
+ * Interface to change the list of methods used in a test.
+ *
+ * <p>Note: Implementations of this interface are expected to be immutable, i.e. they no longer
+ * change after construction.
+ */
+interface TestMethodProcessor {
+
+ /** Allows to transform the test information (name and annotations). */
+ List<TestInfo> calculateTestInfos(TestInfo originalTest);
+
+ /**
+ * If this processor can handle the given constructor, returns the parameters with which it should
+ * be invoked.
+ *
+ * <p>This method is never called for a parameterless constructor.
+ */
+ Optional<List<Object>> maybeGetConstructorParameters(
+ Constructor<?> constructor, TestInfo testInfo);
+
+ /**
+ * If this processor can handle the given test, returns the parameters with which {@code
+ * testInfo.getMethod()} should be invoked.
+ *
+ * <p>This method is never called for a parameterless {@code testInfo.getMethod()}.
+ */
+ Optional<List<Object>> maybeGetTestMethodParameters(TestInfo testInfo);
+
+ /**
+ * Optionally process the test instance right after construction to ready it for the given test
+ * instance.
+ */
+ void postProcessTestInstance(Object testInstance, TestInfo testInfo);
+
+ /** Optionally validates the given constructor. */
+ ExecutableValidationResult validateConstructor(Constructor<?> constructor);
+
+ /**
+ * Optionally validates the given method.
+ *
+ * <p>Note that the given method is not necessarily declared in the given class because test
+ * methods can be inherited.
+ */
+ ExecutableValidationResult validateTestMethod(Method testMethod, Class<?> testClass);
+}
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java
new file mode 100644
index 0000000..2caf531
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Combined version of all {@link TestMethodProcessor} implementations that this package supports.
+ */
+final class TestMethodProcessorList {
+
+ private final ImmutableList<TestMethodProcessor> testMethodProcessors;
+
+ private TestMethodProcessorList(ImmutableList<TestMethodProcessor> testMethodProcessors) {
+ this.testMethodProcessors = testMethodProcessors;
+ }
+
+ /**
+ * Returns a TestMethodProcessorList that supports all features that this package supports, except
+ * the following legacy features:
+ *
+ * <ul>
+ * <li>No support for {@link org.junit.runners.Parameterized}
+ * <li>No support for class and method-level parameters, except for @TestParameters
+ * </ul>
+ */
+ public static TestMethodProcessorList createNewParameterizedProcessors() {
+ return new TestMethodProcessorList(
+ ImmutableList.of(
+ new TestParametersMethodProcessor(),
+ TestParameterAnnotationMethodProcessor.onlyForFieldsAndParameters()));
+ }
+
+ static TestMethodProcessorList empty() {
+ return new TestMethodProcessorList(ImmutableList.of());
+ }
+
+ /**
+ * Calculates the TestInfo instances for the given test method. Each TestInfo corresponds to a
+ * single test.
+ *
+ * <p>The returned list always contains at least one element. If there is no parameterization,
+ * this would be the TestInfo for running the test method without parameters.
+ */
+ public List<TestInfo> calculateTestInfos(Method testMethod, Class<?> testClass) {
+ List<TestInfo> testInfos =
+ ImmutableList.of(
+ TestInfo.createWithoutParameters(
+ testMethod, testClass, ImmutableList.copyOf(testMethod.getAnnotations())));
+
+ for (final TestMethodProcessor testMethodProcessor : testMethodProcessors) {
+ List<TestInfo> list = new ArrayList<>();
+ for (TestInfo lastTestInfo : testInfos) {
+ list.addAll(testMethodProcessor.calculateTestInfos(lastTestInfo));
+ }
+ testInfos = list;
+ }
+
+ testInfos = TestInfo.deduplicateTestNames(TestInfo.shortenNamesIfNecessary(testInfos));
+
+ return testInfos;
+ }
+
+ /**
+ * Returns the parameters with which it should be invoked.
+ *
+ * <p>This method is never called for a parameterless constructor.
+ */
+ public List<Object> getConstructorParameters(Constructor<?> constructor, TestInfo testInfo) {
+ return FluentIterable.from(testMethodProcessors)
+ .transform(processor -> processor.maybeGetConstructorParameters(constructor, testInfo))
+ .filter(Optional::isPresent)
+ .transform(Optional::get)
+ .first()
+ .or(
+ () -> {
+ throw new IllegalStateException(
+ String.format(
+ "Could not generate parameter values for %s. Did you forget an annotation?",
+ constructor));
+ });
+ }
+
+ /**
+ * Returns the parameters with which {@code testInfo.getMethod()} should be invoked.
+ *
+ * <p>This method is never called for a parameterless {@code testInfo.getMethod()}.
+ */
+ public List<Object> getTestMethodParameters(TestInfo testInfo) {
+ return FluentIterable.from(testMethodProcessors)
+ .transform(processor -> processor.maybeGetTestMethodParameters(testInfo))
+ .filter(Optional::isPresent)
+ .transform(Optional::get)
+ .first()
+ .or(
+ () -> {
+ throw new IllegalStateException(
+ String.format(
+ "Could not generate parameter values for %s. Did you forget an annotation?",
+ testInfo.getMethod()));
+ });
+ }
+
+ /**
+ * Optionally process the test instance right after construction to ready it for the given test.
+ */
+ public void postProcessTestInstance(Object testInstance, TestInfo testInfo) {
+ for (TestMethodProcessor testMethodProcessor : testMethodProcessors) {
+ testMethodProcessor.postProcessTestInstance(testInstance, testInfo);
+ }
+ }
+
+ /** Optionally validates the given constructor. */
+ public ExecutableValidationResult validateConstructor(Constructor<?> constructor) {
+ return FluentIterable.from(testMethodProcessors)
+ .transform(processor -> processor.validateConstructor(constructor))
+ .firstMatch(ExecutableValidationResult::wasValidated)
+ .or(ExecutableValidationResult.notValidated());
+ }
+
+ /** Optionally validates the given method. */
+ public ExecutableValidationResult validateTestMethod(Method testMethod, Class<?> testClass) {
+ return FluentIterable.from(testMethodProcessors)
+ .transform(processor -> processor.validateTestMethod(testMethod, testClass))
+ .firstMatch(ExecutableValidationResult::wasValidated)
+ .or(ExecutableValidationResult.notValidated());
+ }
+}
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
new file mode 100644
index 0000000..d193ec6
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Lists;
+import com.google.common.primitives.Primitives;
+import com.google.testing.junit.testparameterinjector.TestParameter.InternalImplementationOfThisParameter;
+import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider.Context;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Test parameter annotation that defines the values that a single parameter can have.
+ *
+ * <p>For enums and booleans, the values can be automatically derived as all possible values:
+ *
+ * <pre>
+ * {@literal @}Test
+ * public void test1(@TestParameter MyEnum myEnum, @TestParameter boolean myBoolean) {
+ * // ... will run for [(A,false), (A,true), (B,false), (B,true), (C,false), (C,true)]
+ * }
+ *
+ * enum MyEnum { A, B, C }
+ * </pre>
+ *
+ * <p>The values can be explicitly defined as a parsed string:
+ *
+ * <pre>
+ * public void test1(
+ * {@literal @}TestParameter({"{name: Hermione, age: 18}", "{name: Dumbledore, age: 115}"})
+ * UpdateCharacterRequest request,
+ * {@literal @}TestParameter({"1", "4"}) int bookNumber) {
+ * // ... will run for [(Hermione,1), (Hermione,4), (Dumbledore,1), (Dumbledore,4)]
+ * }
+ * </pre>
+ *
+ * <p>For more flexibility, see {{@link #valuesProvider()}}. If you don't want to test all possible
+ * combinations but instead want to specify sets of parameters explicitly, use @{@link
+ * TestParameters}.
+ */
+@Retention(RUNTIME)
+@Target({FIELD, PARAMETER})
+@TestParameterAnnotation(valueProvider = InternalImplementationOfThisParameter.class)
+public @interface TestParameter {
+
+ /**
+ * Array of stringified values for the annotated type.
+ *
+ * <p>Types that are supported:
+ *
+ * <ul>
+ * <li>String: No parsing happens
+ * <li>boolean: Specified as YAML boolean
+ * <li>long and int: Specified as YAML integer
+ * <li>float and double: Specified as YAML floating point or integer
+ * <li>Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()}
+ * <li>Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML bytes
+ * (example: "!!binary 'ZGF0YQ=='")
+ * </ul>
+ *
+ * <p>For dynamic sets of parameters or parameter types that are not supported here, use {@link
+ * #valuesProvider()} and leave this field empty.
+ *
+ * <p>For examples, see {@link TestParameter}.
+ */
+ String[] value() default {};
+
+ /**
+ * Sets a provider that will return a list of parameter values.
+ *
+ * <p>If this field is set, {@link #value()} must be empty and vice versa.
+ *
+ * <p><b>Example</b>
+ *
+ * <pre>
+ * import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider;
+ *
+ * {@literal @}Test
+ * public void matchesAllOf_throwsOnNull(
+ * {@literal @}TestParameter(valuesProvider = CharMatcherProvider.class)
+ * CharMatcher charMatcher) {
+ * assertThrows(NullPointerException.class, () -&gt; charMatcher.matchesAllOf(null));
+ * }
+ *
+ * private static final class CharMatcherProvider extends TestParameterValuesProvider {
+ * {@literal @}Override
+ * public {@literal List<CharMatcher>} provideValues(Context context) {
+ * return ImmutableList.of(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace());
+ * }
+ * }
+ * </pre>
+ */
+ Class<? extends TestParameterValuesProvider> valuesProvider() default
+ DefaultTestParameterValuesProvider.class;
+
+ /**
+ * Interface for custom providers of test parameter values.
+ *
+ * @deprecated Use {@link
+ * com.google.testing.junit.testparameterinjector.TestParameterValuesProvider} instead. The
+ * replacement implements this same interface, but with an additional Context parameter.
+ */
+ @Deprecated
+ interface TestParameterValuesProvider {
+ List<?> provideValues();
+
+ /**
+ * Wraps the given value in an object that allows you to give the parameter value a different
+ * name. The TestParameterInjector framework will recognize the returned {@link
+ * TestParameterValue} instances and unwrap them at injection time.
+ *
+ * <p>Usage: {@code value(file.content).withName(file.name)}.
+ *
+ * <p>Do not override this method.
+ */
+ default TestParameterValue value(@javax.annotation.Nullable Object wrappedValue) {
+ return TestParameterValue.wrap(wrappedValue);
+ }
+ }
+
+ /** Default {@link TestParameterValuesProvider} implementation that does nothing. */
+ class DefaultTestParameterValuesProvider implements TestParameterValuesProvider {
+ @Override
+ public List<Object> provideValues() {
+ return com.google.common.collect.ImmutableList.of();
+ }
+ }
+
+ /** Implementation of this parameter annotation. */
+ final class InternalImplementationOfThisParameter implements TestParameterValueProvider {
+ @Override
+ public List<Object> provideValues(
+ Annotation uncastAnnotation,
+ Optional<Class<?>> maybeParameterClass,
+ GenericParameterContext context) {
+ TestParameter annotation = (TestParameter) uncastAnnotation;
+ Class<?> parameterClass = getValueType(annotation.annotationType(), maybeParameterClass);
+
+ boolean valueIsSet = annotation.value().length > 0;
+ boolean valuesProviderIsSet =
+ !annotation.valuesProvider().equals(DefaultTestParameterValuesProvider.class);
+ checkState(
+ !(valueIsSet && valuesProviderIsSet),
+ "It is not allowed to specify both value and valuesProvider on annotation %s",
+ annotation);
+
+ if (valueIsSet) {
+ return Lists.newArrayList(
+ FluentIterable.from(annotation.value())
+ .transform(v -> parseStringValue(v, parameterClass))
+ .toArray(Object.class));
+ } else if (valuesProviderIsSet) {
+ return getValuesFromProvider(annotation.valuesProvider(), new Context(context));
+ } else {
+ if (Enum.class.isAssignableFrom(parameterClass)) {
+ return Arrays.asList((Object[]) parameterClass.asSubclass(Enum.class).getEnumConstants());
+ } else if (Primitives.wrap(parameterClass).equals(Boolean.class)) {
+ return Arrays.asList(false, true);
+ } else {
+ throw new IllegalStateException(
+ String.format(
+ "A @TestParameter without values can only be placed at an enum or a boolean, but"
+ + " was placed by a %s",
+ parameterClass));
+ }
+ }
+ }
+
+ @Override
+ public Class<?> getValueType(
+ Class<? extends Annotation> annotationType, Optional<Class<?>> parameterClass) {
+ if (parameterClass.isPresent()) {
+ return parameterClass.get();
+ }
+ throw new AssertionError(
+ String.format(
+ "An empty parameter class should not be possible since"
+ + " @TestParameter can only target FIELD or PARAMETER, both"
+ + " of which are supported for annotation %s.",
+ annotationType));
+ }
+
+ private static Object parseStringValue(String value, Class<?> parameterClass) {
+ if (parameterClass.equals(String.class)) {
+ return value.equals("null") ? null : value;
+ } else if (Enum.class.isAssignableFrom(parameterClass)) {
+ return value.equals("null") ? null : ParameterValueParsing.parseEnum(value, parameterClass);
+ } else {
+ return ParameterValueParsing.parseYamlStringToJavaType(value, parameterClass);
+ }
+ }
+
+ private static List<Object> getValuesFromProvider(
+ Class<? extends TestParameterValuesProvider> valuesProvider, Context context) {
+ try {
+ Constructor<? extends TestParameterValuesProvider> constructor =
+ valuesProvider.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ TestParameterValuesProvider instance = constructor.newInstance();
+ if (instance
+ instanceof com.google.testing.junit.testparameterinjector.TestParameterValuesProvider) {
+ return new ArrayList<>(
+ ((com.google.testing.junit.testparameterinjector.TestParameterValuesProvider)
+ instance)
+ .provideValues(context));
+ } else {
+ return new ArrayList<>(instance.provideValues());
+ }
+ } catch (NoSuchMethodException e) {
+ if (!Modifier.isStatic(valuesProvider.getModifiers()) && valuesProvider.isMemberClass()) {
+ throw new IllegalStateException(
+ String.format(
+ "Could not find a no-arg constructor for %s, probably because it is a not-static"
+ + " inner class. You can fix this by making %s static.",
+ valuesProvider.getSimpleName(), valuesProvider.getSimpleName()),
+ e);
+ } else {
+ throw new IllegalStateException(
+ String.format(
+ "Could not find a no-arg constructor for %s.", valuesProvider.getSimpleName()),
+ e);
+ }
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException(e);
+ } catch (Exception e) {
+ // Catch any unchecked exception that may come from `provideValues(Context)`
+ if (e instanceof RuntimeException) {
+ throw (RuntimeException) e;
+ } else {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+ }
+}
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java
new file mode 100644
index 0000000..deb4cd5
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Verify.verify;
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Primitives;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.lang.reflect.Array;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+
+/**
+ * Annotation to define a test annotation used to have parameterized methods, in either a
+ * parameterized or non parameterized test.
+ *
+ * <p>Parameterized tests enabled by defining a annotation (see {@link TestParameter} as an example)
+ * for the type of the parameter, defining a member variable annotated with this annotation, and
+ * specifying the parameter with the same annotation for each test, or for the whole class, for
+ * example:
+ *
+ * <pre>{@code
+ * @RunWith(TestParameterInjector.class)
+ * public class ColorTest {
+ * @Retention(RUNTIME)
+ * @Target({TYPE, METHOD, FIELD})
+ * @TestParameterAnnotation
+ * public @interface ColorParameter {
+ * Color[] value() default {};
+ * }
+ *
+ * @ColorParameter({BLUE, WHITE, RED}) private Color color;
+ *
+ * @Test
+ * public void test() {
+ * assertThat(paint(color)).isSuccessful();
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>An alternative is to use a method parameter for injection:
+ *
+ * <pre>{@code
+ * @RunWith(TestParameterInjector.class)
+ * public class ColorTest {
+ * @Retention(RUNTIME)
+ * @Target({TYPE, METHOD, FIELD})
+ * @TestParameterAnnotation
+ * public @interface ColorParameter {
+ * Color[] value() default {};
+ * }
+ *
+ * @Test
+ * @ColorParameter({BLUE, WHITE, RED})
+ * public void test(Color color) {
+ * assertThat(paint(color)).isSuccessful();
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>Yet another alternative is to use a method parameter for injection, but with the annotation
+ * specified on the parameter itself, which helps when multiple arguments share the
+ * same @TestParameterAnnotation annotation.
+ *
+ * <pre>{@code
+ * @RunWith(TestParameterInjector.class)
+ * public class ColorTest {
+ * @Retention(RUNTIME)
+ * @Target({TYPE, METHOD, FIELD})
+ * @TestParameterAnnotation
+ * public @interface ColorParameter {
+ * Color[] value() default {};
+ * }
+ *
+ * @Test
+ * public void test(@ColorParameter({BLUE, WHITE}) Color color1,
+ * @ColorParameter({WHITE, RED}) Color color2) {
+ * assertThat(paint(color1. color2)).isSuccessful();
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>Class constructors can also be annotated with @TestParameterAnnotation annotations, as shown
+ * below:
+ *
+ * <pre>{@code
+ * @RunWith(TestParameterInjector.class)
+ * public class ColorTest {
+ * @Retention(RUNTIME)
+ * @Target({TYPE, METHOD, FIELD})
+ * public @TestParameterAnnotation
+ * public @interface ColorParameter {
+ * Color[] value() default {};
+ * }
+ *
+ * public ColorTest(@ColorParameter({BLUE, WHITE}) Color color) {
+ * ...
+ * }
+ *
+ * @Test
+ * public void test() {...}
+ * }
+ * }</pre>
+ *
+ * <p>Each field that needs to be injected from a parameter requires its dedicated distinct
+ * annotation.
+ *
+ * <p>If the same annotation is defined both on the class and method, the method parameter values
+ * take precedence.
+ *
+ * <p>If the same annotation is defined both on the class and constructor, the constructor parameter
+ * values take precedence.
+ *
+ * <p>Annotations cannot be duplicated between the constructor or constructor parameters and a
+ * method or method parameter.
+ *
+ * <p>Since the parameter values must be specified in an annotation return value, they are
+ * restricted to the annotation method return type set (primitive, Class, Enum, String, etc...). If
+ * parameters have to be dynamically generated, the conventional Parameterized mechanism with {@code
+ * Parameters} has to be used instead.
+ */
+@Retention(RUNTIME)
+@Target({ANNOTATION_TYPE})
+@interface TestParameterAnnotation {
+
+ /** Specifies a validator for the parameter to determine whether test should be skipped. */
+ Class<? extends TestParameterValidator> validator() default DefaultValidator.class;
+
+ /** Specifies a value provider for the parameter to provide the values to test. */
+ Class<? extends TestParameterValueProvider> valueProvider() default DefaultValueProvider.class;
+
+ /** Default {@link TestParameterValidator} implementation which skips no test. */
+ class DefaultValidator implements TestParameterValidator {
+
+ @Override
+ public boolean shouldSkip(Context context) {
+ return false;
+ }
+ }
+
+ /**
+ * Default {@link TestParameterValueProvider} implementation that gets its values from the
+ * annotation's `value` method.
+ */
+ class DefaultValueProvider implements TestParameterValueProvider {
+
+ @Override
+ public List<Object> provideValues(Annotation annotation, Optional<Class<?>> parameterClass) {
+ Object parameters = getParametersAnnotationValues(annotation, annotation.annotationType());
+ checkState(
+ parameters.getClass().isArray(),
+ "The return value of the value method should be an array");
+
+ int parameterCount = Array.getLength(parameters);
+ ImmutableList.Builder<Object> resultBuilder = ImmutableList.builder();
+ for (int i = 0; i < parameterCount; i++) {
+ Object value = Array.get(parameters, i);
+ if (parameterClass.isPresent()) {
+ verify(
+ Primitives.wrap(parameterClass.get()).isInstance(value),
+ "Found %s annotation next to a parameter of type %s which doesn't match"
+ + " (annotation = %s)",
+ annotation.annotationType().getSimpleName(),
+ parameterClass.get().getSimpleName(),
+ annotation);
+ }
+ resultBuilder.add(value);
+ }
+ return resultBuilder.build();
+ }
+
+ @Override
+ public Class<?> getValueType(
+ Class<? extends Annotation> annotationType, Optional<Class<?>> parameterClass) {
+ try {
+ Method valueMethod = annotationType.getMethod("value");
+ return valueMethod.getReturnType().getComponentType();
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(
+ "The @TestParameterAnnotation annotation should have a single value() method.", e);
+ }
+ }
+
+ /**
+ * Returns the parameters of the test parameter, by calling the {@code value} method on the
+ * annotation.
+ */
+ private static Object getParametersAnnotationValues(
+ Annotation annotation, Class<? extends Annotation> annotationType) {
+ Method valueMethod;
+ try {
+ valueMethod = annotationType.getMethod("value");
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(
+ "The @TestParameterAnnotation annotation should have a single value() method.", e);
+ }
+ Object parameters;
+ try {
+ parameters = valueMethod.invoke(annotation);
+ } catch (InvocationTargetException e) {
+ if (e.getCause() instanceof IllegalAccessError) {
+ // There seems to be a bug or at least something weird with the JVM that causes
+ // IllegalAccessError to be thrown because the return value is not visible when it is a
+ // non-public nested type. See
+ // http://mail.openjdk.java.net/pipermail/core-libs-dev/2014-January/024180.html for more
+ // info.
+ throw new RuntimeException(
+ String.format(
+ "Could not access %s.value(). This is probably because %s is not visible to the"
+ + " annotation proxy. To fix this, make %s public.",
+ annotationType.getSimpleName(),
+ valueMethod.getReturnType().getSimpleName(),
+ valueMethod.getReturnType().getSimpleName()));
+ // Note: Not chaining the exception to reduce the clutter for the reader
+ } else {
+ throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e);
+ }
+ return parameters;
+ }
+ }
+}
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
new file mode 100644
index 0000000..16b206a
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
@@ -0,0 +1,1369 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Verify.verify;
+import static com.google.common.collect.Lists.newArrayList;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.auto.value.AutoAnnotation;
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Optional;
+import com.google.common.base.Throwables;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ContiguousSet;
+import com.google.common.collect.DiscreteDomain;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Range;
+import com.google.common.util.concurrent.UncheckedExecutionException;
+import com.google.testing.junit.testparameterinjector.TestInfo.TestInfoParameter;
+import java.io.Serializable;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import javax.annotation.Nullable;
+
+/**
+ * {@code TestMethodProcessor} implementation for supporting parameterized tests annotated with
+ * {@link TestParameterAnnotation}.
+ *
+ * @see TestParameterAnnotation
+ */
+final class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
+
+ /**
+ * Class to hold an annotation type and origin and one of the values as returned by the {@code
+ * value()} method.
+ */
+ @AutoValue
+ abstract static class TestParameterValueHolder implements Serializable {
+
+ private static final long serialVersionUID = -6491624726743872379L;
+
+ /**
+ * Annotation type and origin of the annotation annotated with {@link TestParameterAnnotation}.
+ */
+ abstract AnnotationTypeOrigin annotationTypeOrigin();
+
+ /**
+ * The value used for the test as returned by the @TestParameterAnnotation annotated
+ * annotation's {@code value()} method (e.g. 'true' or 'false' in the case of a Boolean
+ * parameter).
+ */
+ abstract TestParameterValue wrappedValue();
+
+ /** The index of this value in {@link #specifiedValues()}. */
+ abstract int valueIndex();
+
+ /**
+ * The list of values specified by the @TestParameterAnnotation annotated annotation's {@code
+ * value()} method (e.g. {true, false} in the case of a boolean parameter).
+ */
+ @SuppressWarnings("AutoValueImmutableFields") // intentional to allow null values
+ abstract List<Object> specifiedValues();
+
+ /**
+ * The name of the parameter or field that is being annotated. In case the annotation is
+ * annotating a method, constructor or class, {@code paramName} is an absent optional.
+ */
+ abstract Optional<String> paramName();
+
+ /**
+ * Returns {@link #wrappedValue()} without the {@link TestParameterValue} wrapper if it exists.
+ */
+ @Nullable
+ Object unwrappedValue() {
+ return wrappedValue().getWrappedValue();
+ }
+
+ /**
+ * Returns a String that represents this value and is fit for use in a test name (between
+ * brackets).
+ */
+ String toTestNameString() {
+ return ParameterValueParsing.formatTestNameString(paramName(), wrappedValue());
+ }
+
+ public static ImmutableList<TestParameterValueHolder> create(
+ AnnotationWithMetadata annotationWithMetadata, Origin origin) {
+ List<TestParameterValue> specifiedValues =
+ getParametersAnnotationValues(annotationWithMetadata);
+ checkState(
+ !specifiedValues.isEmpty(),
+ "The number of parameter values should not be 0"
+ + ", otherwise the parameter would cause the test to be skipped.");
+ return FluentIterable.from(
+ ContiguousSet.create(
+ Range.closedOpen(0, specifiedValues.size()), DiscreteDomain.integers()))
+ .transform(
+ valueIndex ->
+ (TestParameterValueHolder)
+ new AutoValue_TestParameterAnnotationMethodProcessor_TestParameterValueHolder(
+ AnnotationTypeOrigin.create(
+ annotationWithMetadata.annotation().annotationType(), origin),
+ specifiedValues.get(valueIndex),
+ valueIndex,
+ newArrayList(
+ FluentIterable.from(specifiedValues)
+ .transform(TestParameterValue::getWrappedValue)),
+ annotationWithMetadata.paramName()))
+ .toList();
+ }
+ }
+
+ /**
+ * Returns a {@link TestParameterValues} for retrieving the {@link TestParameterAnnotation}
+ * annotation values for a the {@code testInfo}.
+ */
+ public static TestParameterValues getTestParameterValues(TestInfo testInfo) {
+ TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class);
+ if (testIndexHolder == null) {
+ return annotationType -> Optional.absent();
+ } else {
+ return annotationType ->
+ FluentIterable.from(
+ new TestParameterAnnotationMethodProcessor(
+ /* onlyForFieldsAndParameters= */ false)
+ .getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()))
+ .filter(
+ testParameterValue ->
+ testParameterValue
+ .annotationTypeOrigin()
+ .annotationType()
+ .equals(annotationType))
+ .transform(TestParameterValueHolder::unwrappedValue)
+ .first();
+ }
+ }
+
+ /**
+ * Returns a {@link TestParameterAnnotation} value for the current test as specified by {@code
+ * testInfo}, or {@link Optional#absent()} if the {@code annotationType} is not found.
+ */
+ public static Optional<Object> getTestParameterValue(
+ TestInfo testInfo, Class<? extends Annotation> annotationType) {
+ return getTestParameterValues(testInfo).getValue(annotationType);
+ }
+
+ private static ImmutableList<TestParameterValue> getParametersAnnotationValues(
+ AnnotationWithMetadata annotationWithMetadata) {
+ Annotation annotation = annotationWithMetadata.annotation();
+ TestParameterAnnotation testParameter =
+ annotation.annotationType().getAnnotation(TestParameterAnnotation.class);
+ Class<? extends TestParameterValueProvider> valueProvider = testParameter.valueProvider();
+ try {
+ return FluentIterable.from(
+ valueProvider
+ .getConstructor()
+ .newInstance()
+ .provideValues(
+ annotation,
+ annotationWithMetadata.paramClass(),
+ annotationWithMetadata.context()))
+ .transform(
+ value ->
+ (value instanceof TestParameterValue)
+ ? (TestParameterValue) value
+ : TestParameterValue.wrap(value))
+ .toList();
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(
+ "Unexpected exception while invoking value provider " + valueProvider, e);
+ }
+ }
+
+ /** The origin of an annotation type. */
+ enum Origin {
+ CLASS,
+ FIELD,
+ METHOD,
+ METHOD_PARAMETER,
+ CONSTRUCTOR,
+ CONSTRUCTOR_PARAMETER,
+ }
+
+ /** Class to hold an annotation type and the element where it was declared. */
+ @AutoValue
+ abstract static class AnnotationTypeOrigin implements Serializable {
+
+ private static final long serialVersionUID = 4909750539931241385L;
+
+ /** Annotation type of the @TestParameterAnnotation annotated annotation. */
+ abstract Class<? extends Annotation> annotationType();
+
+ /** Where the annotation was declared. */
+ abstract Origin origin();
+
+ public static AnnotationTypeOrigin create(
+ Class<? extends Annotation> annotationType, Origin origin) {
+ return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationTypeOrigin(
+ annotationType, origin);
+ }
+
+ @Override
+ public final String toString() {
+ return annotationType().getSimpleName() + ":" + origin();
+ }
+ }
+
+ /** Class to hold an annotation type and metadata about the annotated parameter. */
+ @AutoValue
+ abstract static class AnnotationWithMetadata implements Serializable {
+
+ /**
+ * The annotation whose interface is itself annotated by the @TestParameterAnnotation
+ * annotation.
+ */
+ abstract Annotation annotation();
+
+ /**
+ * The class of the parameter or field that is being annotated. In case the annotation is
+ * annotating a method, constructor or class, {@code paramClass} is an absent optional.
+ */
+ abstract Optional<Class<?>> paramClass();
+
+ /**
+ * The name of the parameter or field that is being annotated. In case the annotation is
+ * annotating a method, constructor or class, {@code paramName} is an absent optional.
+ */
+ abstract Optional<String> paramName();
+
+ /**
+ * A value class that contains extra information about the context of this parameter.
+ *
+ * <p>In case the annotation is annotating a method, constructor or class (deprecated
+ * functionality), the annotations in the context will be empty.
+ */
+ abstract GenericParameterContext context();
+
+ public static AnnotationWithMetadata withMetadata(
+ Annotation annotation,
+ Class<?> paramClass,
+ String paramName,
+ GenericParameterContext context) {
+ return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata(
+ annotation, Optional.of(paramClass), Optional.of(paramName), context);
+ }
+
+ public static AnnotationWithMetadata withMetadata(
+ Annotation annotation, Class<?> paramClass, GenericParameterContext context) {
+ return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata(
+ annotation, Optional.of(paramClass), Optional.absent(), context);
+ }
+
+ public static AnnotationWithMetadata withoutMetadata(
+ Annotation annotation, GenericParameterContext context) {
+ return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata(
+ annotation,
+ /* paramClass= */ Optional.absent(),
+ /* paramName= */ Optional.absent(),
+ context);
+ }
+
+ // Prevent anyone relying on equals() and hashCode() so that it remains possible to add fields
+ // to this class without breaking existing code.
+ @Override
+ public final boolean equals(Object other) {
+ throw new UnsupportedOperationException("Equality is not supported");
+ }
+
+ @Override
+ public final int hashCode() {
+ throw new UnsupportedOperationException("hashCode() is not supported");
+ }
+ }
+
+ private final boolean onlyForFieldsAndParameters;
+ private final LoadingCache<Class<?>, ImmutableList<AnnotationTypeOrigin>>
+ annotationTypeOriginsCache =
+ CacheBuilder.newBuilder()
+ .maximumSize(1000)
+ .build(CacheLoader.from(this::calculateAnnotationTypeOrigins));
+ private final Cache<Method, List<List<TestParameterValueHolder>>> parameterValuesCache =
+ CacheBuilder.newBuilder().maximumSize(1000).build();
+
+ private TestParameterAnnotationMethodProcessor(boolean onlyForFieldsAndParameters) {
+ this.onlyForFieldsAndParameters = onlyForFieldsAndParameters;
+ }
+
+ /**
+ * Constructs a new {@link TestMethodProcessor} that handles {@link
+ * TestParameterAnnotation}-annotated annotations that are placed anywhere:
+ *
+ * <ul>
+ * <li>At a method / constructor parameter
+ * <li>At a field
+ * <li>At a method / constructor on the class
+ * <li>At the test class
+ * </ul>
+ */
+ static TestMethodProcessor forAllAnnotationPlacements() {
+ return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ false);
+ }
+
+ /**
+ * Constructs a new {@link TestMethodProcessor} that handles {@link
+ * TestParameterAnnotation}-annotated annotations that are placed at fields or parameters.
+ *
+ * <p>Note that this excludes class and method-level annotations, as is the default (using the
+ * constructor).
+ */
+ static TestMethodProcessor onlyForFieldsAndParameters() {
+ return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ true);
+ }
+
+ private ImmutableList<AnnotationTypeOrigin> calculateAnnotationTypeOrigins(Class<?> testClass) {
+ // Collect all annotations used in declared fields and methods that have themselves a
+ // @TestParameterAnnotation annotation.
+ List<AnnotationTypeOrigin> fieldAnnotations =
+ extractTestParameterAnnotations(
+ FluentIterable.from(listWithParents(testClass))
+ .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields()))
+ .transformAndConcat(field -> Arrays.asList(field.getAnnotations()))
+ .toList(),
+ Origin.FIELD);
+ List<AnnotationTypeOrigin> methodAnnotations =
+ extractTestParameterAnnotations(
+ FluentIterable.from(testClass.getMethods())
+ .transformAndConcat(method -> Arrays.asList(method.getAnnotations()))
+ .toList(),
+ Origin.METHOD);
+ List<AnnotationTypeOrigin> parameterAnnotations =
+ extractTestParameterAnnotations(
+ FluentIterable.from(listWithParents(testClass))
+ .transformAndConcat(c -> Arrays.asList(c.getDeclaredMethods()))
+ .transformAndConcat(method -> Arrays.asList(method.getParameterAnnotations()))
+ .transformAndConcat(Arrays::asList)
+ .toList(),
+ Origin.METHOD_PARAMETER);
+ List<AnnotationTypeOrigin> classAnnotations =
+ extractTestParameterAnnotations(Arrays.asList(testClass.getAnnotations()), Origin.CLASS);
+ List<AnnotationTypeOrigin> constructorAnnotations =
+ extractTestParameterAnnotations(
+ FluentIterable.from(testClass.getDeclaredConstructors())
+ .transformAndConcat(constructor -> Arrays.asList(constructor.getAnnotations()))
+ .toList(),
+ Origin.CONSTRUCTOR);
+ List<AnnotationTypeOrigin> constructorParameterAnnotations =
+ extractTestParameterAnnotations(
+ FluentIterable.from(testClass.getDeclaredConstructors())
+ .transformAndConcat(
+ constructor ->
+ FluentIterable.from(Arrays.asList(constructor.getParameterAnnotations()))
+ .transformAndConcat(Arrays::asList))
+ .toList(),
+ Origin.CONSTRUCTOR_PARAMETER);
+
+ checkDuplicatedClassAndFieldAnnotations(
+ constructorAnnotations, classAnnotations, fieldAnnotations);
+
+ checkDuplicatedFieldsAnnotations(methodAnnotations, fieldAnnotations);
+
+ checkState(
+ FluentIterable.from(constructorAnnotations).toSet().size() == constructorAnnotations.size(),
+ "Annotations should not be duplicated on the constructor.");
+
+ checkState(
+ FluentIterable.from(classAnnotations).toSet().size() == classAnnotations.size(),
+ "Annotations should not be duplicated on the class.");
+
+ if (onlyForFieldsAndParameters) {
+ checkState(
+ methodAnnotations.isEmpty(),
+ "This test runner (constructed by the testparameterinjector package) was configured"
+ + " to disallow method-level annotations that could be field/parameter"
+ + " annotations, but found %s",
+ methodAnnotations);
+ checkState(
+ classAnnotations.isEmpty(),
+ "This test runner (constructed by the testparameterinjector package) was configured"
+ + " to disallow class-level annotations that could be field/parameter annotations,"
+ + " but found %s",
+ classAnnotations);
+ checkState(
+ constructorAnnotations.isEmpty(),
+ "This test runner (constructed by the testparameterinjector package) was configured"
+ + " to disallow constructor-level annotations that could be field/parameter"
+ + " annotations, but found %s",
+ constructorAnnotations);
+ }
+
+ // The order matters, since it will determine which annotation processor is
+ // called first.
+ return FluentIterable.from(classAnnotations)
+ .append(fieldAnnotations)
+ .append(constructorAnnotations)
+ .append(constructorParameterAnnotations)
+ .append(methodAnnotations)
+ .append(parameterAnnotations)
+ .toSet()
+ .asList();
+ }
+
+ private ImmutableList<AnnotationTypeOrigin> getAnnotationTypeOrigins(
+ Class<?> testClass, Origin firstOrigin, Origin... otherOrigins) {
+ Set<Origin> originsToFilterBy =
+ ImmutableSet.<Origin>builder().add(firstOrigin).add(otherOrigins).build();
+ try {
+ return FluentIterable.from(annotationTypeOriginsCache.getUnchecked(testClass))
+ .filter(annotationTypeOrigin -> originsToFilterBy.contains(annotationTypeOrigin.origin()))
+ .toList();
+ } catch (UncheckedExecutionException e) {
+ Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class);
+ throw e;
+ }
+ }
+
+ private void checkDuplicatedFieldsAnnotations(
+ List<AnnotationTypeOrigin> methodAnnotations, List<AnnotationTypeOrigin> fieldAnnotations) {
+ // If an annotation is duplicated on two fields, then it becomes specific, and cannot be
+ // overridden by a method.
+ if (FluentIterable.from(fieldAnnotations).toSet().size() != fieldAnnotations.size()) {
+ List<Class<? extends Annotation>> methodOrFieldAnnotations =
+ new ArrayList<>(
+ FluentIterable.from(methodAnnotations)
+ .append(new HashSet<>(fieldAnnotations))
+ .transform(AnnotationTypeOrigin::annotationType)
+ .toList());
+
+ checkState(
+ FluentIterable.from(methodOrFieldAnnotations).toSet().size()
+ == methodOrFieldAnnotations.size(),
+ "Annotations should not be duplicated on a method and field"
+ + " if they are present on multiple fields");
+ }
+ }
+
+ private void checkDuplicatedClassAndFieldAnnotations(
+ List<AnnotationTypeOrigin> constructorAnnotations,
+ List<AnnotationTypeOrigin> classAnnotations,
+ List<AnnotationTypeOrigin> fieldAnnotations) {
+ ImmutableSet<? extends Class<? extends Annotation>> classAnnotationTypes =
+ FluentIterable.from(classAnnotations)
+ .transform(AnnotationTypeOrigin::annotationType)
+ .toSet();
+
+ ImmutableSet<? extends Class<? extends Annotation>> uniqueFieldAnnotations =
+ FluentIterable.from(fieldAnnotations)
+ .transform(AnnotationTypeOrigin::annotationType)
+ .toSet();
+ ImmutableSet<? extends Class<? extends Annotation>> uniqueConstructorAnnotations =
+ FluentIterable.from(constructorAnnotations)
+ .transform(AnnotationTypeOrigin::annotationType)
+ .toSet();
+
+ checkState(
+ Collections.disjoint(classAnnotationTypes, uniqueFieldAnnotations),
+ "Annotations should not be duplicated on a class and field");
+
+ checkState(
+ Collections.disjoint(classAnnotationTypes, uniqueConstructorAnnotations),
+ "Annotations should not be duplicated on a class and constructor");
+
+ checkState(
+ Collections.disjoint(uniqueConstructorAnnotations, uniqueFieldAnnotations),
+ "Annotations should not be duplicated on a field and constructor");
+ }
+
+ private List<AnnotationTypeOrigin> extractTestParameterAnnotations(
+ List<Annotation> annotations, Origin origin) {
+ return new ArrayList<>(
+ FluentIterable.from(annotations)
+ .transform(Annotation::annotationType)
+ .filter(
+ annotationType -> annotationType.isAnnotationPresent(TestParameterAnnotation.class))
+ .transform(annotationType -> AnnotationTypeOrigin.create(annotationType, origin))
+ .toList());
+ }
+
+ @Override
+ public ExecutableValidationResult validateConstructor(Constructor<?> constructor) {
+ Class<?>[] parameterTypes = constructor.getParameterTypes();
+ if (parameterTypes.length == 0) {
+ return ExecutableValidationResult.notValidated();
+ }
+ // The constructor has parameters, they must be injected by a TestParameterAnnotation
+ // annotation.
+ Annotation[][] parameterAnnotations = constructor.getParameterAnnotations();
+ Class<?> testClass = constructor.getDeclaringClass();
+ return ExecutableValidationResult.validated(
+ validateMethodOrConstructorParameters(
+ removeOverrides(
+ getAnnotationTypeOrigins(
+ testClass, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER),
+ testClass),
+ testClass,
+ constructor,
+ parameterTypes,
+ parameterAnnotations));
+ }
+
+ @Override
+ public ExecutableValidationResult validateTestMethod(Method testMethod, Class<?> testClass) {
+ Class<?>[] methodParameterTypes = testMethod.getParameterTypes();
+ if (methodParameterTypes.length == 0) {
+ return ExecutableValidationResult.notValidated();
+ } else {
+ // The method has parameters, they must be injected by a TestParameterAnnotation annotation.
+ return ExecutableValidationResult.validated(
+ validateMethodOrConstructorParameters(
+ getAnnotationTypeOrigins(
+ testClass, Origin.CLASS, Origin.METHOD, Origin.METHOD_PARAMETER),
+ testClass,
+ testMethod,
+ methodParameterTypes,
+ testMethod.getParameterAnnotations()));
+ }
+ }
+
+ private List<Throwable> validateMethodOrConstructorParameters(
+ List<AnnotationTypeOrigin> annotationTypeOrigins,
+ Class<?> testClass,
+ AnnotatedElement methodOrConstructor,
+ Class<?>[] parameterTypes,
+ Annotation[][] parametersAnnotations) {
+ List<Throwable> errors = new ArrayList<>();
+
+ for (int parameterIndex = 0; parameterIndex < parameterTypes.length; parameterIndex++) {
+ Class<?> parameterType = parameterTypes[parameterIndex];
+ Annotation[] parameterAnnotations = parametersAnnotations[parameterIndex];
+ boolean matchingTestParameterAnnotationFound = false;
+ // First, handle the case where the method parameter specifies the test parameter explicitly,
+ // e.g. {@code public void test(@ColorParameter({...}) Color c)}.
+ for (AnnotationTypeOrigin testParameterAnnotationType : annotationTypeOrigins) {
+ for (Annotation parameterAnnotation : parameterAnnotations) {
+ if (parameterAnnotation
+ .annotationType()
+ .equals(testParameterAnnotationType.annotationType())) {
+ // Verify that the type is assignable with the return type of the 'value' method.
+ Class<?> valueMethodReturnType =
+ getValueMethodReturnType(
+ testParameterAnnotationType.annotationType(),
+ /* paramClass= */ Optional.of(parameterType));
+ if (!parameterType.isAssignableFrom(valueMethodReturnType)) {
+ errors.add(
+ new IllegalStateException(
+ String.format(
+ "Parameter of type %s annotated with %s does not match"
+ + " expected type %s in method/constructor %s",
+ parameterType.getName(),
+ testParameterAnnotationType.annotationType().getName(),
+ valueMethodReturnType.getName(),
+ methodOrConstructor)));
+ } else {
+ matchingTestParameterAnnotationFound = true;
+ }
+ }
+ }
+ }
+ // Second, handle the case where the method parameter does not specify the test parameter,
+ // and instead relies on the type matching, e.g. {@code public void test(Color c)}.
+ if (!matchingTestParameterAnnotationFound) {
+ ImmutableList<? extends Class<? extends Annotation>> testParameterAnnotationTypes =
+ getTestParameterAnnotations(
+ // Do not include METHOD_PARAMETER or CONSTRUCTOR_PARAMETER since they have already
+ // been evaluated.
+ filterAnnotationTypeOriginsByOrigin(
+ annotationTypeOrigins, Origin.CLASS, Origin.CONSTRUCTOR, Origin.METHOD),
+ testClass,
+ methodOrConstructor);
+ // If no annotation is present, simply compare the type.
+ for (Class<? extends Annotation> testParameterAnnotationType :
+ testParameterAnnotationTypes) {
+ if (parameterType.isAssignableFrom(
+ getValueMethodReturnType(
+ testParameterAnnotationType, /* paramClass= */ Optional.absent()))) {
+ if (matchingTestParameterAnnotationFound) {
+ errors.add(
+ new IllegalStateException(
+ String.format(
+ "Ambiguous method/constructor parameter type, matching multiple"
+ + " annotations for parameter of type %s in method %s",
+ parameterType.getName(), methodOrConstructor)));
+ }
+ matchingTestParameterAnnotationFound = true;
+ }
+ }
+ }
+ if (!matchingTestParameterAnnotationFound) {
+ errors.add(
+ new IllegalStateException(
+ String.format(
+ "No matching test parameter annotation found"
+ + " for parameter of type %s in method/constructor %s",
+ parameterType.getName(), methodOrConstructor)));
+ }
+ }
+ return errors;
+ }
+
+ @Override
+ public Optional<List<Object>> maybeGetConstructorParameters(
+ Constructor<?> constructor, TestInfo testInfo) {
+ if (testInfo.getAnnotation(TestIndexHolder.class) == null
+ // Explicitly skip @TestParameters annotated methods to ensure compatibility.
+ //
+ // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported)
+ // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;).
+ // Without the @TestParameters check below, this class would try to find parameters for
+ // these methods. When there are no method parameters, this is a no-op, but when the method
+ // is annotated with @TestParameters, this throws an exception (because there are method
+ // parameters that this processor has no values for - they are provided by the
+ // @TestParameters processor).
+ || constructor.isAnnotationPresent(TestParameters.class)) {
+ return Optional.absent();
+ } else {
+ TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class);
+ List<TestParameterValueHolder> testParameterValues =
+ getParameterValuesForTest(testIndexHolder, testInfo.getTestClass());
+
+ Class<?>[] parameterTypes = constructor.getParameterTypes();
+ Annotation[][] parameterAnnotations = constructor.getParameterAnnotations();
+ List<Object> parameterValues = new ArrayList<>(/* initialCapacity= */ parameterTypes.length);
+ List<Class<? extends Annotation>> processedAnnotationTypes = new ArrayList<>();
+ List<TestParameterValueHolder> parameterValuesForConstructor =
+ filterByOrigin(
+ testParameterValues, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER);
+ for (int i = 0; i < parameterTypes.length; i++) {
+ // Initialize each parameter value from the corresponding TestParameterAnnotation value.
+ parameterValues.add(
+ getParameterValue(
+ parameterValuesForConstructor,
+ parameterTypes[i],
+ parameterAnnotations[i],
+ processedAnnotationTypes));
+ }
+ return Optional.of(parameterValues);
+ }
+ }
+
+ @Override
+ public Optional<List<Object>> maybeGetTestMethodParameters(TestInfo testInfo) {
+ Method testMethod = testInfo.getMethod();
+ if (testInfo.getAnnotation(TestIndexHolder.class) == null
+ // Explicitly skip @TestParameters annotated methods to ensure compatibility.
+ //
+ // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported)
+ // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;).
+ // Without the @TestParameters check below, this class would try to find parameters for
+ // these methods. When there are no method parameters, this is a no-op, but when the method
+ // is annotated with @TestParameters, this throws an exception (because there are method
+ // parameters that this processor has no values for - they are provided by the
+ // @TestParameters processor).
+ || testMethod.isAnnotationPresent(TestParameters.class)) {
+ return Optional.absent();
+ } else {
+ TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class);
+ checkState(testIndexHolder != null);
+ List<TestParameterValueHolder> testParameterValues =
+ filterByOrigin(
+ getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()),
+ Origin.CLASS,
+ Origin.METHOD,
+ Origin.METHOD_PARAMETER);
+
+ Class<?>[] parameterTypes = testMethod.getParameterTypes();
+ Annotation[][] parametersAnnotations = testMethod.getParameterAnnotations();
+ ArrayList<Object> parameterValues =
+ new ArrayList<>(/* initialCapacity= */ parameterTypes.length);
+
+ List<Class<? extends Annotation>> processedAnnotationTypes = new ArrayList<>();
+ for (int i = 0; i < parameterTypes.length; i++) {
+ parameterValues.add(
+ getParameterValue(
+ testParameterValues,
+ parameterTypes[i],
+ parametersAnnotations[i],
+ processedAnnotationTypes));
+ }
+
+ return Optional.of(parameterValues);
+ }
+ }
+
+ /**
+ * Returns the {@link TestInfo}, one for each result of the cartesian product of each test
+ * parameter values.
+ *
+ * <p>For example, given the annotation {@code @ColorParameter({BLUE, WHITE, RED})} on a method,
+ * it method will return the TestParameterValues: "(@ColorParameter, BLUE), (@ColorParameter,
+ * WHITE), (@ColorParameter, RED)}).
+ *
+ * <p>For multiple annotations (say, {@code @TestParameter("foo", "bar")} and
+ * {@code @ColorParameter({BLUE, WHITE})}), it will generate the following result:
+ *
+ * <ul>
+ * <li>("foo", BLUE)
+ * <li>("foo", WHITE)
+ * <li>("bar", BLUE)
+ * <li>("bar", WHITE)
+ * <li>
+ * </ul>
+ *
+ * corresponding to the cartesian product of both annotations.
+ */
+ @Override
+ public List<TestInfo> calculateTestInfos(TestInfo originalTest) {
+ List<List<TestParameterValueHolder>> parameterValuesForMethod =
+ getParameterValuesForMethod(originalTest.getMethod(), originalTest.getTestClass());
+
+ if (parameterValuesForMethod.equals(ImmutableList.of(ImmutableList.of()))) {
+ // This test is not parameterized
+ return ImmutableList.of(originalTest);
+ }
+
+ ImmutableList.Builder<TestInfo> testInfos = ImmutableList.builder();
+ for (int parametersIndex = 0;
+ parametersIndex < parameterValuesForMethod.size();
+ ++parametersIndex) {
+ List<TestParameterValueHolder> testParameterValues =
+ parameterValuesForMethod.get(parametersIndex);
+ testInfos.add(
+ originalTest
+ .withExtraParameters(
+ FluentIterable.from(testParameterValues)
+ .transform(
+ param ->
+ TestInfoParameter.create(
+ param.toTestNameString(),
+ param.unwrappedValue(),
+ param.valueIndex()))
+ .toList())
+ .withExtraAnnotation(
+ TestIndexHolderFactory.create(
+ /* methodIndex= */ strictIndexOf(
+ getMethodsIncludingParentsSorted(originalTest.getTestClass()),
+ originalTest.getMethod()),
+ parametersIndex,
+ originalTest.getTestClass().getName())));
+ }
+
+ return testInfos.build();
+ }
+
+ private List<List<TestParameterValueHolder>> getParameterValuesForMethod(
+ Method method, Class<?> testClass) {
+ try {
+ return parameterValuesCache.get(
+ method,
+ () -> {
+ List<List<TestParameterValueHolder>> testParameterValuesList =
+ getAnnotationValuesForUsedAnnotationTypes(method, testClass);
+
+ return FluentIterable.from(Lists.cartesianProduct(testParameterValuesList))
+ .filter(
+ // Skip tests based on the annotations' {@link Validator#shouldSkip} return
+ // value.
+ testParameterValues ->
+ FluentIterable.from(testParameterValues)
+ .filter(
+ testParameterValue ->
+ callShouldSkip(
+ testParameterValue.annotationTypeOrigin().annotationType(),
+ testParameterValues))
+ .isEmpty())
+ .toList();
+ });
+ } catch (ExecutionException | UncheckedExecutionException e) {
+ Throwables.throwIfUnchecked(e.getCause());
+ throw new RuntimeException(e);
+ }
+ }
+
+ private List<TestParameterValueHolder> getParameterValuesForTest(
+ TestIndexHolder testIndexHolder, Class<?> testClass) {
+ verify(
+ testIndexHolder.testClassName().equals(testClass.getName()),
+ "The class for which the given annotation was created (%s) is not the same as the test"
+ + " class that this runner is handling (%s)",
+ testIndexHolder.testClassName(),
+ testClass.getName());
+ Method testMethod =
+ getMethodsIncludingParentsSorted(testClass).get(testIndexHolder.methodIndex());
+ return getParameterValuesForMethod(testMethod, testClass)
+ .get(testIndexHolder.parametersIndex());
+ }
+
+ /**
+ * Returns the list of annotation index for all annotations defined in a given test method and its
+ * class.
+ */
+ private ImmutableList<List<TestParameterValueHolder>> getAnnotationValuesForUsedAnnotationTypes(
+ Method method, Class<?> testClass) {
+ ImmutableList<AnnotationTypeOrigin> annotationTypes =
+ FluentIterable.from(getAnnotationTypeOrigins(testClass, Origin.CLASS))
+ .append(getAnnotationTypeOrigins(testClass, Origin.FIELD))
+ .append(getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR))
+ .append(getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR_PARAMETER))
+ .append(getAnnotationTypeOrigins(testClass, Origin.METHOD))
+ .append(
+ ImmutableList.sortedCopyOf(
+ annotationComparator(method.getParameterAnnotations()),
+ getAnnotationTypeOrigins(testClass, Origin.METHOD_PARAMETER)))
+ .toList();
+
+ return FluentIterable.from(removeOverrides(annotationTypes, testClass, method))
+ .transform(
+ annotationTypeOrigin ->
+ getAnnotationFromParametersOrTestOrClass(annotationTypeOrigin, method, testClass))
+ .filter(l -> !l.isEmpty())
+ .transformAndConcat(i -> i)
+ .toList();
+ }
+
+ private Comparator<AnnotationTypeOrigin> annotationComparator(
+ Annotation[][] parameterAnnotations) {
+ ImmutableList<String> annotationOrdering =
+ FluentIterable.from(parameterAnnotations)
+ .transformAndConcat(Arrays::asList)
+ .transform(Annotation::annotationType)
+ .transform(Class::getName)
+ .toList();
+ return (annotationTypeOrigin, t1) ->
+ Integer.compare(
+ annotationOrdering.indexOf(annotationTypeOrigin.annotationType().getName()),
+ annotationOrdering.indexOf(t1.annotationType().getName()));
+ }
+
+ /**
+ * Returns a list of {@link AnnotationTypeOrigin} where the overridden annotation are removed for
+ * the current {@code originalTest} and {@code testClass}.
+ *
+ * <p>Specifically, annotation defined on CLASS and FIELD elements will be removed if they are
+ * also defined on the method, method parameter, constructor, or constructor parameters.
+ */
+ private List<AnnotationTypeOrigin> removeOverrides(
+ List<AnnotationTypeOrigin> annotationTypeOrigins, Class<?> testClass, Method method) {
+ return removeOverrides(
+ new ArrayList<>(
+ FluentIterable.from(annotationTypeOrigins)
+ .filter(
+ annotationTypeOrigin -> {
+ switch (annotationTypeOrigin.origin()) {
+ case FIELD: // Fall through.
+ case CLASS:
+ return getAnnotationListWithType(
+ method.getAnnotations(), annotationTypeOrigin.annotationType())
+ .isEmpty();
+ default:
+ return true;
+ }
+ })
+ .toList()),
+ testClass);
+ }
+
+ /**
+ * @see #removeOverrides(List, Class)
+ */
+ private List<AnnotationTypeOrigin> removeOverrides(
+ List<AnnotationTypeOrigin> annotationTypeOrigins, Class<?> testClass) {
+ return new ArrayList<>(
+ FluentIterable.from(annotationTypeOrigins)
+ .filter(
+ annotationTypeOrigin -> {
+ switch (annotationTypeOrigin.origin()) {
+ case FIELD: // Fall through.
+ case CLASS:
+ return getAnnotationListWithType(
+ TestParameterInjectorUtils.getOnlyConstructor(testClass)
+ .getAnnotations(),
+ annotationTypeOrigin.annotationType())
+ .isEmpty();
+ default:
+ return true;
+ }
+ })
+ .toList());
+ }
+
+ /**
+ * Returns the given annotations defined either on the method parameters, method or the test
+ * class.
+ *
+ * <p>The annotation from the parameters takes precedence over the same annotation defined on the
+ * method, and the one defined on the method takes precedence over the same annotation defined on
+ * the class.
+ */
+ private ImmutableList<List<TestParameterValueHolder>> getAnnotationFromParametersOrTestOrClass(
+ AnnotationTypeOrigin annotationTypeOrigin, Method method, Class<?> testClass) {
+ Origin origin = annotationTypeOrigin.origin();
+ Class<? extends Annotation> annotationType = annotationTypeOrigin.annotationType();
+ if (origin == Origin.CONSTRUCTOR_PARAMETER) {
+ Constructor<?> constructor = TestParameterInjectorUtils.getOnlyConstructor(testClass);
+ List<AnnotationWithMetadata> annotations =
+ getAnnotationWithMetadataListWithType(constructor, annotationType, testClass);
+
+ if (!annotations.isEmpty()) {
+ return toTestParameterValueList(annotations, origin);
+ }
+ } else if (origin == Origin.CONSTRUCTOR) {
+ Annotation annotation =
+ TestParameterInjectorUtils.getOnlyConstructor(testClass).getAnnotation(annotationType);
+ if (annotation != null) {
+ return ImmutableList.of(
+ TestParameterValueHolder.create(
+ AnnotationWithMetadata.withoutMetadata(
+ annotation,
+ GenericParameterContext.createWithoutParameterAnnotations(testClass)),
+ origin));
+ }
+
+ } else if (origin == Origin.METHOD_PARAMETER) {
+ List<AnnotationWithMetadata> annotations =
+ getAnnotationWithMetadataListWithType(method, annotationType, testClass);
+ if (!annotations.isEmpty()) {
+ return toTestParameterValueList(annotations, origin);
+ }
+ } else if (origin == Origin.METHOD) {
+ if (method.isAnnotationPresent(annotationType)) {
+ return ImmutableList.of(
+ TestParameterValueHolder.create(
+ AnnotationWithMetadata.withoutMetadata(
+ method.getAnnotation(annotationType),
+ GenericParameterContext.createWithoutParameterAnnotations(testClass)),
+ origin));
+ }
+ } else if (origin == Origin.FIELD) {
+ List<AnnotationWithMetadata> annotations =
+ new ArrayList<>(
+ FluentIterable.from(listWithParents(testClass))
+ .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields()))
+ .transformAndConcat(
+ field ->
+ FluentIterable.from(
+ getAnnotationListWithType(field.getAnnotations(), annotationType))
+ .transform(
+ annotation ->
+ AnnotationWithMetadata.withMetadata(
+ annotation,
+ field.getType(),
+ field.getName(),
+ GenericParameterContext.create(field, testClass))))
+ .toList());
+ if (!annotations.isEmpty()) {
+ return toTestParameterValueList(annotations, origin);
+ }
+ } else if (origin == Origin.CLASS) {
+ Annotation annotation = testClass.getAnnotation(annotationType);
+ if (annotation != null) {
+ return ImmutableList.of(
+ TestParameterValueHolder.create(
+ AnnotationWithMetadata.withoutMetadata(
+ annotation,
+ GenericParameterContext.createWithoutParameterAnnotations(testClass)),
+ origin));
+ }
+ }
+ return ImmutableList.of();
+ }
+
+ private static ImmutableList<List<TestParameterValueHolder>> toTestParameterValueList(
+ List<AnnotationWithMetadata> annotationWithMetadatas, Origin origin) {
+ return FluentIterable.from(annotationWithMetadatas)
+ .transform(
+ annotationWithMetadata ->
+ (List<TestParameterValueHolder>)
+ new ArrayList<>(
+ TestParameterValueHolder.create(annotationWithMetadata, origin)))
+ .toList();
+ }
+
+ private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType(
+ Method callable, Class<? extends Annotation> annotationType, Class<?> testClass) {
+ try {
+ return getAnnotationWithMetadataListWithType(
+ callable.getParameters(), annotationType, testClass);
+ } catch (NoSuchMethodError ignored) {
+ return getAnnotationWithMetadataListWithType(
+ callable.getParameterTypes(),
+ callable.getParameterAnnotations(),
+ annotationType,
+ testClass);
+ }
+ }
+
+ private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType(
+ Constructor<?> callable, Class<? extends Annotation> annotationType, Class<?> testClass) {
+ try {
+ return getAnnotationWithMetadataListWithType(
+ callable.getParameters(), annotationType, testClass);
+ } catch (NoSuchMethodError ignored) {
+ return getAnnotationWithMetadataListWithType(
+ callable.getParameterTypes(),
+ callable.getParameterAnnotations(),
+ annotationType,
+ testClass);
+ }
+ }
+
+ // Parameter is not available on old Android SDKs, and isn't desugared. That's why this method
+ // has a fallback that takes the parameter types and annotations (without the parameter names,
+ // which are optional anyway).
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType(
+ Parameter[] parameters, Class<? extends Annotation> annotationType, Class<?> testClass) {
+ return FluentIterable.from(parameters)
+ .transform(
+ parameter -> {
+ Annotation annotation = parameter.getAnnotation(annotationType);
+ return annotation == null
+ ? null
+ : parameter.isNamePresent()
+ ? AnnotationWithMetadata.withMetadata(
+ annotation,
+ parameter.getType(),
+ parameter.getName(),
+ GenericParameterContext.create(parameter, testClass))
+ : AnnotationWithMetadata.withMetadata(
+ annotation,
+ parameter.getType(),
+ GenericParameterContext.create(parameter, testClass));
+ })
+ .filter(Objects::nonNull)
+ .toList();
+ }
+
+ private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType(
+ Class<?>[] parameterTypes,
+ Annotation[][] annotations,
+ Class<? extends Annotation> annotationType,
+ Class<?> testClass) {
+ checkArgument(parameterTypes.length == annotations.length);
+
+ ImmutableList.Builder<AnnotationWithMetadata> resultBuilder = ImmutableList.builder();
+ for (int i = 0; i < annotations.length; i++) {
+ for (Annotation annotation : annotations[i]) {
+ if (annotation.annotationType().equals(annotationType)) {
+ resultBuilder.add(
+ AnnotationWithMetadata.withMetadata(
+ annotation,
+ parameterTypes[i],
+ GenericParameterContext.createWithRepeatableAnnotationsFallback(
+ annotations[i], testClass)));
+ }
+ }
+ }
+ return resultBuilder.build();
+ }
+
+ private ImmutableList<Annotation> getAnnotationListWithType(
+ Annotation[] annotations, Class<? extends Annotation> annotationType) {
+ return FluentIterable.from(annotations)
+ .filter(annotation -> annotation.annotationType().equals(annotationType))
+ .toList();
+ }
+
+ @Override
+ public void postProcessTestInstance(Object testInstance, TestInfo testInfo) {
+ TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class);
+ try {
+ if (testIndexHolder != null) {
+ List<TestParameterValueHolder> testParameterValues =
+ getParameterValuesForTest(testIndexHolder, testInfo.getTestClass());
+
+ // Do not include {@link Origin#METHOD_PARAMETER} nor {@link Origin#CONSTRUCTOR_PARAMETER}
+ // annotations.
+ List<TestParameterValueHolder> testParameterValuesForFieldInjection =
+ filterByOrigin(testParameterValues, Origin.CLASS, Origin.FIELD, Origin.METHOD);
+ // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class
+ // in the example above.
+ List<TestParameterValueHolder> remainingTestParameterValuesForFieldInjection =
+ new ArrayList<>(testParameterValuesForFieldInjection);
+ for (Field declaredField :
+ FluentIterable.from(listWithParents(testInstance.getClass()))
+ .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields()))
+ .toList()) {
+ for (TestParameterValueHolder testParameterValue :
+ remainingTestParameterValuesForFieldInjection) {
+ if (declaredField.isAnnotationPresent(
+ testParameterValue.annotationTypeOrigin().annotationType())) {
+ if (testParameterValue.paramName().isPresent()
+ && !declaredField.getName().equals(testParameterValue.paramName().get())) {
+ // names don't match
+ continue;
+ }
+ declaredField.setAccessible(true);
+ declaredField.set(testInstance, testParameterValue.unwrappedValue());
+ remainingTestParameterValuesForFieldInjection.remove(testParameterValue);
+ break;
+ }
+ }
+ }
+ }
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Returns an {@link TestParameterValueHolder} list that contains only the values originating from
+ * one of the {@code origins}.
+ */
+ private static ImmutableList<TestParameterValueHolder> filterByOrigin(
+ List<TestParameterValueHolder> testParameterValues, Origin... origins) {
+ Set<Origin> originsToFilterBy = ImmutableSet.copyOf(origins);
+ return FluentIterable.from(testParameterValues)
+ .filter(
+ testParameterValue ->
+ originsToFilterBy.contains(testParameterValue.annotationTypeOrigin().origin()))
+ .toList();
+ }
+
+ /**
+ * Returns an {@link AnnotationTypeOrigin} list that contains only the values originating from one
+ * of the {@code origins}.
+ */
+ private static ImmutableList<AnnotationTypeOrigin> filterAnnotationTypeOriginsByOrigin(
+ List<AnnotationTypeOrigin> annotationTypeOrigins, Origin... origins) {
+ List<Origin> originList = Arrays.asList(origins);
+ return FluentIterable.from(annotationTypeOrigins)
+ .filter(annotationTypeOrigin -> originList.contains(annotationTypeOrigin.origin()))
+ .toList();
+ }
+
+ /** Returns a {@link TestParameterAnnotation}'s value for a method or constructor parameter. */
+ private Object getParameterValue(
+ List<TestParameterValueHolder> testParameterValues,
+ Class<?> methodParameterType,
+ Annotation[] parameterAnnotations,
+ List<Class<? extends Annotation>> processedAnnotationTypes) {
+ List<Class<? extends Annotation>> iteratedAnnotationTypes = new ArrayList<>();
+ for (TestParameterValueHolder testParameterValue : testParameterValues) {
+ // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class
+ // in the example above.
+ for (Annotation parameterAnnotation : parameterAnnotations) {
+ Class<? extends Annotation> annotationType =
+ testParameterValue.annotationTypeOrigin().annotationType();
+ if (parameterAnnotation.annotationType().equals(annotationType)) {
+ // If multiple annotations exist, ensure that the proper one is selected.
+ // For instance, for:
+ // <code>
+ // test(@FooParameter(1,2) Foo foo, @FooParameter(3,4) Foo bar) {}
+ // </code>
+ // Verifies that the correct @FooParameter annotation value will be assigned to the
+ // corresponding variable.
+ if (Collections.frequency(processedAnnotationTypes, annotationType)
+ == Collections.frequency(iteratedAnnotationTypes, annotationType)) {
+ processedAnnotationTypes.add(annotationType);
+ return testParameterValue.unwrappedValue();
+ }
+ iteratedAnnotationTypes.add(annotationType);
+ }
+ }
+ }
+ // If no annotation matches, use the method parameter type.
+ for (TestParameterValueHolder testParameterValue : testParameterValues) {
+ // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class
+ // in the example above.
+ if (methodParameterType.isAssignableFrom(
+ getValueMethodReturnType(
+ testParameterValue.annotationTypeOrigin().annotationType(),
+ /* paramClass= */ Optional.absent()))) {
+ return testParameterValue.unwrappedValue();
+ }
+ }
+ throw new IllegalStateException(
+ "The method parameter should have matched a TestParameterAnnotation");
+ }
+
+ /**
+ * This mechanism is a workaround to be able to store the annotation values in the annotation list
+ * of the {@link TestInfo}, since we cannot carry other information through the test runner.
+ */
+ @Retention(RUNTIME)
+ @interface TestIndexHolder {
+
+ /** The index of the test method in {@code getMethodsIncludingParentsSorted(testClass)} */
+ int methodIndex();
+
+ /**
+ * The index of the set of parameters to run the test method with in the list produced by {@link
+ * #getParameterValuesForMethod}.
+ */
+ int parametersIndex();
+
+ /**
+ * The full name of the test class. Only used for verifying that assumptions about the above
+ * indices are valid.
+ */
+ String testClassName();
+ }
+
+ /** Factory for {@link TestIndexHolder}. */
+ static class TestIndexHolderFactory {
+ @AutoAnnotation
+ static TestIndexHolder create(int methodIndex, int parametersIndex, String testClassName) {
+ return new AutoAnnotation_TestParameterAnnotationMethodProcessor_TestIndexHolderFactory_create(
+ methodIndex, parametersIndex, testClassName);
+ }
+
+ private TestIndexHolderFactory() {}
+ }
+
+ /**
+ * Returns whether the test should be skipped according to the {@code annotationType}'s {@link
+ * TestParameterValidator} and the current list of {@link TestParameterValueHolder}.
+ */
+ private static boolean callShouldSkip(
+ Class<? extends Annotation> annotationType,
+ List<TestParameterValueHolder> testParameterValues) {
+ TestParameterAnnotation annotation =
+ annotationType.getAnnotation(TestParameterAnnotation.class);
+ Class<? extends TestParameterValidator> validator = annotation.validator();
+ try {
+ return validator
+ .getConstructor()
+ .newInstance()
+ .shouldSkip(new ValidatorContext(testParameterValues));
+ } catch (Exception e) {
+ throw new RuntimeException("Unexpected exception while invoking validator " + validator, e);
+ }
+ }
+
+ private static class ValidatorContext implements TestParameterValidator.Context {
+
+ private final List<TestParameterValueHolder> testParameterValues;
+ private final Set<Object> valueList;
+
+ public ValidatorContext(List<TestParameterValueHolder> testParameterValues) {
+ this.testParameterValues = testParameterValues;
+ this.valueList =
+ FluentIterable.from(testParameterValues)
+ .transform(TestParameterValueHolder::unwrappedValue)
+ .filter(Objects::nonNull)
+ .toSet();
+ }
+
+ @Override
+ public boolean has(Class<? extends Annotation> testParameter, Object value) {
+ return getValue(testParameter).transform(value::equals).or(false);
+ }
+
+ @Override
+ public <T extends Enum<T>, U extends Enum<U>> boolean has(T value1, U value2) {
+ return valueList.contains(value1) && valueList.contains(value2);
+ }
+
+ @Override
+ public Optional<Object> getValue(Class<? extends Annotation> testParameter) {
+ return getParameter(testParameter).transform(TestParameterValueHolder::unwrappedValue);
+ }
+
+ @Override
+ public List<Object> getSpecifiedValues(Class<? extends Annotation> testParameter) {
+ return getParameter(testParameter)
+ .transform(TestParameterValueHolder::specifiedValues)
+ .or(ImmutableList.of());
+ }
+
+ private Optional<TestParameterValueHolder> getParameter(
+ Class<? extends Annotation> testParameter) {
+ return FluentIterable.from(testParameterValues)
+ .firstMatch(value -> value.annotationTypeOrigin().annotationType().equals(testParameter));
+ }
+ }
+
+ /**
+ * Returns the class of the list elements returned by {@code provideValues()}.
+ *
+ * @param annotationType The type of the annotation that was encountered in the test class. The
+ * definition of this annotation is itself annotated with the {@link TestParameterAnnotation}
+ * annotation.
+ * @param paramClass The class of the parameter or field that is being annotated. In case the
+ * annotation is annotating a method, constructor or class, {@code paramClass} is an absent
+ * optional.
+ */
+ private static Class<?> getValueMethodReturnType(
+ Class<? extends Annotation> annotationType, Optional<Class<?>> paramClass) {
+ TestParameterAnnotation testParameter =
+ annotationType.getAnnotation(TestParameterAnnotation.class);
+ Class<? extends TestParameterValueProvider> valueProvider = testParameter.valueProvider();
+ try {
+ return valueProvider.getConstructor().newInstance().getValueType(annotationType, paramClass);
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Unexpected exception while invoking value provider " + valueProvider, e);
+ }
+ }
+
+ /** Returns the TestParameterAnnotation annotation types defined for a method or constructor. */
+ private ImmutableList<? extends Class<? extends Annotation>> getTestParameterAnnotations(
+ List<AnnotationTypeOrigin> annotationTypeOrigins,
+ final Class<?> testClass,
+ AnnotatedElement methodOrConstructor) {
+ return FluentIterable.from(annotationTypeOrigins)
+ .transform(AnnotationTypeOrigin::annotationType)
+ .filter(
+ annotationType ->
+ testClass.isAnnotationPresent(annotationType)
+ || methodOrConstructor.isAnnotationPresent(annotationType))
+ .toList();
+ }
+
+ private <T> int strictIndexOf(List<T> haystack, T needle) {
+ int index = haystack.indexOf(needle);
+ checkArgument(index >= 0, "Could not find '%s' in %s", needle, haystack);
+ return index;
+ }
+
+ private ImmutableList<Method> getMethodsIncludingParentsSorted(Class<?> clazz) {
+ ImmutableList.Builder<Method> resultBuilder = ImmutableList.builder();
+ while (clazz != null) {
+ resultBuilder.add(clazz.getDeclaredMethods());
+ clazz = clazz.getSuperclass();
+ }
+ // Because getDeclaredMethods()'s order is not specified, there is the theoretical possibility
+ // that the order of methods is unstable. To partly fix this, we sort the result based on method
+ // name. This is still not perfect because of method overloading, but that should be
+ // sufficiently rare for test names.
+ return ImmutableList.sortedCopyOf(
+ Ordering.natural().onResultOf(Method::getName), resultBuilder.build());
+ }
+
+ private static ImmutableList<Class<?>> listWithParents(Class<?> clazz) {
+ ImmutableList.Builder<Class<?>> resultBuilder = ImmutableList.builder();
+
+ Class<?> currentClass = clazz;
+ while (currentClass != null) {
+ resultBuilder.add(currentClass);
+ currentClass = currentClass.getSuperclass();
+ }
+
+ return resultBuilder.build();
+ }
+}
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java
new file mode 100644
index 0000000..8b23e53
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import org.junit.runners.model.InitializationError;
+
+/**
+ * A JUnit4 test runner which knows how to instantiate and run test classes where each test case may
+ * be parameterized with its own unique set of test parameters.
+ */
+public final class TestParameterInjector extends PluggableTestRunner {
+
+ public TestParameterInjector(Class<?> testClass) throws InitializationError {
+ super(testClass);
+ }
+
+ @Override
+ protected TestMethodProcessorList createTestMethodProcessorList() {
+ return TestMethodProcessorList.createNewParameterizedProcessors();
+ }
+}
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorUtils.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorUtils.java
new file mode 100644
index 0000000..215719a
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorUtils.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.Iterables.getOnlyElement;
+
+import com.google.common.collect.ImmutableList;
+import java.lang.reflect.Constructor;
+
+/** Shared utility methods. */
+class TestParameterInjectorUtils {
+
+ /**
+ * Return the only public constructor of the given test class. If there is none, return the only
+ * constructor.
+ *
+ * <p>Normally, there should be exactly one constructor (public or other), but some frameworks
+ * introduce an extra non-public constructor (see
+ * https://github.com/google/TestParameterInjector/issues/40).
+ */
+ static Constructor<?> getOnlyConstructor(Class<?> testClass) {
+ ImmutableList<Constructor<?>> constructors = ImmutableList.copyOf(testClass.getConstructors());
+ if (constructors.isEmpty()) {
+ // There are no public constructors. This is likely a JUnit5 test, so we should take the only
+ // non-public constructor instead.
+ constructors = ImmutableList.copyOf(testClass.getDeclaredConstructors());
+ }
+ checkState(
+ constructors.size() == 1, "Expected exactly one constructor, but got %s", constructors);
+ return getOnlyElement(constructors);
+ }
+
+ private TestParameterInjectorUtils() {}
+}
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java
new file mode 100644
index 0000000..3733833
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import com.google.common.base.Optional;
+import java.lang.annotation.Annotation;
+import java.util.List;
+
+/**
+ * Validator interface which allows {@link TestParameterAnnotation} annotations to validate the set
+ * of annotation values for a given test instance, and to selectively skip the test.
+ */
+interface TestParameterValidator {
+
+ /**
+ * This interface allows to access information on the current testwhen implementing {@link
+ * TestParameterValidator}.
+ */
+ interface Context {
+
+ /** Returns whether the current test has the {@link TestParameterAnnotation} value(s). */
+ boolean has(Class<? extends Annotation> testParameter, Object value);
+
+ /**
+ * Returns whether the current test has the two {@link TestParameterAnnotation} values, granted
+ * that the value is an enum, and each enum corresponds to a unique annotation.
+ */
+ <T extends Enum<T>, U extends Enum<U>> boolean has(T value1, U value2);
+
+ /**
+ * Returns all the current test value for a given {@link TestParameterAnnotation} annotated
+ * annotation.
+ */
+ Optional<Object> getValue(Class<? extends Annotation> testParameter);
+
+ /**
+ * Returns all the values specified for a given {@link TestParameterAnnotation} annotated
+ * annotation in the test.
+ *
+ * <p>For example, if the test annotates '@Foo(a,b,c)', getSpecifiedValues(Foo.class) will
+ * return [a,b,c].
+ */
+ List<Object> getSpecifiedValues(Class<? extends Annotation> testParameter);
+ }
+
+ /**
+ * Returns whether the test should be skipped based on the annotations' values.
+ *
+ * <p>The {@code testParameterValues} list contains all {@link TestParameterAnnotation}
+ * annotations, including those specified at the class, field, method, method parameter,
+ * constructor, and constructor parameter for a given test.
+ *
+ * <p>This method is not invoked in the context of a running test statement.
+ */
+ boolean shouldSkip(Context context);
+}
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValue.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValue.java
new file mode 100644
index 0000000..a16f0cb
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValue.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Optional;
+import javax.annotation.Nullable;
+
+/**
+ * Wrapper class around a parameter value. Use this to give a value a name that is different from
+ * its {@code toString()} method.
+ */
+public class TestParameterValue {
+ private final @Nullable Object wrappedValue;
+ private final Optional<String> customName;
+
+ private TestParameterValue(@Nullable Object wrappedValue, Optional<String> customName) {
+ this.wrappedValue = wrappedValue;
+ this.customName = checkNotNull(customName);
+ }
+
+ /** Wraps the given value. */
+ public static TestParameterValue wrap(@Nullable Object wrappedValue) {
+ return new TestParameterValue(wrappedValue, /* customName= */ Optional.absent());
+ }
+
+ /**
+ * Returns a new {@link TestParameterValue} instance that stores the given name. The
+ * TestParameterInjector framework will use this name instead of {@code wrappedValue.toString()}
+ * when generating the test name.
+ */
+ public TestParameterValue withName(String name) {
+ return new TestParameterValue(wrappedValue, Optional.of(name));
+ }
+
+ @Nullable
+ Object getWrappedValue() {
+ return wrappedValue;
+ }
+
+ Optional<String> getCustomName() {
+ return customName;
+ }
+}
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java
new file mode 100644
index 0000000..38c3356
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import com.google.common.base.Optional;
+import java.lang.annotation.Annotation;
+import java.util.List;
+
+/**
+ * Interface which allows {@link TestParameterAnnotation} annotations to provide the values to test
+ * in a dynamic way.
+ */
+interface TestParameterValueProvider {
+
+ /**
+ * Returns the parameter values for which the test should run.
+ *
+ * @param annotation The annotation instance that was encountered in the test class. The
+ * definition of this annotation is itself annotated with the {@link TestParameterAnnotation}
+ * annotation.
+ * @param parameterClass The class of the parameter or field that is being annotated. In case the
+ * annotation is annotating a method, constructor or class, {@code parameterClass} is an empty
+ * optional.
+ */
+ default List<Object> provideValues(Annotation annotation, Optional<Class<?>> parameterClass) {
+ throw new UnsupportedOperationException(
+ "If this is called by TestParameterInjector, it means that neither of the"
+ + " provideValues()-type methods have been implemented");
+ }
+
+ /**
+ * Extension of {@link #provideValues(Annotation, Optional<Class<?>>)} with extra context.
+ *
+ * @param annotation The annotation instance that was encountered in the test class. The
+ * definition of this annotation is itself annotated with the {@link TestParameterAnnotation}
+ * annotation.
+ * @param otherAnnotations A list of all other annotations on the field or parameter that was
+ * annotated with {@code annotation}.
+ * <p>For example, if the test code is as follows:
+ * <pre>
+ * @Test
+ * public void myTest_success(
+ * @CustomAnnotation(123) @TestParameter(valuesProvider=MyProvider.class) Foo foo) {
+ * ...
+ * }
+ * </pre>
+ * then this list will contain a single element: @CustomAnnotation(123).
+ * <p>In case the annotation is annotating a method, constructor or class, {@code
+ * parameterClass} is an empty list.
+ * @param parameterClass The class of the parameter or field that is being annotated. In case the
+ * annotation is annotating a method, constructor or class, {@code parameterClass} is an empty
+ * optional.
+ * @param testClass The class that contains the test that is currently being run.
+ * <p>Having this can be useful when sharing providers between tests that have the same base
+ * class. In those cases, an abstract method can be called as follows:
+ * <pre>
+ * ((MyBaseClass) context.testClass().newInstance()).myAbstractMethod()
+ * </pre>
+ *
+ * @deprecated Don't use this method outside of the testparameterinjector codebase, as it is prone
+ * to being changed.
+ */
+ @Deprecated
+ default List<Object> provideValues(
+ Annotation annotation, Optional<Class<?>> parameterClass, GenericParameterContext context) {
+ return provideValues(annotation, parameterClass);
+ }
+
+ /**
+ * Returns the class of the list elements returned by {@link #provideValues(Annotation,
+ * Optional)}.
+ *
+ * @param annotationType The type of the annotation that was encountered in the test class. The
+ * definition of this annotation is itself annotated with the {@link TestParameterAnnotation}
+ * annotation.
+ * @param parameterClass The class of the parameter or field that is being annotated. In case the
+ * annotation is annotating a method, constructor or class, {@code parameterClass} is an empty
+ * optional.
+ */
+ Class<?> getValueType(
+ Class<? extends Annotation> annotationType, Optional<Class<?>> parameterClass);
+}
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java
new file mode 100644
index 0000000..5207ec6
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import com.google.common.base.Optional;
+import java.lang.annotation.Annotation;
+
+/** Interface to retrieve the {@link TestParameterAnnotation} values for a test. */
+interface TestParameterValues {
+ /**
+ * Returns a {@link TestParameterAnnotation} value for the current test as specified by {@code
+ * testInfo}, or {@link Optional#absent()} if the {@code annotationType} is not found.
+ */
+ Optional<Object> getValue(Class<? extends Annotation> annotationType);
+}
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java
new file mode 100644
index 0000000..ccdb18b
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2024 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.Annotation;
+import java.util.List;
+import java.util.NoSuchElementException;
+import javax.annotation.Nullable;
+
+/**
+ * Abstract class for custom providers of @TestParameter values.
+ *
+ * <p>This is a replacement for {@link TestParameter.TestParameterValuesProvider}, which will soon
+ * be deprecated. The difference with the former interface is that this class provides a {@code
+ * Context} instance when invoking {@link #provideValues}.
+ */
+public abstract class TestParameterValuesProvider
+ implements TestParameter.TestParameterValuesProvider {
+
+ protected abstract List<?> provideValues(Context context) throws Exception;
+
+ /**
+ * @deprecated This method should never be called as it will simply throw an {@link
+ * UnsupportedOperationException}.
+ */
+ @Override
+ @Deprecated
+ public final List<?> provideValues() {
+ throw new UnsupportedOperationException(
+ "The TestParameterInjector framework should never call this method, and instead call"
+ + " #provideValues(Context)");
+ }
+
+ /**
+ * Wraps the given value in an object that allows you to give the parameter value a different
+ * name. The TestParameterInjector framework will recognize the returned {@link
+ * TestParameterValue} instances and unwrap them at injection time.
+ *
+ * <p>Usage: {@code value(file.content).withName(file.name)}.
+ */
+ @Override
+ public final TestParameterValue value(@Nullable Object wrappedValue) {
+ // Overriding this method as final because it is not supposed to be overwritten
+ return TestParameterValue.wrap(wrappedValue);
+ }
+
+ /**
+ * An immutable value class that contains extra information about the context of the parameter for
+ * which values are being provided.
+ */
+ public static final class Context {
+
+ private final GenericParameterContext delegate;
+
+ Context(GenericParameterContext delegate) {
+ this.delegate = delegate;
+ }
+
+ /**
+ * Returns the only annotation with the given type on the field or parameter that was annotated
+ * with @TestParameter.
+ *
+ * <p>For example, if the test code is as follows:
+ *
+ * <pre>
+ * {@literal @}Test
+ * public void myTest_success(
+ * {@literal @}CustomAnnotation(123) {@literal @}TestParameter(valuesProvider=MyProvider.class) Foo foo) {
+ * ...
+ * }
+ * </pre>
+ *
+ * then {@code context.getOtherAnnotation(CustomAnnotation.class).value()} will equal 123.
+ *
+ * @throws NoSuchElementException if this there is no annotation with the given type
+ * @throws IllegalArgumentException if there are multiple annotations with the given type
+ * @throws IllegalArgumentException if the argument it TestParameter.class because it is already
+ * handled by the TestParameterInjector framework.
+ */
+ public <A extends Annotation> A getOtherAnnotation(Class<A> annotationType) {
+ checkArgument(
+ !TestParameter.class.equals(annotationType),
+ "Getting the @TestParameter annotating the field or parameter is not allowed because"
+ + " it is already handled by the TestParameterInjector framework.");
+ return delegate.getAnnotation(annotationType);
+ }
+
+ /**
+ * Returns the only annotation with the given type on the field or parameter that was annotated
+ * with @TestParameter.
+ *
+ * <p>For example, if the test code is as follows:
+ *
+ * <pre>
+ * {@literal @}Test
+ * public void myTest_success(
+ * {@literal @}CustomAnnotation(123)
+ * {@literal @}CustomAnnotation(456)
+ * {@literal @}TestParameter(valuesProvider=MyProvider.class)
+ * Foo foo) {
+ * ...
+ * }
+ * </pre>
+ *
+ * then {@code context.getOtherAnnotations(CustomAnnotation.class)} will return the annotation
+ * with 123 and 456.
+ *
+ * <p>Returns an empty list if this there is no annotation with the given type.
+ *
+ * @throws IllegalArgumentException if the argument it TestParameter.class because it is already
+ * handled by the TestParameterInjector framework.
+ */
+ public <A extends Annotation> ImmutableList<A> getOtherAnnotations(Class<A> annotationType) {
+ checkArgument(
+ !TestParameter.class.equals(annotationType),
+ "Getting the @TestParameter annotating the field or parameter is not allowed because"
+ + " it is already handled by the TestParameterInjector framework.");
+ return delegate.getAnnotations(annotationType);
+ }
+
+ /**
+ * The class that contains the test that is currently being run.
+ *
+ * <p>Having this can be useful when sharing providers between tests that have the same base
+ * class. In those cases, an abstract method can be called as follows:
+ *
+ * <pre>
+ * ((MyBaseClass) context.testClass().newInstance()).myAbstractMethod()
+ * </pre>
+ */
+ public Class<?> testClass() {
+ return delegate.testClass();
+ }
+
+ /** A list of all annotations on the field or parameter. */
+ @VisibleForTesting
+ ImmutableList<Annotation> annotationsOnParameter() {
+ return delegate.annotationsOnParameter();
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+ }
+}
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java
new file mode 100644
index 0000000..684e770
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static java.util.Collections.unmodifiableMap;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValuesProvider;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/**
+ * Annotation that can be placed (repeatedly) on @Test-methods or a test constructor to indicate the
+ * sets of parameters that it should be invoked with.
+ *
+ * <p>For @Test-methods, the method will be invoked for every set of parameters that is specified.
+ * For constructors, all the tests in the test class will be invoked on a class instance that was
+ * constructed by each set of parameters.
+ *
+ * <p>Note: If this annotation is used in a test class, the other methods in that class can still
+ * use other types of parameterization, such as {@linkplain TestParameter @TestParameter}.
+ *
+ * <p>See {@link #value()} for simple examples.
+ *
+ * <p>Warning: This annotation can only be used if the compiled java code contains the parameter
+ * names. This is typically done by passing the {@code -parameters} option to the Java compiler,
+ * which requires using Java 8 or higher and may not be available on Android.
+ */
+@Retention(RUNTIME)
+@Target({CONSTRUCTOR, METHOD})
+@Repeatable(TestParameters.RepeatedTestParameters.class)
+public @interface TestParameters {
+
+ /**
+ * Specifies one or more stringified sets of parameters in YAML format. Each set corresponds to a
+ * single invocation of a test method.
+ *
+ * <p>Each element in this array is a full parameter set, formatted as a YAML mapping. The mapping
+ * keys must match the parameter names and the mapping values will be converted to the parameter
+ * type if possible. See yaml.org for the YAML syntax and the section below on the supported
+ * parameter types.
+ *
+ * <p>There are two distinct ways of using this annotation: repeated vs single:
+ *
+ * <p><b>Recommended usage: Separate annotation per parameter set</b>
+ *
+ * <p>This approach uses multiple @TestParameters annotations, one for each set of parameters, for
+ * example:
+ *
+ * <pre>
+ * {@literal @}Test
+ * {@literal @}TestParameters("{age: 17, expectIsAdult: false}")
+ * {@literal @}TestParameters("{age: 22, expectIsAdult: true}")
+ * public void personIsAdult(int age, boolean expectIsAdult) { ... }
+ *
+ * {@literal @}Test
+ * {@literal @}TestParameters("{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}")
+ * {@literal @}TestParameters("{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}")
+ * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... }
+ * </pre>
+ *
+ * <p><b>Old discouraged usage: Single annotation with all parameter sets</b>
+ *
+ * <p>This approach uses a single @TestParameter annotation for all parameter sets, for example:
+ *
+ * <pre>
+ * {@literal @}Test
+ * {@literal @}TestParameters({
+ * "{age: 17, expectIsAdult: false}",
+ * "{age: 22, expectIsAdult: true}",
+ * })
+ * public void personIsAdult(int age, boolean expectIsAdult) { ... }
+ *
+ * {@literal @}Test
+ * {@literal @}TestParameters({
+ * "{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}",
+ * "{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}",
+ * })
+ * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... }
+ * </pre>
+ *
+ * <p><b>Supported parameter types</b>
+ *
+ * <ul>
+ * <li>YAML primitives:
+ * <ul>
+ * <li>String: Specified as YAML string
+ * <li>boolean: Specified as YAML boolean
+ * <li>long and int: Specified as YAML integer
+ * <li>float and double: Specified as YAML floating point or integer
+ * </ul>
+ * <li>
+ * <li>Parsed types:
+ * <ul>
+ * <li>Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()}
+ * <li>Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML
+ * bytes (example: "!!binary 'ZGF0YQ=='")
+ * </ul>
+ * <li>
+ * </ul>
+ *
+ * <p>For dynamic sets of parameters or parameter types that are not supported here, use {@link
+ * #valuesProvider()} and leave this field empty.
+ */
+ String[] value() default {};
+
+ /**
+ * Overrides the name of the parameter set that is used in the test name.
+ *
+ * <p>This can only be set if {@link #value()} has exactly one element. If not set, the YAML
+ * string in {@link #value()} is used in the test name.
+ *
+ * <p>For example: If this name is set to "young adult", then the test name might be
+ * "personIsAdult[young adult]" where the default might have been "personIsAdult[{age: 17,
+ * expectIsAdult: false}]".
+ */
+ String customName() default "";
+
+ /**
+ * Sets a provider that will return a list of parameter sets. Each element in the returned list
+ * corresponds to a single invocation of a test method.
+ *
+ * <p>If this field is set, {@link #value()} must be empty and vice versa.
+ *
+ * <p><b>Example</b>
+ *
+ * <pre>
+ * {@literal @}Test
+ * {@literal @}TestParameters(valuesProvider = IsAdultValueProvider.class)
+ * public void personIsAdult(int age, boolean expectIsAdult) { ... }
+ *
+ * private static final class IsAdultValueProvider implements TestParametersValuesProvider {
+ * {@literal @}Override public {@literal List<TestParametersValues>} provideValues() {
+ * return ImmutableList.of(
+ * TestParametersValues.builder()
+ * .name("teenager")
+ * .addParameter("age", 17)
+ * .addParameter("expectIsAdult", false)
+ * .build(),
+ * TestParametersValues.builder()
+ * .name("young adult")
+ * .addParameter("age", 22)
+ * .addParameter("expectIsAdult", true)
+ * .build()
+ * );
+ * }
+ * }
+ * </pre>
+ */
+ Class<? extends TestParametersValuesProvider> valuesProvider() default
+ DefaultTestParametersValuesProvider.class;
+
+ /** Interface for custom providers of test parameter values. */
+ interface TestParametersValuesProvider {
+ List<TestParametersValues> provideValues();
+ }
+
+ /** A set of parameters for a single method invocation. */
+ @AutoValue
+ abstract class TestParametersValues {
+
+ /**
+ * A name for this set of parameters that will be used for describing this test.
+ *
+ * <p>Example: If a test method is called "personIsAdult" and this name is "teenager", the name
+ * of the resulting test will be "personIsAdult[teenager]".
+ */
+ public abstract String name();
+
+ /** A map, mapping parameter names to their values. */
+ @SuppressWarnings("AutoValueImmutableFields") // intentional to allow null values
+ public abstract Map<String, Object> parametersMap();
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ // Avoid instantiations other than the AutoValue one.
+ TestParametersValues() {}
+
+ /** Builder for {@link TestParametersValues}. */
+ public static final class Builder {
+ private String name;
+ private final LinkedHashMap<String, Object> parametersMap = new LinkedHashMap<>();
+
+ /**
+ * Sets a name for this set of parameters that will be used for describing this test.
+ *
+ * <p>Setting a name is optional. If unset, one will be generated from the parameter values.
+ *
+ * <p>Example: If a test method is called "personIsAdult" and this name is "teenager", the
+ * name of the resulting test will be "personIsAdult[teenager]".
+ */
+ public Builder name(String name) {
+ this.name = name.replaceAll("\\s+", " ");
+ return this;
+ }
+
+ /**
+ * Adds a parameter by its name.
+ *
+ * @param parameterName The name of the parameter of the test method
+ * @param value A value of the same type as the method parameter
+ */
+ public Builder addParameter(String parameterName, @Nullable Object value) {
+ this.parametersMap.put(parameterName, value);
+ return this;
+ }
+
+ /** Adds parameters by thris names. */
+ public Builder addParameters(Map<String, Object> parameterNameToValueMap) {
+ this.parametersMap.putAll(parameterNameToValueMap);
+ return this;
+ }
+
+ public TestParametersValues build() {
+ if (name == null) {
+ // Name is not set. Auto-generate one based on the parameter name and values
+ StringBuilder nameBuilder = new StringBuilder();
+ nameBuilder.append('{');
+ for (String parameterName : parametersMap.keySet()) {
+ if (nameBuilder.length() > 1) {
+ nameBuilder.append(", ");
+ }
+ nameBuilder.append(
+ ParameterValueParsing.formatTestNameString(
+ Optional.of(parameterName), parametersMap.get(parameterName)));
+ }
+ nameBuilder.append('}');
+ name = nameBuilder.toString();
+ }
+ return new AutoValue_TestParameters_TestParametersValues(
+ name, unmodifiableMap(new LinkedHashMap<>(parametersMap)));
+ }
+ }
+ }
+
+ /** Default {@link TestParametersValuesProvider} implementation that does nothing. */
+ class DefaultTestParametersValuesProvider implements TestParametersValuesProvider {
+ @Override
+ public List<TestParametersValues> provideValues() {
+ return ImmutableList.of();
+ }
+ }
+
+ /**
+ * Holder annotation for multiple @TestParameters annotations. This should never be used directly.
+ */
+ @Retention(RUNTIME)
+ @Target({CONSTRUCTOR, METHOD})
+ @interface RepeatedTestParameters {
+ TestParameters[] value();
+ }
+}
diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
new file mode 100644
index 0000000..7dffc29
--- /dev/null
+++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
@@ -0,0 +1,468 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Verify.verify;
+
+import com.google.auto.value.AutoAnnotation;
+import com.google.common.base.Optional;
+import com.google.common.base.Throwables;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.primitives.Primitives;
+import com.google.common.reflect.TypeToken;
+import com.google.common.util.concurrent.UncheckedExecutionException;
+import com.google.testing.junit.testparameterinjector.TestInfo.TestInfoParameter;
+import com.google.testing.junit.testparameterinjector.TestParameters.DefaultTestParametersValuesProvider;
+import com.google.testing.junit.testparameterinjector.TestParameters.RepeatedTestParameters;
+import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValues;
+import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValuesProvider;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Executable;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Parameter;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/** {@code TestMethodProcessor} implementation for supporting {@link TestParameters}. */
+@SuppressWarnings("AndroidJdkLibsChecker") // Parameter is not available on old Android SDKs.
+final class TestParametersMethodProcessor implements TestMethodProcessor {
+
+ private final LoadingCache<Executable, ImmutableList<TestParametersValues>>
+ parameterValuesByConstructorOrMethodCache =
+ CacheBuilder.newBuilder()
+ .maximumSize(1000)
+ .build(CacheLoader.from(TestParametersMethodProcessor::toParameterValuesList));
+
+ @Override
+ public ExecutableValidationResult validateConstructor(Constructor<?> constructor) {
+ if (hasRelevantAnnotation(constructor)) {
+ try {
+ // This method throws an exception if there is a validation error
+ getConstructorParameters(constructor);
+ } catch (Throwable t) {
+ return ExecutableValidationResult.validated(t);
+ }
+ return ExecutableValidationResult.valid();
+ } else {
+ return ExecutableValidationResult.notValidated();
+ }
+ }
+
+ @Override
+ public ExecutableValidationResult validateTestMethod(Method testMethod, Class<?> testClass) {
+ if (hasRelevantAnnotation(testMethod)) {
+ try {
+ // This method throws an exception if there is a validation error
+ getMethodParameters(testMethod);
+ } catch (Throwable t) {
+ return ExecutableValidationResult.validated(t);
+ }
+ return ExecutableValidationResult.valid();
+ } else {
+ return ExecutableValidationResult.notValidated();
+ }
+ }
+
+ @Override
+ public List<TestInfo> calculateTestInfos(TestInfo originalTest) {
+ boolean constructorIsParameterized =
+ hasRelevantAnnotation(
+ TestParameterInjectorUtils.getOnlyConstructor(originalTest.getTestClass()));
+ boolean methodIsParameterized = hasRelevantAnnotation(originalTest.getMethod());
+
+ if (!constructorIsParameterized && !methodIsParameterized) {
+ return ImmutableList.of(originalTest);
+ }
+
+ ImmutableList.Builder<TestInfo> testInfos = ImmutableList.builder();
+
+ ImmutableList<Optional<TestParametersValues>> constructorParametersList =
+ getConstructorParametersOrSingleAbsentElement(originalTest.getTestClass());
+ ImmutableList<Optional<TestParametersValues>> methodParametersList =
+ getMethodParametersOrSingleAbsentElement(originalTest.getMethod());
+ for (int constructorParametersIndex = 0;
+ constructorParametersIndex < constructorParametersList.size();
+ ++constructorParametersIndex) {
+ Optional<TestParametersValues> constructorParameters =
+ constructorParametersList.get(constructorParametersIndex);
+
+ for (int methodParametersIndex = 0;
+ methodParametersIndex < methodParametersList.size();
+ ++methodParametersIndex) {
+ Optional<TestParametersValues> methodParameters =
+ methodParametersList.get(methodParametersIndex);
+
+ // Making final copies of non-final integers for use in lambda
+ int constructorParametersIndexCopy = constructorParametersIndex;
+ int methodParametersIndexCopy = methodParametersIndex;
+
+ testInfos.add(
+ originalTest
+ .withExtraParameters(
+ FluentIterable.of(
+ constructorParameters.transform(
+ param ->
+ TestInfoParameter.create(
+ param.name(),
+ param.parametersMap(),
+ constructorParametersIndexCopy)),
+ methodParameters.transform(
+ param ->
+ TestInfoParameter.create(
+ param.name(),
+ param.parametersMap(),
+ methodParametersIndexCopy)))
+ .filter(Optional::isPresent)
+ .transform(Optional::get)
+ .toList())
+ .withExtraAnnotation(
+ TestIndexHolderFactory.create(
+ constructorParametersIndex, methodParametersIndex)));
+ }
+ }
+ return testInfos.build();
+ }
+
+ private ImmutableList<Optional<TestParametersValues>>
+ getConstructorParametersOrSingleAbsentElement(Class<?> testClass) {
+ Constructor<?> constructor = TestParameterInjectorUtils.getOnlyConstructor(testClass);
+ return hasRelevantAnnotation(constructor)
+ ? FluentIterable.from(getConstructorParameters(constructor))
+ .transform(Optional::of)
+ .toList()
+ : ImmutableList.of(Optional.absent());
+ }
+
+ private ImmutableList<Optional<TestParametersValues>> getMethodParametersOrSingleAbsentElement(
+ Method method) {
+ return hasRelevantAnnotation(method)
+ ? FluentIterable.from(getMethodParameters(method)).transform(Optional::of).toList()
+ : ImmutableList.of(Optional.absent());
+ }
+
+ @Override
+ public Optional<List<Object>> maybeGetConstructorParameters(
+ Constructor<?> constructor, TestInfo testInfo) {
+ if (hasRelevantAnnotation(constructor)) {
+ ImmutableList<TestParametersValues> parameterValuesList =
+ getConstructorParameters(constructor);
+ TestParametersValues parametersValues =
+ parameterValuesList.get(
+ testInfo.getAnnotation(TestIndexHolder.class).constructorParametersIndex());
+
+ return Optional.of(toParameterList(parametersValues, constructor.getParameters()));
+ } else {
+ return Optional.absent();
+ }
+ }
+
+ @Override
+ public Optional<List<Object>> maybeGetTestMethodParameters(TestInfo testInfo) {
+ Method testMethod = testInfo.getMethod();
+ if (hasRelevantAnnotation(testMethod)) {
+ ImmutableList<TestParametersValues> parameterValuesList = getMethodParameters(testMethod);
+ TestParametersValues parametersValues =
+ parameterValuesList.get(
+ testInfo.getAnnotation(TestIndexHolder.class).methodParametersIndex());
+
+ return Optional.of(toParameterList(parametersValues, testMethod.getParameters()));
+ } else {
+ return Optional.absent();
+ }
+ }
+
+ @Override
+ public void postProcessTestInstance(Object testInstance, TestInfo testInfo) {}
+
+ private ImmutableList<TestParametersValues> getConstructorParameters(Constructor<?> constructor) {
+ try {
+ return parameterValuesByConstructorOrMethodCache.getUnchecked(constructor);
+ } catch (UncheckedExecutionException e) {
+ // Rethrow IllegalStateException because they can be caused by user mistakes and the user
+ // doesn't need to know that the caching layer is in between.
+ Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class);
+ throw e;
+ }
+ }
+
+ private ImmutableList<TestParametersValues> getMethodParameters(Method method) {
+ try {
+ return parameterValuesByConstructorOrMethodCache.getUnchecked(method);
+ } catch (UncheckedExecutionException e) {
+ // Rethrow IllegalStateException because they can be caused by user mistakes and the user
+ // doesn't need to know that the caching layer is in between.
+ Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class);
+ throw e;
+ }
+ }
+
+ private static ImmutableList<TestParametersValues> toParameterValuesList(Executable executable) {
+ checkParameterNamesArePresent(executable);
+ ImmutableList<Parameter> parametersList = ImmutableList.copyOf(executable.getParameters());
+
+ if (executable.isAnnotationPresent(TestParameters.class)) {
+ checkState(
+ !executable.isAnnotationPresent(RepeatedTestParameters.class),
+ "Unexpected situation: Both @TestParameters and @RepeatedTestParameters annotating the"
+ + " same method");
+ TestParameters annotation = executable.getAnnotation(TestParameters.class);
+ boolean valueIsSet = annotation.value().length > 0;
+ boolean valuesProviderIsSet =
+ !annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class);
+
+ checkState(
+ !(valueIsSet && valuesProviderIsSet),
+ "It is not allowed to specify both value and valuesProvider in @TestParameters(value=%s,"
+ + " valuesProvider=%s) on %s()",
+ Arrays.toString(annotation.value()),
+ annotation.valuesProvider().getSimpleName(),
+ executable.getName());
+ checkState(
+ valueIsSet || valuesProviderIsSet,
+ "Either a value or a valuesProvider must be set in @TestParameters on %s()",
+ executable.getName());
+ if (!annotation.customName().isEmpty()) {
+ checkState(
+ annotation.value().length == 1,
+ "Setting @TestParameters.customName is only allowed if there is exactly one YAML string"
+ + " in @TestParameters.value (on %s())",
+ executable.getName());
+ }
+
+ if (valueIsSet) {
+ return FluentIterable.from(annotation.value())
+ .transform(
+ yamlMap -> toParameterValues(yamlMap, parametersList, annotation.customName()))
+ .toList();
+ } else {
+ return toParameterValuesList(annotation.valuesProvider(), parametersList);
+ }
+ } else { // Not annotated with @TestParameters
+ verify(
+ executable.isAnnotationPresent(RepeatedTestParameters.class),
+ "This method should only be called for executables with at least one relevant"
+ + " annotation");
+
+ return FluentIterable.from(executable.getAnnotation(RepeatedTestParameters.class).value())
+ .transform(
+ annotation ->
+ toParameterValues(
+ validateAndGetSingleValueFromRepeatedAnnotation(annotation, executable),
+ parametersList,
+ annotation.customName()))
+ .toList();
+ }
+ }
+
+ private static ImmutableList<TestParametersValues> toParameterValuesList(
+ Class<? extends TestParametersValuesProvider> valuesProvider, List<Parameter> parameters) {
+ try {
+ Constructor<? extends TestParametersValuesProvider> constructor =
+ valuesProvider.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ List<TestParametersValues> testParametersValues = constructor.newInstance().provideValues();
+ for (TestParametersValues testParametersValue : testParametersValues) {
+ validateThatValuesMatchParameters(testParametersValue, parameters);
+ }
+ return ImmutableList.copyOf(testParametersValues);
+ } catch (NoSuchMethodException e) {
+ if (!Modifier.isStatic(valuesProvider.getModifiers()) && valuesProvider.isMemberClass()) {
+ throw new IllegalStateException(
+ String.format(
+ "Could not find a no-arg constructor for %s, probably because it is a not-static"
+ + " inner class. You can fix this by making %s static.",
+ valuesProvider.getSimpleName(), valuesProvider.getSimpleName()),
+ e);
+ } else {
+ throw new IllegalStateException(
+ String.format(
+ "Could not find a no-arg constructor for %s.", valuesProvider.getSimpleName()),
+ e);
+ }
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private static void checkParameterNamesArePresent(Executable executable) {
+ checkState(
+ FluentIterable.from(executable.getParameters()).allMatch(Parameter::isNamePresent),
+ ""
+ + "No parameter name could be found for %s, which likely means that parameter names"
+ + " aren't available at runtime. Please ensure that the this test was built with the"
+ + " -parameters compiler option.\n"
+ + "\n"
+ + "In Maven, you do this by adding <parameters>true</parameters> to the"
+ + " maven-compiler-plugin's configuration. For example:\n"
+ + "\n"
+ + "<build>\n"
+ + " <plugins>\n"
+ + " <plugin>\n"
+ + " <groupId>org.apache.maven.plugins</groupId>\n"
+ + " <artifactId>maven-compiler-plugin</artifactId>\n"
+ + " <version>3.8.1</version>\n"
+ + " <configuration>\n"
+ + " <compilerArgs>\n"
+ + " <arg>-parameters</arg>\n"
+ + " </compilerArgs>\n"
+ + " </configuration>\n"
+ + " </plugin>\n"
+ + " </plugins>\n"
+ + "</build>\n"
+ + "\n"
+ + "Don't forget to run `mvn clean` after making this change.",
+ executable.getName());
+ }
+
+ private static String validateAndGetSingleValueFromRepeatedAnnotation(
+ TestParameters annotation, Executable executable) {
+ checkState(
+ annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class),
+ "Setting a valuesProvider is not supported for methods/constructors with"
+ + " multiple @TestParameters annotations on %s()",
+ executable.getName());
+ checkState(
+ annotation.value().length > 0,
+ "Either a value or a valuesProvider must be set in @TestParameters on %s()",
+ executable.getName());
+ checkState(
+ annotation.value().length == 1,
+ "When specifying more than one @TestParameter for a method/constructor, each annotation"
+ + " must have exactly one value. Instead, got %s values on %s(): %s",
+ annotation.value().length,
+ executable.getName(),
+ Arrays.toString(annotation.value()));
+
+ return annotation.value()[0];
+ }
+
+ private static void validateThatValuesMatchParameters(
+ TestParametersValues testParametersValues, List<Parameter> parameters) {
+ ImmutableMap<String, Parameter> parametersByName =
+ Maps.uniqueIndex(parameters, Parameter::getName);
+
+ checkState(
+ testParametersValues.parametersMap().keySet().equals(parametersByName.keySet()),
+ "Cannot map the given TestParametersValues to parameters %s (Given TestParametersValues"
+ + " are %s)",
+ parametersByName.keySet(),
+ testParametersValues);
+
+ testParametersValues
+ .parametersMap()
+ .forEach(
+ (paramName, paramValue) -> {
+ Class<?> expectedClass = Primitives.wrap(parametersByName.get(paramName).getType());
+ if (paramValue != null) {
+ checkState(
+ expectedClass.isInstance(paramValue),
+ "Cannot map value '%s' (class = %s) to parameter %s (class = %s) (for"
+ + " TestParametersValues %s)",
+ paramValue,
+ paramValue.getClass(),
+ paramName,
+ expectedClass,
+ testParametersValues);
+ }
+ });
+ }
+
+ private static TestParametersValues toParameterValues(
+ String yamlString, List<Parameter> parameters, String maybeCustomName) {
+ Object yamlMapObject = ParameterValueParsing.parseYamlStringToObject(yamlString);
+ checkState(
+ yamlMapObject instanceof Map,
+ "Cannot map YAML string '%s' to parameters because it is not a mapping",
+ yamlString);
+ Map<?, ?> yamlMap = (Map<?, ?>) yamlMapObject;
+
+ ImmutableMap<String, Parameter> parametersByName =
+ Maps.uniqueIndex(parameters, Parameter::getName);
+ checkState(
+ yamlMap.keySet().equals(parametersByName.keySet()),
+ "Cannot map YAML string '%s' to parameters %s",
+ yamlString,
+ parametersByName.keySet());
+
+ @SuppressWarnings("unchecked")
+ Map<String, Object> checkedYamlMap = (Map<String, Object>) yamlMap;
+
+ return TestParametersValues.builder()
+ .name(maybeCustomName.isEmpty() ? yamlString : maybeCustomName)
+ .addParameters(
+ Maps.transformEntries(
+ checkedYamlMap,
+ (parameterName, parsedYaml) ->
+ ParameterValueParsing.parseYamlObjectToJavaType(
+ parsedYaml,
+ TypeToken.of(parametersByName.get(parameterName).getParameterizedType()))))
+ .build();
+ }
+
+ // Note: We're not using the Executable interface here because it isn't supported by Java 7 and
+ // this code is called even if only @TestParameter is used. In other places, Executable is usable
+ // because @TestParameters only works for Java 8 anyway.
+ private static boolean hasRelevantAnnotation(Constructor<?> executable) {
+ return executable.isAnnotationPresent(TestParameters.class)
+ || executable.isAnnotationPresent(RepeatedTestParameters.class);
+ }
+
+ private static boolean hasRelevantAnnotation(Method executable) {
+ return executable.isAnnotationPresent(TestParameters.class)
+ || executable.isAnnotationPresent(RepeatedTestParameters.class);
+ }
+
+ private static List<Object> toParameterList(
+ TestParametersValues parametersValues, Parameter[] parameters) {
+ return Arrays.asList(
+ FluentIterable.from(Arrays.asList(parameters))
+ .transform(Parameter::getName)
+ .transform(name -> parametersValues.parametersMap().get(name))
+ .toArray(Object.class));
+ }
+
+ /**
+ * This mechanism is a workaround to be able to store the test index in the annotation list of the
+ * {@link TestInfo}, since we cannot carry other information through the test runner.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface TestIndexHolder {
+ int constructorParametersIndex();
+
+ int methodParametersIndex();
+ }
+
+ /** Factory for {@link TestIndexHolder}. */
+ static class TestIndexHolderFactory {
+ @AutoAnnotation
+ static TestIndexHolder create(int constructorParametersIndex, int methodParametersIndex) {
+ return new AutoAnnotation_TestParametersMethodProcessor_TestIndexHolderFactory_create(
+ constructorParametersIndex, methodParametersIndex);
+ }
+
+ private TestIndexHolderFactory() {}
+ }
+}
diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java
new file mode 100644
index 0000000..a9336b7
--- /dev/null
+++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Optional;
+import com.google.common.primitives.UnsignedLong;
+import com.google.protobuf.ByteString;
+import java.math.BigInteger;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(TestParameterInjector.class)
+public class ParameterValueParsingTest {
+
+ @Test
+ public void parseEnum_success() throws Exception {
+ Enum<?> result = ParameterValueParsing.parseEnum("BBB", TestEnum.class);
+
+ assertThat(result).isEqualTo(TestEnum.BBB);
+ }
+
+ @Test
+ @TestParameters({
+ "{yamlString: '{a: b, c: 15}', valid: true}",
+ "{yamlString: '{a: b c: 15', valid: false}",
+ "{yamlString: 'a: b c: 15', valid: false}",
+ })
+ public void isValidYamlString_success(String yamlString, boolean valid) throws Exception {
+ boolean result = ParameterValueParsing.isValidYamlString(yamlString);
+
+ assertThat(result).isEqualTo(valid);
+ }
+
+ enum ParseYamlValueToJavaTypeCases {
+ STRING_TO_STRING(
+ /* yamlString= */ "abc", /* javaClass= */ String.class, /* expectedResult= */ "abc"),
+ BOOLEAN_TO_STRING(
+ /* yamlString= */ "true", /* javaClass= */ String.class, /* expectedResult= */ "true"),
+ INT_TO_STRING(
+ /* yamlString= */ "123", /* javaClass= */ String.class, /* expectedResult= */ "123"),
+ LONG_TO_STRING(
+ /* yamlString= */ "442147483648",
+ /* javaClass= */ String.class,
+ /* expectedResult= */ "442147483648"),
+ BIG_INTEGER_TO_BIGINTEGER(
+ /* yamlString= */ "1000000000000000000000000000",
+ /* javaClass= */ BigInteger.class,
+ /* expectedResult= */ new BigInteger("1000000000000000000000000000")),
+ BIG_INTEGER_TO_UNSIGNED_LONG(
+ /* yamlString= */ "18446744073709551615", // This is UnsignedLong.MAX_VALUE.
+ /* javaClass= */ UnsignedLong.class,
+ /* expectedResult= */ UnsignedLong.MAX_VALUE),
+ LONG_TO_UNSIGNED_LONG(
+ /* yamlString= */ "10000000000000",
+ /* javaClass= */ UnsignedLong.class,
+ /* expectedResult= */ UnsignedLong.fromLongBits(10000000000000L)),
+ LONG_TO_BIG_INTEGER(
+ /* yamlString= */ "10000000000000",
+ /* javaClass= */ BigInteger.class,
+ /* expectedResult= */ BigInteger.valueOf(10000000000000L)),
+ INTEGER_TO_BIG_INTEGER(
+ /* yamlString= */ "1000000",
+ /* javaClass= */ BigInteger.class,
+ /* expectedResult= */ BigInteger.valueOf(1000000)),
+ INTEGER_TO_UNSIGNED_LONG(
+ /* yamlString= */ "1000000",
+ /* javaClass= */ UnsignedLong.class,
+ /* expectedResult= */ UnsignedLong.fromLongBits(1000000)),
+ DOUBLE_TO_STRING(
+ /* yamlString= */ "1.23", /* javaClass= */ String.class, /* expectedResult= */ "1.23"),
+
+ BOOLEAN_TO_BOOLEAN(
+ /* yamlString= */ "true", /* javaClass= */ Boolean.class, /* expectedResult= */ true),
+
+ INT_TO_INT(/* yamlString= */ "123", /* javaClass= */ int.class, /* expectedResult= */ 123),
+
+ LONG_TO_LONG(
+ /* yamlString= */ "442147483648",
+ /* javaClass= */ long.class,
+ /* expectedResult= */ 442147483648L),
+ INT_TO_LONG(/* yamlString= */ "123", /* javaClass= */ Long.class, /* expectedResult= */ 123L),
+
+ DOUBLE_TO_DOUBLE(
+ /* yamlString= */ "1.23", /* javaClass= */ Double.class, /* expectedResult= */ 1.23),
+ INT_TO_DOUBLE(
+ /* yamlString= */ "123", /* javaClass= */ Double.class, /* expectedResult= */ 123.0),
+ LONG_TO_DOUBLE(
+ /* yamlString= */ "442147483648",
+ /* javaClass= */ Double.class,
+ /* expectedResult= */ 442147483648.0),
+ NAN_TO_DOUBLE(
+ /* yamlString= */ "NaN", /* javaClass= */ Double.class, /* expectedResult= */ Double.NaN),
+ INFINITY_TO_DOUBLE(
+ /* yamlString= */ "Infinity",
+ /* javaClass= */ Double.class,
+ /* expectedResult= */ Double.POSITIVE_INFINITY),
+ POSITIVE_INFINITY_TO_DOUBLE(
+ /* yamlString= */ "+Infinity",
+ /* javaClass= */ Double.class,
+ /* expectedResult= */ Double.POSITIVE_INFINITY),
+ NEGATIVE_INFINITY_TO_DOUBLE(
+ /* yamlString= */ "-Infinity",
+ /* javaClass= */ Double.class,
+ /* expectedResult= */ Double.NEGATIVE_INFINITY),
+
+ DOUBLE_TO_FLOAT(
+ /* yamlString= */ "1.23", /* javaClass= */ Float.class, /* expectedResult= */ 1.23F),
+ INT_TO_FLOAT(/* yamlString= */ "123", /* javaClass= */ Float.class, /* expectedResult= */ 123F),
+
+ STRING_TO_ENUM(
+ /* yamlString= */ "AAA",
+ /* javaClass= */ TestEnum.class,
+ /* expectedResult= */ TestEnum.AAA),
+
+ STRING_TO_BYTES(
+ /* yamlString= */ "data",
+ /* javaClass= */ byte[].class,
+ /* expectedResult= */ "data".getBytes()),
+
+ BYTES_TO_BYTES(
+ /* yamlString= */ "!!binary 'ZGF0YQ=='",
+ /* javaClass= */ byte[].class,
+ /* expectedResult= */ "data".getBytes()),
+
+ STRING_TO_BYTESTRING(
+ /* yamlString= */ "'data'",
+ /* javaClass= */ ByteString.class,
+ /* expectedResult= */ ByteString.copyFromUtf8("data")),
+
+ BINARY_TO_BYTESTRING(
+ /* yamlString= */ "!!binary 'ZGF0YQ=='",
+ /* javaClass= */ ByteString.class,
+ /* expectedResult= */ ByteString.copyFromUtf8("data"));
+
+ final String yamlString;
+ final Class<?> javaClass;
+ final Object expectedResult;
+
+ ParseYamlValueToJavaTypeCases(String yamlString, Class<?> javaClass, Object expectedResult) {
+ this.yamlString = yamlString;
+ this.javaClass = javaClass;
+ this.expectedResult = expectedResult;
+ }
+ }
+
+ @Test
+ public void parseYamlStringToJavaType_success(
+ @TestParameter ParseYamlValueToJavaTypeCases parseYamlValueToJavaTypeCases) throws Exception {
+ Object result =
+ ParameterValueParsing.parseYamlStringToJavaType(
+ parseYamlValueToJavaTypeCases.yamlString, parseYamlValueToJavaTypeCases.javaClass);
+
+ assertThat(result).isEqualTo(parseYamlValueToJavaTypeCases.expectedResult);
+ }
+
+ enum FormatTestNameStringTestCases {
+ NULL_REFERENCE(/* value= */ null, /* expectedResult= */ "param=null"),
+ BOOLEAN(/* value= */ false, /* expectedResult= */ "param=false"),
+ INTEGER(/* value= */ 123, /* expectedResult= */ "param=123"),
+ REGULAR_STRING(/* value= */ "abc", /* expectedResult= */ "abc"),
+ EMPTY_STRING(/* value= */ "", /* expectedResult= */ "param="),
+ NULL_STRING(/* value= */ "null", /* expectedResult= */ "param=null"),
+ INTEGER_STRING(/* value= */ "123", /* expectedResult= */ "param=123"),
+ ARRAY(/* value= */ new byte[] {2, 3, 4}, /* expectedResult= */ "[2, 3, 4]"),
+ CHAR_MATCHER(/* value= */ CharMatcher.any(), /* expectedResult= */ "CharMatcher.any()");
+
+ @Nullable final Object value;
+ final String expectedResult;
+
+ FormatTestNameStringTestCases(@Nullable Object value, String expectedResult) {
+ this.value = value;
+ this.expectedResult = expectedResult;
+ }
+ }
+
+ @Test
+ public void formatTestNameString_success(@TestParameter FormatTestNameStringTestCases testCase)
+ throws Exception {
+ String result =
+ ParameterValueParsing.formatTestNameString(
+ /* parameterName= */ Optional.of("param"), /* value= */ testCase.value);
+
+ assertThat(result).isEqualTo(testCase.expectedResult);
+ }
+
+ private enum TestEnum {
+ AAA,
+ BBB;
+ }
+}
diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java
new file mode 100644
index 0000000..f7afd79
--- /dev/null
+++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.MethodRule;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.Statement;
+
+@RunWith(JUnit4.class)
+public class PluggableTestRunnerTest {
+
+ private static ArrayList<String> ruleInvocations;
+ private static int testMethodInvocationCount;
+ private static List<String> testOrder;
+
+ @Before
+ public void setUp() {
+ ruleInvocations = new ArrayList<>();
+ testMethodInvocationCount = 0;
+ testOrder = new ArrayList<>();
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ private @interface CustomTest {}
+
+ static class TestAndMethodRule implements MethodRule, TestRule {
+ private final String name;
+
+ TestAndMethodRule() {
+ this("DEFAULT_NAME");
+ }
+
+ TestAndMethodRule(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ ruleInvocations.add(name);
+ base.evaluate();
+ }
+ };
+ }
+
+ @Override
+ public Statement apply(Statement base, FrameworkMethod method, Object target) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ ruleInvocations.add(name);
+ base.evaluate();
+ }
+ };
+ }
+ }
+
+ @RunWith(PluggableTestRunner.class)
+ public static class TestAndMethodRuleTestClass {
+
+ @Rule public TestAndMethodRule rule = new TestAndMethodRule();
+
+ @Test
+ public void test() {
+ // no-op
+ }
+ }
+
+ @Test
+ public void ruleThatIsBothTestRuleAndMethodRuleIsInvokedOnceOnly() throws Exception {
+ SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures(
+ new PluggableTestRunner(TestAndMethodRuleTestClass.class) {
+ @Override
+ protected TestMethodProcessorList createTestMethodProcessorList() {
+ return TestMethodProcessorList.empty();
+ }
+ });
+
+ assertThat(ruleInvocations).hasSize(1);
+ }
+
+ @RunWith(PluggableTestRunner.class)
+ public static class RuleOrderingTestClassWithExplicitOrder {
+
+ @Rule(order = 3)
+ public TestAndMethodRule ruleA = new TestAndMethodRule("A");
+
+ @Rule(order = 1)
+ public TestAndMethodRule ruleB = new TestAndMethodRule("B");
+
+ @Rule(order = 2)
+ public TestAndMethodRule ruleC = new TestAndMethodRule("C");
+
+ @Test
+ public void test() {
+ // no-op
+ }
+ }
+
+ @Test
+ public void rulesAreSortedCorrectly_withExplicitOrder() throws Exception {
+ SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures(
+ new PluggableTestRunner(RuleOrderingTestClassWithExplicitOrder.class) {
+ @Override
+ protected TestMethodProcessorList createTestMethodProcessorList() {
+ return TestMethodProcessorList.empty();
+ }
+ });
+
+ assertThat(ruleInvocations).containsExactly("B", "C", "A").inOrder();
+ }
+
+ @RunWith(PluggableTestRunner.class)
+ public static class CustomTestAnnotationTestClass {
+ @SuppressWarnings("JUnit4TestNotRun")
+ @CustomTest
+ public void customTestAnnotatedTest() {
+ testMethodInvocationCount++;
+ }
+
+ @Test
+ public void testAnnotatedTest() {
+ testMethodInvocationCount++;
+ }
+ }
+
+ @Test
+ public void testMarkedWithCustomClassIsInvoked() throws Exception {
+ testMethodInvocationCount = 0;
+ SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures(
+ new PluggableTestRunner(CustomTestAnnotationTestClass.class) {
+ @Override
+ protected TestMethodProcessorList createTestMethodProcessorList() {
+ return TestMethodProcessorList.empty();
+ }
+
+ @Override
+ protected ImmutableList<Class<? extends Annotation>> getSupportedTestAnnotations() {
+ return ImmutableList.of(Test.class, CustomTest.class);
+ }
+ });
+
+ assertThat(testMethodInvocationCount).isEqualTo(2);
+ }
+
+ @RunWith(PluggableTestRunner.class)
+ public static class SortedPluggableTestRunnerTestClass {
+ @Test
+ public void a() {
+ testOrder.add("a");
+ }
+
+ @Test
+ public void b() {
+ testOrder.add("b");
+ }
+
+ @Test
+ public void c() {
+ testOrder.add("c");
+ }
+ }
+
+ @Test
+ public void testsAreSortedCorrectly() throws Exception {
+ testOrder.clear();
+ SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures(
+ new PluggableTestRunner(SortedPluggableTestRunnerTestClass.class) {
+ @Override
+ protected TestMethodProcessorList createTestMethodProcessorList() {
+ return TestMethodProcessorList.empty();
+ }
+
+ @Override
+ protected ImmutableList<FrameworkMethod> sortTestMethods(
+ ImmutableList<FrameworkMethod> methods) {
+ return FluentIterable.from(methods)
+ .toSortedList((o1, o2) -> o2.getName().compareTo(o1.getName())); // reversed
+ }
+ });
+ assertThat(testOrder).containsExactly("c", "b", "a");
+ }
+}
diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java
new file mode 100644
index 0000000..5dfe610
--- /dev/null
+++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Throwables;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.rules.TestName;
+import org.junit.runner.Runner;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
+import org.junit.runner.notification.RunNotifier;
+
+/** Shared utility code for TestParameterInjector (JUnit4) tests. */
+class SharedTestUtilitiesJUnit4 {
+
+ /**
+ * Runs the given test runner.
+ *
+ * @throws AssertionError if the test instance reports any failures
+ */
+ static void runTestsAndAssertNoFailures(Runner testRunner) {
+ ImmutableList<Failure> failures = runTestsAndGetFailures(testRunner);
+
+ if (failures.size() == 1) {
+ throw new AssertionError(getOnlyElement(failures).getException());
+ } else if (failures.size() > 1) {
+ throw new AssertionError(
+ String.format(
+ "Test failed unexpectedly:\n\n%s",
+ FluentIterable.from(failures)
+ .transform(
+ f ->
+ String.format(
+ "<<%s>> %s",
+ f.getDescription(),
+ Throwables.getStackTraceAsString(f.getException())))
+ .join(Joiner.on("\n------------------------------------\n"))));
+ }
+ }
+
+ /**
+ * Runs the given test runner.
+ *
+ * @return all failures reported by the test instance.
+ */
+ static ImmutableList<Failure> runTestsAndGetFailures(Runner testRunner) {
+ final ImmutableList.Builder<Failure> failures = ImmutableList.builder();
+ RunNotifier notifier = new RunNotifier();
+ notifier.addFirstListener(
+ new RunListener() {
+ @Override
+ public void testFailure(Failure failure) throws Exception {
+ failures.add(failure);
+ }
+ });
+
+ testRunner.run(notifier);
+
+ return failures.build();
+ }
+
+ private static String toCopyPastableJavaString(Map<String, String> map) {
+ StringBuilder resultBuilder = new StringBuilder();
+ resultBuilder.append("\n----------------------\n");
+ resultBuilder.append("ImmutableMap.<String, String>builder()\n");
+ for (Entry<String, String> entry : map.entrySet()) {
+ String key = entry.getKey();
+ String value = entry.getValue();
+ resultBuilder.append(String.format(" .put(\"%s\", \"%s\")\n", key, value));
+ }
+ resultBuilder.append(" .build()\n");
+ resultBuilder.append("----------------------\n");
+ return resultBuilder.toString();
+ }
+
+ /**
+ * Base class for a test class that acts as a test case testing a single property of a
+ * TestParameterInjector-run test.
+ */
+ abstract static class SuccessfulTestCaseBase {
+
+ @Rule public TestName testName = new TestName();
+
+ private static Map<String, String> testNameToStringifiedParameters;
+ private static ImmutableMap<String, String> expectedTestNameToStringifiedParameters;
+
+ @BeforeClass
+ public static void checkStaticFieldAreNull() {
+ checkState(testNameToStringifiedParameters == null);
+ checkState(expectedTestNameToStringifiedParameters == null);
+ }
+
+ final void storeTestParametersForThisTest(Object... params) {
+ if (testNameToStringifiedParameters == null) {
+ testNameToStringifiedParameters = new LinkedHashMap<>();
+ // Copying this into a static field because @AfterAll methods have to be static
+ expectedTestNameToStringifiedParameters = expectedTestNameToStringifiedParameters();
+ }
+ checkState(
+ !testNameToStringifiedParameters.containsKey(testName.getMethodName()),
+ "Parameters for the test with name '%s' are already stored. This might mean that there"
+ + " are duplicate test names",
+ testName.getMethodName());
+ testNameToStringifiedParameters.put(
+ testName.getMethodName(),
+ FluentIterable.from(params).transform(String::valueOf).join(Joiner.on(":")));
+ }
+
+ abstract ImmutableMap<String, String> expectedTestNameToStringifiedParameters();
+
+ @AfterClass
+ public static void completedAllTests() {
+ checkNotNull(
+ testNameToStringifiedParameters, "storeTestParametersForThisTest() was never called");
+ try {
+ assertWithMessage(toCopyPastableJavaString(testNameToStringifiedParameters))
+ .that(testNameToStringifiedParameters)
+ .isEqualTo(expectedTestNameToStringifiedParameters);
+ } finally {
+ testNameToStringifiedParameters = null;
+ expectedTestNameToStringifiedParameters = null;
+ }
+ }
+ }
+
+ private SharedTestUtilitiesJUnit4() {}
+}
diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java
new file mode 100644
index 0000000..46af6c4
--- /dev/null
+++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.truth.IterableSubject;
+import com.google.testing.junit.testparameterinjector.TestInfo.TestInfoParameter;
+import java.util.List;
+import java.util.stream.IntStream;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class TestInfoTest {
+
+ @Test
+ public void shortenNamesIfNecessary_emptyTestInfos() throws Exception {
+ ImmutableList<TestInfo> result = TestInfo.shortenNamesIfNecessary(ImmutableList.of());
+
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void shortenNamesIfNecessary_noParameters() throws Exception {
+ ImmutableList<TestInfo> givenTestInfos = ImmutableList.of(fakeTestInfo());
+
+ ImmutableList<TestInfo> result = TestInfo.shortenNamesIfNecessary(givenTestInfos);
+
+ assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder();
+ }
+
+ @Test
+ public void shortenNamesIfNecessary_veryLongTestMethodName_noParameters() throws Exception {
+ ImmutableList<TestInfo> givenTestInfos =
+ ImmutableList.of(
+ TestInfo.createWithoutParameters(
+ getClass()
+ .getMethod(
+ "unusedMethodThatHasAVeryLongNameForTest000000000000000000000000000000000"
+ + "000000000000000000000000000000000000000000000000000000000000000000"
+ + "000000000000000000000000000000000000000000000000000000000000000000"
+ + "000000000000000000000000"),
+ getClass(),
+ /* annotations= */ ImmutableList.of()));
+
+ ImmutableList<TestInfo> result = TestInfo.shortenNamesIfNecessary(givenTestInfos);
+
+ assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder();
+ }
+
+ @Test
+ public void shortenNamesIfNecessary_noShorteningNeeded() throws Exception {
+ ImmutableList<TestInfo> givenTestInfos =
+ ImmutableList.of(
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* valueInTestName= */ "short", /* value= */ 1, /* indexInValueSource= */ 1),
+ TestInfoParameter.create(
+ /* valueInTestName= */ "shorter",
+ /* value= */ null,
+ /* indexInValueSource= */ 3)),
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* valueInTestName= */ "short", /* value= */ 1, /* indexInValueSource= */ 1),
+ TestInfoParameter.create(
+ /* valueInTestName= */ "shortest",
+ /* value= */ 20,
+ /* indexInValueSource= */ 0)));
+
+ ImmutableList<TestInfo> result = TestInfo.shortenNamesIfNecessary(givenTestInfos);
+
+ assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder();
+ }
+
+ @Test
+ public void shortenNamesIfNecessary_singleParameterTooLong_twoParameters() throws Exception {
+ ImmutableList<TestInfo> result =
+ TestInfo.shortenNamesIfNecessary(
+ ImmutableList.of(
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* valueInTestName= */ "short",
+ /* value= */ 1,
+ /* indexInValueSource= */ 0),
+ TestInfoParameter.create(
+ /* valueInTestName= */ "shorter",
+ /* value= */ null,
+ /* indexInValueSource= */ 0)),
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* valueInTestName= */ "short",
+ /* value= */ 1,
+ /* indexInValueSource= */ 0),
+ TestInfoParameter.create(
+ /* valueInTestName= */ "very long parameter name for test"
+ + " 00000000000000000000000000000000000000000000000000000000"
+ + "000000000000000000000000000000000000000000000000000000000"
+ + "0000000000000000000000000000000000000000000000",
+ /* value= */ 20,
+ /* indexInValueSource= */ 1))));
+
+ assertThatTestNamesOf(result)
+ .containsExactly(
+ "toLowerCase[short,1.shorter]",
+ "toLowerCase[short,2.very long parameter name for test "
+ + "0000000000000000000000000000000000000000000000000000...]")
+ .inOrder();
+ }
+
+ @Test
+ public void shortenNamesIfNecessary_singleParameterTooLong_onlyParameter() throws Exception {
+ ImmutableList<TestInfo> result =
+ TestInfo.shortenNamesIfNecessary(
+ ImmutableList.of(
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* valueInTestName= */ "shorter",
+ /* value= */ null,
+ /* indexInValueSource= */ 0)),
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* valueInTestName= */ "very long parameter name for test"
+ + " 00000000000000000000000000000000000000000000000000000000"
+ + "000000000000000000000000000000000000000000000000000000000"
+ + "0000000000000000000000000000000000000000000000",
+ /* value= */ 20,
+ /* indexInValueSource= */ 1))));
+
+ assertThatTestNamesOf(result)
+ .containsExactly(
+ "toLowerCase[1.shorter]",
+ "toLowerCase[2.very long parameter name for test"
+ + " 000000000000000000000000000000000000000000000000000000000000000000000000000000"
+ + "00000000000000000000000000000000000000000000000000000000000000000000...]")
+ .inOrder();
+ }
+
+ @Test
+ public void shortenNamesIfNecessary_tooManyParameters() throws Exception {
+ TestInfo testInfoWithManyParams =
+ fakeTestInfo(
+ IntStream.range(0, 50)
+ .mapToObj(
+ i ->
+ TestInfoParameter.create(
+ /* valueInTestName= */ "short",
+ /* value= */ i,
+ /* indexInValueSource= */ i))
+ .toArray(TestInfoParameter[]::new));
+
+ ImmutableList<TestInfo> result =
+ TestInfo.shortenNamesIfNecessary(ImmutableList.of(testInfoWithManyParams));
+
+ assertThatTestNamesOf(result)
+ .containsExactly(
+ "toLowerCase[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,"
+ + "27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50]");
+ }
+
+ @Test
+ public void deduplicateTestNames_noDuplicates() throws Exception {
+ ImmutableList<TestInfo> givenTestInfos =
+ ImmutableList.of(
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1),
+ TestInfoParameter.create(
+ /* valueInTestName= */ "bbb", /* value= */ null, /* indexInValueSource= */ 3)),
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1),
+ TestInfoParameter.create(
+ /* valueInTestName= */ "ccc", /* value= */ 1, /* indexInValueSource= */ 0)));
+
+ ImmutableList<TestInfo> result = TestInfo.deduplicateTestNames(givenTestInfos);
+
+ assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder();
+ assertThatTestNamesOf(result)
+ .containsExactly("toLowerCase[aaa,bbb]", "toLowerCase[aaa,ccc]")
+ .inOrder();
+ }
+
+ @Test
+ public void deduplicateTestNames_duplicateParameterNamesWithDifferentTypes() throws Exception {
+ ImmutableList<TestInfo> result =
+ TestInfo.deduplicateTestNames(
+ ImmutableList.of(
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1),
+ TestInfoParameter.create(
+ /* valueInTestName= */ "null",
+ /* value= */ null,
+ /* indexInValueSource= */ 3)),
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1),
+ TestInfoParameter.create(
+ /* valueInTestName= */ "null",
+ /* value= */ "null",
+ /* indexInValueSource= */ 0)),
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1),
+ TestInfoParameter.create(
+ /* valueInTestName= */ "bbb",
+ /* value= */ "b",
+ /* indexInValueSource= */ 0))));
+
+ assertThatTestNamesOf(result)
+ .containsExactly(
+ "toLowerCase[aaa,null (null reference)]",
+ "toLowerCase[aaa,null (String)]",
+ "toLowerCase[aaa,bbb]")
+ .inOrder();
+ }
+
+ @Test
+ public void deduplicateTestNames_duplicateParameterNamesWithSameTypes() throws Exception {
+ ImmutableList<TestInfo> result =
+ TestInfo.deduplicateTestNames(
+ ImmutableList.of(
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 0),
+ TestInfoParameter.create(
+ /* valueInTestName= */ "bbb", /* value= */ 1, /* indexInValueSource= */ 0)),
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 0),
+ TestInfoParameter.create(
+ /* valueInTestName= */ "bbb", /* value= */ 1, /* indexInValueSource= */ 1)),
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 0),
+ TestInfoParameter.create(
+ /* valueInTestName= */ "ccc",
+ /* value= */ "b",
+ /* indexInValueSource= */ 2))));
+
+ assertThatTestNamesOf(result)
+ .containsExactly(
+ "toLowerCase[1.aaa,1.bbb]", "toLowerCase[1.aaa,2.bbb]", "toLowerCase[1.aaa,3.ccc]")
+ .inOrder();
+ }
+
+ private static TestInfo fakeTestInfo(TestInfoParameter... parameters)
+ throws NoSuchMethodException {
+ return TestInfo.createWithoutParameters(
+ String.class.getMethod("toLowerCase"),
+ String.class,
+ /* annotations= */ ImmutableList.of())
+ .withExtraParameters(ImmutableList.copyOf(parameters));
+ }
+
+ private static IterableSubject assertThatTestNamesOf(List<TestInfo> result) {
+ return assertThat(result.stream().map(TestInfo::getName).collect(toList()));
+ }
+
+ public void
+ unusedMethodThatHasAVeryLongNameForTest000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000() {}
+}
diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
new file mode 100644
index 0000000..458b623
--- /dev/null
+++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
@@ -0,0 +1,866 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.Lists.newArrayList;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.testing.junit.testparameterinjector.SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Function;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.model.TestClass;
+
+/**
+ * Test class to test the PluggableTestRunner test harness works with {@link
+ * TestParameterAnnotation}s.
+ */
+@RunWith(Parameterized.class)
+public class TestParameterAnnotationMethodProcessorTest {
+
+ @Retention(RUNTIME)
+ @interface ClassTestResult {
+ Result value();
+ }
+
+ enum Result {
+ /**
+ * A successful test run is expected in both for
+ * TestParameterAnnotationMethodProcessor#forAllAnnotationPlacements and
+ * TestParameterAnnotationMethodProcessor#onlyForFieldsAndParameters.
+ */
+ SUCCESS_ALWAYS,
+ SUCCESS_FOR_ALL_PLACEMENTS_ONLY,
+ FAILURE,
+ }
+
+ public enum TestEnum {
+ UNDEFINED,
+ ONE,
+ TWO,
+ THREE,
+ FOUR,
+ FIVE
+ }
+
+ @Retention(RUNTIME)
+ @TestParameterAnnotation
+ public @interface EnumParameter {
+ TestEnum[] value() default {};
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class SingleAnnotationClass extends SuccessfulTestCaseBase {
+
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE})
+ TestEnum enumParameter;
+
+ @Test
+ public void test() {
+ storeTestParametersForThisTest(enumParameter);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test[ONE]", "ONE")
+ .put("test[TWO]", "TWO")
+ .put("test[THREE]", "THREE")
+ .build();
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class MultipleAllEnumValuesAnnotationClass extends SuccessfulTestCaseBase {
+
+ @TestParameter({"ONE", "THREE"})
+ TestEnum enumParameter1;
+
+ @TestParameter TestEnum2 enumParameter2;
+
+ @Test
+ public void test(@TestParameter TestEnum2 enumParameter3) {
+ storeTestParametersForThisTest(enumParameter1, enumParameter2, enumParameter3);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test[ONE,A,A]", "ONE:A:A")
+ .put("test[ONE,A,B]", "ONE:A:B")
+ .put("test[ONE,B,A]", "ONE:B:A")
+ .put("test[ONE,B,B]", "ONE:B:B")
+ .put("test[THREE,A,A]", "THREE:A:A")
+ .put("test[THREE,A,B]", "THREE:A:B")
+ .put("test[THREE,B,A]", "THREE:B:A")
+ .put("test[THREE,B,B]", "THREE:B:B")
+ .build();
+ }
+
+ enum TestEnum2 {
+ A,
+ B;
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_FOR_ALL_PLACEMENTS_ONLY)
+ public static class SingleParameterAnnotationClass extends SuccessfulTestCaseBase {
+
+ @Test
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE})
+ public void test(TestEnum enumParameter) {
+ storeTestParametersForThisTest(enumParameter);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test[ONE]", "ONE")
+ .put("test[TWO]", "TWO")
+ .put("test[THREE]", "THREE")
+ .build();
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class SingleAnnotatedParameterAnnotationClass extends SuccessfulTestCaseBase {
+
+ @Test
+ public void test(
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) TestEnum enumParameter) {
+ storeTestParametersForThisTest(enumParameter);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test[ONE]", "ONE")
+ .put("test[TWO]", "TWO")
+ .put("test[THREE]", "THREE")
+ .build();
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class AnnotatedSuperclassParameter extends SuccessfulTestCaseBase {
+
+ @Test
+ public void test(
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) Object enumParameter) {
+ storeTestParametersForThisTest(enumParameter);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test[ONE]", "ONE")
+ .put("test[TWO]", "TWO")
+ .put("test[THREE]", "THREE")
+ .build();
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class DuplicatedAnnotatedParameterAnnotationClass extends SuccessfulTestCaseBase {
+
+ @Test
+ public void test(
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) TestEnum enumParameter,
+ @EnumParameter({TestEnum.FOUR, TestEnum.FIVE}) TestEnum enumParameter2) {
+ storeTestParametersForThisTest(enumParameter, enumParameter2);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test[ONE,FOUR]", "ONE:FOUR")
+ .put("test[ONE,FIVE]", "ONE:FIVE")
+ .put("test[TWO,FOUR]", "TWO:FOUR")
+ .put("test[TWO,FIVE]", "TWO:FIVE")
+ .put("test[THREE,FOUR]", "THREE:FOUR")
+ .put("test[THREE,FIVE]", "THREE:FIVE")
+ .build();
+ }
+ }
+
+ @ClassTestResult(Result.FAILURE)
+ public static class SingleAnnotatedParameterAnnotationClassWithMissingValue {
+
+ @Test
+ public void test(@EnumParameter TestEnum enumParameter) {}
+ }
+
+ @ClassTestResult(Result.SUCCESS_FOR_ALL_PLACEMENTS_ONLY)
+ public static class MultipleAnnotationTestClass extends SuccessfulTestCaseBase {
+
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO})
+ TestEnum enumParameter;
+
+ @Test
+ @EnumParameter({TestEnum.THREE})
+ public void parameterized() {
+ storeTestParametersForThisTest(enumParameter);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder().put("parameterized[THREE]", "THREE").build();
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class TooLongTestNamesShortened extends SuccessfulTestCaseBase {
+
+ @Test
+ public void test1(
+ @TestParameter({
+ "ABC",
+ "This is a very long string (240 characters) that would normally cause Sponge+Tin to"
+ + " exceed the filename limit of 255 characters."
+ + " ==========================================================================="
+ + "==================================="
+ })
+ String testString) {
+ storeTestParametersForThisTest(testString);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test1[1.ABC]", "ABC")
+ .put(
+ "test1[2.This is a very long string (240 characters) that would normally cause"
+ + " Sponge+Tin to exceed the filename limit of 255 characters."
+ + " =========================================================...]",
+ "This is a very long string (240 characters) that would normally cause Sponge+Tin to"
+ + " exceed the filename limit of 255 characters."
+ + " ============================================================================"
+ + "==================================")
+ .build();
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class DuplicateTestNames extends SuccessfulTestCaseBase {
+
+ @Test
+ public void test1(@TestParameter({"ABC", "ABC"}) String testString) {
+ storeTestParametersForThisTest(testString);
+ }
+
+ private static final class Test2Provider extends TestParameterValuesProvider {
+ @Override
+ public List<Object> provideValues(TestParameterValuesProvider.Context context) {
+ return newArrayList(123, "123", "null", null);
+ }
+ }
+
+ @Test
+ public void test2(@TestParameter(valuesProvider = Test2Provider.class) Object testObject) {
+ storeTestParametersForThisTest(testObject);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test1[1.ABC]", "ABC")
+ .put("test1[2.ABC]", "ABC")
+ .put("test2[testObject=123 (Integer)]", "123")
+ .put("test2[testObject=123 (String)]", "123")
+ .put("test2[testObject=null (String)]", "null")
+ .put("test2[testObject=null (null reference)]", "null")
+ .build();
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class DuplicateFieldAnnotationTestClass extends SuccessfulTestCaseBase {
+
+ @TestParameter({"foo", "bar"})
+ String stringParameter;
+
+ @TestParameter({"baz", "qux"})
+ String stringParameter2;
+
+ @Test
+ public void test() {
+ storeTestParametersForThisTest(stringParameter, stringParameter2);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test[foo,baz]", "foo:baz")
+ .put("test[foo,qux]", "foo:qux")
+ .put("test[bar,baz]", "bar:baz")
+ .put("test[bar,qux]", "bar:qux")
+ .build();
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class DuplicateIdenticalFieldAnnotationTestClass extends SuccessfulTestCaseBase {
+
+ @TestParameter({"foo", "bar"})
+ String stringParameter;
+
+ @TestParameter({"foo", "bar"})
+ String stringParameter2;
+
+ @Test
+ public void test() {
+ storeTestParametersForThisTest(stringParameter, stringParameter2);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test[foo,foo]", "foo:foo")
+ .put("test[foo,bar]", "foo:bar")
+ .put("test[bar,foo]", "bar:foo")
+ .put("test[bar,bar]", "bar:bar")
+ .build();
+ }
+ }
+
+ @ClassTestResult(Result.FAILURE)
+ public static class ErrorDuplicateFieldAnnotationTestClass {
+
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO})
+ TestEnum parameter1;
+
+ @EnumParameter({TestEnum.THREE, TestEnum.FOUR})
+ TestEnum parameter2;
+
+ @Test
+ @EnumParameter(TestEnum.FIVE)
+ public void test() {}
+ }
+
+ @ClassTestResult(Result.FAILURE)
+ public static class ErrorDuplicateFieldAndClassAnnotationTestClass {
+
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO})
+ TestEnum parameter;
+
+ @EnumParameter(TestEnum.FIVE)
+ public ErrorDuplicateFieldAndClassAnnotationTestClass() {}
+
+ @Test
+ public void test() {}
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class SingleAnnotationTestClassWithAnnotation extends SuccessfulTestCaseBase {
+
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE})
+ TestEnum enumParameter;
+
+ @Test
+ public void test() {
+ storeTestParametersForThisTest(enumParameter);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test[ONE]", "ONE")
+ .put("test[TWO]", "TWO")
+ .put("test[THREE]", "THREE")
+ .build();
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class MultipleAnnotationTestClassWithAnnotation extends SuccessfulTestCaseBase {
+
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE})
+ TestEnum enumParameter;
+
+ @Test
+ public void parameterized(@TestParameter({"foo", "bar"}) String stringParameter) {
+ storeTestParametersForThisTest(enumParameter, stringParameter);
+ }
+
+ @Test
+ public void nonParameterized() {
+ storeTestParametersForThisTest(enumParameter);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("parameterized[ONE,foo]", "ONE:foo")
+ .put("parameterized[ONE,bar]", "ONE:bar")
+ .put("parameterized[TWO,foo]", "TWO:foo")
+ .put("parameterized[TWO,bar]", "TWO:bar")
+ .put("parameterized[THREE,foo]", "THREE:foo")
+ .put("parameterized[THREE,bar]", "THREE:bar")
+ .put("nonParameterized[ONE]", "ONE")
+ .put("nonParameterized[TWO]", "TWO")
+ .put("nonParameterized[THREE]", "THREE")
+ .build();
+ }
+ }
+
+ public abstract static class BaseClassWithSingleTest extends SuccessfulTestCaseBase {
+ @Test
+ public void testInBase(@TestParameter boolean b) {
+ storeTestParametersForThisTest(b);
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class SimpleTestInheritedFromBaseClass extends BaseClassWithSingleTest {
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("testInBase[b=false]", "false")
+ .put("testInBase[b=true]", "true")
+ .build();
+ }
+ }
+
+ public abstract static class BaseClassWithAnnotations extends SuccessfulTestCaseBase {
+
+ @TestParameter boolean boolInBase;
+
+ @Test
+ public void testInBase(@TestParameter({"ONE", "TWO"}) TestEnum enumInBase) {
+ storeTestParametersForThisTest(boolInBase, enumInBase);
+ }
+
+ @Test
+ public abstract void abstractTestInBase();
+
+ @Test
+ public void overridableTestInBase() {
+ throw new UnsupportedOperationException("Expected the base class to override this");
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class AnnotationInheritedFromBaseClass extends BaseClassWithAnnotations {
+
+ @TestParameter boolean boolInChild;
+
+ @Test
+ public void testInChild(@TestParameter({"TWO", "THREE"}) TestEnum enumInChild) {
+ storeTestParametersForThisTest(boolInBase, boolInChild, enumInChild);
+ }
+
+ @Override
+ public void abstractTestInBase() {
+ storeTestParametersForThisTest(boolInBase, boolInChild);
+ }
+
+ @Override
+ public void overridableTestInBase() {
+ storeTestParametersForThisTest(boolInBase, boolInChild);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("testInChild[boolInChild=false,boolInBase=false,TWO]", "false:false:TWO")
+ .put("testInChild[boolInChild=false,boolInBase=false,THREE]", "false:false:THREE")
+ .put("testInChild[boolInChild=false,boolInBase=true,TWO]", "true:false:TWO")
+ .put("testInChild[boolInChild=false,boolInBase=true,THREE]", "true:false:THREE")
+ .put("testInChild[boolInChild=true,boolInBase=false,TWO]", "false:true:TWO")
+ .put("testInChild[boolInChild=true,boolInBase=false,THREE]", "false:true:THREE")
+ .put("testInChild[boolInChild=true,boolInBase=true,TWO]", "true:true:TWO")
+ .put("testInChild[boolInChild=true,boolInBase=true,THREE]", "true:true:THREE")
+ .put("abstractTestInBase[boolInChild=false,boolInBase=false]", "false:false")
+ .put("abstractTestInBase[boolInChild=false,boolInBase=true]", "true:false")
+ .put("abstractTestInBase[boolInChild=true,boolInBase=false]", "false:true")
+ .put("abstractTestInBase[boolInChild=true,boolInBase=true]", "true:true")
+ .put("overridableTestInBase[boolInChild=false,boolInBase=false]", "false:false")
+ .put("overridableTestInBase[boolInChild=false,boolInBase=true]", "true:false")
+ .put("overridableTestInBase[boolInChild=true,boolInBase=false]", "false:true")
+ .put("overridableTestInBase[boolInChild=true,boolInBase=true]", "true:true")
+ .put("testInBase[boolInChild=false,boolInBase=false,ONE]", "false:ONE")
+ .put("testInBase[boolInChild=false,boolInBase=false,TWO]", "false:TWO")
+ .put("testInBase[boolInChild=false,boolInBase=true,ONE]", "true:ONE")
+ .put("testInBase[boolInChild=false,boolInBase=true,TWO]", "true:TWO")
+ .put("testInBase[boolInChild=true,boolInBase=false,ONE]", "false:ONE")
+ .put("testInBase[boolInChild=true,boolInBase=false,TWO]", "false:TWO")
+ .put("testInBase[boolInChild=true,boolInBase=true,ONE]", "true:ONE")
+ .put("testInBase[boolInChild=true,boolInBase=true,TWO]", "true:TWO")
+ .build();
+ }
+ }
+
+ @Retention(RUNTIME)
+ @TestParameterAnnotation(validator = TestEnumValidator.class)
+ public @interface EnumEvaluatorParameter {
+ TestEnum[] value() default {};
+ }
+
+ public static class TestEnumValidator implements TestParameterValidator {
+
+ @Override
+ public boolean shouldSkip(Context context) {
+ return context.has(EnumEvaluatorParameter.class, TestEnum.THREE);
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class MethodEvaluatorClass extends SuccessfulTestCaseBase {
+
+ @Test
+ public void test(
+ @EnumEvaluatorParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) TestEnum value) {
+ storeTestParametersForThisTest(value);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test[ONE]", "ONE")
+ .put("test[TWO]", "TWO")
+ .build();
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class FieldEvaluatorClass extends SuccessfulTestCaseBase {
+
+ @EnumEvaluatorParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE})
+ TestEnum value;
+
+ @Test
+ public void test() {
+ storeTestParametersForThisTest(value);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test[ONE]", "ONE")
+ .put("test[TWO]", "TWO")
+ .build();
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class ConstructorClass extends SuccessfulTestCaseBase {
+
+ final TestEnum enumParameter;
+
+ public ConstructorClass(
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) TestEnum enumParameter) {
+ this.enumParameter = enumParameter;
+ }
+
+ @Test
+ public void test() {
+ storeTestParametersForThisTest(enumParameter);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test[ONE]", "ONE")
+ .put("test[TWO]", "TWO")
+ .put("test[THREE]", "THREE")
+ .build();
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_FOR_ALL_PLACEMENTS_ONLY)
+ public static class MethodFieldOverrideClass extends SuccessfulTestCaseBase {
+
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO})
+ TestEnum enumParameter;
+
+ @Test
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE})
+ public void test() {
+ storeTestParametersForThisTest(enumParameter);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test[ONE]", "ONE")
+ .put("test[TWO]", "TWO")
+ .put("test[THREE]", "THREE")
+ .build();
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_FOR_ALL_PLACEMENTS_ONLY)
+ public static class ErrorDuplicatedConstructorMethodAnnotation extends SuccessfulTestCaseBase {
+
+ final TestEnum enumParameter;
+
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE})
+ public ErrorDuplicatedConstructorMethodAnnotation(TestEnum enumParameter) {
+ this.enumParameter = enumParameter;
+ }
+
+ @Test
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO})
+ public void test(TestEnum otherParameter) {
+ storeTestParametersForThisTest(enumParameter, otherParameter);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test[ONE,ONE]", "ONE:ONE")
+ .put("test[ONE,TWO]", "ONE:TWO")
+ .put("test[TWO,ONE]", "TWO:ONE")
+ .put("test[TWO,TWO]", "TWO:TWO")
+ .put("test[THREE,ONE]", "THREE:ONE")
+ .put("test[THREE,TWO]", "THREE:TWO")
+ .build();
+ }
+ }
+
+ @ClassTestResult(Result.FAILURE)
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE})
+ public static class ErrorDuplicatedClassFieldAnnotation {
+
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO})
+ TestEnum enumParameter;
+
+ @Test
+ public void test() {}
+ }
+
+ @ClassTestResult(Result.FAILURE)
+ public static class ErrorNonStaticProviderClass {
+
+ @Test
+ public void test(@TestParameter(valuesProvider = NonStaticProvider.class) int i) {}
+
+ @SuppressWarnings("ClassCanBeStatic")
+ class NonStaticProvider extends TestParameterValuesProvider {
+ @Override
+ public List<?> provideValues(TestParameterValuesProvider.Context context) {
+ return ImmutableList.of();
+ }
+ }
+ }
+
+ @ClassTestResult(Result.FAILURE)
+ public static class ErrorNonPublicTestMethod {
+
+ @Test
+ void test(@TestParameter boolean b) {}
+ }
+
+ @ClassTestResult(Result.FAILURE)
+ public static class ErrorPackagePrivateConstructor {
+ ErrorPackagePrivateConstructor() {}
+
+ @Test
+ public void test1() {}
+ }
+
+ public enum EnumA {
+ A1,
+ A2
+ }
+
+ public enum EnumB {
+ B1,
+ B2
+ }
+
+ public enum EnumC {
+ C1,
+ C2,
+ C3
+ }
+
+ @Retention(RUNTIME)
+ @TestParameterAnnotation(validator = TestBaseValidatorValidator.class)
+ public @interface EnumAParameter {
+ EnumA[] value() default {EnumA.A1, EnumA.A2};
+ }
+
+ @Retention(RUNTIME)
+ @TestParameterAnnotation(validator = TestBaseValidatorValidator.class)
+ public @interface EnumBParameter {
+ EnumB[] value() default {EnumB.B1, EnumB.B2};
+ }
+
+ @Retention(RUNTIME)
+ @TestParameterAnnotation(validator = TestBaseValidatorValidator.class)
+ public @interface EnumCParameter {
+ EnumC[] value() default {EnumC.C1, EnumC.C2, EnumC.C3};
+ }
+
+ public static class TestBaseValidatorValidator extends BaseTestParameterValidator {
+
+ @Override
+ protected List<List<Class<? extends Annotation>>> getIndependentParameters(Context context) {
+ return ImmutableList.of(
+ ImmutableList.of(EnumAParameter.class, EnumBParameter.class, EnumCParameter.class));
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class IndependentAnnotation extends SuccessfulTestCaseBase {
+
+ @EnumAParameter EnumA enumA;
+ @EnumBParameter EnumB enumB;
+ @EnumCParameter EnumC enumC;
+
+ @Test
+ public void test() {
+ storeTestParametersForThisTest(enumA, enumB, enumC);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test[A1,B1,C1]", "A1:B1:C1")
+ .put("test[A2,B2,C2]", "A2:B2:C2")
+ .put("test[A2,B2,C3]", "A2:B2:C3")
+ .build();
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class TestNamesTest extends SuccessfulTestCaseBase {
+
+ @TestParameter("8")
+ long fieldParam;
+
+ @Test
+ public void withPrimitives(
+ @TestParameter("true") boolean param1, @TestParameter("2") int param2) {
+ storeTestParametersForThisTest(fieldParam, param1, param2);
+ }
+
+ @Test
+ public void withString(@TestParameter("AAA") String param1) {
+ storeTestParametersForThisTest(fieldParam, param1);
+ }
+
+ @Test
+ public void withEnum(@EnumParameter(TestEnum.TWO) TestEnum param1) {
+ storeTestParametersForThisTest(fieldParam, param1);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("withString[fieldParam=8,AAA]", "8:AAA")
+ .put("withEnum[fieldParam=8,TWO]", "8:TWO")
+ .put("withPrimitives[fieldParam=8,param1=true,param2=2]", "8:true:2")
+ .build();
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class MethodNameContainsOrderedParameterNames extends SuccessfulTestCaseBase {
+
+ @Test
+ public void pretest(@TestParameter({"a", "b"}) String foo) {
+ storeTestParametersForThisTest(foo);
+ }
+
+ @Test
+ public void test(
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO}) TestEnum e, @TestParameter({"c"}) String foo) {
+ storeTestParametersForThisTest(e, foo);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("pretest[a]", "a")
+ .put("pretest[b]", "b")
+ .put("test[ONE,c]", "ONE:c")
+ .put("test[TWO,c]", "TWO:c")
+ .build();
+ }
+ }
+
+ @Parameters(name = "{0}:{2}")
+ public static Collection<Object[]> parameters() {
+ return Arrays.stream(TestParameterAnnotationMethodProcessorTest.class.getClasses())
+ .filter(cls -> cls.isAnnotationPresent(ClassTestResult.class))
+ .map(
+ cls ->
+ new Object[] {
+ cls.getSimpleName(), cls, cls.getAnnotation(ClassTestResult.class).value()
+ })
+ .collect(toImmutableList());
+ }
+
+ private final Class<?> testClass;
+ private final Result result;
+
+ public TestParameterAnnotationMethodProcessorTest(
+ String name, Class<?> testClass, Result result) {
+ this.testClass = testClass;
+ this.result = result;
+ }
+
+ @Test
+ public void test() throws Exception {
+ switch (result) {
+ case SUCCESS_ALWAYS:
+ SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures(
+ newTestRunnerWithParameterizedSupport(
+ testClass -> TestMethodProcessorList.createNewParameterizedProcessors()));
+ break;
+
+ case SUCCESS_FOR_ALL_PLACEMENTS_ONLY:
+ assertThrows(
+ Exception.class,
+ () ->
+ SharedTestUtilitiesJUnit4.runTestsAndGetFailures(
+ newTestRunnerWithParameterizedSupport(
+ testClass -> TestMethodProcessorList.createNewParameterizedProcessors())));
+ break;
+
+ case FAILURE:
+ assertThrows(
+ Exception.class,
+ () ->
+ SharedTestUtilitiesJUnit4.runTestsAndGetFailures(
+ newTestRunnerWithParameterizedSupport(
+ testClass -> TestMethodProcessorList.createNewParameterizedProcessors())));
+ break;
+ }
+ }
+
+ private PluggableTestRunner newTestRunnerWithParameterizedSupport(
+ Function<TestClass, TestMethodProcessorList> processorListGenerator) throws Exception {
+ return new PluggableTestRunner(testClass) {
+ @Override
+ protected TestMethodProcessorList createTestMethodProcessorList() {
+ return processorListGenerator.apply(getTestClass());
+ }
+ };
+ }
+}
diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt
new file mode 100644
index 0000000..10ce60e
--- /dev/null
+++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt
@@ -0,0 +1,278 @@
+/*
+ * Copyright 2022 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector
+
+import com.google.common.collect.ImmutableList
+import com.google.common.collect.ImmutableMap
+import com.google.testing.junit.testparameterinjector.SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase
+import java.util.Arrays
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
+
+@RunWith(Parameterized::class)
+class TestParameterInjectorKotlinTest {
+
+ // ********** Test cases ********** //
+ // These test classes are all expected to run successfully
+
+ @RunAsTest
+ internal class TestParameter_MethodParam : SuccessfulTestCaseBase() {
+ @Test
+ fun testString(@TestParameter("a", "b") param: String) {
+ storeTestParametersForThisTest(param)
+ }
+
+ @Test
+ fun testEnum_selection(@TestParameter("RED", "GREEN") param: Color) {
+ storeTestParametersForThisTest(param)
+ }
+
+ @Test
+ fun testEnum_all(@TestParameter param: Color) {
+ storeTestParametersForThisTest(param)
+ }
+
+ @Test
+ fun testMultiple(
+ @TestParameter("1", "8") width: Int,
+ @TestParameter("1", "5.5") height: Double,
+ ) {
+ storeTestParametersForThisTest(width, height)
+ }
+
+ override fun expectedTestNameToStringifiedParameters(): ImmutableMap<String, String> {
+ return ImmutableMap.builder<String, String>()
+ .put("testString[a]", "a")
+ .put("testString[b]", "b")
+ .put("testEnum_selection[RED]", "RED")
+ .put("testEnum_selection[GREEN]", "GREEN")
+ .put("testEnum_all[RED]", "RED")
+ .put("testEnum_all[BLUE]", "BLUE")
+ .put("testEnum_all[GREEN]", "GREEN")
+ .put("testMultiple[width=1,height=1.0]", "1:1.0")
+ .put("testMultiple[width=1,height=5.5]", "1:5.5")
+ .put("testMultiple[width=8,height=1.0]", "8:1.0")
+ .put("testMultiple[width=8,height=5.5]", "8:5.5")
+ .buildOrThrow()
+ }
+ }
+
+ @RunAsTest
+ internal class TestParameter_MethodParam_WithValueClasses : SuccessfulTestCaseBase() {
+ @Test
+ fun testString(@TestParameter("a", "b") param: StringValueClass) {
+ storeTestParametersForThisTest(param.onlyValue)
+ }
+
+ @Test
+ fun testEnum_selection(@TestParameter("RED", "GREEN") param: ColorValueClass) {
+ storeTestParametersForThisTest(param.onlyValue)
+ }
+
+ @Test
+ fun testEnum_all(@TestParameter param: ColorValueClass) {
+ storeTestParametersForThisTest(param.onlyValue)
+ }
+
+ @Test
+ fun testMixed(
+ @TestParameter("1", "8") width: Int,
+ @TestParameter("1", "5.5") height: DoubleValueClass,
+ ) {
+ storeTestParametersForThisTest(width, height.onlyValue)
+ }
+
+ override fun expectedTestNameToStringifiedParameters(): ImmutableMap<String, String> {
+ return ImmutableMap.builder<String, String>()
+ .put("testString-HMW45e8[a]", "a")
+ .put("testString-HMW45e8[b]", "b")
+ .put("testEnum_selection-fiSAjMM[RED]", "RED")
+ .put("testEnum_selection-fiSAjMM[GREEN]", "GREEN")
+ .put("testEnum_all-fiSAjMM[RED]", "RED")
+ .put("testEnum_all-fiSAjMM[BLUE]", "BLUE")
+ .put("testEnum_all-fiSAjMM[GREEN]", "GREEN")
+ .put("testMixed-lvZ97mM[width=1,height=1.0]", "1:1.0")
+ .put("testMixed-lvZ97mM[width=1,height=5.5]", "1:5.5")
+ .put("testMixed-lvZ97mM[width=8,height=1.0]", "8:1.0")
+ .put("testMixed-lvZ97mM[width=8,height=5.5]", "8:5.5")
+ .buildOrThrow()
+ }
+ }
+
+ @RunAsTest
+ internal class TestParameter_Field : SuccessfulTestCaseBase() {
+ @TestParameter("1", "2") var width: Int? = null
+
+ @Test
+ fun test() {
+ storeTestParametersForThisTest(width)
+ }
+
+ override fun expectedTestNameToStringifiedParameters(): ImmutableMap<String, String> {
+ return ImmutableMap.builder<String, String>()
+ .put("test[width=1]", "1")
+ .put("test[width=2]", "2")
+ .buildOrThrow()
+ }
+ }
+
+ @RunAsTest
+ internal class TestParameter_Field_WithValueClass : SuccessfulTestCaseBase() {
+ @TestParameter(valuesProvider = DoubleValueClassProvider::class)
+ var width: DoubleValueClass? = null
+
+ @Test
+ fun test() {
+ storeTestParametersForThisTest(width?.onlyValue)
+ }
+
+ override fun expectedTestNameToStringifiedParameters(): ImmutableMap<String, String> {
+ return ImmutableMap.builder<String, String>()
+ .put("test[DoubleValueClass(onlyValue=1.0)]", "1.0")
+ .put("test[DoubleValueClass(onlyValue=2.5)]", "2.5")
+ .buildOrThrow()
+ }
+
+ private class DoubleValueClassProvider : TestParameterValuesProvider() {
+ override fun provideValues(context: Context): List<DoubleValueClass> {
+ return ImmutableList.of(DoubleValueClass(1.0), DoubleValueClass(2.5))
+ }
+ }
+ }
+
+ @RunAsTest
+ internal class TestParameter_ConstructorParam : SuccessfulTestCaseBase {
+ val width: Int
+
+ constructor(@TestParameter("1", "2") width: Int) {
+ this.width = width
+ }
+
+ @Test
+ fun test() {
+ storeTestParametersForThisTest(width)
+ }
+
+ override fun expectedTestNameToStringifiedParameters(): ImmutableMap<String, String> {
+ return ImmutableMap.builder<String, String>()
+ .put("test[width=1]", "1")
+ .put("test[width=2]", "2")
+ .buildOrThrow()
+ }
+ }
+
+ @RunAsTest
+ internal class TestParameters_MethodParam : SuccessfulTestCaseBase() {
+ @TestParameters("{width: 3, height: 8}")
+ @TestParameters("{width: 5, height: 2.5}")
+ @Test
+ fun test(width: Int, height: Double) {
+ storeTestParametersForThisTest(width, height)
+ }
+
+ override fun expectedTestNameToStringifiedParameters(): ImmutableMap<String, String> {
+ return ImmutableMap.builder<String, String>()
+ .put("test[{width: 3, height: 8}]", "3:8.0")
+ .put("test[{width: 5, height: 2.5}]", "5:2.5")
+ .buildOrThrow()
+ }
+ }
+
+ @RunAsTest
+ internal class TestParameters_MethodParam_WithValueClasses : SuccessfulTestCaseBase() {
+ @TestParameters("{width: 3, height: 8}")
+ @TestParameters("{width: 5, height: 2.5}")
+ @Test
+ fun test(width: Int, height: DoubleValueClass) {
+ storeTestParametersForThisTest(width, height.onlyValue)
+ }
+
+ override fun expectedTestNameToStringifiedParameters(): ImmutableMap<String, String> {
+ return ImmutableMap.builder<String, String>()
+ .put("test-lvZ97mM[{width: 3, height: 8}]", "3:8.0")
+ .put("test-lvZ97mM[{width: 5, height: 2.5}]", "5:2.5")
+ .buildOrThrow()
+ }
+ }
+
+ @RunAsTest
+ internal class TestParameters_ConstructorParam : SuccessfulTestCaseBase {
+ val width: Int
+
+ @TestParameters("{width: 1}")
+ @TestParameters("{width: 2}")
+ constructor(width: Int) {
+ this.width = width
+ }
+
+ @Test
+ fun test() {
+ storeTestParametersForThisTest(width)
+ }
+
+ override fun expectedTestNameToStringifiedParameters(): ImmutableMap<String, String> {
+ return ImmutableMap.builder<String, String>()
+ .put("test[{width: 1}]", "1")
+ .put("test[{width: 2}]", "2")
+ .buildOrThrow()
+ }
+ }
+
+ // ********** Test infrastructure ********** //
+
+ private val testClass: Class<*>
+
+ constructor(@Suppress("UNUSED_PARAMETER") testName: String?, testClass: Class<*>) {
+ this.testClass = testClass
+ }
+
+ @Test
+ fun test_success() {
+ SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures(
+ object : PluggableTestRunner(testClass) {
+ override fun createTestMethodProcessorList(): TestMethodProcessorList {
+ return TestMethodProcessorList.createNewParameterizedProcessors()
+ }
+ }
+ )
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun parameters(): Collection<Array<Any>> {
+ return Arrays.stream(TestParameterInjectorKotlinTest::class.java.classes)
+ .filter { cls: Class<*> -> cls.isAnnotationPresent(RunAsTest::class.java) }
+ .map { cls: Class<*> -> arrayOf<Any>(cls.simpleName, cls) }
+ .collect(ImmutableList.toImmutableList())
+ }
+ }
+
+ annotation class RunAsTest
+
+ enum class Color {
+ RED,
+ BLUE,
+ GREEN
+ }
+
+ @JvmInline value class ColorValueClass(val onlyValue: Color)
+
+ @JvmInline value class StringValueClass(val onlyValue: String)
+
+ @JvmInline value class DoubleValueClass(val onlyValue: Double)
+}
diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java
new file mode 100644
index 0000000..7c915ea
--- /dev/null
+++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.Lists.newArrayList;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.testing.junit.testparameterinjector.SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase;
+import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider.Context;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/** Test class to test the @TestParameter's value provider. */
+@RunWith(Parameterized.class)
+public class TestParameterTest {
+
+ @Retention(RUNTIME)
+ @interface RunAsTest {}
+
+ public enum TestEnum {
+ ONE,
+ TWO,
+ THREE,
+ }
+
+ @RunAsTest
+ public static class AnnotatedField extends SuccessfulTestCaseBase {
+
+ @TestParameter TestEnum enumParameter;
+
+ @Test
+ public void test() {
+ storeTestParametersForThisTest(enumParameter);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test[ONE]", "ONE")
+ .put("test[TWO]", "TWO")
+ .put("test[THREE]", "THREE")
+ .build();
+ }
+ }
+
+ @RunAsTest
+ public static class AnnotatedConstructorParameter extends SuccessfulTestCaseBase {
+
+ private final TestEnum constructorEnum;
+
+ @TestParameter TestEnum fieldEnum;
+
+ public AnnotatedConstructorParameter(@TestParameter TestEnum constructorEnum) {
+ this.constructorEnum = constructorEnum;
+ }
+
+ @Test
+ public void test() {
+ storeTestParametersForThisTest(fieldEnum, constructorEnum);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test[ONE,ONE]", "ONE:ONE")
+ .put("test[ONE,TWO]", "ONE:TWO")
+ .put("test[ONE,THREE]", "ONE:THREE")
+ .put("test[TWO,ONE]", "TWO:ONE")
+ .put("test[TWO,TWO]", "TWO:TWO")
+ .put("test[TWO,THREE]", "TWO:THREE")
+ .put("test[THREE,ONE]", "THREE:ONE")
+ .put("test[THREE,TWO]", "THREE:TWO")
+ .put("test[THREE,THREE]", "THREE:THREE")
+ .build();
+ }
+ }
+
+ @RunAsTest
+ public static class MultipleAnnotatedParameters extends SuccessfulTestCaseBase {
+
+ @Test
+ public void test(
+ @TestParameter TestEnum enumParameterA,
+ @TestParameter({"TWO", "THREE"}) TestEnum enumParameterB,
+ @TestParameter({"!!binary 'ZGF0YQ=='", "data2"}) byte[] bytes) {
+ storeTestParametersForThisTest(enumParameterA, enumParameterB, new String(bytes));
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test[ONE,TWO,[100, 97, 116, 97]]", "ONE:TWO:data")
+ .put("test[ONE,TWO,[100, 97, 116, 97, 50]]", "ONE:TWO:data2")
+ .put("test[ONE,THREE,[100, 97, 116, 97]]", "ONE:THREE:data")
+ .put("test[ONE,THREE,[100, 97, 116, 97, 50]]", "ONE:THREE:data2")
+ .put("test[TWO,TWO,[100, 97, 116, 97]]", "TWO:TWO:data")
+ .put("test[TWO,TWO,[100, 97, 116, 97, 50]]", "TWO:TWO:data2")
+ .put("test[TWO,THREE,[100, 97, 116, 97]]", "TWO:THREE:data")
+ .put("test[TWO,THREE,[100, 97, 116, 97, 50]]", "TWO:THREE:data2")
+ .put("test[THREE,TWO,[100, 97, 116, 97]]", "THREE:TWO:data")
+ .put("test[THREE,TWO,[100, 97, 116, 97, 50]]", "THREE:TWO:data2")
+ .put("test[THREE,THREE,[100, 97, 116, 97]]", "THREE:THREE:data")
+ .put("test[THREE,THREE,[100, 97, 116, 97, 50]]", "THREE:THREE:data2")
+ .build();
+ }
+ }
+
+ @RunAsTest
+ public static class WithValuesProvider extends SuccessfulTestCaseBase {
+
+ private final int number1;
+
+ @TestParameter(valuesProvider = TestNumberProvider.class)
+ private int number2;
+
+ public WithValuesProvider(
+ @TestParameter(valuesProvider = TestNumberProvider.class) int number1) {
+ this.number1 = number1;
+ }
+
+ @Test
+ public void stringTest(
+ @TestParameter(valuesProvider = TestStringProvider.class) String stringParam) {
+ storeTestParametersForThisTest(number1, number2, stringParam);
+ }
+
+ @Test
+ public void charMatcherTest(
+ @TestParameter(valuesProvider = CharMatcherProvider.class) CharMatcher charMatcher) {
+ storeTestParametersForThisTest(number1, number2, charMatcher);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("stringTest[one,one,A]", "1:1:A")
+ .put("stringTest[one,one,B]", "1:1:B")
+ .put("stringTest[one,one,stringParam=null]", "1:1:null")
+ .put("stringTest[one,one,nothing]", "1:1:null")
+ .put("stringTest[one,one,wizard]", "1:1:harry")
+ .put("stringTest[one,number1=2,A]", "2:1:A")
+ .put("stringTest[one,number1=2,B]", "2:1:B")
+ .put("stringTest[one,number1=2,stringParam=null]", "2:1:null")
+ .put("stringTest[one,number1=2,nothing]", "2:1:null")
+ .put("stringTest[one,number1=2,wizard]", "2:1:harry")
+ .put("stringTest[number2=2,one,A]", "1:2:A")
+ .put("stringTest[number2=2,one,B]", "1:2:B")
+ .put("stringTest[number2=2,one,stringParam=null]", "1:2:null")
+ .put("stringTest[number2=2,one,nothing]", "1:2:null")
+ .put("stringTest[number2=2,one,wizard]", "1:2:harry")
+ .put("stringTest[number2=2,number1=2,A]", "2:2:A")
+ .put("stringTest[number2=2,number1=2,B]", "2:2:B")
+ .put("stringTest[number2=2,number1=2,stringParam=null]", "2:2:null")
+ .put("stringTest[number2=2,number1=2,nothing]", "2:2:null")
+ .put("stringTest[number2=2,number1=2,wizard]", "2:2:harry")
+ .put("charMatcherTest[one,one,CharMatcher.any()]", "1:1:CharMatcher.any()")
+ .put("charMatcherTest[one,one,CharMatcher.ascii()]", "1:1:CharMatcher.ascii()")
+ .put("charMatcherTest[one,one,CharMatcher.whitespace()]", "1:1:CharMatcher.whitespace()")
+ .put("charMatcherTest[one,number1=2,CharMatcher.any()]", "2:1:CharMatcher.any()")
+ .put("charMatcherTest[one,number1=2,CharMatcher.ascii()]", "2:1:CharMatcher.ascii()")
+ .put(
+ "charMatcherTest[one,number1=2,CharMatcher.whitespace()]",
+ "2:1:CharMatcher.whitespace()")
+ .put("charMatcherTest[number2=2,one,CharMatcher.any()]", "1:2:CharMatcher.any()")
+ .put("charMatcherTest[number2=2,one,CharMatcher.ascii()]", "1:2:CharMatcher.ascii()")
+ .put(
+ "charMatcherTest[number2=2,one,CharMatcher.whitespace()]",
+ "1:2:CharMatcher.whitespace()")
+ .put("charMatcherTest[number2=2,number1=2,CharMatcher.any()]", "2:2:CharMatcher.any()")
+ .put(
+ "charMatcherTest[number2=2,number1=2,CharMatcher.ascii()]", "2:2:CharMatcher.ascii()")
+ .put(
+ "charMatcherTest[number2=2,number1=2,CharMatcher.whitespace()]",
+ "2:2:CharMatcher.whitespace()")
+ .build();
+ }
+
+ private static final class TestNumberProvider extends TestParameterValuesProvider {
+ @Override
+ public List<?> provideValues(Context context) {
+ return newArrayList(value(1).withName("one"), 2);
+ }
+ }
+
+ private static final class TestStringProvider extends TestParameterValuesProvider {
+ @Override
+ public List<?> provideValues(Context context) {
+ return newArrayList(
+ "A", "B", null, value(null).withName("nothing"), value("harry").withName("wizard"));
+ }
+ }
+
+ private static final class CharMatcherProvider extends TestParameterValuesProvider {
+ @Override
+ public List<CharMatcher> provideValues(Context context) {
+ return newArrayList(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace());
+ }
+ }
+ }
+
+ @Parameters(name = "{0}")
+ public static Collection<Object[]> parameters() {
+ return Arrays.stream(TestParameterTest.class.getClasses())
+ .filter(cls -> cls.isAnnotationPresent(RunAsTest.class))
+ .map(cls -> new Object[] {cls.getSimpleName(), cls})
+ .collect(toImmutableList());
+ }
+
+ private final Class<?> testClass;
+
+ public TestParameterTest(String name, Class<?> testClass) {
+ this.testClass = testClass;
+ }
+
+ @Test
+ public void test() throws Exception {
+ SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures(
+ new PluggableTestRunner(testClass) {
+ @Override
+ protected TestMethodProcessorList createTestMethodProcessorList() {
+ return TestMethodProcessorList.createNewParameterizedProcessors();
+ }
+ });
+ }
+
+ private static ImmutableList<Class<? extends Annotation>> annotationTypes(
+ Iterable<Annotation> annotations) {
+ return FluentIterable.from(annotations).transform(Annotation::annotationType).toList();
+ }
+}
diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java
new file mode 100644
index 0000000..5628330
--- /dev/null
+++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java
@@ -0,0 +1,554 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.testing.junit.testparameterinjector.SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase;
+import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValues;
+import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValuesProvider;
+import java.lang.annotation.Retention;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class TestParametersMethodProcessorTest {
+
+ @Retention(RUNTIME)
+ @interface RunAsTest {
+ String failsWithMessage() default "";
+ }
+
+ public enum TestEnum {
+ ONE,
+ TWO,
+ THREE;
+ }
+
+ private static final class TestEnumValuesProvider implements TestParametersValuesProvider {
+ @Override
+ public List<TestParametersValues> provideValues() {
+ return ImmutableList.of(
+ TestParametersValues.builder().name("one").addParameter("testEnum", TestEnum.ONE).build(),
+ TestParametersValues.builder().addParameter("testEnum", TestEnum.TWO).build(),
+ TestParametersValues.builder().name("null-case").addParameter("testEnum", null).build());
+ }
+ }
+
+ @RunAsTest
+ public static class SimpleMethodAnnotation extends SuccessfulTestCaseBase {
+
+ @Test
+ @TestParameters("{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}")
+ @TestParameters("{testEnum: TWO,\ntestLong: 22,\ntestBoolean: true,\r\n\r\n testString: 'DEF'}")
+ @TestParameters("{testEnum: null, testLong: 33, testBoolean: false, testString: null}")
+ public void test(TestEnum testEnum, long testLong, boolean testBoolean, String testString) {
+ storeTestParametersForThisTest(testEnum, testLong, testBoolean, testString);
+ }
+
+ @Test
+ @TestParameters({
+ "{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}",
+ "{testEnum: TWO,\ntestLong: 22,\ntestBoolean: true,\r\n\r\n testString: 'DEF'}",
+ "{testEnum: null, testLong: 33, testBoolean: false, testString: null}",
+ })
+ public void test_singleAnnotation(
+ TestEnum testEnum, long testLong, boolean testBoolean, String testString) {
+ storeTestParametersForThisTest(testEnum, testLong, testBoolean, testString);
+ }
+
+ @Test
+ @TestParameters("{testString: ABC}")
+ @TestParameters(
+ "{testString: 'This is a very long string (240 characters) that would normally cause"
+ + " Sponge+Tin to exceed the filename limit of 255 characters."
+ + " ================================================================================="
+ + "=============='}")
+ public void test2_withLongNames(String testString) {
+ storeTestParametersForThisTest(testString);
+ }
+
+ @Test
+ @TestParameters(
+ "{testEnums: [ONE, TWO, THREE], testLongs: [11, 4], testBooleans: [false, true],"
+ + " testStrings: [ABC, '123']}")
+ @TestParameters(
+ "{testEnums: [TWO],\ntestLongs: [22],\ntestBooleans: [true],\r\n\r\n testStrings: ['DEF']}")
+ @TestParameters("{testEnums: [], testLongs: [], testBooleans: [], testStrings: []}")
+ public void test3_withRepeatedParams(
+ List<TestEnum> testEnums,
+ List<Long> testLongs,
+ List<Boolean> testBooleans,
+ List<String> testStrings) {
+ storeTestParametersForThisTest(testEnums, testLongs, testBooleans, testStrings);
+ }
+
+ @Test
+ @TestParameters(customName = "custom1", value = "{testEnum: ONE}")
+ @TestParameters("{testEnum: TWO}")
+ @TestParameters(customName = "custom3", value = "{testEnum: THREE}")
+ public void test4_withCustomName(TestEnum testEnum) {
+ storeTestParametersForThisTest(testEnum);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put(
+ "test[{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}]",
+ "ONE:11:false:ABC")
+ .put(
+ "test[{testEnum: TWO, testLong: 22, testBoolean: true, testString: 'DEF'}]",
+ "TWO:22:true:DEF")
+ .put(
+ "test[{testEnum: null, testLong: 33, testBoolean: false, testString: null}]",
+ "null:33:false:null")
+ .put(
+ "test_singleAnnotation[{testEnum: ONE, testLong: 11, testBoolean: false, testString:"
+ + " ABC}]",
+ "ONE:11:false:ABC")
+ .put(
+ "test_singleAnnotation[{testEnum: TWO, testLong: 22, testBoolean: true, testString:"
+ + " 'DEF'}]",
+ "TWO:22:true:DEF")
+ .put(
+ "test_singleAnnotation[{testEnum: null, testLong: 33, testBoolean: false, testString:"
+ + " null}]",
+ "null:33:false:null")
+ .put("test2_withLongNames[1.{testString: ABC}]", "ABC")
+ .put(
+ "test2_withLongNames[2.{testString: 'This is a very long string (240 characters) that"
+ + " would normally cause Sponge+Tin to exceed the filename limit of 255"
+ + " characters. =============================...]",
+ "This is a very long string (240 characters) that would normally cause Sponge+Tin to"
+ + " exceed the filename limit of 255 characters."
+ + " ===============================================================================================")
+ .put(
+ "test3_withRepeatedParams[{testEnums: [ONE, TWO, THREE], testLongs: [11, 4],"
+ + " testBooleans: [false, true], testStrings: [ABC, '123']}]",
+ "[ONE, TWO, THREE]:[11, 4]:[false, true]:[ABC, 123]")
+ .put(
+ "test3_withRepeatedParams[{testEnums: [TWO], testLongs: [22], testBooleans: [true],"
+ + " testStrings: ['DEF']}]",
+ "[TWO]:[22]:[true]:[DEF]")
+ .put(
+ "test3_withRepeatedParams[{testEnums: [], testLongs: [], testBooleans: [],"
+ + " testStrings: []}]",
+ "[]:[]:[]:[]")
+ .put("test4_withCustomName[custom1]", "ONE")
+ .put("test4_withCustomName[{testEnum: TWO}]", "TWO")
+ .put("test4_withCustomName[custom3]", "THREE")
+ .build();
+ }
+ }
+
+ @RunAsTest
+ public static class SimpleConstructorAnnotation extends SuccessfulTestCaseBase {
+
+ private final TestEnum testEnum;
+ private final long testLong;
+ private final boolean testBoolean;
+ private final String testString;
+
+ @TestParameters({
+ "{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}",
+ "{testEnum: TWO, testLong: 22, testBoolean: true, testString: DEF}",
+ "{testEnum: null, testLong: 33, testBoolean: false, testString: null}",
+ })
+ public SimpleConstructorAnnotation(
+ TestEnum testEnum, long testLong, boolean testBoolean, String testString) {
+ this.testEnum = testEnum;
+ this.testLong = testLong;
+ this.testBoolean = testBoolean;
+ this.testString = testString;
+ }
+
+ @Test
+ public void test1() {
+ storeTestParametersForThisTest(testEnum, testLong, testBoolean, testString);
+ }
+
+ @Test
+ public void test2() {
+ storeTestParametersForThisTest(testEnum, testLong, testBoolean, testString);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put(
+ "test1[{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}]",
+ "ONE:11:false:ABC")
+ .put(
+ "test1[{testEnum: TWO, testLong: 22, testBoolean: true, testString: DEF}]",
+ "TWO:22:true:DEF")
+ .put(
+ "test1[{testEnum: null, testLong: 33, testBoolean: false, testString: null}]",
+ "null:33:false:null")
+ .put(
+ "test2[{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}]",
+ "ONE:11:false:ABC")
+ .put(
+ "test2[{testEnum: TWO, testLong: 22, testBoolean: true, testString: DEF}]",
+ "TWO:22:true:DEF")
+ .put(
+ "test2[{testEnum: null, testLong: 33, testBoolean: false, testString: null}]",
+ "null:33:false:null")
+ .build();
+ }
+ }
+
+ @RunAsTest
+ public static class ConstructorAnnotationWithProvider extends SuccessfulTestCaseBase {
+
+ private final TestEnum testEnum;
+
+ @TestParameters(valuesProvider = TestEnumValuesProvider.class)
+ public ConstructorAnnotationWithProvider(TestEnum testEnum) {
+ this.testEnum = testEnum;
+ }
+
+ @Test
+ public void test() {
+ storeTestParametersForThisTest(testEnum);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test[one]", "ONE")
+ .put("test[{TWO}]", "TWO")
+ .put("test[null-case]", "null")
+ .build();
+ }
+ }
+
+ @RunAsTest
+ public static class MethodAnnotationWithProvider extends SuccessfulTestCaseBase {
+
+ @TestParameters(valuesProvider = CustomProvider.class)
+ @Test
+ public void test(int testInt, TestEnum testEnum) {
+ storeTestParametersForThisTest(testInt, testEnum);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test[{testInt=5, ONE}]", "5:ONE")
+ .put("test[{testInt=10, TWO}]", "10:TWO")
+ .build();
+ }
+
+ private static final class CustomProvider implements TestParametersValuesProvider {
+ @Override
+ public List<TestParametersValues> provideValues() {
+ return ImmutableList.of(
+ TestParametersValues.builder()
+ .addParameter("testInt", 5)
+ .addParameter("testEnum", TestEnum.ONE)
+ .build(),
+ TestParametersValues.builder()
+ .addParameter("testInt", 10)
+ .addParameter("testEnum", TestEnum.TWO)
+ .build());
+ }
+ }
+ }
+
+ public abstract static class BaseClassWithMethodAnnotation extends SuccessfulTestCaseBase {
+
+ @Test
+ @TestParameters("{testEnum: ONE}")
+ @TestParameters("{testEnum: TWO}")
+ public void testInBase(TestEnum testEnum) {
+ storeTestParametersForThisTest(testEnum);
+ }
+ }
+
+ @RunAsTest
+ public static class AnnotationInheritedFromBaseClass extends BaseClassWithMethodAnnotation {
+
+ @Test
+ @TestParameters({"{testEnum: TWO}", "{testEnum: THREE}"})
+ public void testInChild(TestEnum testEnum) {
+ storeTestParametersForThisTest(testEnum);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("testInChild[{testEnum: TWO}]", "TWO")
+ .put("testInChild[{testEnum: THREE}]", "THREE")
+ .put("testInBase[{testEnum: ONE}]", "ONE")
+ .put("testInBase[{testEnum: TWO}]", "TWO")
+ .build();
+ }
+ }
+
+ @RunAsTest
+ public static class MixedWithTestParameterMethodAnnotation extends SuccessfulTestCaseBase {
+
+ private final TestEnum testEnumFromConstructor;
+
+ @TestParameters("{testEnum: ONE}")
+ @TestParameters("{testEnum: TWO}")
+ public MixedWithTestParameterMethodAnnotation(TestEnum testEnum) {
+ this.testEnumFromConstructor = testEnum;
+ }
+
+ @Test
+ public void test1(@TestParameter TestEnum testEnum) {
+ storeTestParametersForThisTest(testEnumFromConstructor, testEnum);
+ }
+
+ @Test
+ @TestParameters("{testString: ABC}")
+ @TestParameters("{testString: DEF}")
+ public void test2(String testString) {
+ storeTestParametersForThisTest(testEnumFromConstructor, testString);
+ }
+
+ @Test
+ @TestParameters("{testString: ABC}")
+ @TestParameters(
+ "{testString: 'This is a very long string (240 characters) that would normally cause"
+ + " Sponge+Tin to exceed the filename limit of 255 characters."
+ + " ================================================================================="
+ + "=============='}")
+ public void test3_withLongNames(String testString) {
+ storeTestParametersForThisTest(testEnumFromConstructor, testString);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test1[{testEnum: ONE},ONE]", "ONE:ONE")
+ .put("test1[{testEnum: ONE},TWO]", "ONE:TWO")
+ .put("test1[{testEnum: ONE},THREE]", "ONE:THREE")
+ .put("test1[{testEnum: TWO},ONE]", "TWO:ONE")
+ .put("test1[{testEnum: TWO},TWO]", "TWO:TWO")
+ .put("test1[{testEnum: TWO},THREE]", "TWO:THREE")
+ .put("test2[{testEnum: ONE},{testString: ABC}]", "ONE:ABC")
+ .put("test2[{testEnum: ONE},{testString: DEF}]", "ONE:DEF")
+ .put("test2[{testEnum: TWO},{testString: ABC}]", "TWO:ABC")
+ .put("test2[{testEnum: TWO},{testString: DEF}]", "TWO:DEF")
+ .put("test3_withLongNames[{testEnum: ONE},1.{testString: ABC}]", "ONE:ABC")
+ .put(
+ "test3_withLongNames[{testEnum: ONE},2.{testString: 'This is a very long string (240"
+ + " characters) that would normally caus...]",
+ "ONE:This is a very long string (240 characters) that would normally cause Sponge+Tin"
+ + " to exceed the filename limit of 255 characters."
+ + " ==================================================================="
+ + "============================")
+ .put("test3_withLongNames[{testEnum: TWO},1.{testString: ABC}]", "TWO:ABC")
+ .put(
+ "test3_withLongNames[{testEnum: TWO},2.{testString: 'This is a very long string (240"
+ + " characters) that would normally caus...]",
+ "TWO:This is a very long string (240 characters) that would normally cause Sponge+Tin"
+ + " to exceed the filename limit of 255 characters."
+ + " ======================================================================"
+ + "=========================")
+ .build();
+ }
+ }
+
+ @RunAsTest
+ public static class MixedWithTestParameterFieldAnnotation extends SuccessfulTestCaseBase {
+
+ private final TestEnum testEnumB;
+
+ @TestParameter TestEnum testEnumA;
+
+ @TestParameters("{testEnumB: ONE}")
+ @TestParameters("{testEnumB: TWO}")
+ public MixedWithTestParameterFieldAnnotation(TestEnum testEnumB) {
+ this.testEnumB = testEnumB;
+ }
+
+ @Test
+ @TestParameters({"{testString: ABC}", "{testString: DEF}"})
+ public void test1(String testString) {
+ storeTestParametersForThisTest(testEnumA, testEnumB, testString);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test1[{testEnumB: ONE},{testString: ABC},ONE]", "ONE:ONE:ABC")
+ .put("test1[{testEnumB: ONE},{testString: ABC},TWO]", "TWO:ONE:ABC")
+ .put("test1[{testEnumB: ONE},{testString: ABC},THREE]", "THREE:ONE:ABC")
+ .put("test1[{testEnumB: ONE},{testString: DEF},ONE]", "ONE:ONE:DEF")
+ .put("test1[{testEnumB: ONE},{testString: DEF},TWO]", "TWO:ONE:DEF")
+ .put("test1[{testEnumB: ONE},{testString: DEF},THREE]", "THREE:ONE:DEF")
+ .put("test1[{testEnumB: TWO},{testString: ABC},ONE]", "ONE:TWO:ABC")
+ .put("test1[{testEnumB: TWO},{testString: ABC},TWO]", "TWO:TWO:ABC")
+ .put("test1[{testEnumB: TWO},{testString: ABC},THREE]", "THREE:TWO:ABC")
+ .put("test1[{testEnumB: TWO},{testString: DEF},ONE]", "ONE:TWO:DEF")
+ .put("test1[{testEnumB: TWO},{testString: DEF},TWO]", "TWO:TWO:DEF")
+ .put("test1[{testEnumB: TWO},{testString: DEF},THREE]", "THREE:TWO:DEF")
+ .build();
+ }
+ }
+
+ @RunAsTest(
+ failsWithMessage =
+ "Either a value or a valuesProvider must be set in @TestParameters on test1()")
+ public static class InvalidTestBecauseEmptyAnnotation {
+ @Test
+ @TestParameters
+ public void test1() {}
+ }
+
+ @RunAsTest(
+ failsWithMessage =
+ "Either a value or a valuesProvider must be set in @TestParameters on"
+ + " com.google.testing.junit.testparameterinjector.TestParametersMethodProcessorTest"
+ + "$InvalidTestBecauseEmptyAnnotationOnConstructor()")
+ public static class InvalidTestBecauseEmptyAnnotationOnConstructor {
+ @TestParameters
+ public InvalidTestBecauseEmptyAnnotationOnConstructor() {}
+
+ @Test
+ public void test1() {}
+ }
+
+ @RunAsTest(
+ failsWithMessage =
+ "It is not allowed to specify both value and valuesProvider in"
+ + " @TestParameters(value=[{testEnum: ONE}], valuesProvider=TestEnumValuesProvider)"
+ + " on test1()")
+ public static class InvalidTestBecauseCombiningValueWithProvider {
+
+ @Test
+ @TestParameters(value = "{testEnum: ONE}", valuesProvider = TestEnumValuesProvider.class)
+ public void test1(TestEnum testEnum) {}
+ }
+
+ @RunAsTest(
+ failsWithMessage =
+ "Either a value or a valuesProvider must be set in @TestParameters on test1()")
+ public static class InvalidTestBecauseRepeatedAnnotationIsEmpty {
+ @Test
+ @TestParameters(value = "{testEnum: ONE}")
+ @TestParameters
+ public void test1(TestEnum testEnum) {}
+ }
+
+ @RunAsTest(
+ failsWithMessage =
+ "When specifying more than one @TestParameter for a method/constructor, each annotation"
+ + " must have exactly one value. Instead, got 2 values on test1(): [{testEnum: TWO},"
+ + " {testEnum: THREE}]")
+ public static class InvalidTestBecauseRepeatedAnnotationHasMultipleValues {
+ @Test
+ @TestParameters(value = "{testEnum: ONE}")
+ @TestParameters(value = {"{testEnum: TWO}", "{testEnum: THREE}"})
+ public void test1(TestEnum testEnum) {}
+ }
+
+ @RunAsTest(
+ failsWithMessage =
+ "Setting a valuesProvider is not supported for methods/constructors with"
+ + " multiple @TestParameters annotations on test1()")
+ public static class InvalidTestBecauseRepeatedAnnotationHasProvider {
+ @Test
+ @TestParameters(valuesProvider = TestEnumValuesProvider.class)
+ @TestParameters(valuesProvider = TestEnumValuesProvider.class)
+ public void test1(TestEnum testEnum) {}
+ }
+
+ @RunAsTest(
+ failsWithMessage =
+ "Setting @TestParameters.customName is only allowed if there is exactly one YAML string"
+ + " in @TestParameters.value (on test1())")
+ public static class InvalidTestBecauseNamedAnnotationHasMultipleValues {
+ @Test
+ @TestParameters(
+ customName = "custom",
+ value = {"{testEnum: TWO}", "{testEnum: THREE}"})
+ public void test1(TestEnum testEnum) {}
+ }
+
+ @RunAsTest(failsWithMessage = "Test class should have exactly one public constructor")
+ public static class InvalidTestBecausePackagePrivateConstructor {
+ InvalidTestBecausePackagePrivateConstructor() {}
+
+ @Test
+ public void test1() {}
+ }
+
+ @Parameters(name = "{0}")
+ public static Collection<Object[]> parameters() {
+ return Arrays.stream(TestParametersMethodProcessorTest.class.getClasses())
+ .filter(cls -> cls.isAnnotationPresent(RunAsTest.class))
+ .map(
+ cls ->
+ new Object[] {
+ cls.getSimpleName(), cls, cls.getAnnotation(RunAsTest.class).failsWithMessage()
+ })
+ .collect(toImmutableList());
+ }
+
+ private final Class<?> testClass;
+ private final Optional<String> maybeFailureMessage;
+
+ public TestParametersMethodProcessorTest(
+ String name, Class<?> testClass, String failsWithMessage) {
+ this.testClass = testClass;
+ this.maybeFailureMessage =
+ failsWithMessage.isEmpty() ? Optional.empty() : Optional.of(failsWithMessage);
+ }
+
+ @Test
+ public void test_success() throws Exception {
+ assume().that(maybeFailureMessage.isPresent()).isFalse();
+
+ SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures(newTestRunner());
+ }
+
+ @Test
+ public void test_failure() throws Exception {
+ assume().that(maybeFailureMessage.isPresent()).isTrue();
+
+ Exception exception =
+ assertThrows(
+ Exception.class,
+ () -> SharedTestUtilitiesJUnit4.runTestsAndGetFailures(newTestRunner()));
+
+ assertThat(exception).hasMessageThat().contains(maybeFailureMessage.get());
+ }
+
+ private PluggableTestRunner newTestRunner() throws Exception {
+ return new PluggableTestRunner(testClass) {
+ @Override
+ protected TestMethodProcessorList createTestMethodProcessorList() {
+ return TestMethodProcessorList.createNewParameterizedProcessors();
+ }
+ };
+ }
+}
diff --git a/junit5/pom.xml b/junit5/pom.xml
new file mode 100644
index 0000000..345604b
--- /dev/null
+++ b/junit5/pom.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright 2021 Google Inc.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>com.google.testparameterinjector</groupId>
+ <artifactId>test-parameter-injector-parent</artifactId>
+ <version>HEAD-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>test-parameter-injector-junit5</artifactId>
+
+ <name>TestParameterInjector for JUnit5</name>
+
+ <dependencies>
+ <!-- Compile-time dependencies -->
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <version>5.8.1</version>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-engine</artifactId>
+ <version>5.8.1</version>
+ </dependency>
+
+ <!-- Test dependencies -->
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-params</artifactId>
+ <version>5.8.1</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.platform</groupId>
+ <artifactId>junit-platform-launcher</artifactId>
+ <version>1.8.1</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/BaseTestParameterValidator.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/BaseTestParameterValidator.java
new file mode 100644
index 0000000..2386278
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/BaseTestParameterValidator.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static java.lang.Math.min;
+
+import com.google.common.collect.FluentIterable;
+import java.lang.annotation.Annotation;
+import java.util.List;
+
+/**
+ * Default base class for {@link TestParameterValidator}, simplifying how validators can exclude
+ * variable independent test parameters annotations.
+ */
+abstract class BaseTestParameterValidator implements TestParameterValidator {
+
+ @Override
+ public boolean shouldSkip(Context context) {
+ for (List<Class<? extends Annotation>> parameters : getIndependentParameters(context)) {
+ checkArgument(!parameters.isEmpty());
+ // For independent test parameters, the only allowed tests will be those that use the same
+ // Nth specified parameter, except for parameter values that have less specified values than
+ // others.
+
+ // For example, if parameter A has values a1 and a2, parameter B has values b1 and b2, and
+ // parameter C has values c1, c2 and c3, given that A, B and C are independent, the only
+ // tests that will not be skipped will be {(a1, b1, c1), (a2, b2, c2), (a2, b2, c3)},
+ // instead of 12 tests that would constitute their cartesian product.
+
+ // First, find the largest specified value count (parameter C in the example above),
+ // so that we can easily determine which parameter value should be used for validating the
+ // other parameters (e.g. should this test be for (a1, b1, c1), (a2, b2, c2), or
+ // (a2, b2, c3). The test parameter 'C' will be the 'leadingParameter'.
+
+ Class<? extends Annotation> leadingParameter =
+ FluentIterable.from(parameters)
+ .toSortedList(
+ (o1, o2) ->
+ Integer.compare(
+ context.getSpecifiedValues(o1).size(),
+ context.getSpecifiedValues(o2).size()))
+ .reverse()
+ .get(0);
+
+ // Second, determine which index is the current value in the specified value list of
+ // the leading parameter. In the example above, the index of the current value 'c2' of the
+ // leading parameter 'C' would be '1', given the specified values (c1, c2, c3).
+ int leadingParameterValueIndex =
+ getValueIndex(context, leadingParameter, context.getValue(leadingParameter).get());
+ checkState(leadingParameterValueIndex >= 0);
+ // Each independent test parameter should be the same index, or the last available index.
+ // For example, if the parameter is A, and the leading parameter (C) index is 2, the A's index
+ // should be 1, since a2 is the only available value.
+ for (Class<? extends Annotation> parameter : parameters) {
+ List<Object> specifiedValues = context.getSpecifiedValues(parameter);
+ int valueIndex = specifiedValues.indexOf(context.getValue(parameter).get());
+ int requiredValueIndex = min(leadingParameterValueIndex, specifiedValues.size() - 1);
+ if (valueIndex != requiredValueIndex) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private int getValueIndex(Context context, Class<? extends Annotation> annotation, Object value) {
+ return context.getSpecifiedValues(annotation).indexOf(value);
+ }
+
+ /**
+ * Returns a list of TestParameterAnnotation annotated annotation types that are mutually
+ * independent, and therefore the combinations of their values do not need to be tested.
+ */
+ protected abstract List<List<Class<? extends Annotation>>> getIndependentParameters(
+ Context context);
+}
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ByteStringReflection.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ByteStringReflection.java
new file mode 100644
index 0000000..80cac0b
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ByteStringReflection.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2022 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableMap;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Utility methods to interact with com.google.protobuf.ByteString via reflection.
+ *
+ * <p>This is a hack to avoid the open source project to depend on protobuf-lite/javalite, which is
+ * causing conflicts for users (see https://github.com/google/TestParameterInjector/issues/24).
+ */
+final class ByteStringReflection {
+
+ static final Optional<Class<?>> MAYBE_BYTE_STRING_CLASS = maybeGetByteStringClass();
+
+ /** Equivalent of {@code object instanceof ByteString} */
+ static boolean isInstanceOfByteString(Object object) {
+ if (MAYBE_BYTE_STRING_CLASS.isPresent()) {
+ return MAYBE_BYTE_STRING_CLASS.get().isInstance(object);
+ } else {
+ return false;
+ }
+ }
+
+ /** Eqvuivalent of {@code ((ByteString) byteString).toByteArray()} */
+ static byte[] byteStringToByteArray(Object byteString) {
+ return (byte[])
+ invokeByteStringMethod("toByteArray", /* obj= */ byteString, /* args= */ ImmutableMap.of());
+ }
+
+ /**
+ * Eqvuivalent of {@code ByteString.copyFromUtf8(text)}.
+ *
+ * <p>Encodes {@code text} into a sequence of UTF-8 bytes and returns the result as a {@code
+ * ByteString}.
+ */
+ static Object copyFromUtf8(String text) {
+ return invokeByteStringMethod(
+ "copyFromUtf8", /* obj= */ null, /* args= */ ImmutableMap.of(String.class, text));
+ }
+
+ /**
+ * Eqvuivalent of {@code ByteString.copyFrom(bytes)}.
+ *
+ * <p>Copies the given bytes into a {@code ByteString}.
+ */
+ static Object copyFrom(byte[] bytes) {
+ return invokeByteStringMethod(
+ "copyFrom", /* obj= */ null, /* args= */ ImmutableMap.of(byte[].class, bytes));
+ }
+
+ @SuppressWarnings("UseMultiCatch")
+ private static Object invokeByteStringMethod(
+ String methodName, Object obj, ImmutableMap<Class<?>, ?> args) {
+ try {
+ return MAYBE_BYTE_STRING_CLASS
+ .get()
+ .getMethod(methodName, args.keySet().toArray(new Class<?>[0]))
+ .invoke(obj, args.values().toArray());
+ /*
+ * Do not merge the 3 catch blocks below. javac would infer a type of
+ * ReflectiveOperationException, which Animal Sniffer would reject. (Old versions of
+ * Android don't *seem* to mind, but there might be edge cases of which we're unaware.)
+ */
+ } catch (IllegalAccessException e) {
+ throw new LinkageError(String.format("Accessing %s()", methodName), e);
+ } catch (InvocationTargetException e) {
+ throw new LinkageError(String.format("Calling %s()", methodName), e);
+ } catch (NoSuchMethodException e) {
+ throw new LinkageError(String.format("Calling %s()", methodName), e);
+ }
+ }
+
+ private static Optional<Class<?>> maybeGetByteStringClass() {
+ try {
+ return Optional.of(Class.forName("com.google.protobuf.ByteString"));
+ } catch (ClassNotFoundException | LinkageError unused) {
+ return Optional.absent();
+ }
+ }
+
+ private ByteStringReflection() {} // Inhibit instantiation
+}
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ExecutableValidationResult.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ExecutableValidationResult.java
new file mode 100644
index 0000000..4c0c40d
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ExecutableValidationResult.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.Iterables.getOnlyElement;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+
+/**
+ * Value class that captures the result of a validating a single constructor or test method.
+ *
+ * <p>If the validation is not validated by any processor, it will be validated using the default
+ * validator. If a processor validates a constructor/test method, the remaining processors will
+ * *not* be called.
+ */
+@AutoValue
+abstract class ExecutableValidationResult {
+
+ /** Returns true if the properties of the given constructor/test method were validated. */
+ public abstract boolean wasValidated();
+
+ /** Returns the validation errors, if any. */
+ public abstract ImmutableList<Throwable> validationErrors();
+
+ static ExecutableValidationResult notValidated() {
+ return of(/* wasValidated= */ false, /* validationErrors= */ ImmutableList.of());
+ }
+
+ static ExecutableValidationResult validated(Collection<Throwable> errors) {
+ return of(/* wasValidated= */ true, /* validationErrors= */ errors);
+ }
+
+ static ExecutableValidationResult validated(Throwable error) {
+ return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of(error));
+ }
+
+ static ExecutableValidationResult valid() {
+ return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of());
+ }
+
+ private static ExecutableValidationResult of(
+ boolean wasValidated, Collection<Throwable> validationErrors) {
+ checkArgument(wasValidated || validationErrors.isEmpty());
+ return new AutoValue_ExecutableValidationResult(
+ wasValidated, ImmutableList.copyOf(validationErrors));
+ }
+
+ void assertValid() {
+ if (wasValidated() && !validationErrors().isEmpty()) {
+ if (validationErrors().size() == 1) {
+ throw new AssertionError(getOnlyElement(validationErrors()));
+ } else {
+ throw new AssertionError(String.format("Found validation errors: %s", validationErrors()));
+ }
+ }
+ }
+}
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/GenericParameterContext.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/GenericParameterContext.java
new file mode 100644
index 0000000..02e5367
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/GenericParameterContext.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2024 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import static com.google.common.collect.Iterables.getOnlyElement;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Ordering;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Repeatable;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.lang.reflect.Proxy;
+import java.util.NoSuchElementException;
+
+/** A value class that contains extra information about the context of a field or parameter. */
+final class GenericParameterContext {
+
+ private final ImmutableList<Annotation> annotationsOnParameter;
+
+ /** Same contract as #getAnnotations */
+ private final Function<Class<? extends Annotation>, ImmutableList<? extends Annotation>>
+ getAnnotationsFunction;
+
+ private final Class<?> testClass;
+
+ private GenericParameterContext(
+ ImmutableList<Annotation> annotationsOnParameter,
+ Function<Class<? extends Annotation>, ImmutableList<? extends Annotation>>
+ getAnnotationsFunction,
+ Class<?> testClass) {
+ this.annotationsOnParameter = annotationsOnParameter;
+ this.getAnnotationsFunction = getAnnotationsFunction;
+ this.testClass = testClass;
+ }
+
+ // Field.getAnnotationsByType() is not available on old Android SDKs. There is a fallback in that
+ // case in this method.
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ static GenericParameterContext create(Field field, Class<?> testClass) {
+ return new GenericParameterContext(
+ ImmutableList.copyOf(field.getAnnotations()),
+ /* getAnnotationsFunction= */ annotationType -> {
+ try {
+ return ImmutableList.copyOf(field.getAnnotationsByType(annotationType));
+ } catch (NoSuchMethodError ignored) {
+ return getAnnotationsFallback(
+ ImmutableList.copyOf(field.getAnnotations()), annotationType);
+ }
+ },
+ testClass);
+ }
+
+ // Parameter is not available on old Android SDKs, and isn't desugared. That's why this method
+ // should only be called with a fallback.
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ static GenericParameterContext create(Parameter parameter, Class<?> testClass) {
+ return new GenericParameterContext(
+ ImmutableList.copyOf(parameter.getAnnotations()),
+ /* getAnnotationsFunction= */ annotationType ->
+ ImmutableList.copyOf(parameter.getAnnotationsByType(annotationType)),
+ testClass);
+ }
+
+ static GenericParameterContext createWithRepeatableAnnotationsFallback(
+ Annotation[] annotationsOnParameter, Class<?> testClass) {
+ return new GenericParameterContext(
+ ImmutableList.copyOf(annotationsOnParameter),
+ /* getAnnotationsFunction= */ annotationType ->
+ getAnnotationsFallback(ImmutableList.copyOf(annotationsOnParameter), annotationType),
+ testClass);
+ }
+
+ static GenericParameterContext createWithoutParameterAnnotations(Class<?> testClass) {
+ return new GenericParameterContext(
+ /* annotationsOnParameter= */ ImmutableList.of(),
+ /* getAnnotationsFunction= */ annotationType ->
+ getAnnotationsFallback(ImmutableList.of(), annotationType),
+ testClass);
+ }
+
+ /**
+ * Returns the only annotation with the given type on the field or parameter.
+ *
+ * @throws NoSuchElementException if this there is no annotation with the given type
+ * @throws IllegalArgumentException if there are multiple annotations with the given type
+ */
+ @SuppressWarnings("unchecked") // Safe because of the filter operation
+ <A extends Annotation> A getAnnotation(Class<A> annotationType) {
+ return (A)
+ getOnlyElement(
+ FluentIterable.from(annotationsOnParameter)
+ .filter(annotation -> annotation.annotationType().equals(annotationType))
+ .toList());
+ }
+
+ /**
+ * Returns the annotations with the given type on the field or parameter.
+ *
+ * <p>Returns an empty list if this there is no annotation with the given type.
+ */
+ @SuppressWarnings("unchecked") // Safe because of the getAnnotationsFunction contract
+ <A extends Annotation> ImmutableList<A> getAnnotations(Class<A> annotationType) {
+ return (ImmutableList<A>) getAnnotationsFunction.apply(annotationType);
+ }
+
+ /** The class that contains the test that is currently being run. */
+ Class<?> testClass() {
+ return testClass;
+ }
+
+ /** A list of all annotations on the field or parameter. */
+ ImmutableList<Annotation> annotationsOnParameter() {
+ return annotationsOnParameter;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "context(annotationsOnParameter=[%s],testClass=%s)",
+ FluentIterable.from(
+ ImmutableList.sortedCopyOf(
+ Ordering.natural().onResultOf(Annotation::toString), annotationsOnParameter))
+ .transform(
+ annotation -> String.format("@%s", annotation.annotationType().getSimpleName()))
+ .join(Joiner.on(',')),
+ testClass().getSimpleName());
+ }
+
+ private static ImmutableList<Annotation> getAnnotationsFallback(
+ ImmutableList<Annotation> annotationsOnParameter,
+ Class<? extends Annotation> annotationType) {
+ ImmutableList<Annotation> candidates =
+ FluentIterable.from(annotationsOnParameter)
+ .filter(annotation -> annotation.annotationType().equals(annotationType))
+ .toList();
+ if (candidates.isEmpty() && getContainerType(annotationType).isPresent()) {
+ ImmutableList<Annotation> containerAnnotations =
+ getAnnotationsFallback(annotationsOnParameter, getContainerType(annotationType).get());
+ if (containerAnnotations.size() == 1) {
+ Annotation containerAnnotation = getOnlyElement(containerAnnotations);
+ try {
+ Method annotationValueMethod =
+ containerAnnotation.annotationType().getDeclaredMethod("value");
+ annotationValueMethod.setAccessible(true);
+ return ImmutableList.copyOf(
+ (Annotation[])
+ Proxy.getInvocationHandler(containerAnnotation)
+ .invoke(containerAnnotation, annotationValueMethod, null));
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return ImmutableList.of();
+ } else {
+ return candidates;
+ }
+ }
+
+ private static Optional<Class<? extends Annotation>> getContainerType(
+ Class<? extends Annotation> annotationType) {
+ try {
+ Repeatable repeatable = annotationType.getAnnotation(Repeatable.class);
+ if (repeatable == null) {
+ return Optional.absent();
+ } else {
+ return Optional.of(repeatable.value());
+ }
+ } catch (NoClassDefFoundError ignored) {
+ // If @Repeatable does not exist, then there is no container type by definition
+ return Optional.absent();
+ }
+ }
+}
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java
new file mode 100644
index 0000000..130c186
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.collect.Lists;
+import com.google.common.primitives.Primitives;
+import com.google.common.primitives.UnsignedLong;
+import com.google.common.reflect.TypeToken;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.lang.reflect.Array;
+import java.lang.reflect.ParameterizedType;
+import java.math.BigInteger;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.annotation.Nullable;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
+
+/** A helper class for parsing parameter values from strings. */
+final class ParameterValueParsing {
+
+ @SuppressWarnings("unchecked")
+ static <E extends Enum<E>> Enum<?> parseEnum(String str, Class<?> enumType) {
+ return Enum.valueOf((Class<E>) enumType, str);
+ }
+
+ static boolean isValidYamlString(String yamlString) {
+ try {
+ new Yaml(new SafeConstructor(new LoaderOptions())).load(yamlString);
+ return true;
+ } catch (RuntimeException e) {
+ return false;
+ }
+ }
+
+ static Object parseYamlStringToJavaType(String yamlString, Class<?> javaType) {
+ return parseYamlObjectToJavaType(parseYamlStringToObject(yamlString), TypeToken.of(javaType));
+ }
+
+ static Object parseYamlStringToObject(String yamlString) {
+ return new Yaml(new SafeConstructor(new LoaderOptions())).load(yamlString);
+ }
+
+ private static UnsignedLong parseYamlSignedLongToUnsignedLong(long number) {
+ checkState(number >= 0, "%s should be greater than or equal to zero", number);
+ return UnsignedLong.fromLongBits(number);
+ }
+
+ @SuppressWarnings({"unchecked"})
+ static Object parseYamlObjectToJavaType(Object parsedYaml, TypeToken<?> javaType) {
+ // Pass along null so we don't have to worry about it below
+ if (parsedYaml == null) {
+ return null;
+ }
+
+ YamlValueTransformer yamlValueTransformer =
+ new YamlValueTransformer(parsedYaml, javaType.getRawType());
+
+ yamlValueTransformer
+ .ifJavaType(String.class)
+ .supportParsedType(String.class, self -> self)
+ // Also support other primitives because it's easy to accidentally write e.g. a number when
+ // a string was intended in YAML
+ .supportParsedType(Boolean.class, Object::toString)
+ .supportParsedType(Integer.class, Object::toString)
+ .supportParsedType(Long.class, Object::toString)
+ .supportParsedType(Double.class, Object::toString);
+
+ yamlValueTransformer.ifJavaType(Boolean.class).supportParsedType(Boolean.class, self -> self);
+
+ yamlValueTransformer.ifJavaType(Integer.class).supportParsedType(Integer.class, self -> self);
+
+ yamlValueTransformer
+ .ifJavaType(Long.class)
+ .supportParsedType(Long.class, self -> self)
+ .supportParsedType(Integer.class, Integer::longValue);
+
+ yamlValueTransformer
+ .ifJavaType(UnsignedLong.class)
+ .supportParsedType(Long.class, self -> parseYamlSignedLongToUnsignedLong(self.longValue()))
+ .supportParsedType(
+ Integer.class, self -> parseYamlSignedLongToUnsignedLong(self.longValue()))
+ // UnsignedLong::valueOf(BigInteger) will validate that BigInteger is in the valid range and
+ // throws otherwise.
+ .supportParsedType(BigInteger.class, UnsignedLong::valueOf);
+
+ yamlValueTransformer
+ .ifJavaType(BigInteger.class)
+ .supportParsedType(Long.class, self -> BigInteger.valueOf(self.longValue()))
+ .supportParsedType(Integer.class, self -> BigInteger.valueOf(self.longValue()))
+ .supportParsedType(BigInteger.class, self -> self);
+
+ yamlValueTransformer
+ .ifJavaType(Float.class)
+ .supportParsedType(Float.class, self -> self)
+ .supportParsedType(Double.class, Double::floatValue)
+ .supportParsedType(Integer.class, Integer::floatValue)
+ .supportParsedType(String.class, Float::valueOf);
+
+ yamlValueTransformer
+ .ifJavaType(Double.class)
+ .supportParsedType(Double.class, self -> self)
+ .supportParsedType(Integer.class, Integer::doubleValue)
+ .supportParsedType(Long.class, Long::doubleValue)
+ .supportParsedType(String.class, Double::valueOf);
+
+ yamlValueTransformer
+ .ifJavaType(Enum.class)
+ .supportParsedType(
+ String.class, str -> ParameterValueParsing.parseEnum(str, javaType.getRawType()));
+
+ yamlValueTransformer
+ .ifJavaType(byte[].class)
+ .supportParsedType(byte[].class, self -> self)
+ // Uses String based charset because StandardCharsets was not introduced until later
+ // versions of Android
+ // See https://developer.android.com/reference/java/nio/charset/StandardCharsets.
+ .supportParsedType(String.class, s -> s.getBytes(Charset.forName("UTF-8")));
+
+ if (ByteStringReflection.MAYBE_BYTE_STRING_CLASS.isPresent()) {
+ yamlValueTransformer
+ .ifJavaType((Class<Object>) ByteStringReflection.MAYBE_BYTE_STRING_CLASS.get())
+ .supportParsedType(String.class, ByteStringReflection::copyFromUtf8)
+ .supportParsedType(byte[].class, ByteStringReflection::copyFrom);
+ }
+
+ // Added mainly for protocol buffer parsing
+ yamlValueTransformer
+ .ifJavaType(List.class)
+ .supportParsedType(
+ List.class,
+ list ->
+ Lists.transform(
+ list,
+ e ->
+ parseYamlObjectToJavaType(
+ e, getGenericParameterType(javaType, /* parameterIndex= */ 0))));
+ yamlValueTransformer
+ .ifJavaType(Map.class)
+ .supportParsedType(Map.class, map -> parseYamlMapToJavaMap(map, javaType));
+
+ return yamlValueTransformer.transformedJavaValue();
+ }
+
+ private static Map<?, ?> parseYamlMapToJavaMap(Map<?, ?> map, TypeToken<?> javaType) {
+ Map<Object, Object> returnedMap = new LinkedHashMap<>();
+ for (Entry<?, ?> entry : map.entrySet()) {
+ returnedMap.put(
+ parseYamlObjectToJavaType(
+ entry.getKey(), getGenericParameterType(javaType, /* parameterIndex= */ 0)),
+ parseYamlObjectToJavaType(
+ entry.getValue(), getGenericParameterType(javaType, /* parameterIndex= */ 1)));
+ }
+ return returnedMap;
+ }
+
+ private static TypeToken<?> getGenericParameterType(TypeToken<?> typeToken, int parameterIndex) {
+ checkArgument(
+ typeToken.getType() instanceof ParameterizedType,
+ "Could not parse the generic parameter of type %s",
+ typeToken);
+
+ ParameterizedType parameterizedType = (ParameterizedType) typeToken.getType();
+ return TypeToken.of(parameterizedType.getActualTypeArguments()[parameterIndex]);
+ }
+
+ private static final class YamlValueTransformer {
+ private final Object parsedYaml;
+ private final Class<?> javaType;
+ @Nullable private Object transformedJavaValue;
+
+ YamlValueTransformer(Object parsedYaml, Class<?> javaType) {
+ this.parsedYaml = parsedYaml;
+ this.javaType = javaType;
+ }
+
+ <JavaT> SupportedJavaType<JavaT> ifJavaType(Class<JavaT> supportedJavaType) {
+ return new SupportedJavaType<>(supportedJavaType);
+ }
+
+ Object transformedJavaValue() {
+ checkArgument(
+ transformedJavaValue != null,
+ "Could not map YAML value %s (class = %s) to java class %s",
+ parsedYaml,
+ parsedYaml.getClass(),
+ javaType);
+ return transformedJavaValue;
+ }
+
+ final class SupportedJavaType<JavaT> {
+
+ private final Class<JavaT> supportedJavaType;
+
+ private SupportedJavaType(Class<JavaT> supportedJavaType) {
+ this.supportedJavaType = supportedJavaType;
+ }
+
+ @SuppressWarnings("unchecked")
+ @CanIgnoreReturnValue
+ <ParsedYamlT> SupportedJavaType<JavaT> supportParsedType(
+ Class<ParsedYamlT> parsedYamlType, Function<ParsedYamlT, JavaT> transformation) {
+ if (Primitives.wrap(supportedJavaType).isAssignableFrom(Primitives.wrap(javaType))) {
+ if (Primitives.wrap(parsedYamlType).isInstance(parsedYaml)) {
+ checkState(
+ transformedJavaValue == null,
+ "This case is already handled. This is a bug in"
+ + " testparameterinjector.TestParametersMethodProcessor.");
+ try {
+ transformedJavaValue = checkNotNull(transformation.apply((ParsedYamlT) parsedYaml));
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Could not map YAML value %s (class = %s) to java class %s",
+ parsedYaml, parsedYaml.getClass(), javaType),
+ e);
+ }
+ }
+ }
+
+ return this;
+ }
+ }
+ }
+
+ static String formatTestNameString(Optional<String> parameterName, @Nullable Object value) {
+ Object unwrappedValue;
+ Optional<String> customName;
+
+ if (value instanceof TestParameterValue) {
+ TestParameterValue tpValue = (TestParameterValue) value;
+ unwrappedValue = tpValue.getWrappedValue();
+ customName = tpValue.getCustomName();
+ } else {
+ unwrappedValue = value;
+ customName = Optional.absent();
+ }
+
+ String result = customName.or(() -> valueAsString(unwrappedValue));
+ if (parameterName.isPresent() && !customName.isPresent()) {
+ if (unwrappedValue == null
+ ||
+ // Primitives are often ambiguous
+ Primitives.unwrap(unwrappedValue.getClass()).isPrimitive()
+ // Ambiguous String cases
+ || unwrappedValue.equals("null")
+ || (unwrappedValue instanceof CharSequence
+ && CharMatcher.anyOf("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
+ .matchesNoneOf((CharSequence) unwrappedValue))) {
+ // Prefix the parameter value with its field name. This is to avoid test names
+ // such as myMethod_success[true,false,2]. Instead, it'll be
+ // myMethod_success[dryRun=true,experimentFlag=false,retries=2].
+ result = String.format("%s=%s", parameterName.get(), valueAsString(unwrappedValue));
+ }
+ }
+ return result.trim().replaceAll("\\s+", " ");
+ }
+
+ private static String valueAsString(Object value) {
+ if (value != null && value.getClass().isArray()) {
+ StringBuilder resultBuider = new StringBuilder();
+ resultBuider.append("[");
+ for (int i = 0; i < Array.getLength(value); i++) {
+ if (i > 0) {
+ resultBuider.append(", ");
+ }
+ resultBuider.append(Array.get(value, i));
+ }
+ resultBuider.append("]");
+ return resultBuider.toString();
+ } else if (ByteStringReflection.isInstanceOfByteString(value)) {
+ return Arrays.toString(ByteStringReflection.byteStringToByteArray(value));
+ } else {
+ return String.valueOf(value);
+ }
+ }
+
+ private ParameterValueParsing() {}
+}
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java
new file mode 100644
index 0000000..7ed3412
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ContiguousSet;
+import com.google.common.collect.DiscreteDomain;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.Range;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.Nullable;
+
+/** A POJO containing information about a test (name and anotations). */
+@AutoValue
+abstract class TestInfo {
+
+ /**
+ * The maximum amount of characters that {@link #getName()} can have.
+ *
+ * <p>See b/168325767 for the reason behind this. tl;dr the name is put into a Unix file with max
+ * 255 characters. The surrounding constant characters take up 31 characters. The max is reduced
+ * by an additional 24 characters to account for future changes.
+ */
+ static final int MAX_TEST_NAME_LENGTH = 200;
+
+ public abstract Method getMethod();
+
+ /**
+ * The test class that is being run.
+ *
+ * <p>Note that this is not always the same as the class that declares {@link #getMethod()}
+ * because test methods can be inherited.
+ */
+ public abstract Class<?> getTestClass();
+
+ public final String getName() {
+ if (getParameters().isEmpty()) {
+ return getMethod().getName();
+ } else {
+ return String.format(
+ "%s[%s]",
+ getMethod().getName(),
+ FluentIterable.from(getParameters())
+ .transform(TestInfoParameter::getValueInTestName)
+ .join(Joiner.on(",")));
+ }
+ }
+
+ abstract ImmutableList<TestInfoParameter> getParameters();
+
+ public abstract ImmutableList<Annotation> getAnnotations();
+
+ @Nullable
+ public final <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
+ for (Annotation annotation : getAnnotations()) {
+ if (annotationClass.isInstance(annotation)) {
+ return annotationClass.cast(annotation);
+ }
+ }
+ return null;
+ }
+
+ final TestInfo withExtraParameters(List<TestInfoParameter> parameters) {
+ return new AutoValue_TestInfo(
+ getMethod(),
+ getTestClass(),
+ ImmutableList.<TestInfoParameter>builder()
+ .addAll(this.getParameters())
+ .addAll(parameters)
+ .build(),
+ getAnnotations());
+ }
+
+ final TestInfo withExtraAnnotation(Annotation annotation) {
+ ImmutableList<Annotation> newAnnotations =
+ ImmutableList.<Annotation>builder().addAll(this.getAnnotations()).add(annotation).build();
+ return new AutoValue_TestInfo(getMethod(), getTestClass(), getParameters(), newAnnotations);
+ }
+
+ /**
+ * Returns a new TestInfo instance with updated parameter names.
+ *
+ * @param parameterWithIndexToNewName A function of the parameter and its index in the {@link
+ * #getParameters()} list to the new name.
+ */
+ private TestInfo withUpdatedParameterNames(
+ Java8BiFunction<TestInfoParameter, Integer, String> parameterWithIndexToNewName) {
+ return new AutoValue_TestInfo(
+ getMethod(),
+ getTestClass(),
+ FluentIterable.from(
+ ContiguousSet.create(
+ Range.closedOpen(0, getParameters().size()), DiscreteDomain.integers()))
+ .transform(
+ parameterIndex -> {
+ TestInfoParameter parameter = getParameters().get(parameterIndex);
+ return parameter.withValueInTestName(
+ parameterWithIndexToNewName.apply(parameter, parameterIndex));
+ })
+ .toList(),
+ getAnnotations());
+ }
+
+ public static TestInfo legacyCreate(
+ Method method, Class<?> testClass, String name, List<Annotation> annotations) {
+ return new AutoValue_TestInfo(
+ method, testClass, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations));
+ }
+
+ static TestInfo createWithoutParameters(
+ Method method, Class<?> testClass, List<Annotation> annotations) {
+ return new AutoValue_TestInfo(
+ method, testClass, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations));
+ }
+
+ static ImmutableList<TestInfo> shortenNamesIfNecessary(List<TestInfo> testInfos) {
+ if (FluentIterable.from(testInfos)
+ .anyMatch(info -> info.getName().length() > MAX_TEST_NAME_LENGTH)) {
+ int numberOfParameters = testInfos.get(0).getParameters().size();
+
+ if (numberOfParameters == 0) {
+ return ImmutableList.copyOf(testInfos);
+ } else {
+ Set<Integer> parameterIndicesThatNeedUpdate =
+ FluentIterable.from(
+ ContiguousSet.create(
+ Range.closedOpen(0, numberOfParameters), DiscreteDomain.integers()))
+ .filter(
+ parameterIndex ->
+ FluentIterable.from(testInfos)
+ .anyMatch(
+ info ->
+ info.getParameters()
+ .get(parameterIndex)
+ .getValueInTestName()
+ .length()
+ > getMaxCharactersPerParameter(info, numberOfParameters)))
+ .toSet();
+
+ return FluentIterable.from(testInfos)
+ .transform(
+ info ->
+ info.withUpdatedParameterNames(
+ (parameter, parameterIndex) ->
+ parameterIndicesThatNeedUpdate.contains(parameterIndex)
+ ? getShortenedName(
+ parameter,
+ getMaxCharactersPerParameter(info, numberOfParameters))
+ : info.getParameters().get(parameterIndex).getValueInTestName()))
+ .toList();
+ }
+ } else {
+ return ImmutableList.copyOf(testInfos);
+ }
+ }
+
+ private static int getMaxCharactersPerParameter(TestInfo testInfo, int numberOfParameters) {
+ int maxLengthOfAllParameters =
+ // Subtract 2 characters for square brackets
+ MAX_TEST_NAME_LENGTH - testInfo.getMethod().getName().length() - 2;
+
+ // Subtract 4 characters to leave place for joining commas and the parameter index.
+ return maxLengthOfAllParameters / numberOfParameters - 4;
+ }
+
+ static ImmutableList<TestInfo> deduplicateTestNames(List<TestInfo> testInfos) {
+ long uniqueTestNameCount =
+ FluentIterable.from(testInfos).transform(TestInfo::getName).toSet().size();
+ if (testInfos.size() == uniqueTestNameCount) {
+ // Return early if there are no duplicates
+ return ImmutableList.copyOf(testInfos);
+ } else {
+ return deduplicateWithNumberPrefixes(maybeAddTypesIfDuplicate(testInfos));
+ }
+ }
+
+ private static String getShortenedName(
+ TestInfoParameter parameter, int maxCharactersPerParameter) {
+ if (maxCharactersPerParameter < 4) {
+ // Not enough characters for "..." suffix
+ return String.valueOf(parameter.getIndexInValueSource() + 1);
+ } else {
+ String shortenedName =
+ parameter.getValueInTestName().length() > maxCharactersPerParameter
+ ? parameter.getValueInTestName().substring(0, maxCharactersPerParameter - 3) + "..."
+ : parameter.getValueInTestName();
+ return String.format("%s.%s", parameter.getIndexInValueSource() + 1, shortenedName);
+ }
+ }
+
+ private static ImmutableList<TestInfo> maybeAddTypesIfDuplicate(List<TestInfo> testInfos) {
+ Multimap<String, TestInfo> testNameToInfo =
+ MultimapBuilder.linkedHashKeys().arrayListValues().build();
+ for (TestInfo testInfo : testInfos) {
+ testNameToInfo.put(testInfo.getName(), testInfo);
+ }
+
+ return FluentIterable.from(testNameToInfo.keySet())
+ .transformAndConcat(
+ testName -> {
+ Collection<TestInfo> matchedInfos = testNameToInfo.get(testName);
+ if (matchedInfos.size() == 1) {
+ // There was only one method with this name, so no deduplication is necessary
+ return matchedInfos;
+ } else {
+ // Found tests with duplicate test names
+ int numParameters = matchedInfos.iterator().next().getParameters().size();
+ Set<Integer> indicesThatShouldGetSuffix =
+ // Find parameter indices for which a suffix would allow the reader to
+ // differentiate
+ FluentIterable.from(
+ ContiguousSet.create(
+ Range.closedOpen(0, numParameters), DiscreteDomain.integers()))
+ .filter(
+ parameterIndex ->
+ FluentIterable.from(matchedInfos)
+ .transform(
+ info ->
+ getTypeSuffix(
+ info.getParameters()
+ .get(parameterIndex)
+ .getValue()))
+ .toSet()
+ .size()
+ > 1)
+ .toSet();
+
+ return FluentIterable.from(matchedInfos)
+ .transform(
+ testInfo ->
+ testInfo.withUpdatedParameterNames(
+ (parameter, parameterIndex) ->
+ indicesThatShouldGetSuffix.contains(parameterIndex)
+ ? parameter.getValueInTestName()
+ + getTypeSuffix(parameter.getValue())
+ : parameter.getValueInTestName()));
+ }
+ })
+ .toList();
+ }
+
+ private static String getTypeSuffix(@Nullable Object value) {
+ if (value == null) {
+ return " (null reference)";
+ } else {
+ return String.format(" (%s)", value.getClass().getSimpleName());
+ }
+ }
+
+ private static ImmutableList<TestInfo> deduplicateWithNumberPrefixes(
+ ImmutableList<TestInfo> testInfos) {
+ long uniqueTestNameCount =
+ FluentIterable.from(testInfos).transform(TestInfo::getName).toSet().size();
+ if (testInfos.size() == uniqueTestNameCount) {
+ return ImmutableList.copyOf(testInfos);
+ } else {
+ // There are still duplicates, even after adding type suffixes. As a last resort: add a
+ // counter to all parameters to guarantee that each case is unique.
+ return FluentIterable.from(testInfos)
+ .transform(
+ testInfo ->
+ testInfo.withUpdatedParameterNames(
+ (parameter, parameterIndex) ->
+ String.format(
+ "%s.%s",
+ parameter.getIndexInValueSource() + 1,
+ parameter.getValueInTestName())))
+ .toList();
+ }
+ }
+
+ @AutoValue
+ abstract static class TestInfoParameter {
+
+ abstract String getValueInTestName();
+
+ @Nullable
+ abstract Object getValue();
+
+ /**
+ * The index of this parameter value in the list of all values provided by the provider that
+ * returned this value.
+ */
+ abstract int getIndexInValueSource();
+
+ final TestInfoParameter withValueInTestName(String newValueInTestName) {
+ return create(newValueInTestName, getValue(), getIndexInValueSource());
+ }
+
+ static TestInfoParameter create(
+ String valueInTestName, @Nullable Object value, int indexInValueSource) {
+ checkArgument(indexInValueSource >= 0);
+ return new AutoValue_TestInfo_TestInfoParameter(
+ checkNotNull(valueInTestName), value, indexInValueSource);
+ }
+ }
+
+ /** Copy of Java8's java.util.BiFunction which is not available in older versions of the JDK */
+ interface Java8BiFunction<I, J, K> {
+ K apply(I a, J b);
+ }
+}
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessor.java
new file mode 100644
index 0000000..48e9a5e
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessor.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import com.google.common.base.Optional;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.List;
+
+/**
+ * Interface to change the list of methods used in a test.
+ *
+ * <p>Note: Implementations of this interface are expected to be immutable, i.e. they no longer
+ * change after construction.
+ */
+interface TestMethodProcessor {
+
+ /** Allows to transform the test information (name and annotations). */
+ List<TestInfo> calculateTestInfos(TestInfo originalTest);
+
+ /**
+ * If this processor can handle the given constructor, returns the parameters with which it should
+ * be invoked.
+ *
+ * <p>This method is never called for a parameterless constructor.
+ */
+ Optional<List<Object>> maybeGetConstructorParameters(
+ Constructor<?> constructor, TestInfo testInfo);
+
+ /**
+ * If this processor can handle the given test, returns the parameters with which {@code
+ * testInfo.getMethod()} should be invoked.
+ *
+ * <p>This method is never called for a parameterless {@code testInfo.getMethod()}.
+ */
+ Optional<List<Object>> maybeGetTestMethodParameters(TestInfo testInfo);
+
+ /**
+ * Optionally process the test instance right after construction to ready it for the given test
+ * instance.
+ */
+ void postProcessTestInstance(Object testInstance, TestInfo testInfo);
+
+ /** Optionally validates the given constructor. */
+ ExecutableValidationResult validateConstructor(Constructor<?> constructor);
+
+ /**
+ * Optionally validates the given method.
+ *
+ * <p>Note that the given method is not necessarily declared in the given class because test
+ * methods can be inherited.
+ */
+ ExecutableValidationResult validateTestMethod(Method testMethod, Class<?> testClass);
+}
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessorList.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessorList.java
new file mode 100644
index 0000000..d1020c8
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessorList.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Combined version of all {@link TestMethodProcessor} implementations that this package supports.
+ */
+final class TestMethodProcessorList {
+
+ private final ImmutableList<TestMethodProcessor> testMethodProcessors;
+
+ private TestMethodProcessorList(ImmutableList<TestMethodProcessor> testMethodProcessors) {
+ this.testMethodProcessors = testMethodProcessors;
+ }
+
+ /**
+ * Returns a TestMethodProcessorList that supports all features that this package supports, except
+ * the following legacy features:
+ *
+ * <ul>
+ * <li>No support for {@link org.junit.runners.Parameterized}
+ * <li>No support for class and method-level parameters, except for @TestParameters
+ * </ul>
+ */
+ public static TestMethodProcessorList createNewParameterizedProcessors() {
+ return new TestMethodProcessorList(
+ ImmutableList.of(
+ new TestParametersMethodProcessor(),
+ TestParameterAnnotationMethodProcessor.onlyForFieldsAndParameters()));
+ }
+
+ static TestMethodProcessorList empty() {
+ return new TestMethodProcessorList(ImmutableList.of());
+ }
+
+ /**
+ * Calculates the TestInfo instances for the given test method. Each TestInfo corresponds to a
+ * single test.
+ *
+ * <p>The returned list always contains at least one element. If there is no parameterization,
+ * this would be the TestInfo for running the test method without parameters.
+ */
+ public List<TestInfo> calculateTestInfos(Method testMethod, Class<?> testClass) {
+ List<TestInfo> testInfos =
+ ImmutableList.of(
+ TestInfo.createWithoutParameters(
+ testMethod, testClass, ImmutableList.copyOf(testMethod.getAnnotations())));
+
+ for (final TestMethodProcessor testMethodProcessor : testMethodProcessors) {
+ List<TestInfo> list = new ArrayList<>();
+ for (TestInfo lastTestInfo : testInfos) {
+ list.addAll(testMethodProcessor.calculateTestInfos(lastTestInfo));
+ }
+ testInfos = list;
+ }
+
+ testInfos = TestInfo.deduplicateTestNames(TestInfo.shortenNamesIfNecessary(testInfos));
+
+ return testInfos;
+ }
+
+ /**
+ * Returns the parameters with which it should be invoked.
+ *
+ * <p>This method is never called for a parameterless constructor.
+ */
+ public List<Object> getConstructorParameters(Constructor<?> constructor, TestInfo testInfo) {
+ return FluentIterable.from(testMethodProcessors)
+ .transform(processor -> processor.maybeGetConstructorParameters(constructor, testInfo))
+ .filter(Optional::isPresent)
+ .transform(Optional::get)
+ .first()
+ .or(
+ () -> {
+ throw new IllegalStateException(
+ String.format(
+ "Could not generate parameter values for %s. Did you forget an annotation?",
+ constructor));
+ });
+ }
+
+ /**
+ * Returns the parameters with which {@code testInfo.getMethod()} should be invoked.
+ *
+ * <p>This method is never called for a parameterless {@code testInfo.getMethod()}.
+ */
+ public List<Object> getTestMethodParameters(TestInfo testInfo) {
+ return FluentIterable.from(testMethodProcessors)
+ .transform(processor -> processor.maybeGetTestMethodParameters(testInfo))
+ .filter(Optional::isPresent)
+ .transform(Optional::get)
+ .first()
+ .or(
+ () -> {
+ throw new IllegalStateException(
+ String.format(
+ "Could not generate parameter values for %s. Did you forget an annotation?",
+ testInfo.getMethod()));
+ });
+ }
+
+ /**
+ * Optionally process the test instance right after construction to ready it for the given test.
+ */
+ public void postProcessTestInstance(Object testInstance, TestInfo testInfo) {
+ for (TestMethodProcessor testMethodProcessor : testMethodProcessors) {
+ testMethodProcessor.postProcessTestInstance(testInstance, testInfo);
+ }
+ }
+
+ /** Optionally validates the given constructor. */
+ public ExecutableValidationResult validateConstructor(Constructor<?> constructor) {
+ return FluentIterable.from(testMethodProcessors)
+ .transform(processor -> processor.validateConstructor(constructor))
+ .firstMatch(ExecutableValidationResult::wasValidated)
+ .or(ExecutableValidationResult.notValidated());
+ }
+
+ /** Optionally validates the given method. */
+ public ExecutableValidationResult validateTestMethod(Method testMethod, Class<?> testClass) {
+ return FluentIterable.from(testMethodProcessors)
+ .transform(processor -> processor.validateTestMethod(testMethod, testClass))
+ .firstMatch(ExecutableValidationResult::wasValidated)
+ .or(ExecutableValidationResult.notValidated());
+ }
+}
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java
new file mode 100644
index 0000000..c26c8ed
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Lists;
+import com.google.common.primitives.Primitives;
+import com.google.testing.junit.testparameterinjector.junit5.TestParameter.InternalImplementationOfThisParameter;
+import com.google.testing.junit.testparameterinjector.junit5.TestParameterValuesProvider.Context;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Test parameter annotation that defines the values that a single parameter can have.
+ *
+ * <p>For enums and booleans, the values can be automatically derived as all possible values:
+ *
+ * <pre>
+ * {@literal @}Test
+ * public void test1(@TestParameter MyEnum myEnum, @TestParameter boolean myBoolean) {
+ * // ... will run for [(A,false), (A,true), (B,false), (B,true), (C,false), (C,true)]
+ * }
+ *
+ * enum MyEnum { A, B, C }
+ * </pre>
+ *
+ * <p>The values can be explicitly defined as a parsed string:
+ *
+ * <pre>
+ * public void test1(
+ * {@literal @}TestParameter({"{name: Hermione, age: 18}", "{name: Dumbledore, age: 115}"})
+ * UpdateCharacterRequest request,
+ * {@literal @}TestParameter({"1", "4"}) int bookNumber) {
+ * // ... will run for [(Hermione,1), (Hermione,4), (Dumbledore,1), (Dumbledore,4)]
+ * }
+ * </pre>
+ *
+ * <p>For more flexibility, see {{@link #valuesProvider()}}. If you don't want to test all possible
+ * combinations but instead want to specify sets of parameters explicitly, use @{@link
+ * TestParameters}.
+ */
+@Retention(RUNTIME)
+@Target({FIELD, PARAMETER})
+@TestParameterAnnotation(valueProvider = InternalImplementationOfThisParameter.class)
+public @interface TestParameter {
+
+ /**
+ * Array of stringified values for the annotated type.
+ *
+ * <p>Types that are supported:
+ *
+ * <ul>
+ * <li>String: No parsing happens
+ * <li>boolean: Specified as YAML boolean
+ * <li>long and int: Specified as YAML integer
+ * <li>float and double: Specified as YAML floating point or integer
+ * <li>Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()}
+ * <li>Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML bytes
+ * (example: "!!binary 'ZGF0YQ=='")
+ * </ul>
+ *
+ * <p>For dynamic sets of parameters or parameter types that are not supported here, use {@link
+ * #valuesProvider()} and leave this field empty.
+ *
+ * <p>For examples, see {@link TestParameter}.
+ */
+ String[] value() default {};
+
+ /**
+ * Sets a provider that will return a list of parameter values.
+ *
+ * <p>If this field is set, {@link #value()} must be empty and vice versa.
+ *
+ * <p><b>Example</b>
+ *
+ * <pre>
+ * import com.google.testing.junit.testparameterinjector.junit5.TestParameterValuesProvider;
+ *
+ * {@literal @}Test
+ * public void matchesAllOf_throwsOnNull(
+ * {@literal @}TestParameter(valuesProvider = CharMatcherProvider.class)
+ * CharMatcher charMatcher) {
+ * assertThrows(NullPointerException.class, () -&gt; charMatcher.matchesAllOf(null));
+ * }
+ *
+ * private static final class CharMatcherProvider extends TestParameterValuesProvider {
+ * {@literal @}Override
+ * public {@literal List<CharMatcher>} provideValues(Context context) {
+ * return ImmutableList.of(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace());
+ * }
+ * }
+ * </pre>
+ */
+ Class<? extends TestParameterValuesProvider> valuesProvider() default
+ DefaultTestParameterValuesProvider.class;
+
+ /**
+ * Interface for custom providers of test parameter values.
+ *
+ * @deprecated Use {@link
+ * com.google.testing.junit.testparameterinjector.junit5.TestParameterValuesProvider} instead. The
+ * replacement implements this same interface, but with an additional Context parameter.
+ */
+ @Deprecated
+ interface TestParameterValuesProvider {
+ List<?> provideValues();
+
+ /**
+ * Wraps the given value in an object that allows you to give the parameter value a different
+ * name. The TestParameterInjector framework will recognize the returned {@link
+ * TestParameterValue} instances and unwrap them at injection time.
+ *
+ * <p>Usage: {@code value(file.content).withName(file.name)}.
+ *
+ * <p>Do not override this method.
+ */
+ default TestParameterValue value(@javax.annotation.Nullable Object wrappedValue) {
+ return TestParameterValue.wrap(wrappedValue);
+ }
+ }
+
+ /** Default {@link TestParameterValuesProvider} implementation that does nothing. */
+ class DefaultTestParameterValuesProvider implements TestParameterValuesProvider {
+ @Override
+ public List<Object> provideValues() {
+ return com.google.common.collect.ImmutableList.of();
+ }
+ }
+
+ /** Implementation of this parameter annotation. */
+ final class InternalImplementationOfThisParameter implements TestParameterValueProvider {
+ @Override
+ public List<Object> provideValues(
+ Annotation uncastAnnotation,
+ Optional<Class<?>> maybeParameterClass,
+ GenericParameterContext context) {
+ TestParameter annotation = (TestParameter) uncastAnnotation;
+ Class<?> parameterClass = getValueType(annotation.annotationType(), maybeParameterClass);
+
+ boolean valueIsSet = annotation.value().length > 0;
+ boolean valuesProviderIsSet =
+ !annotation.valuesProvider().equals(DefaultTestParameterValuesProvider.class);
+ checkState(
+ !(valueIsSet && valuesProviderIsSet),
+ "It is not allowed to specify both value and valuesProvider on annotation %s",
+ annotation);
+
+ if (valueIsSet) {
+ return Lists.newArrayList(
+ FluentIterable.from(annotation.value())
+ .transform(v -> parseStringValue(v, parameterClass))
+ .toArray(Object.class));
+ } else if (valuesProviderIsSet) {
+ return getValuesFromProvider(annotation.valuesProvider(), new Context(context));
+ } else {
+ if (Enum.class.isAssignableFrom(parameterClass)) {
+ return Arrays.asList((Object[]) parameterClass.asSubclass(Enum.class).getEnumConstants());
+ } else if (Primitives.wrap(parameterClass).equals(Boolean.class)) {
+ return Arrays.asList(false, true);
+ } else {
+ throw new IllegalStateException(
+ String.format(
+ "A @TestParameter without values can only be placed at an enum or a boolean, but"
+ + " was placed by a %s",
+ parameterClass));
+ }
+ }
+ }
+
+ @Override
+ public Class<?> getValueType(
+ Class<? extends Annotation> annotationType, Optional<Class<?>> parameterClass) {
+ if (parameterClass.isPresent()) {
+ return parameterClass.get();
+ }
+ throw new AssertionError(
+ String.format(
+ "An empty parameter class should not be possible since"
+ + " @TestParameter can only target FIELD or PARAMETER, both"
+ + " of which are supported for annotation %s.",
+ annotationType));
+ }
+
+ private static Object parseStringValue(String value, Class<?> parameterClass) {
+ if (parameterClass.equals(String.class)) {
+ return value.equals("null") ? null : value;
+ } else if (Enum.class.isAssignableFrom(parameterClass)) {
+ return value.equals("null") ? null : ParameterValueParsing.parseEnum(value, parameterClass);
+ } else {
+ return ParameterValueParsing.parseYamlStringToJavaType(value, parameterClass);
+ }
+ }
+
+ private static List<Object> getValuesFromProvider(
+ Class<? extends TestParameterValuesProvider> valuesProvider, Context context) {
+ try {
+ Constructor<? extends TestParameterValuesProvider> constructor =
+ valuesProvider.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ TestParameterValuesProvider instance = constructor.newInstance();
+ if (instance
+ instanceof com.google.testing.junit.testparameterinjector.junit5.TestParameterValuesProvider) {
+ return new ArrayList<>(
+ ((com.google.testing.junit.testparameterinjector.junit5.TestParameterValuesProvider)
+ instance)
+ .provideValues(context));
+ } else {
+ return new ArrayList<>(instance.provideValues());
+ }
+ } catch (NoSuchMethodException e) {
+ if (!Modifier.isStatic(valuesProvider.getModifiers()) && valuesProvider.isMemberClass()) {
+ throw new IllegalStateException(
+ String.format(
+ "Could not find a no-arg constructor for %s, probably because it is a not-static"
+ + " inner class. You can fix this by making %s static.",
+ valuesProvider.getSimpleName(), valuesProvider.getSimpleName()),
+ e);
+ } else {
+ throw new IllegalStateException(
+ String.format(
+ "Could not find a no-arg constructor for %s.", valuesProvider.getSimpleName()),
+ e);
+ }
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException(e);
+ } catch (Exception e) {
+ // Catch any unchecked exception that may come from `provideValues(Context)`
+ if (e instanceof RuntimeException) {
+ throw (RuntimeException) e;
+ } else {
+ throw new IllegalStateException(e);
+ }
+ }
+ }
+ }
+}
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotation.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotation.java
new file mode 100644
index 0000000..255e4f5
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotation.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Verify.verify;
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Primitives;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.lang.reflect.Array;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+
+/**
+ * Annotation to define a test annotation used to have parameterized methods, in either a
+ * parameterized or non parameterized test.
+ *
+ * <p>Parameterized tests enabled by defining a annotation (see {@link TestParameter} as an example)
+ * for the type of the parameter, defining a member variable annotated with this annotation, and
+ * specifying the parameter with the same annotation for each test, or for the whole class, for
+ * example:
+ *
+ * <pre>{@code
+ * @RunWith(TestParameterInjector.class)
+ * public class ColorTest {
+ * @Retention(RUNTIME)
+ * @Target({TYPE, METHOD, FIELD})
+ * @TestParameterAnnotation
+ * public @interface ColorParameter {
+ * Color[] value() default {};
+ * }
+ *
+ * @ColorParameter({BLUE, WHITE, RED}) private Color color;
+ *
+ * @Test
+ * public void test() {
+ * assertThat(paint(color)).isSuccessful();
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>An alternative is to use a method parameter for injection:
+ *
+ * <pre>{@code
+ * @RunWith(TestParameterInjector.class)
+ * public class ColorTest {
+ * @Retention(RUNTIME)
+ * @Target({TYPE, METHOD, FIELD})
+ * @TestParameterAnnotation
+ * public @interface ColorParameter {
+ * Color[] value() default {};
+ * }
+ *
+ * @Test
+ * @ColorParameter({BLUE, WHITE, RED})
+ * public void test(Color color) {
+ * assertThat(paint(color)).isSuccessful();
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>Yet another alternative is to use a method parameter for injection, but with the annotation
+ * specified on the parameter itself, which helps when multiple arguments share the
+ * same @TestParameterAnnotation annotation.
+ *
+ * <pre>{@code
+ * @RunWith(TestParameterInjector.class)
+ * public class ColorTest {
+ * @Retention(RUNTIME)
+ * @Target({TYPE, METHOD, FIELD})
+ * @TestParameterAnnotation
+ * public @interface ColorParameter {
+ * Color[] value() default {};
+ * }
+ *
+ * @Test
+ * public void test(@ColorParameter({BLUE, WHITE}) Color color1,
+ * @ColorParameter({WHITE, RED}) Color color2) {
+ * assertThat(paint(color1. color2)).isSuccessful();
+ * }
+ * }
+ * }</pre>
+ *
+ * <p>Class constructors can also be annotated with @TestParameterAnnotation annotations, as shown
+ * below:
+ *
+ * <pre>{@code
+ * @RunWith(TestParameterInjector.class)
+ * public class ColorTest {
+ * @Retention(RUNTIME)
+ * @Target({TYPE, METHOD, FIELD})
+ * public @TestParameterAnnotation
+ * public @interface ColorParameter {
+ * Color[] value() default {};
+ * }
+ *
+ * public ColorTest(@ColorParameter({BLUE, WHITE}) Color color) {
+ * ...
+ * }
+ *
+ * @Test
+ * public void test() {...}
+ * }
+ * }</pre>
+ *
+ * <p>Each field that needs to be injected from a parameter requires its dedicated distinct
+ * annotation.
+ *
+ * <p>If the same annotation is defined both on the class and method, the method parameter values
+ * take precedence.
+ *
+ * <p>If the same annotation is defined both on the class and constructor, the constructor parameter
+ * values take precedence.
+ *
+ * <p>Annotations cannot be duplicated between the constructor or constructor parameters and a
+ * method or method parameter.
+ *
+ * <p>Since the parameter values must be specified in an annotation return value, they are
+ * restricted to the annotation method return type set (primitive, Class, Enum, String, etc...). If
+ * parameters have to be dynamically generated, the conventional Parameterized mechanism with {@code
+ * Parameters} has to be used instead.
+ */
+@Retention(RUNTIME)
+@Target({ANNOTATION_TYPE})
+@interface TestParameterAnnotation {
+
+ /** Specifies a validator for the parameter to determine whether test should be skipped. */
+ Class<? extends TestParameterValidator> validator() default DefaultValidator.class;
+
+ /** Specifies a value provider for the parameter to provide the values to test. */
+ Class<? extends TestParameterValueProvider> valueProvider() default DefaultValueProvider.class;
+
+ /** Default {@link TestParameterValidator} implementation which skips no test. */
+ class DefaultValidator implements TestParameterValidator {
+
+ @Override
+ public boolean shouldSkip(Context context) {
+ return false;
+ }
+ }
+
+ /**
+ * Default {@link TestParameterValueProvider} implementation that gets its values from the
+ * annotation's `value` method.
+ */
+ class DefaultValueProvider implements TestParameterValueProvider {
+
+ @Override
+ public List<Object> provideValues(Annotation annotation, Optional<Class<?>> parameterClass) {
+ Object parameters = getParametersAnnotationValues(annotation, annotation.annotationType());
+ checkState(
+ parameters.getClass().isArray(),
+ "The return value of the value method should be an array");
+
+ int parameterCount = Array.getLength(parameters);
+ ImmutableList.Builder<Object> resultBuilder = ImmutableList.builder();
+ for (int i = 0; i < parameterCount; i++) {
+ Object value = Array.get(parameters, i);
+ if (parameterClass.isPresent()) {
+ verify(
+ Primitives.wrap(parameterClass.get()).isInstance(value),
+ "Found %s annotation next to a parameter of type %s which doesn't match"
+ + " (annotation = %s)",
+ annotation.annotationType().getSimpleName(),
+ parameterClass.get().getSimpleName(),
+ annotation);
+ }
+ resultBuilder.add(value);
+ }
+ return resultBuilder.build();
+ }
+
+ @Override
+ public Class<?> getValueType(
+ Class<? extends Annotation> annotationType, Optional<Class<?>> parameterClass) {
+ try {
+ Method valueMethod = annotationType.getMethod("value");
+ return valueMethod.getReturnType().getComponentType();
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(
+ "The @TestParameterAnnotation annotation should have a single value() method.", e);
+ }
+ }
+
+ /**
+ * Returns the parameters of the test parameter, by calling the {@code value} method on the
+ * annotation.
+ */
+ private static Object getParametersAnnotationValues(
+ Annotation annotation, Class<? extends Annotation> annotationType) {
+ Method valueMethod;
+ try {
+ valueMethod = annotationType.getMethod("value");
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(
+ "The @TestParameterAnnotation annotation should have a single value() method.", e);
+ }
+ Object parameters;
+ try {
+ parameters = valueMethod.invoke(annotation);
+ } catch (InvocationTargetException e) {
+ if (e.getCause() instanceof IllegalAccessError) {
+ // There seems to be a bug or at least something weird with the JVM that causes
+ // IllegalAccessError to be thrown because the return value is not visible when it is a
+ // non-public nested type. See
+ // http://mail.openjdk.java.net/pipermail/core-libs-dev/2014-January/024180.html for more
+ // info.
+ throw new RuntimeException(
+ String.format(
+ "Could not access %s.value(). This is probably because %s is not visible to the"
+ + " annotation proxy. To fix this, make %s public.",
+ annotationType.getSimpleName(),
+ valueMethod.getReturnType().getSimpleName(),
+ valueMethod.getReturnType().getSimpleName()));
+ // Note: Not chaining the exception to reduce the clutter for the reader
+ } else {
+ throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e);
+ }
+ return parameters;
+ }
+ }
+}
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java
new file mode 100644
index 0000000..7fd2336
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java
@@ -0,0 +1,1369 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Verify.verify;
+import static com.google.common.collect.Lists.newArrayList;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.auto.value.AutoAnnotation;
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Optional;
+import com.google.common.base.Throwables;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ContiguousSet;
+import com.google.common.collect.DiscreteDomain;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Range;
+import com.google.common.util.concurrent.UncheckedExecutionException;
+import com.google.testing.junit.testparameterinjector.junit5.TestInfo.TestInfoParameter;
+import java.io.Serializable;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import javax.annotation.Nullable;
+
+/**
+ * {@code TestMethodProcessor} implementation for supporting parameterized tests annotated with
+ * {@link TestParameterAnnotation}.
+ *
+ * @see TestParameterAnnotation
+ */
+final class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
+
+ /**
+ * Class to hold an annotation type and origin and one of the values as returned by the {@code
+ * value()} method.
+ */
+ @AutoValue
+ abstract static class TestParameterValueHolder implements Serializable {
+
+ private static final long serialVersionUID = -6491624726743872379L;
+
+ /**
+ * Annotation type and origin of the annotation annotated with {@link TestParameterAnnotation}.
+ */
+ abstract AnnotationTypeOrigin annotationTypeOrigin();
+
+ /**
+ * The value used for the test as returned by the @TestParameterAnnotation annotated
+ * annotation's {@code value()} method (e.g. 'true' or 'false' in the case of a Boolean
+ * parameter).
+ */
+ abstract TestParameterValue wrappedValue();
+
+ /** The index of this value in {@link #specifiedValues()}. */
+ abstract int valueIndex();
+
+ /**
+ * The list of values specified by the @TestParameterAnnotation annotated annotation's {@code
+ * value()} method (e.g. {true, false} in the case of a boolean parameter).
+ */
+ @SuppressWarnings("AutoValueImmutableFields") // intentional to allow null values
+ abstract List<Object> specifiedValues();
+
+ /**
+ * The name of the parameter or field that is being annotated. In case the annotation is
+ * annotating a method, constructor or class, {@code paramName} is an absent optional.
+ */
+ abstract Optional<String> paramName();
+
+ /**
+ * Returns {@link #wrappedValue()} without the {@link TestParameterValue} wrapper if it exists.
+ */
+ @Nullable
+ Object unwrappedValue() {
+ return wrappedValue().getWrappedValue();
+ }
+
+ /**
+ * Returns a String that represents this value and is fit for use in a test name (between
+ * brackets).
+ */
+ String toTestNameString() {
+ return ParameterValueParsing.formatTestNameString(paramName(), wrappedValue());
+ }
+
+ public static ImmutableList<TestParameterValueHolder> create(
+ AnnotationWithMetadata annotationWithMetadata, Origin origin) {
+ List<TestParameterValue> specifiedValues =
+ getParametersAnnotationValues(annotationWithMetadata);
+ checkState(
+ !specifiedValues.isEmpty(),
+ "The number of parameter values should not be 0"
+ + ", otherwise the parameter would cause the test to be skipped.");
+ return FluentIterable.from(
+ ContiguousSet.create(
+ Range.closedOpen(0, specifiedValues.size()), DiscreteDomain.integers()))
+ .transform(
+ valueIndex ->
+ (TestParameterValueHolder)
+ new AutoValue_TestParameterAnnotationMethodProcessor_TestParameterValueHolder(
+ AnnotationTypeOrigin.create(
+ annotationWithMetadata.annotation().annotationType(), origin),
+ specifiedValues.get(valueIndex),
+ valueIndex,
+ newArrayList(
+ FluentIterable.from(specifiedValues)
+ .transform(TestParameterValue::getWrappedValue)),
+ annotationWithMetadata.paramName()))
+ .toList();
+ }
+ }
+
+ /**
+ * Returns a {@link TestParameterValues} for retrieving the {@link TestParameterAnnotation}
+ * annotation values for a the {@code testInfo}.
+ */
+ public static TestParameterValues getTestParameterValues(TestInfo testInfo) {
+ TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class);
+ if (testIndexHolder == null) {
+ return annotationType -> Optional.absent();
+ } else {
+ return annotationType ->
+ FluentIterable.from(
+ new TestParameterAnnotationMethodProcessor(
+ /* onlyForFieldsAndParameters= */ false)
+ .getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()))
+ .filter(
+ testParameterValue ->
+ testParameterValue
+ .annotationTypeOrigin()
+ .annotationType()
+ .equals(annotationType))
+ .transform(TestParameterValueHolder::unwrappedValue)
+ .first();
+ }
+ }
+
+ /**
+ * Returns a {@link TestParameterAnnotation} value for the current test as specified by {@code
+ * testInfo}, or {@link Optional#absent()} if the {@code annotationType} is not found.
+ */
+ public static Optional<Object> getTestParameterValue(
+ TestInfo testInfo, Class<? extends Annotation> annotationType) {
+ return getTestParameterValues(testInfo).getValue(annotationType);
+ }
+
+ private static ImmutableList<TestParameterValue> getParametersAnnotationValues(
+ AnnotationWithMetadata annotationWithMetadata) {
+ Annotation annotation = annotationWithMetadata.annotation();
+ TestParameterAnnotation testParameter =
+ annotation.annotationType().getAnnotation(TestParameterAnnotation.class);
+ Class<? extends TestParameterValueProvider> valueProvider = testParameter.valueProvider();
+ try {
+ return FluentIterable.from(
+ valueProvider
+ .getConstructor()
+ .newInstance()
+ .provideValues(
+ annotation,
+ annotationWithMetadata.paramClass(),
+ annotationWithMetadata.context()))
+ .transform(
+ value ->
+ (value instanceof TestParameterValue)
+ ? (TestParameterValue) value
+ : TestParameterValue.wrap(value))
+ .toList();
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(
+ "Unexpected exception while invoking value provider " + valueProvider, e);
+ }
+ }
+
+ /** The origin of an annotation type. */
+ enum Origin {
+ CLASS,
+ FIELD,
+ METHOD,
+ METHOD_PARAMETER,
+ CONSTRUCTOR,
+ CONSTRUCTOR_PARAMETER,
+ }
+
+ /** Class to hold an annotation type and the element where it was declared. */
+ @AutoValue
+ abstract static class AnnotationTypeOrigin implements Serializable {
+
+ private static final long serialVersionUID = 4909750539931241385L;
+
+ /** Annotation type of the @TestParameterAnnotation annotated annotation. */
+ abstract Class<? extends Annotation> annotationType();
+
+ /** Where the annotation was declared. */
+ abstract Origin origin();
+
+ public static AnnotationTypeOrigin create(
+ Class<? extends Annotation> annotationType, Origin origin) {
+ return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationTypeOrigin(
+ annotationType, origin);
+ }
+
+ @Override
+ public final String toString() {
+ return annotationType().getSimpleName() + ":" + origin();
+ }
+ }
+
+ /** Class to hold an annotation type and metadata about the annotated parameter. */
+ @AutoValue
+ abstract static class AnnotationWithMetadata implements Serializable {
+
+ /**
+ * The annotation whose interface is itself annotated by the @TestParameterAnnotation
+ * annotation.
+ */
+ abstract Annotation annotation();
+
+ /**
+ * The class of the parameter or field that is being annotated. In case the annotation is
+ * annotating a method, constructor or class, {@code paramClass} is an absent optional.
+ */
+ abstract Optional<Class<?>> paramClass();
+
+ /**
+ * The name of the parameter or field that is being annotated. In case the annotation is
+ * annotating a method, constructor or class, {@code paramName} is an absent optional.
+ */
+ abstract Optional<String> paramName();
+
+ /**
+ * A value class that contains extra information about the context of this parameter.
+ *
+ * <p>In case the annotation is annotating a method, constructor or class (deprecated
+ * functionality), the annotations in the context will be empty.
+ */
+ abstract GenericParameterContext context();
+
+ public static AnnotationWithMetadata withMetadata(
+ Annotation annotation,
+ Class<?> paramClass,
+ String paramName,
+ GenericParameterContext context) {
+ return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata(
+ annotation, Optional.of(paramClass), Optional.of(paramName), context);
+ }
+
+ public static AnnotationWithMetadata withMetadata(
+ Annotation annotation, Class<?> paramClass, GenericParameterContext context) {
+ return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata(
+ annotation, Optional.of(paramClass), Optional.absent(), context);
+ }
+
+ public static AnnotationWithMetadata withoutMetadata(
+ Annotation annotation, GenericParameterContext context) {
+ return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata(
+ annotation,
+ /* paramClass= */ Optional.absent(),
+ /* paramName= */ Optional.absent(),
+ context);
+ }
+
+ // Prevent anyone relying on equals() and hashCode() so that it remains possible to add fields
+ // to this class without breaking existing code.
+ @Override
+ public final boolean equals(Object other) {
+ throw new UnsupportedOperationException("Equality is not supported");
+ }
+
+ @Override
+ public final int hashCode() {
+ throw new UnsupportedOperationException("hashCode() is not supported");
+ }
+ }
+
+ private final boolean onlyForFieldsAndParameters;
+ private final LoadingCache<Class<?>, ImmutableList<AnnotationTypeOrigin>>
+ annotationTypeOriginsCache =
+ CacheBuilder.newBuilder()
+ .maximumSize(1000)
+ .build(CacheLoader.from(this::calculateAnnotationTypeOrigins));
+ private final Cache<Method, List<List<TestParameterValueHolder>>> parameterValuesCache =
+ CacheBuilder.newBuilder().maximumSize(1000).build();
+
+ private TestParameterAnnotationMethodProcessor(boolean onlyForFieldsAndParameters) {
+ this.onlyForFieldsAndParameters = onlyForFieldsAndParameters;
+ }
+
+ /**
+ * Constructs a new {@link TestMethodProcessor} that handles {@link
+ * TestParameterAnnotation}-annotated annotations that are placed anywhere:
+ *
+ * <ul>
+ * <li>At a method / constructor parameter
+ * <li>At a field
+ * <li>At a method / constructor on the class
+ * <li>At the test class
+ * </ul>
+ */
+ static TestMethodProcessor forAllAnnotationPlacements() {
+ return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ false);
+ }
+
+ /**
+ * Constructs a new {@link TestMethodProcessor} that handles {@link
+ * TestParameterAnnotation}-annotated annotations that are placed at fields or parameters.
+ *
+ * <p>Note that this excludes class and method-level annotations, as is the default (using the
+ * constructor).
+ */
+ static TestMethodProcessor onlyForFieldsAndParameters() {
+ return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ true);
+ }
+
+ private ImmutableList<AnnotationTypeOrigin> calculateAnnotationTypeOrigins(Class<?> testClass) {
+ // Collect all annotations used in declared fields and methods that have themselves a
+ // @TestParameterAnnotation annotation.
+ List<AnnotationTypeOrigin> fieldAnnotations =
+ extractTestParameterAnnotations(
+ FluentIterable.from(listWithParents(testClass))
+ .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields()))
+ .transformAndConcat(field -> Arrays.asList(field.getAnnotations()))
+ .toList(),
+ Origin.FIELD);
+ List<AnnotationTypeOrigin> methodAnnotations =
+ extractTestParameterAnnotations(
+ FluentIterable.from(testClass.getMethods())
+ .transformAndConcat(method -> Arrays.asList(method.getAnnotations()))
+ .toList(),
+ Origin.METHOD);
+ List<AnnotationTypeOrigin> parameterAnnotations =
+ extractTestParameterAnnotations(
+ FluentIterable.from(listWithParents(testClass))
+ .transformAndConcat(c -> Arrays.asList(c.getDeclaredMethods()))
+ .transformAndConcat(method -> Arrays.asList(method.getParameterAnnotations()))
+ .transformAndConcat(Arrays::asList)
+ .toList(),
+ Origin.METHOD_PARAMETER);
+ List<AnnotationTypeOrigin> classAnnotations =
+ extractTestParameterAnnotations(Arrays.asList(testClass.getAnnotations()), Origin.CLASS);
+ List<AnnotationTypeOrigin> constructorAnnotations =
+ extractTestParameterAnnotations(
+ FluentIterable.from(testClass.getDeclaredConstructors())
+ .transformAndConcat(constructor -> Arrays.asList(constructor.getAnnotations()))
+ .toList(),
+ Origin.CONSTRUCTOR);
+ List<AnnotationTypeOrigin> constructorParameterAnnotations =
+ extractTestParameterAnnotations(
+ FluentIterable.from(testClass.getDeclaredConstructors())
+ .transformAndConcat(
+ constructor ->
+ FluentIterable.from(Arrays.asList(constructor.getParameterAnnotations()))
+ .transformAndConcat(Arrays::asList))
+ .toList(),
+ Origin.CONSTRUCTOR_PARAMETER);
+
+ checkDuplicatedClassAndFieldAnnotations(
+ constructorAnnotations, classAnnotations, fieldAnnotations);
+
+ checkDuplicatedFieldsAnnotations(methodAnnotations, fieldAnnotations);
+
+ checkState(
+ FluentIterable.from(constructorAnnotations).toSet().size() == constructorAnnotations.size(),
+ "Annotations should not be duplicated on the constructor.");
+
+ checkState(
+ FluentIterable.from(classAnnotations).toSet().size() == classAnnotations.size(),
+ "Annotations should not be duplicated on the class.");
+
+ if (onlyForFieldsAndParameters) {
+ checkState(
+ methodAnnotations.isEmpty(),
+ "This test runner (constructed by the testparameterinjector package) was configured"
+ + " to disallow method-level annotations that could be field/parameter"
+ + " annotations, but found %s",
+ methodAnnotations);
+ checkState(
+ classAnnotations.isEmpty(),
+ "This test runner (constructed by the testparameterinjector package) was configured"
+ + " to disallow class-level annotations that could be field/parameter annotations,"
+ + " but found %s",
+ classAnnotations);
+ checkState(
+ constructorAnnotations.isEmpty(),
+ "This test runner (constructed by the testparameterinjector package) was configured"
+ + " to disallow constructor-level annotations that could be field/parameter"
+ + " annotations, but found %s",
+ constructorAnnotations);
+ }
+
+ // The order matters, since it will determine which annotation processor is
+ // called first.
+ return FluentIterable.from(classAnnotations)
+ .append(fieldAnnotations)
+ .append(constructorAnnotations)
+ .append(constructorParameterAnnotations)
+ .append(methodAnnotations)
+ .append(parameterAnnotations)
+ .toSet()
+ .asList();
+ }
+
+ private ImmutableList<AnnotationTypeOrigin> getAnnotationTypeOrigins(
+ Class<?> testClass, Origin firstOrigin, Origin... otherOrigins) {
+ Set<Origin> originsToFilterBy =
+ ImmutableSet.<Origin>builder().add(firstOrigin).add(otherOrigins).build();
+ try {
+ return FluentIterable.from(annotationTypeOriginsCache.getUnchecked(testClass))
+ .filter(annotationTypeOrigin -> originsToFilterBy.contains(annotationTypeOrigin.origin()))
+ .toList();
+ } catch (UncheckedExecutionException e) {
+ Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class);
+ throw e;
+ }
+ }
+
+ private void checkDuplicatedFieldsAnnotations(
+ List<AnnotationTypeOrigin> methodAnnotations, List<AnnotationTypeOrigin> fieldAnnotations) {
+ // If an annotation is duplicated on two fields, then it becomes specific, and cannot be
+ // overridden by a method.
+ if (FluentIterable.from(fieldAnnotations).toSet().size() != fieldAnnotations.size()) {
+ List<Class<? extends Annotation>> methodOrFieldAnnotations =
+ new ArrayList<>(
+ FluentIterable.from(methodAnnotations)
+ .append(new HashSet<>(fieldAnnotations))
+ .transform(AnnotationTypeOrigin::annotationType)
+ .toList());
+
+ checkState(
+ FluentIterable.from(methodOrFieldAnnotations).toSet().size()
+ == methodOrFieldAnnotations.size(),
+ "Annotations should not be duplicated on a method and field"
+ + " if they are present on multiple fields");
+ }
+ }
+
+ private void checkDuplicatedClassAndFieldAnnotations(
+ List<AnnotationTypeOrigin> constructorAnnotations,
+ List<AnnotationTypeOrigin> classAnnotations,
+ List<AnnotationTypeOrigin> fieldAnnotations) {
+ ImmutableSet<? extends Class<? extends Annotation>> classAnnotationTypes =
+ FluentIterable.from(classAnnotations)
+ .transform(AnnotationTypeOrigin::annotationType)
+ .toSet();
+
+ ImmutableSet<? extends Class<? extends Annotation>> uniqueFieldAnnotations =
+ FluentIterable.from(fieldAnnotations)
+ .transform(AnnotationTypeOrigin::annotationType)
+ .toSet();
+ ImmutableSet<? extends Class<? extends Annotation>> uniqueConstructorAnnotations =
+ FluentIterable.from(constructorAnnotations)
+ .transform(AnnotationTypeOrigin::annotationType)
+ .toSet();
+
+ checkState(
+ Collections.disjoint(classAnnotationTypes, uniqueFieldAnnotations),
+ "Annotations should not be duplicated on a class and field");
+
+ checkState(
+ Collections.disjoint(classAnnotationTypes, uniqueConstructorAnnotations),
+ "Annotations should not be duplicated on a class and constructor");
+
+ checkState(
+ Collections.disjoint(uniqueConstructorAnnotations, uniqueFieldAnnotations),
+ "Annotations should not be duplicated on a field and constructor");
+ }
+
+ private List<AnnotationTypeOrigin> extractTestParameterAnnotations(
+ List<Annotation> annotations, Origin origin) {
+ return new ArrayList<>(
+ FluentIterable.from(annotations)
+ .transform(Annotation::annotationType)
+ .filter(
+ annotationType -> annotationType.isAnnotationPresent(TestParameterAnnotation.class))
+ .transform(annotationType -> AnnotationTypeOrigin.create(annotationType, origin))
+ .toList());
+ }
+
+ @Override
+ public ExecutableValidationResult validateConstructor(Constructor<?> constructor) {
+ Class<?>[] parameterTypes = constructor.getParameterTypes();
+ if (parameterTypes.length == 0) {
+ return ExecutableValidationResult.notValidated();
+ }
+ // The constructor has parameters, they must be injected by a TestParameterAnnotation
+ // annotation.
+ Annotation[][] parameterAnnotations = constructor.getParameterAnnotations();
+ Class<?> testClass = constructor.getDeclaringClass();
+ return ExecutableValidationResult.validated(
+ validateMethodOrConstructorParameters(
+ removeOverrides(
+ getAnnotationTypeOrigins(
+ testClass, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER),
+ testClass),
+ testClass,
+ constructor,
+ parameterTypes,
+ parameterAnnotations));
+ }
+
+ @Override
+ public ExecutableValidationResult validateTestMethod(Method testMethod, Class<?> testClass) {
+ Class<?>[] methodParameterTypes = testMethod.getParameterTypes();
+ if (methodParameterTypes.length == 0) {
+ return ExecutableValidationResult.notValidated();
+ } else {
+ // The method has parameters, they must be injected by a TestParameterAnnotation annotation.
+ return ExecutableValidationResult.validated(
+ validateMethodOrConstructorParameters(
+ getAnnotationTypeOrigins(
+ testClass, Origin.CLASS, Origin.METHOD, Origin.METHOD_PARAMETER),
+ testClass,
+ testMethod,
+ methodParameterTypes,
+ testMethod.getParameterAnnotations()));
+ }
+ }
+
+ private List<Throwable> validateMethodOrConstructorParameters(
+ List<AnnotationTypeOrigin> annotationTypeOrigins,
+ Class<?> testClass,
+ AnnotatedElement methodOrConstructor,
+ Class<?>[] parameterTypes,
+ Annotation[][] parametersAnnotations) {
+ List<Throwable> errors = new ArrayList<>();
+
+ for (int parameterIndex = 0; parameterIndex < parameterTypes.length; parameterIndex++) {
+ Class<?> parameterType = parameterTypes[parameterIndex];
+ Annotation[] parameterAnnotations = parametersAnnotations[parameterIndex];
+ boolean matchingTestParameterAnnotationFound = false;
+ // First, handle the case where the method parameter specifies the test parameter explicitly,
+ // e.g. {@code public void test(@ColorParameter({...}) Color c)}.
+ for (AnnotationTypeOrigin testParameterAnnotationType : annotationTypeOrigins) {
+ for (Annotation parameterAnnotation : parameterAnnotations) {
+ if (parameterAnnotation
+ .annotationType()
+ .equals(testParameterAnnotationType.annotationType())) {
+ // Verify that the type is assignable with the return type of the 'value' method.
+ Class<?> valueMethodReturnType =
+ getValueMethodReturnType(
+ testParameterAnnotationType.annotationType(),
+ /* paramClass= */ Optional.of(parameterType));
+ if (!parameterType.isAssignableFrom(valueMethodReturnType)) {
+ errors.add(
+ new IllegalStateException(
+ String.format(
+ "Parameter of type %s annotated with %s does not match"
+ + " expected type %s in method/constructor %s",
+ parameterType.getName(),
+ testParameterAnnotationType.annotationType().getName(),
+ valueMethodReturnType.getName(),
+ methodOrConstructor)));
+ } else {
+ matchingTestParameterAnnotationFound = true;
+ }
+ }
+ }
+ }
+ // Second, handle the case where the method parameter does not specify the test parameter,
+ // and instead relies on the type matching, e.g. {@code public void test(Color c)}.
+ if (!matchingTestParameterAnnotationFound) {
+ ImmutableList<? extends Class<? extends Annotation>> testParameterAnnotationTypes =
+ getTestParameterAnnotations(
+ // Do not include METHOD_PARAMETER or CONSTRUCTOR_PARAMETER since they have already
+ // been evaluated.
+ filterAnnotationTypeOriginsByOrigin(
+ annotationTypeOrigins, Origin.CLASS, Origin.CONSTRUCTOR, Origin.METHOD),
+ testClass,
+ methodOrConstructor);
+ // If no annotation is present, simply compare the type.
+ for (Class<? extends Annotation> testParameterAnnotationType :
+ testParameterAnnotationTypes) {
+ if (parameterType.isAssignableFrom(
+ getValueMethodReturnType(
+ testParameterAnnotationType, /* paramClass= */ Optional.absent()))) {
+ if (matchingTestParameterAnnotationFound) {
+ errors.add(
+ new IllegalStateException(
+ String.format(
+ "Ambiguous method/constructor parameter type, matching multiple"
+ + " annotations for parameter of type %s in method %s",
+ parameterType.getName(), methodOrConstructor)));
+ }
+ matchingTestParameterAnnotationFound = true;
+ }
+ }
+ }
+ if (!matchingTestParameterAnnotationFound) {
+ errors.add(
+ new IllegalStateException(
+ String.format(
+ "No matching test parameter annotation found"
+ + " for parameter of type %s in method/constructor %s",
+ parameterType.getName(), methodOrConstructor)));
+ }
+ }
+ return errors;
+ }
+
+ @Override
+ public Optional<List<Object>> maybeGetConstructorParameters(
+ Constructor<?> constructor, TestInfo testInfo) {
+ if (testInfo.getAnnotation(TestIndexHolder.class) == null
+ // Explicitly skip @TestParameters annotated methods to ensure compatibility.
+ //
+ // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported)
+ // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;).
+ // Without the @TestParameters check below, this class would try to find parameters for
+ // these methods. When there are no method parameters, this is a no-op, but when the method
+ // is annotated with @TestParameters, this throws an exception (because there are method
+ // parameters that this processor has no values for - they are provided by the
+ // @TestParameters processor).
+ || constructor.isAnnotationPresent(TestParameters.class)) {
+ return Optional.absent();
+ } else {
+ TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class);
+ List<TestParameterValueHolder> testParameterValues =
+ getParameterValuesForTest(testIndexHolder, testInfo.getTestClass());
+
+ Class<?>[] parameterTypes = constructor.getParameterTypes();
+ Annotation[][] parameterAnnotations = constructor.getParameterAnnotations();
+ List<Object> parameterValues = new ArrayList<>(/* initialCapacity= */ parameterTypes.length);
+ List<Class<? extends Annotation>> processedAnnotationTypes = new ArrayList<>();
+ List<TestParameterValueHolder> parameterValuesForConstructor =
+ filterByOrigin(
+ testParameterValues, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER);
+ for (int i = 0; i < parameterTypes.length; i++) {
+ // Initialize each parameter value from the corresponding TestParameterAnnotation value.
+ parameterValues.add(
+ getParameterValue(
+ parameterValuesForConstructor,
+ parameterTypes[i],
+ parameterAnnotations[i],
+ processedAnnotationTypes));
+ }
+ return Optional.of(parameterValues);
+ }
+ }
+
+ @Override
+ public Optional<List<Object>> maybeGetTestMethodParameters(TestInfo testInfo) {
+ Method testMethod = testInfo.getMethod();
+ if (testInfo.getAnnotation(TestIndexHolder.class) == null
+ // Explicitly skip @TestParameters annotated methods to ensure compatibility.
+ //
+ // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported)
+ // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;).
+ // Without the @TestParameters check below, this class would try to find parameters for
+ // these methods. When there are no method parameters, this is a no-op, but when the method
+ // is annotated with @TestParameters, this throws an exception (because there are method
+ // parameters that this processor has no values for - they are provided by the
+ // @TestParameters processor).
+ || testMethod.isAnnotationPresent(TestParameters.class)) {
+ return Optional.absent();
+ } else {
+ TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class);
+ checkState(testIndexHolder != null);
+ List<TestParameterValueHolder> testParameterValues =
+ filterByOrigin(
+ getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()),
+ Origin.CLASS,
+ Origin.METHOD,
+ Origin.METHOD_PARAMETER);
+
+ Class<?>[] parameterTypes = testMethod.getParameterTypes();
+ Annotation[][] parametersAnnotations = testMethod.getParameterAnnotations();
+ ArrayList<Object> parameterValues =
+ new ArrayList<>(/* initialCapacity= */ parameterTypes.length);
+
+ List<Class<? extends Annotation>> processedAnnotationTypes = new ArrayList<>();
+ for (int i = 0; i < parameterTypes.length; i++) {
+ parameterValues.add(
+ getParameterValue(
+ testParameterValues,
+ parameterTypes[i],
+ parametersAnnotations[i],
+ processedAnnotationTypes));
+ }
+
+ return Optional.of(parameterValues);
+ }
+ }
+
+ /**
+ * Returns the {@link TestInfo}, one for each result of the cartesian product of each test
+ * parameter values.
+ *
+ * <p>For example, given the annotation {@code @ColorParameter({BLUE, WHITE, RED})} on a method,
+ * it method will return the TestParameterValues: "(@ColorParameter, BLUE), (@ColorParameter,
+ * WHITE), (@ColorParameter, RED)}).
+ *
+ * <p>For multiple annotations (say, {@code @TestParameter("foo", "bar")} and
+ * {@code @ColorParameter({BLUE, WHITE})}), it will generate the following result:
+ *
+ * <ul>
+ * <li>("foo", BLUE)
+ * <li>("foo", WHITE)
+ * <li>("bar", BLUE)
+ * <li>("bar", WHITE)
+ * <li>
+ * </ul>
+ *
+ * corresponding to the cartesian product of both annotations.
+ */
+ @Override
+ public List<TestInfo> calculateTestInfos(TestInfo originalTest) {
+ List<List<TestParameterValueHolder>> parameterValuesForMethod =
+ getParameterValuesForMethod(originalTest.getMethod(), originalTest.getTestClass());
+
+ if (parameterValuesForMethod.equals(ImmutableList.of(ImmutableList.of()))) {
+ // This test is not parameterized
+ return ImmutableList.of(originalTest);
+ }
+
+ ImmutableList.Builder<TestInfo> testInfos = ImmutableList.builder();
+ for (int parametersIndex = 0;
+ parametersIndex < parameterValuesForMethod.size();
+ ++parametersIndex) {
+ List<TestParameterValueHolder> testParameterValues =
+ parameterValuesForMethod.get(parametersIndex);
+ testInfos.add(
+ originalTest
+ .withExtraParameters(
+ FluentIterable.from(testParameterValues)
+ .transform(
+ param ->
+ TestInfoParameter.create(
+ param.toTestNameString(),
+ param.unwrappedValue(),
+ param.valueIndex()))
+ .toList())
+ .withExtraAnnotation(
+ TestIndexHolderFactory.create(
+ /* methodIndex= */ strictIndexOf(
+ getMethodsIncludingParentsSorted(originalTest.getTestClass()),
+ originalTest.getMethod()),
+ parametersIndex,
+ originalTest.getTestClass().getName())));
+ }
+
+ return testInfos.build();
+ }
+
+ private List<List<TestParameterValueHolder>> getParameterValuesForMethod(
+ Method method, Class<?> testClass) {
+ try {
+ return parameterValuesCache.get(
+ method,
+ () -> {
+ List<List<TestParameterValueHolder>> testParameterValuesList =
+ getAnnotationValuesForUsedAnnotationTypes(method, testClass);
+
+ return FluentIterable.from(Lists.cartesianProduct(testParameterValuesList))
+ .filter(
+ // Skip tests based on the annotations' {@link Validator#shouldSkip} return
+ // value.
+ testParameterValues ->
+ FluentIterable.from(testParameterValues)
+ .filter(
+ testParameterValue ->
+ callShouldSkip(
+ testParameterValue.annotationTypeOrigin().annotationType(),
+ testParameterValues))
+ .isEmpty())
+ .toList();
+ });
+ } catch (ExecutionException | UncheckedExecutionException e) {
+ Throwables.throwIfUnchecked(e.getCause());
+ throw new RuntimeException(e);
+ }
+ }
+
+ private List<TestParameterValueHolder> getParameterValuesForTest(
+ TestIndexHolder testIndexHolder, Class<?> testClass) {
+ verify(
+ testIndexHolder.testClassName().equals(testClass.getName()),
+ "The class for which the given annotation was created (%s) is not the same as the test"
+ + " class that this runner is handling (%s)",
+ testIndexHolder.testClassName(),
+ testClass.getName());
+ Method testMethod =
+ getMethodsIncludingParentsSorted(testClass).get(testIndexHolder.methodIndex());
+ return getParameterValuesForMethod(testMethod, testClass)
+ .get(testIndexHolder.parametersIndex());
+ }
+
+ /**
+ * Returns the list of annotation index for all annotations defined in a given test method and its
+ * class.
+ */
+ private ImmutableList<List<TestParameterValueHolder>> getAnnotationValuesForUsedAnnotationTypes(
+ Method method, Class<?> testClass) {
+ ImmutableList<AnnotationTypeOrigin> annotationTypes =
+ FluentIterable.from(getAnnotationTypeOrigins(testClass, Origin.CLASS))
+ .append(getAnnotationTypeOrigins(testClass, Origin.FIELD))
+ .append(getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR))
+ .append(getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR_PARAMETER))
+ .append(getAnnotationTypeOrigins(testClass, Origin.METHOD))
+ .append(
+ ImmutableList.sortedCopyOf(
+ annotationComparator(method.getParameterAnnotations()),
+ getAnnotationTypeOrigins(testClass, Origin.METHOD_PARAMETER)))
+ .toList();
+
+ return FluentIterable.from(removeOverrides(annotationTypes, testClass, method))
+ .transform(
+ annotationTypeOrigin ->
+ getAnnotationFromParametersOrTestOrClass(annotationTypeOrigin, method, testClass))
+ .filter(l -> !l.isEmpty())
+ .transformAndConcat(i -> i)
+ .toList();
+ }
+
+ private Comparator<AnnotationTypeOrigin> annotationComparator(
+ Annotation[][] parameterAnnotations) {
+ ImmutableList<String> annotationOrdering =
+ FluentIterable.from(parameterAnnotations)
+ .transformAndConcat(Arrays::asList)
+ .transform(Annotation::annotationType)
+ .transform(Class::getName)
+ .toList();
+ return (annotationTypeOrigin, t1) ->
+ Integer.compare(
+ annotationOrdering.indexOf(annotationTypeOrigin.annotationType().getName()),
+ annotationOrdering.indexOf(t1.annotationType().getName()));
+ }
+
+ /**
+ * Returns a list of {@link AnnotationTypeOrigin} where the overridden annotation are removed for
+ * the current {@code originalTest} and {@code testClass}.
+ *
+ * <p>Specifically, annotation defined on CLASS and FIELD elements will be removed if they are
+ * also defined on the method, method parameter, constructor, or constructor parameters.
+ */
+ private List<AnnotationTypeOrigin> removeOverrides(
+ List<AnnotationTypeOrigin> annotationTypeOrigins, Class<?> testClass, Method method) {
+ return removeOverrides(
+ new ArrayList<>(
+ FluentIterable.from(annotationTypeOrigins)
+ .filter(
+ annotationTypeOrigin -> {
+ switch (annotationTypeOrigin.origin()) {
+ case FIELD: // Fall through.
+ case CLASS:
+ return getAnnotationListWithType(
+ method.getAnnotations(), annotationTypeOrigin.annotationType())
+ .isEmpty();
+ default:
+ return true;
+ }
+ })
+ .toList()),
+ testClass);
+ }
+
+ /**
+ * @see #removeOverrides(List, Class)
+ */
+ private List<AnnotationTypeOrigin> removeOverrides(
+ List<AnnotationTypeOrigin> annotationTypeOrigins, Class<?> testClass) {
+ return new ArrayList<>(
+ FluentIterable.from(annotationTypeOrigins)
+ .filter(
+ annotationTypeOrigin -> {
+ switch (annotationTypeOrigin.origin()) {
+ case FIELD: // Fall through.
+ case CLASS:
+ return getAnnotationListWithType(
+ TestParameterInjectorUtils.getOnlyConstructor(testClass)
+ .getAnnotations(),
+ annotationTypeOrigin.annotationType())
+ .isEmpty();
+ default:
+ return true;
+ }
+ })
+ .toList());
+ }
+
+ /**
+ * Returns the given annotations defined either on the method parameters, method or the test
+ * class.
+ *
+ * <p>The annotation from the parameters takes precedence over the same annotation defined on the
+ * method, and the one defined on the method takes precedence over the same annotation defined on
+ * the class.
+ */
+ private ImmutableList<List<TestParameterValueHolder>> getAnnotationFromParametersOrTestOrClass(
+ AnnotationTypeOrigin annotationTypeOrigin, Method method, Class<?> testClass) {
+ Origin origin = annotationTypeOrigin.origin();
+ Class<? extends Annotation> annotationType = annotationTypeOrigin.annotationType();
+ if (origin == Origin.CONSTRUCTOR_PARAMETER) {
+ Constructor<?> constructor = TestParameterInjectorUtils.getOnlyConstructor(testClass);
+ List<AnnotationWithMetadata> annotations =
+ getAnnotationWithMetadataListWithType(constructor, annotationType, testClass);
+
+ if (!annotations.isEmpty()) {
+ return toTestParameterValueList(annotations, origin);
+ }
+ } else if (origin == Origin.CONSTRUCTOR) {
+ Annotation annotation =
+ TestParameterInjectorUtils.getOnlyConstructor(testClass).getAnnotation(annotationType);
+ if (annotation != null) {
+ return ImmutableList.of(
+ TestParameterValueHolder.create(
+ AnnotationWithMetadata.withoutMetadata(
+ annotation,
+ GenericParameterContext.createWithoutParameterAnnotations(testClass)),
+ origin));
+ }
+
+ } else if (origin == Origin.METHOD_PARAMETER) {
+ List<AnnotationWithMetadata> annotations =
+ getAnnotationWithMetadataListWithType(method, annotationType, testClass);
+ if (!annotations.isEmpty()) {
+ return toTestParameterValueList(annotations, origin);
+ }
+ } else if (origin == Origin.METHOD) {
+ if (method.isAnnotationPresent(annotationType)) {
+ return ImmutableList.of(
+ TestParameterValueHolder.create(
+ AnnotationWithMetadata.withoutMetadata(
+ method.getAnnotation(annotationType),
+ GenericParameterContext.createWithoutParameterAnnotations(testClass)),
+ origin));
+ }
+ } else if (origin == Origin.FIELD) {
+ List<AnnotationWithMetadata> annotations =
+ new ArrayList<>(
+ FluentIterable.from(listWithParents(testClass))
+ .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields()))
+ .transformAndConcat(
+ field ->
+ FluentIterable.from(
+ getAnnotationListWithType(field.getAnnotations(), annotationType))
+ .transform(
+ annotation ->
+ AnnotationWithMetadata.withMetadata(
+ annotation,
+ field.getType(),
+ field.getName(),
+ GenericParameterContext.create(field, testClass))))
+ .toList());
+ if (!annotations.isEmpty()) {
+ return toTestParameterValueList(annotations, origin);
+ }
+ } else if (origin == Origin.CLASS) {
+ Annotation annotation = testClass.getAnnotation(annotationType);
+ if (annotation != null) {
+ return ImmutableList.of(
+ TestParameterValueHolder.create(
+ AnnotationWithMetadata.withoutMetadata(
+ annotation,
+ GenericParameterContext.createWithoutParameterAnnotations(testClass)),
+ origin));
+ }
+ }
+ return ImmutableList.of();
+ }
+
+ private static ImmutableList<List<TestParameterValueHolder>> toTestParameterValueList(
+ List<AnnotationWithMetadata> annotationWithMetadatas, Origin origin) {
+ return FluentIterable.from(annotationWithMetadatas)
+ .transform(
+ annotationWithMetadata ->
+ (List<TestParameterValueHolder>)
+ new ArrayList<>(
+ TestParameterValueHolder.create(annotationWithMetadata, origin)))
+ .toList();
+ }
+
+ private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType(
+ Method callable, Class<? extends Annotation> annotationType, Class<?> testClass) {
+ try {
+ return getAnnotationWithMetadataListWithType(
+ callable.getParameters(), annotationType, testClass);
+ } catch (NoSuchMethodError ignored) {
+ return getAnnotationWithMetadataListWithType(
+ callable.getParameterTypes(),
+ callable.getParameterAnnotations(),
+ annotationType,
+ testClass);
+ }
+ }
+
+ private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType(
+ Constructor<?> callable, Class<? extends Annotation> annotationType, Class<?> testClass) {
+ try {
+ return getAnnotationWithMetadataListWithType(
+ callable.getParameters(), annotationType, testClass);
+ } catch (NoSuchMethodError ignored) {
+ return getAnnotationWithMetadataListWithType(
+ callable.getParameterTypes(),
+ callable.getParameterAnnotations(),
+ annotationType,
+ testClass);
+ }
+ }
+
+ // Parameter is not available on old Android SDKs, and isn't desugared. That's why this method
+ // has a fallback that takes the parameter types and annotations (without the parameter names,
+ // which are optional anyway).
+ @SuppressWarnings("AndroidJdkLibsChecker")
+ private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType(
+ Parameter[] parameters, Class<? extends Annotation> annotationType, Class<?> testClass) {
+ return FluentIterable.from(parameters)
+ .transform(
+ parameter -> {
+ Annotation annotation = parameter.getAnnotation(annotationType);
+ return annotation == null
+ ? null
+ : parameter.isNamePresent()
+ ? AnnotationWithMetadata.withMetadata(
+ annotation,
+ parameter.getType(),
+ parameter.getName(),
+ GenericParameterContext.create(parameter, testClass))
+ : AnnotationWithMetadata.withMetadata(
+ annotation,
+ parameter.getType(),
+ GenericParameterContext.create(parameter, testClass));
+ })
+ .filter(Objects::nonNull)
+ .toList();
+ }
+
+ private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType(
+ Class<?>[] parameterTypes,
+ Annotation[][] annotations,
+ Class<? extends Annotation> annotationType,
+ Class<?> testClass) {
+ checkArgument(parameterTypes.length == annotations.length);
+
+ ImmutableList.Builder<AnnotationWithMetadata> resultBuilder = ImmutableList.builder();
+ for (int i = 0; i < annotations.length; i++) {
+ for (Annotation annotation : annotations[i]) {
+ if (annotation.annotationType().equals(annotationType)) {
+ resultBuilder.add(
+ AnnotationWithMetadata.withMetadata(
+ annotation,
+ parameterTypes[i],
+ GenericParameterContext.createWithRepeatableAnnotationsFallback(
+ annotations[i], testClass)));
+ }
+ }
+ }
+ return resultBuilder.build();
+ }
+
+ private ImmutableList<Annotation> getAnnotationListWithType(
+ Annotation[] annotations, Class<? extends Annotation> annotationType) {
+ return FluentIterable.from(annotations)
+ .filter(annotation -> annotation.annotationType().equals(annotationType))
+ .toList();
+ }
+
+ @Override
+ public void postProcessTestInstance(Object testInstance, TestInfo testInfo) {
+ TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class);
+ try {
+ if (testIndexHolder != null) {
+ List<TestParameterValueHolder> testParameterValues =
+ getParameterValuesForTest(testIndexHolder, testInfo.getTestClass());
+
+ // Do not include {@link Origin#METHOD_PARAMETER} nor {@link Origin#CONSTRUCTOR_PARAMETER}
+ // annotations.
+ List<TestParameterValueHolder> testParameterValuesForFieldInjection =
+ filterByOrigin(testParameterValues, Origin.CLASS, Origin.FIELD, Origin.METHOD);
+ // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class
+ // in the example above.
+ List<TestParameterValueHolder> remainingTestParameterValuesForFieldInjection =
+ new ArrayList<>(testParameterValuesForFieldInjection);
+ for (Field declaredField :
+ FluentIterable.from(listWithParents(testInstance.getClass()))
+ .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields()))
+ .toList()) {
+ for (TestParameterValueHolder testParameterValue :
+ remainingTestParameterValuesForFieldInjection) {
+ if (declaredField.isAnnotationPresent(
+ testParameterValue.annotationTypeOrigin().annotationType())) {
+ if (testParameterValue.paramName().isPresent()
+ && !declaredField.getName().equals(testParameterValue.paramName().get())) {
+ // names don't match
+ continue;
+ }
+ declaredField.setAccessible(true);
+ declaredField.set(testInstance, testParameterValue.unwrappedValue());
+ remainingTestParameterValuesForFieldInjection.remove(testParameterValue);
+ break;
+ }
+ }
+ }
+ }
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Returns an {@link TestParameterValueHolder} list that contains only the values originating from
+ * one of the {@code origins}.
+ */
+ private static ImmutableList<TestParameterValueHolder> filterByOrigin(
+ List<TestParameterValueHolder> testParameterValues, Origin... origins) {
+ Set<Origin> originsToFilterBy = ImmutableSet.copyOf(origins);
+ return FluentIterable.from(testParameterValues)
+ .filter(
+ testParameterValue ->
+ originsToFilterBy.contains(testParameterValue.annotationTypeOrigin().origin()))
+ .toList();
+ }
+
+ /**
+ * Returns an {@link AnnotationTypeOrigin} list that contains only the values originating from one
+ * of the {@code origins}.
+ */
+ private static ImmutableList<AnnotationTypeOrigin> filterAnnotationTypeOriginsByOrigin(
+ List<AnnotationTypeOrigin> annotationTypeOrigins, Origin... origins) {
+ List<Origin> originList = Arrays.asList(origins);
+ return FluentIterable.from(annotationTypeOrigins)
+ .filter(annotationTypeOrigin -> originList.contains(annotationTypeOrigin.origin()))
+ .toList();
+ }
+
+ /** Returns a {@link TestParameterAnnotation}'s value for a method or constructor parameter. */
+ private Object getParameterValue(
+ List<TestParameterValueHolder> testParameterValues,
+ Class<?> methodParameterType,
+ Annotation[] parameterAnnotations,
+ List<Class<? extends Annotation>> processedAnnotationTypes) {
+ List<Class<? extends Annotation>> iteratedAnnotationTypes = new ArrayList<>();
+ for (TestParameterValueHolder testParameterValue : testParameterValues) {
+ // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class
+ // in the example above.
+ for (Annotation parameterAnnotation : parameterAnnotations) {
+ Class<? extends Annotation> annotationType =
+ testParameterValue.annotationTypeOrigin().annotationType();
+ if (parameterAnnotation.annotationType().equals(annotationType)) {
+ // If multiple annotations exist, ensure that the proper one is selected.
+ // For instance, for:
+ // <code>
+ // test(@FooParameter(1,2) Foo foo, @FooParameter(3,4) Foo bar) {}
+ // </code>
+ // Verifies that the correct @FooParameter annotation value will be assigned to the
+ // corresponding variable.
+ if (Collections.frequency(processedAnnotationTypes, annotationType)
+ == Collections.frequency(iteratedAnnotationTypes, annotationType)) {
+ processedAnnotationTypes.add(annotationType);
+ return testParameterValue.unwrappedValue();
+ }
+ iteratedAnnotationTypes.add(annotationType);
+ }
+ }
+ }
+ // If no annotation matches, use the method parameter type.
+ for (TestParameterValueHolder testParameterValue : testParameterValues) {
+ // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class
+ // in the example above.
+ if (methodParameterType.isAssignableFrom(
+ getValueMethodReturnType(
+ testParameterValue.annotationTypeOrigin().annotationType(),
+ /* paramClass= */ Optional.absent()))) {
+ return testParameterValue.unwrappedValue();
+ }
+ }
+ throw new IllegalStateException(
+ "The method parameter should have matched a TestParameterAnnotation");
+ }
+
+ /**
+ * This mechanism is a workaround to be able to store the annotation values in the annotation list
+ * of the {@link TestInfo}, since we cannot carry other information through the test runner.
+ */
+ @Retention(RUNTIME)
+ @interface TestIndexHolder {
+
+ /** The index of the test method in {@code getMethodsIncludingParentsSorted(testClass)} */
+ int methodIndex();
+
+ /**
+ * The index of the set of parameters to run the test method with in the list produced by {@link
+ * #getParameterValuesForMethod}.
+ */
+ int parametersIndex();
+
+ /**
+ * The full name of the test class. Only used for verifying that assumptions about the above
+ * indices are valid.
+ */
+ String testClassName();
+ }
+
+ /** Factory for {@link TestIndexHolder}. */
+ static class TestIndexHolderFactory {
+ @AutoAnnotation
+ static TestIndexHolder create(int methodIndex, int parametersIndex, String testClassName) {
+ return new AutoAnnotation_TestParameterAnnotationMethodProcessor_TestIndexHolderFactory_create(
+ methodIndex, parametersIndex, testClassName);
+ }
+
+ private TestIndexHolderFactory() {}
+ }
+
+ /**
+ * Returns whether the test should be skipped according to the {@code annotationType}'s {@link
+ * TestParameterValidator} and the current list of {@link TestParameterValueHolder}.
+ */
+ private static boolean callShouldSkip(
+ Class<? extends Annotation> annotationType,
+ List<TestParameterValueHolder> testParameterValues) {
+ TestParameterAnnotation annotation =
+ annotationType.getAnnotation(TestParameterAnnotation.class);
+ Class<? extends TestParameterValidator> validator = annotation.validator();
+ try {
+ return validator
+ .getConstructor()
+ .newInstance()
+ .shouldSkip(new ValidatorContext(testParameterValues));
+ } catch (Exception e) {
+ throw new RuntimeException("Unexpected exception while invoking validator " + validator, e);
+ }
+ }
+
+ private static class ValidatorContext implements TestParameterValidator.Context {
+
+ private final List<TestParameterValueHolder> testParameterValues;
+ private final Set<Object> valueList;
+
+ public ValidatorContext(List<TestParameterValueHolder> testParameterValues) {
+ this.testParameterValues = testParameterValues;
+ this.valueList =
+ FluentIterable.from(testParameterValues)
+ .transform(TestParameterValueHolder::unwrappedValue)
+ .filter(Objects::nonNull)
+ .toSet();
+ }
+
+ @Override
+ public boolean has(Class<? extends Annotation> testParameter, Object value) {
+ return getValue(testParameter).transform(value::equals).or(false);
+ }
+
+ @Override
+ public <T extends Enum<T>, U extends Enum<U>> boolean has(T value1, U value2) {
+ return valueList.contains(value1) && valueList.contains(value2);
+ }
+
+ @Override
+ public Optional<Object> getValue(Class<? extends Annotation> testParameter) {
+ return getParameter(testParameter).transform(TestParameterValueHolder::unwrappedValue);
+ }
+
+ @Override
+ public List<Object> getSpecifiedValues(Class<? extends Annotation> testParameter) {
+ return getParameter(testParameter)
+ .transform(TestParameterValueHolder::specifiedValues)
+ .or(ImmutableList.of());
+ }
+
+ private Optional<TestParameterValueHolder> getParameter(
+ Class<? extends Annotation> testParameter) {
+ return FluentIterable.from(testParameterValues)
+ .firstMatch(value -> value.annotationTypeOrigin().annotationType().equals(testParameter));
+ }
+ }
+
+ /**
+ * Returns the class of the list elements returned by {@code provideValues()}.
+ *
+ * @param annotationType The type of the annotation that was encountered in the test class. The
+ * definition of this annotation is itself annotated with the {@link TestParameterAnnotation}
+ * annotation.
+ * @param paramClass The class of the parameter or field that is being annotated. In case the
+ * annotation is annotating a method, constructor or class, {@code paramClass} is an absent
+ * optional.
+ */
+ private static Class<?> getValueMethodReturnType(
+ Class<? extends Annotation> annotationType, Optional<Class<?>> paramClass) {
+ TestParameterAnnotation testParameter =
+ annotationType.getAnnotation(TestParameterAnnotation.class);
+ Class<? extends TestParameterValueProvider> valueProvider = testParameter.valueProvider();
+ try {
+ return valueProvider.getConstructor().newInstance().getValueType(annotationType, paramClass);
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Unexpected exception while invoking value provider " + valueProvider, e);
+ }
+ }
+
+ /** Returns the TestParameterAnnotation annotation types defined for a method or constructor. */
+ private ImmutableList<? extends Class<? extends Annotation>> getTestParameterAnnotations(
+ List<AnnotationTypeOrigin> annotationTypeOrigins,
+ final Class<?> testClass,
+ AnnotatedElement methodOrConstructor) {
+ return FluentIterable.from(annotationTypeOrigins)
+ .transform(AnnotationTypeOrigin::annotationType)
+ .filter(
+ annotationType ->
+ testClass.isAnnotationPresent(annotationType)
+ || methodOrConstructor.isAnnotationPresent(annotationType))
+ .toList();
+ }
+
+ private <T> int strictIndexOf(List<T> haystack, T needle) {
+ int index = haystack.indexOf(needle);
+ checkArgument(index >= 0, "Could not find '%s' in %s", needle, haystack);
+ return index;
+ }
+
+ private ImmutableList<Method> getMethodsIncludingParentsSorted(Class<?> clazz) {
+ ImmutableList.Builder<Method> resultBuilder = ImmutableList.builder();
+ while (clazz != null) {
+ resultBuilder.add(clazz.getDeclaredMethods());
+ clazz = clazz.getSuperclass();
+ }
+ // Because getDeclaredMethods()'s order is not specified, there is the theoretical possibility
+ // that the order of methods is unstable. To partly fix this, we sort the result based on method
+ // name. This is still not perfect because of method overloading, but that should be
+ // sufficiently rare for test names.
+ return ImmutableList.sortedCopyOf(
+ Ordering.natural().onResultOf(Method::getName), resultBuilder.build());
+ }
+
+ private static ImmutableList<Class<?>> listWithParents(Class<?> clazz) {
+ ImmutableList.Builder<Class<?>> resultBuilder = ImmutableList.builder();
+
+ Class<?> currentClass = clazz;
+ while (currentClass != null) {
+ resultBuilder.add(currentClass);
+ currentClass = currentClass.getSuperclass();
+ }
+
+ return resultBuilder.build();
+ }
+}
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorExtension.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorExtension.java
new file mode 100644
index 0000000..6d6aa51
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorExtension.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.Iterables.getOnlyElement;
+
+import com.google.auto.value.AutoValue;
+import com.google.auto.value.extension.memoized.Memoized;
+import com.google.common.collect.ImmutableList;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.extension.Extension;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolver;
+import org.junit.jupiter.api.extension.TestInstancePostProcessor;
+import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
+import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
+
+/** Implements the TestParameterInjector logic for JUnit5 (Jupiter). */
+class TestParameterInjectorExtension implements TestTemplateInvocationContextProvider {
+
+ private static final TestMethodProcessorList testMethodProcessors =
+ TestMethodProcessorList.createNewParameterizedProcessors();
+
+ @Override
+ public boolean supportsTestTemplate(ExtensionContext context) {
+ return true;
+ }
+
+ @Override
+ public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
+ ExtensionContext extensionContext) {
+ validateTestMethodAndConstructor(
+ extensionContext.getRequiredTestMethod(), extensionContext.getRequiredTestClass());
+ List<TestInfo> testInfos =
+ testMethodProcessors.calculateTestInfos(
+ extensionContext.getRequiredTestMethod(), extensionContext.getRequiredTestClass());
+
+ return testInfos.stream().map(CustomInvocationContext::of);
+ }
+
+ private void validateTestMethodAndConstructor(Method testMethod, Class<?> testClass) {
+ checkState(
+ testClass.getDeclaredConstructors().length == 1,
+ "Only a single constructor is allowed, but found %s in %s",
+ testClass.getDeclaredConstructors().length,
+ testClass.getSimpleName());
+ Constructor<?> constructor =
+ getOnlyElement(ImmutableList.copyOf(testClass.getDeclaredConstructors()));
+
+ testMethodProcessors.validateConstructor(constructor).assertValid();
+
+ testMethodProcessors.validateTestMethod(testMethod, testClass).assertValid();
+
+ checkState(
+ testMethod.getAnnotation(TestParameterInjectorTest.class) != null,
+ "Each test method handled by this extension should be annotated with"
+ + " @TestParameterInjectorTest");
+ }
+
+ @AutoValue
+ abstract static class CustomInvocationContext implements TestTemplateInvocationContext {
+
+ abstract TestInfo testInfo();
+
+ @Memoized
+ List<Object> getConstructorParameters() {
+ Constructor<?> constructor =
+ getOnlyElement(ImmutableList.copyOf(testInfo().getTestClass().getDeclaredConstructors()));
+
+ return testMethodProcessors.getConstructorParameters(constructor, testInfo());
+ }
+
+ @Memoized
+ List<Object> getTestMethodParameters() {
+ return testMethodProcessors.getTestMethodParameters(testInfo());
+ }
+
+ static CustomInvocationContext of(TestInfo testInfo) {
+ return new AutoValue_TestParameterInjectorExtension_CustomInvocationContext(testInfo);
+ }
+
+ @Override
+ public String getDisplayName(int invocationIndex) {
+ return testInfo().getName();
+ }
+
+ @Override
+ public List<Extension> getAdditionalExtensions() {
+ return ImmutableList.of(new CustomAdditionalExtension());
+ }
+
+ class CustomAdditionalExtension implements ParameterResolver, TestInstancePostProcessor {
+
+ @Override
+ public boolean supportsParameter(
+ ParameterContext parameterContext, ExtensionContext extensionContext) {
+ if (parameterContext.getDeclaringExecutable() instanceof Constructor) {
+ return true;
+ } else {
+ return parameterContext
+ .getDeclaringExecutable()
+ .isAnnotationPresent(TestParameterInjectorTest.class);
+ }
+ }
+
+ @Override
+ public Object resolveParameter(
+ ParameterContext parameterContext, ExtensionContext extensionContext) {
+ if (parameterContext.getDeclaringExecutable() instanceof Constructor) {
+ return getConstructorParameters().get(parameterContext.getIndex());
+ } else {
+ return getTestMethodParameters().get(parameterContext.getIndex());
+ }
+ }
+
+ @Override
+ public void postProcessTestInstance(Object testInstance, ExtensionContext extensionContext)
+ throws Exception {
+ testMethodProcessors.postProcessTestInstance(testInstance, testInfo());
+ }
+ }
+ }
+}
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorTest.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorTest.java
new file mode 100644
index 0000000..e17179a
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Replacement for JUnit5's @Test for test methods that want to use @TestParameter[s].
+ *
+ * <p>Example:
+ *
+ * <pre>
+ * class MyTest {
+ * {@literal @}TestParameterInjectorTest
+ * void withParameter_success({@literal @}TestParameter boolean bool) {
+ * // ...
+ * }
+ *
+ * {@literal @}TestParameterInjectorTest
+ * {@literal @}TestParameters("{name: 1, number: 3.3}")
+ * {@literal @}TestParameters("{name: abc, number: 5}")
+ * void withParameters_success(String name, double number) {
+ * // ...
+ * }
+ * }
+ * </pre>
+ */
+@TestTemplate
+@ExtendWith(TestParameterInjectorExtension.class)
+@Retention(RUNTIME)
+@Target({METHOD})
+public @interface TestParameterInjectorTest {}
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorUtils.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorUtils.java
new file mode 100644
index 0000000..cd47ea1
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorUtils.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.Iterables.getOnlyElement;
+
+import com.google.common.collect.ImmutableList;
+import java.lang.reflect.Constructor;
+
+/** Shared utility methods. */
+class TestParameterInjectorUtils {
+
+ /**
+ * Return the only public constructor of the given test class. If there is none, return the only
+ * constructor.
+ *
+ * <p>Normally, there should be exactly one constructor (public or other), but some frameworks
+ * introduce an extra non-public constructor (see
+ * https://github.com/google/TestParameterInjector/issues/40).
+ */
+ static Constructor<?> getOnlyConstructor(Class<?> testClass) {
+ ImmutableList<Constructor<?>> constructors = ImmutableList.copyOf(testClass.getConstructors());
+ if (constructors.isEmpty()) {
+ // There are no public constructors. This is likely a JUnit5 test, so we should take the only
+ // non-public constructor instead.
+ constructors = ImmutableList.copyOf(testClass.getDeclaredConstructors());
+ }
+ checkState(
+ constructors.size() == 1, "Expected exactly one constructor, but got %s", constructors);
+ return getOnlyElement(constructors);
+ }
+
+ private TestParameterInjectorUtils() {}
+}
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValidator.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValidator.java
new file mode 100644
index 0000000..70db746
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValidator.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import com.google.common.base.Optional;
+import java.lang.annotation.Annotation;
+import java.util.List;
+
+/**
+ * Validator interface which allows {@link TestParameterAnnotation} annotations to validate the set
+ * of annotation values for a given test instance, and to selectively skip the test.
+ */
+interface TestParameterValidator {
+
+ /**
+ * This interface allows to access information on the current testwhen implementing {@link
+ * TestParameterValidator}.
+ */
+ interface Context {
+
+ /** Returns whether the current test has the {@link TestParameterAnnotation} value(s). */
+ boolean has(Class<? extends Annotation> testParameter, Object value);
+
+ /**
+ * Returns whether the current test has the two {@link TestParameterAnnotation} values, granted
+ * that the value is an enum, and each enum corresponds to a unique annotation.
+ */
+ <T extends Enum<T>, U extends Enum<U>> boolean has(T value1, U value2);
+
+ /**
+ * Returns all the current test value for a given {@link TestParameterAnnotation} annotated
+ * annotation.
+ */
+ Optional<Object> getValue(Class<? extends Annotation> testParameter);
+
+ /**
+ * Returns all the values specified for a given {@link TestParameterAnnotation} annotated
+ * annotation in the test.
+ *
+ * <p>For example, if the test annotates '@Foo(a,b,c)', getSpecifiedValues(Foo.class) will
+ * return [a,b,c].
+ */
+ List<Object> getSpecifiedValues(Class<? extends Annotation> testParameter);
+ }
+
+ /**
+ * Returns whether the test should be skipped based on the annotations' values.
+ *
+ * <p>The {@code testParameterValues} list contains all {@link TestParameterAnnotation}
+ * annotations, including those specified at the class, field, method, method parameter,
+ * constructor, and constructor parameter for a given test.
+ *
+ * <p>This method is not invoked in the context of a running test statement.
+ */
+ boolean shouldSkip(Context context);
+}
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValue.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValue.java
new file mode 100644
index 0000000..f748521
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValue.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Optional;
+import javax.annotation.Nullable;
+
+/**
+ * Wrapper class around a parameter value. Use this to give a value a name that is different from
+ * its {@code toString()} method.
+ */
+public class TestParameterValue {
+ private final @Nullable Object wrappedValue;
+ private final Optional<String> customName;
+
+ private TestParameterValue(@Nullable Object wrappedValue, Optional<String> customName) {
+ this.wrappedValue = wrappedValue;
+ this.customName = checkNotNull(customName);
+ }
+
+ /** Wraps the given value. */
+ public static TestParameterValue wrap(@Nullable Object wrappedValue) {
+ return new TestParameterValue(wrappedValue, /* customName= */ Optional.absent());
+ }
+
+ /**
+ * Returns a new {@link TestParameterValue} instance that stores the given name. The
+ * TestParameterInjector framework will use this name instead of {@code wrappedValue.toString()}
+ * when generating the test name.
+ */
+ public TestParameterValue withName(String name) {
+ return new TestParameterValue(wrappedValue, Optional.of(name));
+ }
+
+ @Nullable
+ Object getWrappedValue() {
+ return wrappedValue;
+ }
+
+ Optional<String> getCustomName() {
+ return customName;
+ }
+}
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java
new file mode 100644
index 0000000..9cc9f88
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import com.google.common.base.Optional;
+import java.lang.annotation.Annotation;
+import java.util.List;
+
+/**
+ * Interface which allows {@link TestParameterAnnotation} annotations to provide the values to test
+ * in a dynamic way.
+ */
+interface TestParameterValueProvider {
+
+ /**
+ * Returns the parameter values for which the test should run.
+ *
+ * @param annotation The annotation instance that was encountered in the test class. The
+ * definition of this annotation is itself annotated with the {@link TestParameterAnnotation}
+ * annotation.
+ * @param parameterClass The class of the parameter or field that is being annotated. In case the
+ * annotation is annotating a method, constructor or class, {@code parameterClass} is an empty
+ * optional.
+ */
+ default List<Object> provideValues(Annotation annotation, Optional<Class<?>> parameterClass) {
+ throw new UnsupportedOperationException(
+ "If this is called by TestParameterInjector, it means that neither of the"
+ + " provideValues()-type methods have been implemented");
+ }
+
+ /**
+ * Extension of {@link #provideValues(Annotation, Optional<Class<?>>)} with extra context.
+ *
+ * @param annotation The annotation instance that was encountered in the test class. The
+ * definition of this annotation is itself annotated with the {@link TestParameterAnnotation}
+ * annotation.
+ * @param otherAnnotations A list of all other annotations on the field or parameter that was
+ * annotated with {@code annotation}.
+ * <p>For example, if the test code is as follows:
+ * <pre>
+ * @Test
+ * public void myTest_success(
+ * @CustomAnnotation(123) @TestParameter(valuesProvider=MyProvider.class) Foo foo) {
+ * ...
+ * }
+ * </pre>
+ * then this list will contain a single element: @CustomAnnotation(123).
+ * <p>In case the annotation is annotating a method, constructor or class, {@code
+ * parameterClass} is an empty list.
+ * @param parameterClass The class of the parameter or field that is being annotated. In case the
+ * annotation is annotating a method, constructor or class, {@code parameterClass} is an empty
+ * optional.
+ * @param testClass The class that contains the test that is currently being run.
+ * <p>Having this can be useful when sharing providers between tests that have the same base
+ * class. In those cases, an abstract method can be called as follows:
+ * <pre>
+ * ((MyBaseClass) context.testClass().newInstance()).myAbstractMethod()
+ * </pre>
+ *
+ * @deprecated Don't use this method outside of the testparameterinjector codebase, as it is prone
+ * to being changed.
+ */
+ @Deprecated
+ default List<Object> provideValues(
+ Annotation annotation, Optional<Class<?>> parameterClass, GenericParameterContext context) {
+ return provideValues(annotation, parameterClass);
+ }
+
+ /**
+ * Returns the class of the list elements returned by {@link #provideValues(Annotation,
+ * Optional)}.
+ *
+ * @param annotationType The type of the annotation that was encountered in the test class. The
+ * definition of this annotation is itself annotated with the {@link TestParameterAnnotation}
+ * annotation.
+ * @param parameterClass The class of the parameter or field that is being annotated. In case the
+ * annotation is annotating a method, constructor or class, {@code parameterClass} is an empty
+ * optional.
+ */
+ Class<?> getValueType(
+ Class<? extends Annotation> annotationType, Optional<Class<?>> parameterClass);
+}
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValues.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValues.java
new file mode 100644
index 0000000..b2c88a6
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValues.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import com.google.common.base.Optional;
+import java.lang.annotation.Annotation;
+
+/** Interface to retrieve the {@link TestParameterAnnotation} values for a test. */
+interface TestParameterValues {
+ /**
+ * Returns a {@link TestParameterAnnotation} value for the current test as specified by {@code
+ * testInfo}, or {@link Optional#absent()} if the {@code annotationType} is not found.
+ */
+ Optional<Object> getValue(Class<? extends Annotation> annotationType);
+}
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java
new file mode 100644
index 0000000..29f945f
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2024 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import java.lang.annotation.Annotation;
+import java.util.List;
+import java.util.NoSuchElementException;
+import javax.annotation.Nullable;
+
+/**
+ * Abstract class for custom providers of @TestParameter values.
+ *
+ * <p>This is a replacement for {@link TestParameter.TestParameterValuesProvider}, which will soon
+ * be deprecated. The difference with the former interface is that this class provides a {@code
+ * Context} instance when invoking {@link #provideValues}.
+ */
+public abstract class TestParameterValuesProvider
+ implements TestParameter.TestParameterValuesProvider {
+
+ protected abstract List<?> provideValues(Context context) throws Exception;
+
+ /**
+ * @deprecated This method should never be called as it will simply throw an {@link
+ * UnsupportedOperationException}.
+ */
+ @Override
+ @Deprecated
+ public final List<?> provideValues() {
+ throw new UnsupportedOperationException(
+ "The TestParameterInjector framework should never call this method, and instead call"
+ + " #provideValues(Context)");
+ }
+
+ /**
+ * Wraps the given value in an object that allows you to give the parameter value a different
+ * name. The TestParameterInjector framework will recognize the returned {@link
+ * TestParameterValue} instances and unwrap them at injection time.
+ *
+ * <p>Usage: {@code value(file.content).withName(file.name)}.
+ */
+ @Override
+ public final TestParameterValue value(@Nullable Object wrappedValue) {
+ // Overriding this method as final because it is not supposed to be overwritten
+ return TestParameterValue.wrap(wrappedValue);
+ }
+
+ /**
+ * An immutable value class that contains extra information about the context of the parameter for
+ * which values are being provided.
+ */
+ public static final class Context {
+
+ private final GenericParameterContext delegate;
+
+ Context(GenericParameterContext delegate) {
+ this.delegate = delegate;
+ }
+
+ /**
+ * Returns the only annotation with the given type on the field or parameter that was annotated
+ * with @TestParameter.
+ *
+ * <p>For example, if the test code is as follows:
+ *
+ * <pre>
+ * {@literal @}Test
+ * public void myTest_success(
+ * {@literal @}CustomAnnotation(123) {@literal @}TestParameter(valuesProvider=MyProvider.class) Foo foo) {
+ * ...
+ * }
+ * </pre>
+ *
+ * then {@code context.getOtherAnnotation(CustomAnnotation.class).value()} will equal 123.
+ *
+ * @throws NoSuchElementException if this there is no annotation with the given type
+ * @throws IllegalArgumentException if there are multiple annotations with the given type
+ * @throws IllegalArgumentException if the argument it TestParameter.class because it is already
+ * handled by the TestParameterInjector framework.
+ */
+ public <A extends Annotation> A getOtherAnnotation(Class<A> annotationType) {
+ checkArgument(
+ !TestParameter.class.equals(annotationType),
+ "Getting the @TestParameter annotating the field or parameter is not allowed because"
+ + " it is already handled by the TestParameterInjector framework.");
+ return delegate.getAnnotation(annotationType);
+ }
+
+ /**
+ * Returns the only annotation with the given type on the field or parameter that was annotated
+ * with @TestParameter.
+ *
+ * <p>For example, if the test code is as follows:
+ *
+ * <pre>
+ * {@literal @}Test
+ * public void myTest_success(
+ * {@literal @}CustomAnnotation(123)
+ * {@literal @}CustomAnnotation(456)
+ * {@literal @}TestParameter(valuesProvider=MyProvider.class)
+ * Foo foo) {
+ * ...
+ * }
+ * </pre>
+ *
+ * then {@code context.getOtherAnnotations(CustomAnnotation.class)} will return the annotation
+ * with 123 and 456.
+ *
+ * <p>Returns an empty list if this there is no annotation with the given type.
+ *
+ * @throws IllegalArgumentException if the argument it TestParameter.class because it is already
+ * handled by the TestParameterInjector framework.
+ */
+ public <A extends Annotation> ImmutableList<A> getOtherAnnotations(Class<A> annotationType) {
+ checkArgument(
+ !TestParameter.class.equals(annotationType),
+ "Getting the @TestParameter annotating the field or parameter is not allowed because"
+ + " it is already handled by the TestParameterInjector framework.");
+ return delegate.getAnnotations(annotationType);
+ }
+
+ /**
+ * The class that contains the test that is currently being run.
+ *
+ * <p>Having this can be useful when sharing providers between tests that have the same base
+ * class. In those cases, an abstract method can be called as follows:
+ *
+ * <pre>
+ * ((MyBaseClass) context.testClass().newInstance()).myAbstractMethod()
+ * </pre>
+ */
+ public Class<?> testClass() {
+ return delegate.testClass();
+ }
+
+ /** A list of all annotations on the field or parameter. */
+ @VisibleForTesting
+ ImmutableList<Annotation> annotationsOnParameter() {
+ return delegate.annotationsOnParameter();
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+ }
+}
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameters.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameters.java
new file mode 100644
index 0000000..07d0fff
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameters.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static java.util.Collections.unmodifiableMap;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValuesProvider;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/**
+ * Annotation that can be placed (repeatedly) on @Test-methods or a test constructor to indicate the
+ * sets of parameters that it should be invoked with.
+ *
+ * <p>For @Test-methods, the method will be invoked for every set of parameters that is specified.
+ * For constructors, all the tests in the test class will be invoked on a class instance that was
+ * constructed by each set of parameters.
+ *
+ * <p>Note: If this annotation is used in a test class, the other methods in that class can still
+ * use other types of parameterization, such as {@linkplain TestParameter @TestParameter}.
+ *
+ * <p>See {@link #value()} for simple examples.
+ *
+ * <p>Warning: This annotation can only be used if the compiled java code contains the parameter
+ * names. This is typically done by passing the {@code -parameters} option to the Java compiler,
+ * which requires using Java 8 or higher and may not be available on Android.
+ */
+@Retention(RUNTIME)
+@Target({CONSTRUCTOR, METHOD})
+@Repeatable(TestParameters.RepeatedTestParameters.class)
+public @interface TestParameters {
+
+ /**
+ * Specifies one or more stringified sets of parameters in YAML format. Each set corresponds to a
+ * single invocation of a test method.
+ *
+ * <p>Each element in this array is a full parameter set, formatted as a YAML mapping. The mapping
+ * keys must match the parameter names and the mapping values will be converted to the parameter
+ * type if possible. See yaml.org for the YAML syntax and the section below on the supported
+ * parameter types.
+ *
+ * <p>There are two distinct ways of using this annotation: repeated vs single:
+ *
+ * <p><b>Recommended usage: Separate annotation per parameter set</b>
+ *
+ * <p>This approach uses multiple @TestParameters annotations, one for each set of parameters, for
+ * example:
+ *
+ * <pre>
+ * {@literal @}Test
+ * {@literal @}TestParameters("{age: 17, expectIsAdult: false}")
+ * {@literal @}TestParameters("{age: 22, expectIsAdult: true}")
+ * public void personIsAdult(int age, boolean expectIsAdult) { ... }
+ *
+ * {@literal @}Test
+ * {@literal @}TestParameters("{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}")
+ * {@literal @}TestParameters("{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}")
+ * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... }
+ * </pre>
+ *
+ * <p><b>Old discouraged usage: Single annotation with all parameter sets</b>
+ *
+ * <p>This approach uses a single @TestParameter annotation for all parameter sets, for example:
+ *
+ * <pre>
+ * {@literal @}Test
+ * {@literal @}TestParameters({
+ * "{age: 17, expectIsAdult: false}",
+ * "{age: 22, expectIsAdult: true}",
+ * })
+ * public void personIsAdult(int age, boolean expectIsAdult) { ... }
+ *
+ * {@literal @}Test
+ * {@literal @}TestParameters({
+ * "{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}",
+ * "{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}",
+ * })
+ * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... }
+ * </pre>
+ *
+ * <p><b>Supported parameter types</b>
+ *
+ * <ul>
+ * <li>YAML primitives:
+ * <ul>
+ * <li>String: Specified as YAML string
+ * <li>boolean: Specified as YAML boolean
+ * <li>long and int: Specified as YAML integer
+ * <li>float and double: Specified as YAML floating point or integer
+ * </ul>
+ * <li>
+ * <li>Parsed types:
+ * <ul>
+ * <li>Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()}
+ * <li>Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML
+ * bytes (example: "!!binary 'ZGF0YQ=='")
+ * </ul>
+ * <li>
+ * </ul>
+ *
+ * <p>For dynamic sets of parameters or parameter types that are not supported here, use {@link
+ * #valuesProvider()} and leave this field empty.
+ */
+ String[] value() default {};
+
+ /**
+ * Overrides the name of the parameter set that is used in the test name.
+ *
+ * <p>This can only be set if {@link #value()} has exactly one element. If not set, the YAML
+ * string in {@link #value()} is used in the test name.
+ *
+ * <p>For example: If this name is set to "young adult", then the test name might be
+ * "personIsAdult[young adult]" where the default might have been "personIsAdult[{age: 17,
+ * expectIsAdult: false}]".
+ */
+ String customName() default "";
+
+ /**
+ * Sets a provider that will return a list of parameter sets. Each element in the returned list
+ * corresponds to a single invocation of a test method.
+ *
+ * <p>If this field is set, {@link #value()} must be empty and vice versa.
+ *
+ * <p><b>Example</b>
+ *
+ * <pre>
+ * {@literal @}Test
+ * {@literal @}TestParameters(valuesProvider = IsAdultValueProvider.class)
+ * public void personIsAdult(int age, boolean expectIsAdult) { ... }
+ *
+ * private static final class IsAdultValueProvider implements TestParametersValuesProvider {
+ * {@literal @}Override public {@literal List<TestParametersValues>} provideValues() {
+ * return ImmutableList.of(
+ * TestParametersValues.builder()
+ * .name("teenager")
+ * .addParameter("age", 17)
+ * .addParameter("expectIsAdult", false)
+ * .build(),
+ * TestParametersValues.builder()
+ * .name("young adult")
+ * .addParameter("age", 22)
+ * .addParameter("expectIsAdult", true)
+ * .build()
+ * );
+ * }
+ * }
+ * </pre>
+ */
+ Class<? extends TestParametersValuesProvider> valuesProvider() default
+ DefaultTestParametersValuesProvider.class;
+
+ /** Interface for custom providers of test parameter values. */
+ interface TestParametersValuesProvider {
+ List<TestParametersValues> provideValues();
+ }
+
+ /** A set of parameters for a single method invocation. */
+ @AutoValue
+ abstract class TestParametersValues {
+
+ /**
+ * A name for this set of parameters that will be used for describing this test.
+ *
+ * <p>Example: If a test method is called "personIsAdult" and this name is "teenager", the name
+ * of the resulting test will be "personIsAdult[teenager]".
+ */
+ public abstract String name();
+
+ /** A map, mapping parameter names to their values. */
+ @SuppressWarnings("AutoValueImmutableFields") // intentional to allow null values
+ public abstract Map<String, Object> parametersMap();
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ // Avoid instantiations other than the AutoValue one.
+ TestParametersValues() {}
+
+ /** Builder for {@link TestParametersValues}. */
+ public static final class Builder {
+ private String name;
+ private final LinkedHashMap<String, Object> parametersMap = new LinkedHashMap<>();
+
+ /**
+ * Sets a name for this set of parameters that will be used for describing this test.
+ *
+ * <p>Setting a name is optional. If unset, one will be generated from the parameter values.
+ *
+ * <p>Example: If a test method is called "personIsAdult" and this name is "teenager", the
+ * name of the resulting test will be "personIsAdult[teenager]".
+ */
+ public Builder name(String name) {
+ this.name = name.replaceAll("\\s+", " ");
+ return this;
+ }
+
+ /**
+ * Adds a parameter by its name.
+ *
+ * @param parameterName The name of the parameter of the test method
+ * @param value A value of the same type as the method parameter
+ */
+ public Builder addParameter(String parameterName, @Nullable Object value) {
+ this.parametersMap.put(parameterName, value);
+ return this;
+ }
+
+ /** Adds parameters by thris names. */
+ public Builder addParameters(Map<String, Object> parameterNameToValueMap) {
+ this.parametersMap.putAll(parameterNameToValueMap);
+ return this;
+ }
+
+ public TestParametersValues build() {
+ if (name == null) {
+ // Name is not set. Auto-generate one based on the parameter name and values
+ StringBuilder nameBuilder = new StringBuilder();
+ nameBuilder.append('{');
+ for (String parameterName : parametersMap.keySet()) {
+ if (nameBuilder.length() > 1) {
+ nameBuilder.append(", ");
+ }
+ nameBuilder.append(
+ ParameterValueParsing.formatTestNameString(
+ Optional.of(parameterName), parametersMap.get(parameterName)));
+ }
+ nameBuilder.append('}');
+ name = nameBuilder.toString();
+ }
+ return new AutoValue_TestParameters_TestParametersValues(
+ name, unmodifiableMap(new LinkedHashMap<>(parametersMap)));
+ }
+ }
+ }
+
+ /** Default {@link TestParametersValuesProvider} implementation that does nothing. */
+ class DefaultTestParametersValuesProvider implements TestParametersValuesProvider {
+ @Override
+ public List<TestParametersValues> provideValues() {
+ return ImmutableList.of();
+ }
+ }
+
+ /**
+ * Holder annotation for multiple @TestParameters annotations. This should never be used directly.
+ */
+ @Retention(RUNTIME)
+ @Target({CONSTRUCTOR, METHOD})
+ @interface RepeatedTestParameters {
+ TestParameters[] value();
+ }
+}
diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParametersMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParametersMethodProcessor.java
new file mode 100644
index 0000000..26a1e65
--- /dev/null
+++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParametersMethodProcessor.java
@@ -0,0 +1,468 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Verify.verify;
+
+import com.google.auto.value.AutoAnnotation;
+import com.google.common.base.Optional;
+import com.google.common.base.Throwables;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.primitives.Primitives;
+import com.google.common.reflect.TypeToken;
+import com.google.common.util.concurrent.UncheckedExecutionException;
+import com.google.testing.junit.testparameterinjector.junit5.TestInfo.TestInfoParameter;
+import com.google.testing.junit.testparameterinjector.junit5.TestParameters.DefaultTestParametersValuesProvider;
+import com.google.testing.junit.testparameterinjector.junit5.TestParameters.RepeatedTestParameters;
+import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValues;
+import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValuesProvider;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Executable;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Parameter;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/** {@code TestMethodProcessor} implementation for supporting {@link TestParameters}. */
+@SuppressWarnings("AndroidJdkLibsChecker") // Parameter is not available on old Android SDKs.
+final class TestParametersMethodProcessor implements TestMethodProcessor {
+
+ private final LoadingCache<Executable, ImmutableList<TestParametersValues>>
+ parameterValuesByConstructorOrMethodCache =
+ CacheBuilder.newBuilder()
+ .maximumSize(1000)
+ .build(CacheLoader.from(TestParametersMethodProcessor::toParameterValuesList));
+
+ @Override
+ public ExecutableValidationResult validateConstructor(Constructor<?> constructor) {
+ if (hasRelevantAnnotation(constructor)) {
+ try {
+ // This method throws an exception if there is a validation error
+ getConstructorParameters(constructor);
+ } catch (Throwable t) {
+ return ExecutableValidationResult.validated(t);
+ }
+ return ExecutableValidationResult.valid();
+ } else {
+ return ExecutableValidationResult.notValidated();
+ }
+ }
+
+ @Override
+ public ExecutableValidationResult validateTestMethod(Method testMethod, Class<?> testClass) {
+ if (hasRelevantAnnotation(testMethod)) {
+ try {
+ // This method throws an exception if there is a validation error
+ getMethodParameters(testMethod);
+ } catch (Throwable t) {
+ return ExecutableValidationResult.validated(t);
+ }
+ return ExecutableValidationResult.valid();
+ } else {
+ return ExecutableValidationResult.notValidated();
+ }
+ }
+
+ @Override
+ public List<TestInfo> calculateTestInfos(TestInfo originalTest) {
+ boolean constructorIsParameterized =
+ hasRelevantAnnotation(
+ TestParameterInjectorUtils.getOnlyConstructor(originalTest.getTestClass()));
+ boolean methodIsParameterized = hasRelevantAnnotation(originalTest.getMethod());
+
+ if (!constructorIsParameterized && !methodIsParameterized) {
+ return ImmutableList.of(originalTest);
+ }
+
+ ImmutableList.Builder<TestInfo> testInfos = ImmutableList.builder();
+
+ ImmutableList<Optional<TestParametersValues>> constructorParametersList =
+ getConstructorParametersOrSingleAbsentElement(originalTest.getTestClass());
+ ImmutableList<Optional<TestParametersValues>> methodParametersList =
+ getMethodParametersOrSingleAbsentElement(originalTest.getMethod());
+ for (int constructorParametersIndex = 0;
+ constructorParametersIndex < constructorParametersList.size();
+ ++constructorParametersIndex) {
+ Optional<TestParametersValues> constructorParameters =
+ constructorParametersList.get(constructorParametersIndex);
+
+ for (int methodParametersIndex = 0;
+ methodParametersIndex < methodParametersList.size();
+ ++methodParametersIndex) {
+ Optional<TestParametersValues> methodParameters =
+ methodParametersList.get(methodParametersIndex);
+
+ // Making final copies of non-final integers for use in lambda
+ int constructorParametersIndexCopy = constructorParametersIndex;
+ int methodParametersIndexCopy = methodParametersIndex;
+
+ testInfos.add(
+ originalTest
+ .withExtraParameters(
+ FluentIterable.of(
+ constructorParameters.transform(
+ param ->
+ TestInfoParameter.create(
+ param.name(),
+ param.parametersMap(),
+ constructorParametersIndexCopy)),
+ methodParameters.transform(
+ param ->
+ TestInfoParameter.create(
+ param.name(),
+ param.parametersMap(),
+ methodParametersIndexCopy)))
+ .filter(Optional::isPresent)
+ .transform(Optional::get)
+ .toList())
+ .withExtraAnnotation(
+ TestIndexHolderFactory.create(
+ constructorParametersIndex, methodParametersIndex)));
+ }
+ }
+ return testInfos.build();
+ }
+
+ private ImmutableList<Optional<TestParametersValues>>
+ getConstructorParametersOrSingleAbsentElement(Class<?> testClass) {
+ Constructor<?> constructor = TestParameterInjectorUtils.getOnlyConstructor(testClass);
+ return hasRelevantAnnotation(constructor)
+ ? FluentIterable.from(getConstructorParameters(constructor))
+ .transform(Optional::of)
+ .toList()
+ : ImmutableList.of(Optional.absent());
+ }
+
+ private ImmutableList<Optional<TestParametersValues>> getMethodParametersOrSingleAbsentElement(
+ Method method) {
+ return hasRelevantAnnotation(method)
+ ? FluentIterable.from(getMethodParameters(method)).transform(Optional::of).toList()
+ : ImmutableList.of(Optional.absent());
+ }
+
+ @Override
+ public Optional<List<Object>> maybeGetConstructorParameters(
+ Constructor<?> constructor, TestInfo testInfo) {
+ if (hasRelevantAnnotation(constructor)) {
+ ImmutableList<TestParametersValues> parameterValuesList =
+ getConstructorParameters(constructor);
+ TestParametersValues parametersValues =
+ parameterValuesList.get(
+ testInfo.getAnnotation(TestIndexHolder.class).constructorParametersIndex());
+
+ return Optional.of(toParameterList(parametersValues, constructor.getParameters()));
+ } else {
+ return Optional.absent();
+ }
+ }
+
+ @Override
+ public Optional<List<Object>> maybeGetTestMethodParameters(TestInfo testInfo) {
+ Method testMethod = testInfo.getMethod();
+ if (hasRelevantAnnotation(testMethod)) {
+ ImmutableList<TestParametersValues> parameterValuesList = getMethodParameters(testMethod);
+ TestParametersValues parametersValues =
+ parameterValuesList.get(
+ testInfo.getAnnotation(TestIndexHolder.class).methodParametersIndex());
+
+ return Optional.of(toParameterList(parametersValues, testMethod.getParameters()));
+ } else {
+ return Optional.absent();
+ }
+ }
+
+ @Override
+ public void postProcessTestInstance(Object testInstance, TestInfo testInfo) {}
+
+ private ImmutableList<TestParametersValues> getConstructorParameters(Constructor<?> constructor) {
+ try {
+ return parameterValuesByConstructorOrMethodCache.getUnchecked(constructor);
+ } catch (UncheckedExecutionException e) {
+ // Rethrow IllegalStateException because they can be caused by user mistakes and the user
+ // doesn't need to know that the caching layer is in between.
+ Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class);
+ throw e;
+ }
+ }
+
+ private ImmutableList<TestParametersValues> getMethodParameters(Method method) {
+ try {
+ return parameterValuesByConstructorOrMethodCache.getUnchecked(method);
+ } catch (UncheckedExecutionException e) {
+ // Rethrow IllegalStateException because they can be caused by user mistakes and the user
+ // doesn't need to know that the caching layer is in between.
+ Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class);
+ throw e;
+ }
+ }
+
+ private static ImmutableList<TestParametersValues> toParameterValuesList(Executable executable) {
+ checkParameterNamesArePresent(executable);
+ ImmutableList<Parameter> parametersList = ImmutableList.copyOf(executable.getParameters());
+
+ if (executable.isAnnotationPresent(TestParameters.class)) {
+ checkState(
+ !executable.isAnnotationPresent(RepeatedTestParameters.class),
+ "Unexpected situation: Both @TestParameters and @RepeatedTestParameters annotating the"
+ + " same method");
+ TestParameters annotation = executable.getAnnotation(TestParameters.class);
+ boolean valueIsSet = annotation.value().length > 0;
+ boolean valuesProviderIsSet =
+ !annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class);
+
+ checkState(
+ !(valueIsSet && valuesProviderIsSet),
+ "It is not allowed to specify both value and valuesProvider in @TestParameters(value=%s,"
+ + " valuesProvider=%s) on %s()",
+ Arrays.toString(annotation.value()),
+ annotation.valuesProvider().getSimpleName(),
+ executable.getName());
+ checkState(
+ valueIsSet || valuesProviderIsSet,
+ "Either a value or a valuesProvider must be set in @TestParameters on %s()",
+ executable.getName());
+ if (!annotation.customName().isEmpty()) {
+ checkState(
+ annotation.value().length == 1,
+ "Setting @TestParameters.customName is only allowed if there is exactly one YAML string"
+ + " in @TestParameters.value (on %s())",
+ executable.getName());
+ }
+
+ if (valueIsSet) {
+ return FluentIterable.from(annotation.value())
+ .transform(
+ yamlMap -> toParameterValues(yamlMap, parametersList, annotation.customName()))
+ .toList();
+ } else {
+ return toParameterValuesList(annotation.valuesProvider(), parametersList);
+ }
+ } else { // Not annotated with @TestParameters
+ verify(
+ executable.isAnnotationPresent(RepeatedTestParameters.class),
+ "This method should only be called for executables with at least one relevant"
+ + " annotation");
+
+ return FluentIterable.from(executable.getAnnotation(RepeatedTestParameters.class).value())
+ .transform(
+ annotation ->
+ toParameterValues(
+ validateAndGetSingleValueFromRepeatedAnnotation(annotation, executable),
+ parametersList,
+ annotation.customName()))
+ .toList();
+ }
+ }
+
+ private static ImmutableList<TestParametersValues> toParameterValuesList(
+ Class<? extends TestParametersValuesProvider> valuesProvider, List<Parameter> parameters) {
+ try {
+ Constructor<? extends TestParametersValuesProvider> constructor =
+ valuesProvider.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ List<TestParametersValues> testParametersValues = constructor.newInstance().provideValues();
+ for (TestParametersValues testParametersValue : testParametersValues) {
+ validateThatValuesMatchParameters(testParametersValue, parameters);
+ }
+ return ImmutableList.copyOf(testParametersValues);
+ } catch (NoSuchMethodException e) {
+ if (!Modifier.isStatic(valuesProvider.getModifiers()) && valuesProvider.isMemberClass()) {
+ throw new IllegalStateException(
+ String.format(
+ "Could not find a no-arg constructor for %s, probably because it is a not-static"
+ + " inner class. You can fix this by making %s static.",
+ valuesProvider.getSimpleName(), valuesProvider.getSimpleName()),
+ e);
+ } else {
+ throw new IllegalStateException(
+ String.format(
+ "Could not find a no-arg constructor for %s.", valuesProvider.getSimpleName()),
+ e);
+ }
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private static void checkParameterNamesArePresent(Executable executable) {
+ checkState(
+ FluentIterable.from(executable.getParameters()).allMatch(Parameter::isNamePresent),
+ ""
+ + "No parameter name could be found for %s, which likely means that parameter names"
+ + " aren't available at runtime. Please ensure that the this test was built with the"
+ + " -parameters compiler option.\n"
+ + "\n"
+ + "In Maven, you do this by adding <parameters>true</parameters> to the"
+ + " maven-compiler-plugin's configuration. For example:\n"
+ + "\n"
+ + "<build>\n"
+ + " <plugins>\n"
+ + " <plugin>\n"
+ + " <groupId>org.apache.maven.plugins</groupId>\n"
+ + " <artifactId>maven-compiler-plugin</artifactId>\n"
+ + " <version>3.8.1</version>\n"
+ + " <configuration>\n"
+ + " <compilerArgs>\n"
+ + " <arg>-parameters</arg>\n"
+ + " </compilerArgs>\n"
+ + " </configuration>\n"
+ + " </plugin>\n"
+ + " </plugins>\n"
+ + "</build>\n"
+ + "\n"
+ + "Don't forget to run `mvn clean` after making this change.",
+ executable.getName());
+ }
+
+ private static String validateAndGetSingleValueFromRepeatedAnnotation(
+ TestParameters annotation, Executable executable) {
+ checkState(
+ annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class),
+ "Setting a valuesProvider is not supported for methods/constructors with"
+ + " multiple @TestParameters annotations on %s()",
+ executable.getName());
+ checkState(
+ annotation.value().length > 0,
+ "Either a value or a valuesProvider must be set in @TestParameters on %s()",
+ executable.getName());
+ checkState(
+ annotation.value().length == 1,
+ "When specifying more than one @TestParameter for a method/constructor, each annotation"
+ + " must have exactly one value. Instead, got %s values on %s(): %s",
+ annotation.value().length,
+ executable.getName(),
+ Arrays.toString(annotation.value()));
+
+ return annotation.value()[0];
+ }
+
+ private static void validateThatValuesMatchParameters(
+ TestParametersValues testParametersValues, List<Parameter> parameters) {
+ ImmutableMap<String, Parameter> parametersByName =
+ Maps.uniqueIndex(parameters, Parameter::getName);
+
+ checkState(
+ testParametersValues.parametersMap().keySet().equals(parametersByName.keySet()),
+ "Cannot map the given TestParametersValues to parameters %s (Given TestParametersValues"
+ + " are %s)",
+ parametersByName.keySet(),
+ testParametersValues);
+
+ testParametersValues
+ .parametersMap()
+ .forEach(
+ (paramName, paramValue) -> {
+ Class<?> expectedClass = Primitives.wrap(parametersByName.get(paramName).getType());
+ if (paramValue != null) {
+ checkState(
+ expectedClass.isInstance(paramValue),
+ "Cannot map value '%s' (class = %s) to parameter %s (class = %s) (for"
+ + " TestParametersValues %s)",
+ paramValue,
+ paramValue.getClass(),
+ paramName,
+ expectedClass,
+ testParametersValues);
+ }
+ });
+ }
+
+ private static TestParametersValues toParameterValues(
+ String yamlString, List<Parameter> parameters, String maybeCustomName) {
+ Object yamlMapObject = ParameterValueParsing.parseYamlStringToObject(yamlString);
+ checkState(
+ yamlMapObject instanceof Map,
+ "Cannot map YAML string '%s' to parameters because it is not a mapping",
+ yamlString);
+ Map<?, ?> yamlMap = (Map<?, ?>) yamlMapObject;
+
+ ImmutableMap<String, Parameter> parametersByName =
+ Maps.uniqueIndex(parameters, Parameter::getName);
+ checkState(
+ yamlMap.keySet().equals(parametersByName.keySet()),
+ "Cannot map YAML string '%s' to parameters %s",
+ yamlString,
+ parametersByName.keySet());
+
+ @SuppressWarnings("unchecked")
+ Map<String, Object> checkedYamlMap = (Map<String, Object>) yamlMap;
+
+ return TestParametersValues.builder()
+ .name(maybeCustomName.isEmpty() ? yamlString : maybeCustomName)
+ .addParameters(
+ Maps.transformEntries(
+ checkedYamlMap,
+ (parameterName, parsedYaml) ->
+ ParameterValueParsing.parseYamlObjectToJavaType(
+ parsedYaml,
+ TypeToken.of(parametersByName.get(parameterName).getParameterizedType()))))
+ .build();
+ }
+
+ // Note: We're not using the Executable interface here because it isn't supported by Java 7 and
+ // this code is called even if only @TestParameter is used. In other places, Executable is usable
+ // because @TestParameters only works for Java 8 anyway.
+ private static boolean hasRelevantAnnotation(Constructor<?> executable) {
+ return executable.isAnnotationPresent(TestParameters.class)
+ || executable.isAnnotationPresent(RepeatedTestParameters.class);
+ }
+
+ private static boolean hasRelevantAnnotation(Method executable) {
+ return executable.isAnnotationPresent(TestParameters.class)
+ || executable.isAnnotationPresent(RepeatedTestParameters.class);
+ }
+
+ private static List<Object> toParameterList(
+ TestParametersValues parametersValues, Parameter[] parameters) {
+ return Arrays.asList(
+ FluentIterable.from(Arrays.asList(parameters))
+ .transform(Parameter::getName)
+ .transform(name -> parametersValues.parametersMap().get(name))
+ .toArray(Object.class));
+ }
+
+ /**
+ * This mechanism is a workaround to be able to store the test index in the annotation list of the
+ * {@link TestInfo}, since we cannot carry other information through the test runner.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface TestIndexHolder {
+ int constructorParametersIndex();
+
+ int methodParametersIndex();
+ }
+
+ /** Factory for {@link TestIndexHolder}. */
+ static class TestIndexHolderFactory {
+ @AutoAnnotation
+ static TestIndexHolder create(int constructorParametersIndex, int methodParametersIndex) {
+ return new AutoAnnotation_TestParametersMethodProcessor_TestIndexHolderFactory_create(
+ constructorParametersIndex, methodParametersIndex);
+ }
+
+ private TestIndexHolderFactory() {}
+ }
+}
diff --git a/junit5/src/test/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorJUnit5Test.java b/junit5/src/test/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorJUnit5Test.java
new file mode 100644
index 0000000..0ebf54b
--- /dev/null
+++ b/junit5/src/test/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorJUnit5Test.java
@@ -0,0 +1,608 @@
+/*
+ * Copyright 2021 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.testing.junit.testparameterinjector.junit5_otherpackage;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.common.collect.Lists.newArrayList;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static java.util.Arrays.stream;
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.testing.junit.testparameterinjector.junit5.TestParameter;
+import com.google.testing.junit.testparameterinjector.junit5.TestParameter.TestParameterValuesProvider;
+import com.google.testing.junit.testparameterinjector.junit5.TestParameterInjectorTest;
+import com.google.testing.junit.testparameterinjector.junit5.TestParameters;
+import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValues;
+import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValuesProvider;
+import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.platform.engine.TestExecutionResult;
+import org.junit.platform.engine.TestExecutionResult.Status;
+import org.junit.platform.engine.discovery.DiscoverySelectors;
+import org.junit.platform.launcher.Launcher;
+import org.junit.platform.launcher.LauncherDiscoveryRequest;
+import org.junit.platform.launcher.TestExecutionListener;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.TestPlan;
+import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
+import org.junit.platform.launcher.core.LauncherFactory;
+
+/** Tests the full feature set of TestParameterInjector with JUnit5 (Jupiter). */
+class TestParameterInjectorJUnit5Test {
+
+ abstract static class SuccessfulTestCaseBase {
+ private static Map<String, String> testNameToStringifiedParameters;
+ private static ImmutableMap<String, String> expectedTestNameToStringifiedParameters;
+ private String testName;
+
+ @BeforeAll
+ private static void checkStaticFieldAreNull() {
+ checkState(testNameToStringifiedParameters == null);
+ checkState(expectedTestNameToStringifiedParameters == null);
+ }
+
+ @BeforeEach
+ private void storeTestName(org.junit.jupiter.api.TestInfo testInfo) {
+ testName = testInfo.getDisplayName();
+ }
+
+ final void storeTestParametersForThisTest(Object... params) {
+ if (testNameToStringifiedParameters == null) {
+ testNameToStringifiedParameters = new LinkedHashMap<>();
+ // Copying this into a static field because @AfterAll methods have to be static
+ expectedTestNameToStringifiedParameters = expectedTestNameToStringifiedParameters();
+ }
+ checkState(
+ !testNameToStringifiedParameters.containsKey(testName),
+ "Parameters for the test with name '%s' are already stored. This might mean that there"
+ + " are duplicate test names",
+ testName);
+ testNameToStringifiedParameters.put(
+ testName, stream(params).map(String::valueOf).collect(joining(":")));
+ }
+
+ abstract ImmutableMap<String, String> expectedTestNameToStringifiedParameters();
+
+ @AfterAll
+ private static void completedAllTests() {
+ try {
+ assertWithMessage(toCopyPastableJavaString(testNameToStringifiedParameters))
+ .that(testNameToStringifiedParameters)
+ .isEqualTo(expectedTestNameToStringifiedParameters);
+ } finally {
+ testNameToStringifiedParameters = null;
+ expectedTestNameToStringifiedParameters = null;
+ }
+ }
+ }
+
+ @RunAsTest
+ static class SimpleCases_WithoutExplicitConstructor extends SuccessfulTestCaseBase {
+ @Test
+ void withoutCustomAnnotation() {
+ storeTestParametersForThisTest();
+ }
+
+ @TestParameterInjectorTest
+ void withoutParameters() {
+ storeTestParametersForThisTest();
+ }
+
+ @TestParameterInjectorTest
+ @TestParameters("{name: 1, number: 3.3}")
+ @TestParameters("{name: abc, number: 5}")
+ void withParameters_success(String name, double number) {
+ storeTestParametersForThisTest(name, number);
+ }
+
+ @TestParameterInjectorTest
+ void withParameter_success(
+ @TestParameter({"2", "xyz"}) String name, @TestParameter boolean bool) {
+ storeTestParametersForThisTest(name, bool);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("withoutCustomAnnotation()", "")
+ .put("withoutParameters", "")
+ .put("withParameters_success[{name: 1, number: 3.3}]", "1:3.3")
+ .put("withParameters_success[{name: abc, number: 5}]", "abc:5.0")
+ .put("withParameter_success[name=2,bool=false]", "2:false")
+ .put("withParameter_success[name=2,bool=true]", "2:true")
+ .put("withParameter_success[xyz,bool=false]", "xyz:false")
+ .put("withParameter_success[xyz,bool=true]", "xyz:true")
+ .build();
+ }
+ }
+
+ @RunAsTest
+ static class SimpleCases_WithParameterizedConstructor_TestParameter
+ extends SuccessfulTestCaseBase {
+ private final boolean constr;
+
+ @TestParameter({"AAA", "BBB"})
+ private String field;
+
+ SimpleCases_WithParameterizedConstructor_TestParameter(@TestParameter boolean constr) {
+ this.constr = constr;
+ }
+
+ @TestParameterInjectorTest
+ void withoutParameters() {
+ storeTestParametersForThisTest(constr, field);
+ }
+
+ @TestParameterInjectorTest
+ @TestParameters("{name: 1}")
+ @TestParameters("{name: abc}")
+ void withParameters_success(String name) {
+ storeTestParametersForThisTest(constr, field, name);
+ }
+
+ @TestParameterInjectorTest
+ void withParameter_success(@TestParameter({"2", "xyz"}) String name) {
+ storeTestParametersForThisTest(constr, field, name);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("withParameters_success[{name: 1},AAA,constr=false]", "false:AAA:1")
+ .put("withParameters_success[{name: 1},AAA,constr=true]", "true:AAA:1")
+ .put("withParameters_success[{name: 1},BBB,constr=false]", "false:BBB:1")
+ .put("withParameters_success[{name: 1},BBB,constr=true]", "true:BBB:1")
+ .put("withParameters_success[{name: abc},AAA,constr=false]", "false:AAA:abc")
+ .put("withParameters_success[{name: abc},AAA,constr=true]", "true:AAA:abc")
+ .put("withParameters_success[{name: abc},BBB,constr=false]", "false:BBB:abc")
+ .put("withParameters_success[{name: abc},BBB,constr=true]", "true:BBB:abc")
+ .put("withParameter_success[AAA,constr=false,name=2]", "false:AAA:2")
+ .put("withParameter_success[AAA,constr=false,xyz]", "false:AAA:xyz")
+ .put("withParameter_success[AAA,constr=true,name=2]", "true:AAA:2")
+ .put("withParameter_success[AAA,constr=true,xyz]", "true:AAA:xyz")
+ .put("withParameter_success[BBB,constr=false,name=2]", "false:BBB:2")
+ .put("withParameter_success[BBB,constr=false,xyz]", "false:BBB:xyz")
+ .put("withParameter_success[BBB,constr=true,name=2]", "true:BBB:2")
+ .put("withParameter_success[BBB,constr=true,xyz]", "true:BBB:xyz")
+ .put("withoutParameters[AAA,constr=false]", "false:AAA")
+ .put("withoutParameters[AAA,constr=true]", "true:AAA")
+ .put("withoutParameters[BBB,constr=false]", "false:BBB")
+ .put("withoutParameters[BBB,constr=true]", "true:BBB")
+ .build();
+ }
+ }
+
+ @RunAsTest
+ static class SimpleCases_WithParameterizedConstructor_TestParameters
+ extends SuccessfulTestCaseBase {
+ private final boolean constr;
+
+ @TestParameter({"AAA", "BBB"})
+ private String field;
+
+ @TestParameters("{constr: true}")
+ @TestParameters("{constr: false}")
+ SimpleCases_WithParameterizedConstructor_TestParameters(boolean constr) {
+ this.constr = constr;
+ }
+
+ @TestParameterInjectorTest
+ void withoutParameters() {
+ storeTestParametersForThisTest(constr, field);
+ }
+
+ @TestParameterInjectorTest
+ @TestParameters("{name: 1}")
+ @TestParameters("{name: abc}")
+ void withParameters_success(String name) {
+ storeTestParametersForThisTest(constr, field, name);
+ }
+
+ @TestParameterInjectorTest
+ void withParameter_success(@TestParameter({"2", "xyz"}) String name) {
+ storeTestParametersForThisTest(constr, field, name);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("withParameters_success[{constr: true},{name: 1},AAA]", "true:AAA:1")
+ .put("withParameters_success[{constr: true},{name: 1},BBB]", "true:BBB:1")
+ .put("withParameters_success[{constr: true},{name: abc},AAA]", "true:AAA:abc")
+ .put("withParameters_success[{constr: true},{name: abc},BBB]", "true:BBB:abc")
+ .put("withParameters_success[{constr: false},{name: 1},AAA]", "false:AAA:1")
+ .put("withParameters_success[{constr: false},{name: 1},BBB]", "false:BBB:1")
+ .put("withParameters_success[{constr: false},{name: abc},AAA]", "false:AAA:abc")
+ .put("withParameters_success[{constr: false},{name: abc},BBB]", "false:BBB:abc")
+ .put("withParameter_success[{constr: true},AAA,name=2]", "true:AAA:2")
+ .put("withParameter_success[{constr: true},AAA,xyz]", "true:AAA:xyz")
+ .put("withParameter_success[{constr: true},BBB,name=2]", "true:BBB:2")
+ .put("withParameter_success[{constr: true},BBB,xyz]", "true:BBB:xyz")
+ .put("withParameter_success[{constr: false},AAA,name=2]", "false:AAA:2")
+ .put("withParameter_success[{constr: false},AAA,xyz]", "false:AAA:xyz")
+ .put("withParameter_success[{constr: false},BBB,name=2]", "false:BBB:2")
+ .put("withParameter_success[{constr: false},BBB,xyz]", "false:BBB:xyz")
+ .put("withoutParameters[{constr: true},AAA]", "true:AAA")
+ .put("withoutParameters[{constr: true},BBB]", "true:BBB")
+ .put("withoutParameters[{constr: false},AAA]", "false:AAA")
+ .put("withoutParameters[{constr: false},BBB]", "false:BBB")
+ .build();
+ }
+ }
+
+ @RunAsTest
+ public static class AdvancedCases_WithValuesProvider extends SuccessfulTestCaseBase {
+ private final TestEnum testEnum;
+
+ @TestParameters(valuesProvider = TestEnumValuesProvider.class)
+ public AdvancedCases_WithValuesProvider(TestEnum testEnum) {
+ this.testEnum = testEnum;
+ }
+
+ @TestParameterInjectorTest
+ void test1() {
+ storeTestParametersForThisTest(testEnum);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("test1[one]", "ONE")
+ .put("test1[two]", "TWO")
+ .put("test1[null-case]", "null")
+ .build();
+ }
+ }
+
+ @RunAsTest
+ public static class AdvancedCases_WithValueProvider extends SuccessfulTestCaseBase {
+ @TestParameterInjectorTest
+ void stringTest(@TestParameter(valuesProvider = TestStringProvider.class) String string) {
+ storeTestParametersForThisTest(string);
+ }
+
+ @TestParameterInjectorTest
+ void charMatcherTest(
+ @TestParameter(valuesProvider = CharMatcherProvider.class) CharMatcher charMatcher) {
+ storeTestParametersForThisTest(charMatcher);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("stringTest[A]", "A")
+ .put("stringTest[B]", "B")
+ .put("stringTest[string=null]", "null")
+ .put("stringTest[wizard]", "harry")
+ .put("charMatcherTest[CharMatcher.any()]", "CharMatcher.any()")
+ .put("charMatcherTest[CharMatcher.ascii()]", "CharMatcher.ascii()")
+ .put("charMatcherTest[CharMatcher.whitespace()]", "CharMatcher.whitespace()")
+ .build();
+ }
+
+ private static final class TestStringProvider implements TestParameterValuesProvider {
+ @Override
+ public List<?> provideValues() {
+ return newArrayList("A", "B", null, value("harry").withName("wizard"));
+ }
+ }
+
+ private static final class CharMatcherProvider implements TestParameterValuesProvider {
+ @Override
+ public List<CharMatcher> provideValues() {
+ return newArrayList(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace());
+ }
+ }
+ }
+
+ public abstract static class BaseClassWithTestParametersMethod extends SuccessfulTestCaseBase {
+ @TestParameterInjectorTest
+ @TestParameters("{testEnum: ONE}")
+ @TestParameters("{testEnum: TWO}")
+ void testInBase(TestEnum testEnum) {
+ storeTestParametersForThisTest(testEnum);
+ }
+ }
+
+ @RunAsTest
+ public static class AdvancedCases_WithBaseClass_TestParametersMethodInBase
+ extends BaseClassWithTestParametersMethod {
+ @TestParameterInjectorTest
+ @TestParameters({"{testEnum: TWO}", "{testEnum: THREE}"})
+ void testInChild(TestEnum testEnum) {
+ storeTestParametersForThisTest(testEnum);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("testInBase[{testEnum: ONE}]", "ONE")
+ .put("testInBase[{testEnum: TWO}]", "TWO")
+ .put("testInChild[{testEnum: TWO}]", "TWO")
+ .put("testInChild[{testEnum: THREE}]", "THREE")
+ .build();
+ }
+ }
+
+ public abstract static class BaseClassWithTestParameterMethod extends SuccessfulTestCaseBase {
+ @TestParameterInjectorTest
+ void testInBase(@TestParameter({"ONE", "TWO"}) TestEnum testEnum) {
+ storeTestParametersForThisTest(testEnum);
+ }
+ }
+
+ @RunAsTest
+ public static class AdvancedCases_WithBaseClass_TestParameterMethodInBase
+ extends BaseClassWithTestParameterMethod {
+ @TestParameterInjectorTest
+ @TestParameters({"{testEnum: TWO}", "{testEnum: THREE}"})
+ void testInChild(TestEnum testEnum) {
+ storeTestParametersForThisTest(testEnum);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("testInBase[ONE]", "ONE")
+ .put("testInBase[TWO]", "TWO")
+ .put("testInChild[{testEnum: TWO}]", "TWO")
+ .put("testInChild[{testEnum: THREE}]", "THREE")
+ .build();
+ }
+ }
+
+ public abstract static class BaseClassWithTestParameterField extends SuccessfulTestCaseBase {
+ @TestParameter TestEnum fieldInBase;
+ }
+
+ @RunAsTest
+ public static class AdvancedCases_WithBaseClass_TestParameterFieldInBase
+ extends BaseClassWithTestParameterField {
+ @TestParameterInjectorTest
+ void testInChild() {
+ storeTestParametersForThisTest(fieldInBase);
+ }
+
+ @Override
+ ImmutableMap<String, String> expectedTestNameToStringifiedParameters() {
+ return ImmutableMap.<String, String>builder()
+ .put("testInChild[ONE]", "ONE")
+ .put("testInChild[TWO]", "TWO")
+ .put("testInChild[THREE]", "THREE")
+ .build();
+ }
+ }
+
+ @RunAsTest(
+ failsWithMessage =
+ "Either a value or a valuesProvider must be set in @TestParameters on test1()")
+ public static class InvalidTest_TestParameters_EmptyAnnotation {
+ @TestParameterInjectorTest
+ @TestParameters
+ void test1() {}
+ }
+
+ @RunAsTest(
+ failsWithMessage = "Either a value or a valuesProvider must be set in @TestParameters on ")
+ public static class InvalidTest_TestParameters_EmptyAnnotationOnConstructor {
+ @TestParameters
+ public InvalidTest_TestParameters_EmptyAnnotationOnConstructor() {}
+
+ @TestParameterInjectorTest
+ void test1() {}
+ }
+
+ @RunAsTest(
+ failsWithMessage =
+ "It is not allowed to specify both value and valuesProvider in"
+ + " @TestParameters(value=[{testEnum: ONE}], valuesProvider=TestEnumValuesProvider)"
+ + " on test1()")
+ public static class InvalidTest_TestParameters_CombiningValueWithProvider {
+ @TestParameterInjectorTest
+ @TestParameters(value = "{testEnum: ONE}", valuesProvider = TestEnumValuesProvider.class)
+ void test1(TestEnum testEnum) {}
+ }
+
+ @RunAsTest(
+ failsWithMessage =
+ "Either a value or a valuesProvider must be set in @TestParameters on test1()")
+ public static class InvalidTest_TestParameters_RepeatedAnnotationIsEmpty {
+ @TestParameterInjectorTest
+ @TestParameters(value = "{testEnum: ONE}")
+ @TestParameters
+ void test1(TestEnum testEnum) {}
+ }
+
+ @RunAsTest(
+ failsWithMessage =
+ "When specifying more than one @TestParameter for a method/constructor, each annotation"
+ + " must have exactly one value. Instead, got 2 values on test1(): [{testEnum: TWO},"
+ + " {testEnum: THREE}]")
+ public static class InvalidTest_TestParameters_RepeatedAnnotationHasMultipleValues {
+ @TestParameterInjectorTest
+ @TestParameters(value = "{testEnum: ONE}")
+ @TestParameters(value = {"{testEnum: TWO}", "{testEnum: THREE}"})
+ void test1(TestEnum testEnum) {}
+ }
+
+ @RunAsTest(
+ failsWithMessage =
+ "Setting a valuesProvider is not supported for methods/constructors with"
+ + " multiple @TestParameters annotations on test1()")
+ public static class InvalidTest_TestParameters_RepeatedAnnotationHasProvider {
+ @TestParameterInjectorTest
+ @TestParameters(valuesProvider = TestEnumValuesProvider.class)
+ @TestParameters(valuesProvider = TestEnumValuesProvider.class)
+ void test1(TestEnum testEnum) {}
+ }
+
+ @RunAsTest(
+ failsWithMessage =
+ "Setting @TestParameters.customName is only allowed if there is exactly one YAML string"
+ + " in @TestParameters.value (on test1())")
+ public static class InvalidTest_TestParameters_NamedAnnotationHasMultipleValues {
+ @TestParameterInjectorTest
+ @TestParameters(
+ customName = "custom",
+ value = {"{testEnum: TWO}", "{testEnum: THREE}"})
+ void test1(TestEnum testEnum) {}
+ }
+
+ @RunAsTest(
+ failsWithMessage =
+ "Could not find a no-arg constructor for NonStaticProvider, probably because it is a"
+ + " not-static inner class. You can fix this by making NonStaticProvider static.")
+ public static class InvalidTest_TestParameter_NonStaticProviderClass {
+ @TestParameterInjectorTest
+ void test(@TestParameter(valuesProvider = NonStaticProvider.class) int i) {}
+
+ @SuppressWarnings("ClassCanBeStatic")
+ class NonStaticProvider implements TestParameterValuesProvider {
+ @Override
+ public List<?> provideValues() {
+ return ImmutableList.of();
+ }
+ }
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("provideTestClassesThatExpectSuccess")
+ void runTest_success(Class<?> testClass) {
+ FailureListener listener = new FailureListener();
+ LauncherDiscoveryRequest request =
+ LauncherDiscoveryRequestBuilder.request()
+ .selectors(DiscoverySelectors.selectClass(testClass))
+ .build();
+ Launcher launcher = LauncherFactory.create();
+ launcher.registerTestExecutionListeners(listener);
+ launcher.execute(request);
+
+ assertNoFailures(listener.failures);
+ }
+
+ @ParameterizedTest(name = "{0} fails with '{1}'")
+ @MethodSource("provideTestClassesThatExpectFailure")
+ void runTest_failure(Class<?> testClass, String failureMessage) {
+ FailureListener listener = new FailureListener();
+ LauncherDiscoveryRequest request =
+ LauncherDiscoveryRequestBuilder.request()
+ .selectors(DiscoverySelectors.selectClass(testClass))
+ .build();
+ Launcher launcher = LauncherFactory.create();
+ TestPlan testPlan = launcher.discover(request);
+ launcher.registerTestExecutionListeners(listener);
+ launcher.execute(request);
+
+ assertThat(listener.failures).hasSize(1);
+ assertThat(getOnlyElement(listener.failures)).contains(failureMessage);
+ }
+
+ private static Stream<Class<?>> provideTestClassesThatExpectSuccess() {
+ return stream(TestParameterInjectorJUnit5Test.class.getDeclaredClasses())
+ .filter(
+ cls ->
+ cls.isAnnotationPresent(RunAsTest.class)
+ && cls.getAnnotation(RunAsTest.class).failsWithMessage().isEmpty());
+ }
+
+ private static Stream<Arguments> provideTestClassesThatExpectFailure() {
+ return stream(TestParameterInjectorJUnit5Test.class.getDeclaredClasses())
+ .filter(
+ cls ->
+ cls.isAnnotationPresent(RunAsTest.class)
+ && !cls.getAnnotation(RunAsTest.class).failsWithMessage().isEmpty())
+ .map(cls -> Arguments.of(cls, cls.getAnnotation(RunAsTest.class).failsWithMessage()));
+ }
+
+ private static void assertNoFailures(List<String> failures) {
+ if (failures.size() == 1) {
+ throw new AssertionError(getOnlyElement(failures));
+ } else if (failures.size() > 1) {
+ throw new AssertionError(
+ String.format(
+ "Test failed unexpectedly:\n\n%s",
+ failures.stream().collect(joining("\n------------------------------------\n"))));
+ }
+ }
+
+ private static String toCopyPastableJavaString(Map<String, String> map) {
+ StringBuilder resultBuilder = new StringBuilder();
+ resultBuilder.append("\n----------------------\n");
+ resultBuilder.append("ImmutableMap.<String, String>builder()\n");
+ map.forEach(
+ (key, value) ->
+ resultBuilder.append(String.format(" .put(\"%s\", \"%s\")\n", key, value)));
+ resultBuilder.append(" .build()\n");
+ resultBuilder.append("----------------------\n");
+ return resultBuilder.toString();
+ }
+
+ class FailureListener implements TestExecutionListener {
+ final List<String> failures = new ArrayList<>();
+
+ @Override
+ public void executionFinished(
+ TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
+ if (testExecutionResult.getStatus() != Status.SUCCESSFUL) {
+ failures.add(
+ String.format(
+ "%s --> %s",
+ testIdentifier.getDisplayName(),
+ testExecutionResult.getThrowable().isPresent()
+ ? Throwables.getStackTraceAsString(testExecutionResult.getThrowable().get())
+ : testExecutionResult));
+ }
+ }
+ }
+
+ @Retention(RUNTIME)
+ @interface RunAsTest {
+ String failsWithMessage() default "";
+ }
+
+ public enum TestEnum {
+ ONE,
+ TWO,
+ THREE;
+ }
+
+ private static final class TestEnumValuesProvider implements TestParametersValuesProvider {
+ @Override
+ public List<TestParametersValues> provideValues() {
+ return ImmutableList.of(
+ TestParametersValues.builder().name("one").addParameter("testEnum", TestEnum.ONE).build(),
+ TestParametersValues.builder().name("two").addParameter("testEnum", TestEnum.TWO).build(),
+ TestParametersValues.builder().name("null-case").addParameter("testEnum", null).build());
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index b753189..ae0bf6b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,10 +21,17 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.testparameterinjector</groupId>
- <artifactId>test-parameter-injector</artifactId>
+ <artifactId>test-parameter-injector-parent</artifactId>
<version>HEAD-SNAPSHOT</version>
- <name>TestParameterInjector</name>
+ <packaging>pom</packaging>
+
+ <modules>
+ <module>junit4</module>
+ <module>junit5</module>
+ </modules>
+
+ <name>TestParameterInjector parent project for internal use</name>
<description>
A simple yet powerful parameterized test runner.
@@ -71,17 +78,6 @@
</roles>
<timezone>+0</timezone>
</developer>
- <developer>
- <id>ajurkowski</id>
- <name>Alex Jurkowski</name>
- <email>ajurkowski@google.com</email>
- <organization>Google Inc.</organization>
- <organizationUrl>http://www.google.com/</organizationUrl>
- <roles>
- <role>developer</role>
- </roles>
- <timezone>-6</timezone>
- </developer>
</developers>
<scm>
@@ -130,22 +126,12 @@
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
- <version>30.1-jre</version>
- </dependency>
- <dependency>
- <groupId>com.google.protobuf</groupId>
- <artifactId>protobuf-lite</artifactId>
- <version>3.0.1</version>
- </dependency>
- <dependency>
- <groupId>junit</groupId>
- <artifactId>junit</artifactId>
- <version>4.13.2</version>
+ <version>32.0.0-jre</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
- <version>1.27</version>
+ <version>2.0</version>
</dependency>
<!-- Test dependencies -->
@@ -155,6 +141,12 @@
<version>1.1.2</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.google.protobuf</groupId>
+ <artifactId>protobuf-javalite</artifactId>
+ <version>3.20.3</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
@@ -173,6 +165,7 @@
<testSource>1.8</testSource>
<testTarget>1.8</testTarget>
<parameters>true</parameters>
+ <compilerArgument>-Xlint:deprecation</compilerArgument>
<annotationProcessorPaths>
<path>
<groupId>com.google.auto.value</groupId>