aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonathan Scott <scottjonathan@google.com>2021-08-05 16:56:21 +0100
committerJonathan Scott <scottjonathan@google.com>2021-08-05 18:09:49 +0100
commitb51cebd6ba9ca7f524418ed3b2d9b2540308b8d7 (patch)
tree13a630d6c68439dc4dd20f07df2657da81189de9
parentd515d44bc91c3e4747c0469fe2ad0ee38cd97804 (diff)
downloadTestParameterInjector-b51cebd6ba9ca7f524418ed3b2d9b2540308b8d7.tar.gz
Import TestParameterInjector.
Test: atest TestParameterInjectorTest Change-Id: I4eee53057041be2223ae133c2d2cf1d14fa752d0
-rw-r--r--Android.bp31
-rw-r--r--CHANGELOG.md22
-rw-r--r--CONTRIBUTING.md29
-rw-r--r--LICENSE202
-rw-r--r--METADATA17
-rw-r--r--MODULE_LICENSE_APACHE20
-rw-r--r--OWNERS2
-rw-r--r--README.md275
-rw-r--r--TestParameterInjector.iml17
-rw-r--r--pom.xml248
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java83
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java233
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java226
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java412
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java25
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java308
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java99
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessors.java54
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java224
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java266
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java1382
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java36
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameterProcessor.java31
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java68
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java52
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java27
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java208
-rw-r--r--src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java426
-rw-r--r--src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java136
-rw-r--r--src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java74
-rw-r--r--src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java249
-rw-r--r--src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java1077
-rw-r--r--src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java211
-rw-r--r--src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java474
34 files changed, 7224 insertions, 0 deletions
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..27f8749
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,31 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "TestParameterInjector",
+ srcs: [
+ "src/main/java/**/*.java"
+ ],
+ static_libs: [
+ "guava",
+ "auto_value_annotations",
+ "junit",
+ "libprotobuf-java-lite",
+ "snakeyaml"
+ ],
+ plugins: ["auto_value_plugin", "auto_annotation_plugin"],
+ host_supported: true
+}
+
+java_test_host {
+ name: "TestParameterInjectorTest",
+ srcs: ["src/test/java/**/*.java"],
+ static_libs: [
+ "TestParameterInjector",
+ "truth-prebuilt"
+ ],
+ test_options: {
+ unit_test: true,
+ },
+} \ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..ebe26a6
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,22 @@
+## 1.4
+
+- Bugfix: Run test methods declared in a base class (instead of throwing an
+ exception)
+- Test names with very long parameter strings are now abbreviated with a snippet
+ of the shortened parameter
+- Duplicate test names are given a suffix for deduplication
+- Replaced dependency on `protobuf-java` by a dependency on `protobuf-javalite`
+
+## 1.3
+
+- Treat 'null' as a magic string that results in a null value
+
+## 1.2
+
+- Don't use the parameter name if it's not explicitly provided by the compiler
+- Add support for older Android SDK versions by removing the dependency on
+ `j.l.r.Parameter`. The minimum Android SDK version is now 24.
+
+## 1.1
+
+- Add support for `ByteString` and `byte[]`
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..22b241c
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,29 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement (CLA). You (or your employer) retain the copyright to your
+contribution; this simply gives us permission to use and redistribute your
+contributions as part of the project. Head over to
+<https://cla.developers.google.com/> to see your current agreements on file or
+to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
+
+## Community Guidelines
+
+This project follows
+[Google's Open Source Community Guidelines](https://opensource.google/conduct/).
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..0a617ab
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,17 @@
+name: "TestParameterInjector"
+description:
+ "JUnit runner for parameterized tests"
+
+third_party {
+ url {
+ type: HOMEPAGE
+ value: "https://github.com/google/TestParameterInjector"
+ }
+ url {
+ type: GIT
+ value: "https://github.com/google/TestParameterInjector"
+ }
+ version: "e65d6bebdba9df211b258fae996fe34b6eadb787"
+ last_upgrade_date { year: 2021 month: 7 day: 26 }
+ license_type: NOTICE
+}
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..d36a34e
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,2 @@
+scottjonathan@google.com
+kholoudm@google.com
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7af215b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,275 @@
+TestParameterInjector
+=====================
+
+[Link to Javadoc.](https://google.github.io/TestParameterInjector/docs/latest/)
+
+## Introduction
+
+`TestParameterInjector` is a JUnit4 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
+promote high test coverage for data-driven tests.
+
+There are a lot of alternative parameterized test frameworks, such as
+[junit.runners.Parameterized](https://github.com/junit-team/junit4/wiki/parameterized-tests)
+and [JUnitParams](https://github.com/Pragmatists/JUnitParams). We believe
+`TestParameterInjector` is an improvement of those because it is more powerful
+and simpler to use.
+
+[This blogpost](https://opensource.googleblog.com/2021/03/introducing-testparameterinjector.html)
+goes into a bit more detail about how `TestParameterInjector` compares to other
+frameworks used at Google.
+
+## Getting started
+
+To start using `TestParameterInjector` right away, copy the following snippet:
+
+```java
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+import com.google.testing.junit.testparameterinjector.TestParameter;
+
+@RunWith(TestParameterInjector.class)
+public class MyTest {
+
+ @TestParameter boolean isDryRun;
+
+ @Test public void test1(@TestParameter boolean enableFlag) {
+ // ...
+ }
+
+ @Test public 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</artifactId>
+ <version>1.4</version>
+</dependency>
+```
+
+or see [this maven.org
+page](https://search.maven.org/artifact/com.google.testparameterinjector/test-parameter-injector)
+for instructions for other build tools.
+
+
+## Basics
+
+### `@TestParameter` for testing all combinations
+
+#### Parameterizing a single test method
+
+The simplest way to use this library is to use `@TestParameter`. For example:
+
+```java
+@RunWith(TestParameterInjector.class)
+public class MyTest {
+
+ @Test
+ public void test(@TestParameter boolean isOwner) {...}
+}
+```
+
+In this example, two tests will be automatically generated by the test framework:
+
+- One with `isOwner` set to `true`
+- One with `isOwner` set to `false`
+
+When running the tests, the result will show the following test names:
+
+```
+MyTest#test[isOwner=true]
+MyTest#test[isOwner=false]
+```
+
+#### Parameterizing the whole class
+
+`@TestParameter` can also annotate a field:
+
+```java
+@RunWith(TestParameterInjector.class)
+public class MyTest {
+
+ @TestParameter private boolean isOwner;
+
+ @Test public void test1() {...}
+ @Test public void test2() {...}
+}
+```
+
+In this example, both `test1` and `test2` will be run twice (once for each
+parameter value).
+
+#### Supported types
+
+The following examples show most of the supported types. See the `@TestParameter` javadoc for more details.
+
+```java
+// Enums
+@TestParameter AnimalEnum a; // Implies all possible values of AnimalEnum
+@TestParameter({"CAT", "DOG"}) AnimalEnum a; // Implies AnimalEnum.CAT and AnimalEnum.DOG.
+
+// Strings
+@TestParameter({"cat", "dog"}) String animalName;
+
+// Java primitives
+@TestParameter boolean b; // Implies {true, false}
+@TestParameter({"1", "2", "3"}) int i;
+@TestParameter({"1", "1.5", "2"}) double d;
+
+// Bytes
+@TestParameter({"!!binary 'ZGF0YQ=='", "some_string"}) byte[] bytes;
+```
+
+For non-primitive types (e.g. String, enums, bytes), `"null"` is always parsed as the `null` reference.
+
+#### Multiple parameters: All combinations are run
+
+If there are multiple `@TestParameter`-annotated values applicable to one test
+method, the test is run for all possible combinations of those values. Example:
+
+```java
+@RunWith(TestParameterInjector.class)
+public class MyTest {
+
+ @TestParameter private boolean a;
+
+ @Test public void test1(@TestParameter boolean b, @TestParameter boolean c) {
+ // Run for these combinations:
+ // (a=false, b=false, c=false)
+ // (a=false, b=false, c=true )
+ // (a=false, b=true, c=false)
+ // (a=false, b=true, c=true )
+ // (a=true, b=false, c=false)
+ // (a=true, b=false, c=true )
+ // (a=true, b=true, c=false)
+ // (a=true, b=true, c=true )
+ }
+}
+```
+
+If you want to explicitly define which combinations are run, see the next
+sections.
+
+### Use a test enum for enumerating more complex parameter combinations
+
+Use this strategy if you want to:
+
+- Explicitly specify the combination of parameters
+- or your parameters are too large to be encoded in a `String` in a readable
+ way
+
+Example:
+
+```java
+@RunWith(TestParameterInjector.class)
+class MyTest {
+
+ enum FruitVolumeTestCase {
+ APPLE(Fruit.newBuilder().setName("Apple").setShape(SPHERE).build(), /* expectedVolume= */ 3.1),
+ BANANA(Fruit.newBuilder().setName("Banana").setShape(CURVED).build(), /* expectedVolume= */ 2.1),
+ MELON(Fruit.newBuilder().setName("Melon").setShape(SPHERE).build(), /* expectedVolume= */ 6);
+
+ final Fruit fruit;
+ final double expectedVolume;
+
+ FruitVolumeTestCase(Fruit fruit, double expectedVolume) { ... }
+ }
+
+ @Test
+ public void calculateVolume_success(@TestParameter FruitVolumeTestCase fruitVolumeTestCase) {
+ assertThat(calculateVolume(fruitVolumeTestCase.fruit))
+ .isEqualTo(fruitVolumeTestCase.expectedVolume);
+ }
+}
+```
+
+The enum constant name has the added benefit of making for sensible test names:
+
+```
+MyTest#calculateVolume_success[APPLE]
+MyTest#calculateVolume_success[BANANA]
+MyTest#calculateVolume_success[MELON]
+```
+
+### `@TestParameters` for defining sets of parameters
+
+You can also explicitly enumerate the sets of test parameters via a list of YAML
+mappings:
+
+```java
+@Test
+@TestParameters({
+ "{age: 17, expectIsAdult: false}",
+ "{age: 22, expectIsAdult: true}",
+})
+public void personIsAdult(int age, boolean expectIsAdult) { ... }
+```
+
+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.
+
+## Advanced usage
+
+### Dynamic parameter generation for `@TestParameter`
+
+Instead of providing a list of parsable strings, you can implement your own
+`TestParameterValuesProvider` as follows:
+
+```java
+@Test
+public void matchesAllOf_throwsOnNull(
+ @TestParameter(valuesProvider = CharMatcherProvider.class) CharMatcher charMatcher) {
+ assertThrows(NullPointerException.class, () -> charMatcher.matchesAllOf(null));
+}
+
+private static final class CharMatcherProvider implements TestParameterValuesProvider {
+ @Override
+ public List<CharMatcher> provideValues() {
+ 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.
+
+### Dynamic parameter generation for `@TestParameters`
+
+Instead of providing a YAML mapping of parameters, you can implement your own
+`TestParametersValuesProvider` as follows:
+
+```java
+@Test
+@TestParameters(valuesProvider = IsAdultValueProvider.class)
+public void personIsAdult(int age, boolean expectIsAdult) { ... }
+
+static final class IsAdultValueProvider implements TestParametersValuesProvider {
+ @Override public ImmutableList<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()
+ );
+ }
+}
+```
diff --git a/TestParameterInjector.iml b/TestParameterInjector.iml
new file mode 100644
index 0000000..77d3a30
--- /dev/null
+++ b/TestParameterInjector.iml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
+ <exclude-output />
+ <content url="file://$MODULE_DIR$">
+ <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
+ </content>
+ <orderEntry type="sourceFolder" forTests="false" />
+ <orderEntry type="module" module-name="framework_srcjars" />
+ <orderEntry type="module" module-name="base" />
+ <orderEntry type="module" module-name="modules-utils" />
+ <orderEntry type="module" module-name="Connectivity" />
+ <orderEntry type="module" module-name="dependencies" />
+ <orderEntry type="inheritedJdk" />
+ </component>
+</module> \ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..b753189
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,248 @@
+<?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>
+
+ <groupId>com.google.testparameterinjector</groupId>
+ <artifactId>test-parameter-injector</artifactId>
+ <version>HEAD-SNAPSHOT</version>
+
+ <name>TestParameterInjector</name>
+
+ <description>
+ A simple yet powerful parameterized test runner.
+ </description>
+
+ <url>https://github.com/google/testparameterinjector</url>
+
+ <inceptionYear>2021</inceptionYear>
+
+ <organization>
+ <name>Google Inc.</name>
+ <url>http://www.google.com/</url>
+ </organization>
+
+ <licenses>
+ <license>
+ <name>The Apache Software License, Version 2.0</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+
+ <developers>
+ <developer>
+ <id>nymanjens</id>
+ <name>Jens Nyman</name>
+ <email>jnyman@google.com</email>
+ <organization>Google Inc.</organization>
+ <organizationUrl>http://www.google.com/</organizationUrl>
+ <roles>
+ <role>owner</role>
+ <role>developer</role>
+ </roles>
+ <timezone>+1</timezone>
+ </developer>
+ <developer>
+ <id>sergebeauchamp</id>
+ <name>Serge Beauchamp</name>
+ <email>sergebeauchamp@google.com</email>
+ <organization>Google Inc.</organization>
+ <organizationUrl>http://www.google.com/</organizationUrl>
+ <roles>
+ <role>developer</role>
+ </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>
+ <url>http://github.com/google/testparameterinjector/</url>
+ <connection>scm:git:git://github.com/google/testparameterinjector.git</connection>
+ <developerConnection>scm:git:ssh://git@github.com/google/testparameterinjector.git</developerConnection>
+ </scm>
+
+ <issueManagement>
+ <system>GitHub Issues</system>
+ <url>http://github.com/google/testparameterinjector/issues</url>
+ </issueManagement>
+ <distributionManagement>
+ <snapshotRepository>
+ <id>sonatype-nexus-snapshots</id>
+ <name>Sonatype Nexus Snapshots</name>
+ <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
+ </snapshotRepository>
+ <repository>
+ <id>sonatype-nexus-staging</id>
+ <name>Nexus Release Repository</name>
+ <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
+ </repository>
+ </distributionManagement>
+
+ <prerequisites>
+ <maven>3.0.3</maven>
+ </prerequisites>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+
+ <dependencies>
+ <!-- Compile-time dependencies -->
+ <dependency>
+ <groupId>com.google.auto.value</groupId>
+ <artifactId>auto-value-annotations</artifactId>
+ <version>1.7.4</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.code.findbugs</groupId>
+ <artifactId>jsr305</artifactId>
+ <version>3.0.2</version>
+ </dependency>
+ <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>
+ </dependency>
+ <dependency>
+ <groupId>org.yaml</groupId>
+ <artifactId>snakeyaml</artifactId>
+ <version>1.27</version>
+ </dependency>
+
+ <!-- Test dependencies -->
+ <dependency>
+ <groupId>com.google.truth</groupId>
+ <artifactId>truth</artifactId>
+ <version>1.1.2</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-jar-plugin</artifactId>
+ <version>3.2.0</version>
+ </plugin>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>3.8.1</version>
+ <configuration>
+ <source>1.8</source>
+ <target>1.8</target>
+ <testSource>1.8</testSource>
+ <testTarget>1.8</testTarget>
+ <parameters>true</parameters>
+ <annotationProcessorPaths>
+ <path>
+ <groupId>com.google.auto.value</groupId>
+ <artifactId>auto-value</artifactId>
+ <version>1.7.4</version>
+ </path>
+ </annotationProcessorPaths>
+ </configuration>
+ </plugin>
+ <plugin>
+ <artifactId>maven-source-plugin</artifactId>
+ <version>3.2.1</version>
+ </plugin>
+ <plugin>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>2.22.2</version>
+ </plugin>
+ </plugins>
+ </build>
+
+ <profiles>
+ <profile>
+ <id>sonatype-oss-release</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-source-plugin</artifactId>
+ <version>3.2.1</version>
+ <executions>
+ <execution>
+ <id>attach-sources</id>
+ <goals>
+ <goal>jar-no-fork</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ <configuration>
+ <source>8</source>
+ </configuration>
+ <version>3.2.0</version>
+ <executions>
+ <execution>
+ <id>attach-javadocs</id>
+ <goals>
+ <goal>jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-gpg-plugin</artifactId>
+ <version>1.1</version>
+ <executions>
+ <execution>
+ <id>sign-artifacts</id>
+ <phase>verify</phase>
+ <goals>
+ <goal>sign</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java b/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java
new file mode 100644
index 0000000..ab5003e
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java
@@ -0,0 +1,83 @@
+/*
+ * 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 java.lang.annotation.Annotation;
+import java.util.Comparator;
+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 =
+ parameters.stream()
+ .max(Comparator.comparing(parameter -> context.getSpecifiedValues(parameter).size()))
+ .get();
+ // 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/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java
new file mode 100644
index 0000000..624ee9b
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java
@@ -0,0 +1,233 @@
+/*
+ * 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 static java.util.function.Function.identity;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.primitives.Primitives;
+import com.google.common.reflect.TypeToken;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.MessageLite;
+import java.lang.reflect.ParameterizedType;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import javax.annotation.Nullable;
+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 MessageLite parseTextprotoMessage(String textprotoString, Class<?> javaType) {
+ return getProtoValueParser().parseTextprotoMessage(textprotoString, javaType);
+ }
+
+ static boolean isValidYamlString(String yamlString) {
+ try {
+ new Yaml(new SafeConstructor()).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()).load(yamlString);
+ }
+
+ @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, identity())
+ // 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, identity());
+
+ yamlValueTransformer.ifJavaType(Integer.class).supportParsedType(Integer.class, identity());
+
+ yamlValueTransformer
+ .ifJavaType(Long.class)
+ .supportParsedType(Long.class, identity())
+ .supportParsedType(Integer.class, Integer::longValue);
+
+ yamlValueTransformer
+ .ifJavaType(Float.class)
+ .supportParsedType(Float.class, identity())
+ .supportParsedType(Double.class, Double::floatValue)
+ .supportParsedType(Integer.class, Integer::floatValue);
+
+ yamlValueTransformer
+ .ifJavaType(Double.class)
+ .supportParsedType(Double.class, identity())
+ .supportParsedType(Integer.class, Integer::doubleValue)
+ .supportParsedType(Long.class, Long::doubleValue);
+
+ yamlValueTransformer
+ .ifJavaType(Enum.class)
+ .supportParsedType(
+ String.class, str -> ParameterValueParsing.parseEnum(str, javaType.getRawType()));
+
+ yamlValueTransformer
+ .ifJavaType(MessageLite.class)
+ .supportParsedType(String.class, str -> parseTextprotoMessage(str, javaType.getRawType()))
+ .supportParsedType(
+ Map.class,
+ map ->
+ getProtoValueParser()
+ .parseProtobufMessage((Map<String, Object>) map, javaType.getRawType()));
+
+ yamlValueTransformer
+ .ifJavaType(byte[].class)
+ .supportParsedType(byte[].class, identity())
+ .supportParsedType(String.class, s -> s.getBytes(StandardCharsets.UTF_8));
+
+ yamlValueTransformer
+ .ifJavaType(ByteString.class)
+ .supportParsedType(String.class, ByteString::copyFromUtf8)
+ .supportParsedType(byte[].class, ByteString::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 ->
+ Maps.transformValues(
+ map,
+ v ->
+ parseYamlObjectToJavaType(
+ v, getGenericParameterType(javaType, /* parameterIndex= */ 1))));
+
+ return yamlValueTransformer.transformedJavaValue();
+ }
+
+ 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")
+ <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.");
+ transformedJavaValue = checkNotNull(transformation.apply((ParsedYamlT) parsedYaml));
+ }
+ }
+
+ return this;
+ }
+ }
+ }
+
+ static ProtoValueParsing getProtoValueParser() {
+ try {
+ // This is called reflectively so that the android target doesn't have to build in
+ // ProtoValueParsing, which has no Android-compatible target.
+ Class<?> clazz =
+ Class.forName("com.google.testing.junit.testparameterinjector.ProtoValueParsingImpl");
+ return (ProtoValueParsing) clazz.getDeclaredConstructor().newInstance();
+ } catch (ClassNotFoundException unused) {
+ throw new UnsupportedOperationException(
+ "Textproto support is not available when using the Android version of"
+ + " testparameterinjector.");
+ } catch (ReflectiveOperationException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ private ParameterValueParsing() {}
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java
new file mode 100644
index 0000000..dbafc6a
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java
@@ -0,0 +1,226 @@
+/*
+ * 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 com.google.auto.value.AutoAnnotation;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.testing.junit.testparameterinjector.TestInfo.TestInfoParameter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Constructor;
+import java.text.MessageFormat;
+import java.util.List;
+import org.junit.runner.Description;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.Statement;
+import org.junit.runners.model.TestClass;
+
+/**
+ * {@code TestMethodProcessor} implementation for supporting {@link org.junit.runners.Parameterized}
+ * tests.
+ *
+ * <p>Supports parameterized class if a method with the {@link Parameters} annotation is defined. As
+ * opposed to the junit {@link org.junit.runners.Parameterized} class, only one method can have the
+ * {@link Parameters} annotation, and has to be both public and static.
+ *
+ * <p>The {@link Parameters} annotated method can return either a {@code Collection<Object>} or a
+ * {@code Collection<Object[]>}.
+ *
+ * <p>Does not support injected {@link org.junit.runners.Parameterized.Parameter} fields, and
+ * instead requires a single class constructor with one argument for each parameter returned by the
+ * {@link Parameters} method.
+ */
+class ParameterizedTestMethodProcessor implements TestMethodProcessor {
+
+ /**
+ * The parameters as returned by the {@link Parameters} annotated method, or {@link
+ * Optional#absent()} if the class is not parameterized.
+ */
+ private final Optional<Iterable<?>> parametersForAllTests;
+ /**
+ * The test name pattern as defined by the 'name' attribute of the {@link Parameters} annotation,
+ * or {@link Optional#absent()} if the class is not parameterized.
+ */
+ private final Optional<String> testNamePattern;
+
+ ParameterizedTestMethodProcessor(TestClass testClass) {
+ Optional<FrameworkMethod> parametersMethod = getParametersMethod(testClass);
+ if (parametersMethod.isPresent()) {
+ Object parameters;
+ try {
+ parameters = parametersMethod.get().invokeExplosively(null);
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ if (parameters instanceof Iterable) {
+ parametersForAllTests = Optional.<Iterable<?>>of((Iterable<?>) parameters);
+ } else if (parameters instanceof Object[]) {
+ parametersForAllTests =
+ Optional.<Iterable<?>>of(ImmutableList.copyOf((Object[]) parameters));
+ } else {
+ throw new IllegalStateException(
+ "Unsupported @Parameters return value type: " + parameters.getClass());
+ }
+ testNamePattern = Optional.of(parametersMethod.get().getAnnotation(Parameters.class).name());
+ } else {
+ parametersForAllTests = Optional.absent();
+ testNamePattern = Optional.absent();
+ }
+ }
+
+ @Override
+ public ValidationResult validateConstructor(TestClass testClass, List<Throwable> list) {
+ if (parametersForAllTests.isPresent()) {
+ if (testClass.getJavaClass().getConstructors().length != 1) {
+ list.add(
+ new IllegalStateException("Test class should have exactly one public constructor"));
+ return ValidationResult.HANDLED;
+ }
+ Constructor<?> constructor = testClass.getOnlyConstructor();
+ Class<?>[] parameterTypes = constructor.getParameterTypes();
+ Object[] testParameters = getTestParameters(0);
+ if (parameterTypes.length != testParameters.length) {
+ list.add(
+ new IllegalStateException(
+ "Mismatch constructor parameter count with values"
+ + " returned by the @Parameters method"));
+ return ValidationResult.HANDLED;
+ }
+ for (int i = 0; i < testParameters.length; i++) {
+ if (!parameterTypes[i].isAssignableFrom(testParameters[i].getClass())) {
+ list.add(
+ new IllegalStateException(
+ String.format(
+ "Mismatch constructor parameter type %s with value"
+ + " returned by the @Parameters method: %s",
+ parameterTypes[i], testParameters[i])));
+ }
+ }
+ return ValidationResult.HANDLED;
+ }
+ return ValidationResult.NOT_HANDLED;
+ }
+
+ @Override
+ public ValidationResult validateTestMethod(
+ TestClass testClass, FrameworkMethod testMethod, List<Throwable> errorsReturned) {
+ return ValidationResult.NOT_HANDLED;
+ }
+
+ @Override
+ public List<TestInfo> processTest(Class<?> testClass, TestInfo originalTest) {
+ if (parametersForAllTests.isPresent()) {
+ ImmutableList.Builder<TestInfo> tests = ImmutableList.builder();
+ int testIndex = 0;
+ for (Object parameters : parametersForAllTests.get()) {
+ Object[] parametersForOneTest;
+ if (parameters instanceof Object[]) {
+ parametersForOneTest = (Object[]) parameters;
+ } else {
+ parametersForOneTest = new Object[] {parameters};
+ }
+ String namePattern = testNamePattern.get().replace("{index}", Integer.toString(testIndex));
+ String testParametersString = MessageFormat.format(namePattern, parametersForOneTest);
+ tests.add(
+ originalTest
+ .withExtraParameters(
+ ImmutableList.of(
+ TestInfoParameter.create(
+ testParametersString, parametersForOneTest, testIndex)))
+ .withExtraAnnotation(TestIndexHolderFactory.create(testIndex)));
+ testIndex++;
+ }
+ return tests.build();
+ }
+ return ImmutableList.of(originalTest);
+ }
+
+ @Override
+ public Statement processStatement(Statement originalStatement, Description finalTestDescription) {
+ return originalStatement;
+ }
+
+ @Override
+ public Optional<Object> createTest(
+ TestClass testClass, FrameworkMethod method, Optional<Object> test) {
+ if (parametersForAllTests.isPresent()) {
+ Object[] testParameters =
+ getTestParameters(method.getAnnotation(TestIndexHolder.class).testIndex());
+ try {
+ Constructor<?> constructor = testClass.getOnlyConstructor();
+ return Optional.<Object>of(constructor.newInstance(testParameters));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return test;
+ }
+
+ @Override
+ public Optional<Statement> createStatement(
+ TestClass testClass,
+ FrameworkMethod method,
+ Object testObject,
+ Optional<Statement> statement) {
+ return statement;
+ }
+
+ /**
+ * 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 testIndex();
+ }
+
+ /** Factory for {@link TestIndexHolder}. */
+ static class TestIndexHolderFactory {
+ @AutoAnnotation
+ static TestIndexHolder create(int testIndex) {
+ return new AutoAnnotation_ParameterizedTestMethodProcessor_TestIndexHolderFactory_create(
+ testIndex);
+ }
+
+ private TestIndexHolderFactory() {}
+ }
+
+ private Object[] getTestParameters(int testIndex) {
+ Object parameters = Iterables.get(parametersForAllTests.get(), testIndex);
+ if (parameters instanceof Object[]) {
+ return (Object[]) parameters;
+ } else {
+ return new Object[] {parameters};
+ }
+ }
+
+ private Optional<FrameworkMethod> getParametersMethod(TestClass testClass) {
+ List<FrameworkMethod> methods = testClass.getAnnotatedMethods(Parameters.class);
+ if (methods.isEmpty()) {
+ return Optional.absent();
+ }
+ FrameworkMethod method = Iterables.getOnlyElement(methods);
+ checkState(
+ method.isPublic() && method.isStatic(),
+ "@Parameters method %s should be static and public",
+ method.getName());
+ return Optional.of(method);
+ }
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java
new file mode 100644
index 0000000..2c9a199
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java
@@ -0,0 +1,412 @@
+/*
+ * 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.util.Comparator.comparing;
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.testing.junit.testparameterinjector.TestMethodProcessor.ValidationResult;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.Test;
+import org.junit.internal.runners.model.ReflectiveCallable;
+import org.junit.internal.runners.statements.Fail;
+import org.junit.rules.MethodRule;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.Statement;
+
+/**
+ * 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 ImmutableList<TestRule> testRules;
+ private List<TestMethodProcessor> testMethodProcessors;
+
+ protected PluggableTestRunner(Class<?> klass) throws InitializationError {
+ super(klass);
+ }
+
+ /**
+ * Returns the list of {@link TestMethodProcessor}s to use. This is meant to be overridden by
+ * subclasses.
+ */
+ protected abstract List<TestMethodProcessor> 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 by their test name.
+ *
+ * <p>Deterministic means that the order will not change, even when tests are added/removed or
+ * between releases.
+ */
+ protected boolean shouldSortTestMethodsDeterministically() {
+ return false; // Don't sort methods by default
+ }
+
+ /**
+ * {@link TestRule}s that will be executed after the ones defined in the test class (but still
+ * before all {@link MethodRule}s). This is meant to be overridden by subclasses.
+ */
+ protected List<TestRule> getInnerTestRules() {
+ return ImmutableList.of();
+ }
+
+ /**
+ * {@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> getOuterTestRules() {
+ return ImmutableList.of();
+ }
+
+ /**
+ * {@link MethodRule}s that will be executed after the ones defined in the test class. This is
+ * meant to be overridden by subclasses.
+ */
+ protected List<MethodRule> getInnerMethodRules() {
+ return ImmutableList.of();
+ }
+
+ /**
+ * {@link MethodRule}s that will be executed before the ones defined in the test class (but still
+ * after all {@link TestRule}s). This is meant to be overridden by subclasses.
+ */
+ protected List<MethodRule> getOuterMethodRules() {
+ return ImmutableList.of();
+ }
+
+ /**
+ * Runs a {@code testClass} with the {@link PluggableTestRunner}, and returns a list of test
+ * {@link Failure}, or an empty list if no failure occurred.
+ */
+ @VisibleForTesting
+ public static ImmutableList<Failure> run(PluggableTestRunner testRunner) throws Exception {
+ 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();
+ }
+
+ @Override
+ protected final ImmutableList<FrameworkMethod> computeTestMethods() {
+ Stream<FrameworkMethod> processedMethods =
+ super.computeTestMethods().stream().flatMap(method -> processMethod(method).stream());
+
+ if (shouldSortTestMethodsDeterministically()) {
+ processedMethods =
+ processedMethods.sorted(
+ comparing((FrameworkMethod method) -> method.getName().hashCode())
+ .thenComparing(FrameworkMethod::getName));
+ }
+
+ return processedMethods.collect(toImmutableList());
+ }
+
+ /** 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) {
+ ImmutableList<TestInfo> testInfos =
+ ImmutableList.of(
+ TestInfo.createWithoutParameters(
+ initialMethod.getMethod(), ImmutableList.copyOf(initialMethod.getAnnotations())));
+
+ for (final TestMethodProcessor testMethodProcessor : getTestMethodProcessors()) {
+ testInfos =
+ testInfos.stream()
+ .flatMap(
+ lastTestInfo ->
+ testMethodProcessor
+ .processTest(getTestClass().getJavaClass(), lastTestInfo)
+ .stream())
+ .collect(toImmutableList());
+ }
+
+ testInfos = TestInfo.deduplicateTestNames(TestInfo.shortenNamesIfNecessary(testInfos));
+
+ return testInfos.stream()
+ .map(testInfo -> new OverriddenFrameworkMethod(testInfo.getMethod(), testInfo))
+ .collect(toImmutableList());
+ }
+
+ // 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 = withPotentialTimeout(method, testObject, statement);
+ statement = withBefores(method, testObject, statement);
+ statement = withAfters(method, testObject, statement);
+ statement = withRules(method, testObject, statement);
+ return statement;
+ }
+
+ @Override
+ protected final Statement methodInvoker(FrameworkMethod frameworkMethod, Object testObject) {
+ Optional<Statement> statement = Optional.absent();
+ for (TestMethodProcessor testMethodProcessor : getTestMethodProcessors()) {
+ statement =
+ testMethodProcessor.createStatement(
+ getTestClass(), frameworkMethod, testObject, statement);
+ }
+ if (statement.isPresent()) {
+ return statement.get();
+ }
+ return super.methodInvoker(frameworkMethod, testObject);
+ }
+
+ /** Modifies the statement with each {@link MethodRule} and {@link TestRule} */
+ private Statement withRules(FrameworkMethod method, Object target, Statement statement) {
+ ImmutableList<TestRule> testRules =
+ Stream.of(
+ getTestRulesForProcessors().stream(),
+ getInnerTestRules().stream(),
+ getTestRules(target).stream(),
+ getOuterTestRules().stream())
+ .flatMap(x -> x)
+ .collect(toImmutableList());
+
+ Iterable<MethodRule> methodRules =
+ Iterables.concat(
+ Lists.reverse(getInnerMethodRules()),
+ rules(target),
+ Lists.reverse(getOuterMethodRules()));
+ for (MethodRule methodRule : methodRules) {
+ // For rules that implement both TestRule and MethodRule, only apply the TestRule.
+ if (!testRules.contains(methodRule)) {
+ statement = methodRule.apply(statement, method, target);
+ }
+ }
+ Description testDescription = describeChild(method);
+ for (TestRule testRule : testRules) {
+ statement = testRule.apply(statement, testDescription);
+ }
+ return new ContextMethodRule().apply(statement, method, target);
+ }
+
+ private Object createTestForMethod(FrameworkMethod method) throws Exception {
+ Optional<Object> maybeTestInstance = Optional.absent();
+ for (TestMethodProcessor testMethodProcessor : getTestMethodProcessors()) {
+ maybeTestInstance = testMethodProcessor.createTest(getTestClass(), method, maybeTestInstance);
+ }
+ // If no processor created the test instance, fallback on the default implementation.
+ Object testInstance =
+ maybeTestInstance.isPresent() ? maybeTestInstance.get() : super.createTest();
+
+ finalizeCreatedTestInstance(testInstance);
+
+ return testInstance;
+ }
+
+ @Override
+ protected final void validateZeroArgConstructor(List<Throwable> errorsReturned) {
+ for (TestMethodProcessor testMethodProcessor : getTestMethodProcessors()) {
+ if (testMethodProcessor.validateConstructor(getTestClass(), errorsReturned)
+ == ValidationResult.HANDLED) {
+ return;
+ }
+ }
+ super.validateZeroArgConstructor(errorsReturned);
+ }
+
+ @Override
+ protected final void validateTestMethods(List<Throwable> list) {
+ List<FrameworkMethod> testMethods = getTestClass().getAnnotatedMethods(Test.class);
+ for (FrameworkMethod testMethod : testMethods) {
+ boolean isHandled = false;
+ for (TestMethodProcessor testMethodProcessor : getTestMethodProcessors()) {
+ if (testMethodProcessor.validateTestMethod(getTestClass(), testMethod, list)
+ == ValidationResult.HANDLED) {
+ isHandled = true;
+ break;
+ }
+ }
+ if (!isHandled) {
+ testMethod.validatePublicVoidNoArg(false /* isStatic */, list);
+ }
+ }
+ }
+
+ // 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(),
+ errors.stream()
+ .map(Throwables::getStackTraceAsString)
+ .collect(joining("\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 List<TestMethodProcessor> getTestMethodProcessors() {
+ if (testMethodProcessors == null) {
+ testMethodProcessors = createTestMethodProcessorList();
+ }
+ return testMethodProcessors;
+ }
+
+ private synchronized ImmutableList<TestRule> getTestRulesForProcessors() {
+ if (testRules == null) {
+ testRules =
+ testMethodProcessors.stream()
+ .map(testMethodProcessor -> (TestRule) testMethodProcessor::processStatement)
+ .collect(toImmutableList());
+ }
+ return testRules;
+ }
+
+ /** {@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);
+ }
+ }
+ };
+ }
+ }
+
+ private static <E> Collector<E, ?, ImmutableList<E>> toImmutableList() {
+ return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf);
+ }
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java b/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java
new file mode 100644
index 0000000..61cf13b
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java
@@ -0,0 +1,25 @@
+/*
+ * 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.protobuf.MessageLite;
+import java.util.Map;
+
+/** A helper class for parsing proto values from strings. */
+interface ProtoValueParsing {
+ MessageLite parseTextprotoMessage(String textprotoString, Class<?> javaType);
+
+ MessageLite parseProtobufMessage(Map<String, Object> map, Class<?> javaType);
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java
new file mode 100644
index 0000000..7d16b6e
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java
@@ -0,0 +1,308 @@
+/*
+ * 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 java.lang.Math.min;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.MultimapBuilder;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.function.BiFunction;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+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;
+
+ /** The maximum amount of characters that a single parameter can take up in {@link #getName()}. */
+ static final int MAX_PARAMETER_NAME_LENGTH = 100;
+
+ public abstract Method getMethod();
+
+ public String getName() {
+ if (getParameters().isEmpty()) {
+ return getMethod().getName();
+ } else {
+ return String.format(
+ "%s[%s]",
+ getMethod().getName(),
+ getParameters().stream().map(TestInfoParameter::getName).collect(joining(",")));
+ }
+ }
+
+ abstract ImmutableList<TestInfoParameter> getParameters();
+
+ public abstract ImmutableList<Annotation> getAnnotations();
+
+ @Nullable
+ public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
+ for (Annotation annotation : getAnnotations()) {
+ if (annotationClass.isInstance(annotation)) {
+ return annotationClass.cast(annotation);
+ }
+ }
+ return null;
+ }
+
+ TestInfo withExtraParameters(List<TestInfoParameter> parameters) {
+ return new AutoValue_TestInfo(
+ getMethod(),
+ ImmutableList.<TestInfoParameter>builder()
+ .addAll(this.getParameters())
+ .addAll(parameters)
+ .build(),
+ getAnnotations());
+ }
+
+ TestInfo withExtraAnnotation(Annotation annotation) {
+ ImmutableList<Annotation> newAnnotations =
+ ImmutableList.<Annotation>builder().addAll(this.getAnnotations()).add(annotation).build();
+ return new AutoValue_TestInfo(getMethod(), 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(
+ BiFunction<TestInfoParameter, Integer, String> parameterWithIndexToNewName) {
+ return new AutoValue_TestInfo(
+ getMethod(),
+ IntStream.range(0, getParameters().size())
+ .mapToObj(
+ parameterIndex -> {
+ TestInfoParameter parameter = getParameters().get(parameterIndex);
+ return parameter.withName(
+ parameterWithIndexToNewName.apply(parameter, parameterIndex));
+ })
+ .collect(toImmutableList()),
+ getAnnotations());
+ }
+
+ public static TestInfo legacyCreate(Method method, String name, List<Annotation> annotations) {
+ return new AutoValue_TestInfo(
+ method, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations));
+ }
+
+ static TestInfo createWithoutParameters(Method method, List<Annotation> annotations) {
+ return new AutoValue_TestInfo(
+ method, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations));
+ }
+
+ static ImmutableList<TestInfo> shortenNamesIfNecessary(List<TestInfo> testInfos) {
+ if (testInfos.stream()
+ .anyMatch(
+ info ->
+ info.getName().length() > MAX_TEST_NAME_LENGTH
+ || info.getParameters().stream()
+ .anyMatch(param -> param.getName().length() > MAX_PARAMETER_NAME_LENGTH))) {
+ int numberOfParameters = testInfos.get(0).getParameters().size();
+
+ if (numberOfParameters == 0) {
+ return ImmutableList.copyOf(testInfos);
+ } else {
+ Set<Integer> parameterIndicesThatNeedUpdate =
+ IntStream.range(0, numberOfParameters)
+ .filter(
+ parameterIndex ->
+ testInfos.stream()
+ .anyMatch(
+ info ->
+ info.getParameters().get(parameterIndex).getName().length()
+ > getMaxCharactersPerParameter(info, numberOfParameters)))
+ .boxed()
+ .collect(toSet());
+
+ return testInfos.stream()
+ .map(
+ info ->
+ info.withUpdatedParameterNames(
+ (parameter, parameterIndex) ->
+ parameterIndicesThatNeedUpdate.contains(parameterIndex)
+ ? getShortenedName(
+ parameter,
+ getMaxCharactersPerParameter(info, numberOfParameters))
+ : info.getParameters().get(parameterIndex).getName()))
+ .collect(toImmutableList());
+ }
+ } 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;
+ return min(
+ // Subtract 4 characters to leave place for joining commas and the parameter index.
+ maxLengthOfAllParameters / numberOfParameters - 4,
+ // Subtract 3 characters to leave place for the parameter index
+ MAX_PARAMETER_NAME_LENGTH - 3);
+ }
+
+ static ImmutableList<TestInfo> deduplicateTestNames(List<TestInfo> testInfos) {
+ long uniqueTestNameCount = testInfos.stream().map(TestInfo::getName).distinct().count();
+ 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.getName().length() > maxCharactersPerParameter
+ ? parameter.getName().substring(0, maxCharactersPerParameter - 3) + "..."
+ : parameter.getName();
+ 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 testNameToInfo.keySet().stream()
+ .flatMap(
+ 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.stream();
+ } 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
+ IntStream.range(0, numParameters)
+ .filter(
+ parameterIndex ->
+ matchedInfos.stream()
+ .map(
+ info ->
+ getTypeSuffix(
+ info.getParameters()
+ .get(parameterIndex)
+ .getValue()))
+ .distinct()
+ .count()
+ > 1)
+ .boxed()
+ .collect(toSet());
+
+ return matchedInfos.stream()
+ .map(
+ testInfo ->
+ testInfo.withUpdatedParameterNames(
+ (parameter, parameterIndex) ->
+ indicesThatShouldGetSuffix.contains(parameterIndex)
+ ? parameter.getName() + getTypeSuffix(parameter.getValue())
+ : parameter.getName()));
+ }
+ })
+ .collect(toImmutableList());
+ }
+
+ 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 = testInfos.stream().map(TestInfo::getName).distinct().count();
+ 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 testInfos.stream()
+ .map(
+ testInfo ->
+ testInfo.withUpdatedParameterNames(
+ (parameter, parameterIndex) ->
+ String.format(
+ "%s.%s", parameter.getIndexInValueSource() + 1, parameter.getName())))
+ .collect(toImmutableList());
+ }
+ }
+
+ private static <E> Collector<E, ?, ImmutableList<E>> toImmutableList() {
+ return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf);
+ }
+
+ @AutoValue
+ abstract static class TestInfoParameter {
+
+ abstract String getName();
+
+ @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();
+
+ TestInfoParameter withName(String newName) {
+ return create(newName, getValue(), getIndexInValueSource());
+ }
+
+ static TestInfoParameter create(String name, @Nullable Object value, int indexInValueSource) {
+ checkArgument(indexInValueSource >= 0);
+ return new AutoValue_TestInfo_TestInfoParameter(
+ checkNotNull(name), value, indexInValueSource);
+ }
+ }
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java
new file mode 100644
index 0000000..880327f
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java
@@ -0,0 +1,99 @@
+/*
+ * 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.util.List;
+import org.junit.runner.Description;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.Statement;
+import org.junit.runners.model.TestClass;
+
+/**
+ * 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> processTest(Class<?> testClass, TestInfo originalTest);
+
+ /**
+ * Allows to change the code executed during the test.
+ *
+ * @param finalTestDescription the final description calculated taking into account this and all
+ * other test processors
+ */
+ Statement processStatement(Statement originalStatement, Description finalTestDescription);
+
+ /**
+ * This method allows to transform the test object used for {@link #processStatement(Statement,
+ * Description)}.
+ *
+ * @param test the value returned by the previous processor, or {@link Optional#absent()} if this
+ * processor is the first.
+ * @return {@link Optional#absent()} if the default test instance will be used from instantiating
+ * the test class with the default constructor.
+ * <p>The default implementation should return {@code test}.
+ */
+ Optional<Object> createTest(TestClass testClass, FrameworkMethod method, Optional<Object> test);
+
+ /**
+ * This method allows to transform the statement object used for {@link
+ * #processStatement(Statement, Description)}.
+ *
+ * @param statement the value returned by the previous processor, or {@link Optional#absent()} if
+ * this processor is the first.
+ * @return {@link Optional#absent()} if the default statement will be used from invoking the test
+ * method with no parameters.
+ * <p>The default implementation should return {@code statement}.
+ */
+ Optional<Statement> createStatement(
+ TestClass testClass,
+ FrameworkMethod method,
+ Object testObject,
+ Optional<Statement> statement);
+
+ /**
+ * Optionally validates the {@code testClass} constructor, and returns whether the validation
+ * should continue or stop.
+ *
+ * @param errorsReturned A mutable list that any validation error should be added to.
+ */
+ ValidationResult validateConstructor(TestClass testClass, List<Throwable> errorsReturned);
+
+ /**
+ * Optionally validates the {@code testClass} methods, and returns whether the validation should
+ * continue or stop.
+ *
+ * @param errorsReturned A mutable list that any validation error should be added to.
+ */
+ ValidationResult validateTestMethod(
+ TestClass testClass, FrameworkMethod testMethod, List<Throwable> errorsReturned);
+
+ /**
+ * Whether the constructor or method validation has been handled or not.
+ *
+ * <p>If the validation is not handled by a processor, it will be handled using the default {@link
+ * BlockJUnit4ClassRunner} validator.
+ */
+ enum ValidationResult {
+ NOT_HANDLED,
+ HANDLED,
+ }
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessors.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessors.java
new file mode 100644
index 0000000..b6dc4c2
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessors.java
@@ -0,0 +1,54 @@
+/*
+ * 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.collect.ImmutableList;
+import org.junit.runners.model.TestClass;
+
+/** Factory for all {@link TestMethodProcessor} implementations that this package supports. */
+final class TestMethodProcessors {
+
+ /**
+ * Returns a new instance of every {@link TestMethodProcessor} implementation that this package
+ * supports.
+ *
+ * <p>Note that this includes support for {@link org.junit.runners.Parameterized}.
+ */
+ public static ImmutableList<TestMethodProcessor>
+ createNewParameterizedProcessorsWithLegacyFeatures(TestClass testClass) {
+ return ImmutableList.of(
+ new ParameterizedTestMethodProcessor(testClass),
+ new TestParametersMethodProcessor(testClass),
+ TestParameterAnnotationMethodProcessor.forAllAnnotationPlacements(testClass));
+ }
+
+ /**
+ * Returns a new instance of every {@link TestMethodProcessor} implementation 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 ImmutableList<TestMethodProcessor> createNewParameterizedProcessors(
+ TestClass testClass) {
+ return ImmutableList.of(
+ new TestParametersMethodProcessor(testClass),
+ TestParameterAnnotationMethodProcessor.onlyForFieldsAndParameters(testClass));
+ }
+
+ private TestMethodProcessors() {}
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
new file mode 100644
index 0000000..6725d16
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
@@ -0,0 +1,224 @@
+/*
+ * 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 static java.util.Arrays.stream;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.Primitives;
+import com.google.protobuf.MessageLite;
+import com.google.testing.junit.testparameterinjector.TestParameter.InternalImplementationOfThisParameter;
+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.List;
+import java.util.Optional;
+
+/**
+ * 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>
+ * {@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 implements TestParameterValuesProvider {
+ * {@literal @}Override
+ * public {@literal List<CharMatcher>} provideValues() {
+ * 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. */
+ interface TestParameterValuesProvider {
+ List<?> provideValues();
+ }
+
+ /** Default {@link TestParameterValuesProvider} implementation that does nothing. */
+ class DefaultTestParameterValuesProvider implements TestParameterValuesProvider {
+ @Override
+ public List<Object> provideValues() {
+ return ImmutableList.of();
+ }
+ }
+
+ /** Implementation of this parameter annotation. */
+ final class InternalImplementationOfThisParameter implements TestParameterValueProvider {
+ @Override
+ public List<Object> provideValues(
+ Annotation uncastAnnotation, Optional<Class<?>> maybeParameterClass) {
+ 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 stream(annotation.value())
+ .map(v -> parseStringValue(v, parameterClass))
+ .collect(toList());
+ } else if (valuesProviderIsSet) {
+ return getValuesFromProvider(annotation.valuesProvider());
+ } else {
+ if (Enum.class.isAssignableFrom(parameterClass)) {
+ return ImmutableList.copyOf(parameterClass.asSubclass(Enum.class).getEnumConstants());
+ } else if (Primitives.wrap(parameterClass).equals(Boolean.class)) {
+ return ImmutableList.of(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) {
+ return parameterClass.orElseThrow(
+ () ->
+ 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 if (MessageLite.class.isAssignableFrom(parameterClass)) {
+ if (ParameterValueParsing.isValidYamlString(value)) {
+ return ParameterValueParsing.parseYamlStringToJavaType(value, parameterClass);
+ } else {
+ return ParameterValueParsing.parseTextprotoMessage(value, parameterClass);
+ }
+ } else {
+ return ParameterValueParsing.parseYamlStringToJavaType(value, parameterClass);
+ }
+ }
+
+ private static List<Object> getValuesFromProvider(
+ Class<? extends TestParameterValuesProvider> valuesProvider) {
+ try {
+ Constructor<? extends TestParameterValuesProvider> constructor =
+ valuesProvider.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ return new ArrayList<>(constructor.newInstance().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);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java
new file mode 100644
index 0000000..a859a4f
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java
@@ -0,0 +1,266 @@
+/*
+ * 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.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.text.MessageFormat;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * 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 {
+ /**
+ * Pattern of the {@link MessageFormat} format to derive the test's name from the parameters.
+ *
+ * @see {@code Parameters#name()}
+ */
+ String name() default "{0}";
+
+ /** Specifies a validator for the parameter to determine whether test should be skipped. */
+ Class<? extends TestParameterValidator> validator() default DefaultValidator.class;
+
+ /**
+ * Specifies a processor for the parameter to invoke arbitrary code before and after the test
+ * statement's execution.
+ */
+ Class<? extends TestParameterProcessor> processor() default DefaultProcessor.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 TestParameterProcessor} implementation which does nothing. */
+ class DefaultProcessor implements TestParameterProcessor {
+ @Override
+ public void before(Object testParameterValue) {}
+
+ @Override
+ public void after(Object testParameterValue) {}
+ }
+
+ /**
+ * 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/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
new file mode 100644
index 0000000..4380f57
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
@@ -0,0 +1,1382 @@
+/*
+ * 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 java.lang.annotation.RetentionPolicy.RUNTIME;
+import static java.util.Arrays.stream;
+import static java.util.stream.Collectors.toCollection;
+import static java.util.stream.Collectors.toSet;
+
+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.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.primitives.Primitives;
+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.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Predicate;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import javax.annotation.Nullable;
+import org.junit.runner.Description;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.Statement;
+import org.junit.runners.model.TestClass;
+
+/**
+ * {@code TestMethodProcessor} implementation for supporting parameterized tests annotated with
+ * {@link TestParameterAnnotation}.
+ *
+ * @see TestParameterAnnotation
+ */
+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 TestParameterValue 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).
+ */
+ @Nullable
+ abstract Object value();
+
+ /** 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 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();
+
+ /**
+ * Returns a String that represents this value and is fit for use in a test name (between
+ * brackets).
+ */
+ String toTestNameString() {
+ Class<? extends Annotation> annotationType = annotationTypeOrigin().annotationType();
+ String namePattern = annotationType.getAnnotation(TestParameterAnnotation.class).name();
+
+ if (paramName().isPresent()
+ && paramClass().isPresent()
+ && namePattern.equals("{0}")
+ && Primitives.unwrap(paramClass().get()).isPrimitive()) {
+ // If no custom name pattern was set and this parameter is a primitive (e.g.
+ // boolean
+ // or integer), 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].
+ return String.format("%s=%s", paramName().get(), value()).trim().replaceAll("\\s+", " ");
+ } else {
+ return MessageFormat.format(namePattern, value()).trim().replaceAll("\\s+", " ");
+ }
+ }
+
+ public static ImmutableList<TestParameterValue> create(
+ AnnotationWithMetadata annotationWithMetadata, Origin origin) {
+ List<Object> 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 IntStream.range(0, specifiedValues.size())
+ .mapToObj(
+ valueIndex ->
+ new AutoValue_TestParameterAnnotationMethodProcessor_TestParameterValue(
+ AnnotationTypeOrigin.create(
+ annotationWithMetadata.annotation().annotationType(), origin),
+ specifiedValues.get(valueIndex),
+ valueIndex,
+ new ArrayList<>(specifiedValues),
+ annotationWithMetadata.paramClass(),
+ annotationWithMetadata.paramName()))
+ .collect(toImmutableList());
+ }
+ }
+ /**
+ * 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 ->
+ Optional.fromNullable(
+ new TestParameterAnnotationMethodProcessor(
+ new TestClass(testInfo.getMethod().getDeclaringClass()),
+ /* onlyForFieldsAndParameters= */ false)
+ .getParameterValuesForTest(testIndexHolder).stream()
+ .filter(matches(annotationType))
+ .map(TestParameterValue::value)
+ .findFirst()
+ .orElse(null));
+ }
+ }
+
+ /**
+ * 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 List<Object> getParametersAnnotationValues(
+ AnnotationWithMetadata annotationWithMetadata) {
+ Annotation annotation = annotationWithMetadata.annotation();
+ TestParameterAnnotation testParameter =
+ annotation.annotationType().getAnnotation(TestParameterAnnotation.class);
+ Class<? extends TestParameterValueProvider> valueProvider = testParameter.valueProvider();
+ try {
+ return valueProvider
+ .getConstructor()
+ .newInstance()
+ .provideValues(
+ annotation,
+ java.util.Optional.ofNullable(annotationWithMetadata.paramClass().orNull()));
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(
+ "Unexpected exception while invoking value provider " + valueProvider, e);
+ }
+ }
+
+ private static Predicate<TestParameterValue> matches(Class<? extends Annotation> annotationType) {
+ return testParameterValue ->
+ testParameterValue.annotationTypeOrigin().annotationType().equals(annotationType);
+ }
+
+ /** 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();
+
+ public static AnnotationWithMetadata withMetadata(
+ Annotation annotation, Class<?> paramClass, String paramName) {
+ return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata(
+ annotation, Optional.of(paramClass), Optional.of(paramName));
+ }
+
+ public static AnnotationWithMetadata withMetadata(Annotation annotation, Class<?> paramClass) {
+ return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata(
+ annotation, Optional.of(paramClass), Optional.absent());
+ }
+
+ public static AnnotationWithMetadata withoutMetadata(Annotation annotation) {
+ return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata(
+ annotation, Optional.absent(), Optional.absent());
+ }
+ }
+
+ private final TestClass testClass;
+ private final boolean onlyForFieldsAndParameters;
+ private volatile ImmutableList<AnnotationTypeOrigin> cachedAnnotationTypeOrigins;
+ private final Cache<Method, List<List<TestParameterValue>>> parameterValuesCache =
+ CacheBuilder.newBuilder().maximumSize(1000).build();
+
+ private TestParameterAnnotationMethodProcessor(
+ TestClass testClass, boolean onlyForFieldsAndParameters) {
+ this.testClass = testClass;
+ 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(TestClass testClass) {
+ return new TestParameterAnnotationMethodProcessor(
+ testClass, /* 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(TestClass testClass) {
+ return new TestParameterAnnotationMethodProcessor(
+ testClass, /* onlyForFieldsAndParameters= */ true);
+ }
+
+ private ImmutableList<AnnotationTypeOrigin> getAnnotationTypeOrigins(
+ Origin firstOrigin, Origin... otherOrigins) {
+ if (cachedAnnotationTypeOrigins == null) {
+ // Collect all annotations used in declared fields and methods that have themselves a
+ // @TestParameterAnnotation annotation.
+ List<AnnotationTypeOrigin> fieldAnnotations =
+ extractTestParameterAnnotations(
+ streamWithParents(testClass.getJavaClass())
+ .flatMap(c -> stream(c.getDeclaredFields()))
+ .flatMap(field -> stream(field.getAnnotations())),
+ Origin.FIELD);
+ List<AnnotationTypeOrigin> methodAnnotations =
+ extractTestParameterAnnotations(
+ stream(testClass.getJavaClass().getMethods())
+ .flatMap(method -> stream(method.getAnnotations())),
+ Origin.METHOD);
+ List<AnnotationTypeOrigin> parameterAnnotations =
+ extractTestParameterAnnotations(
+ stream(testClass.getJavaClass().getMethods())
+ .flatMap(method -> stream(method.getParameterAnnotations()).flatMap(Stream::of)),
+ Origin.METHOD_PARAMETER);
+ List<AnnotationTypeOrigin> classAnnotations =
+ extractTestParameterAnnotations(
+ stream(testClass.getJavaClass().getAnnotations()), Origin.CLASS);
+ List<AnnotationTypeOrigin> constructorAnnotations =
+ extractTestParameterAnnotations(
+ stream(testClass.getJavaClass().getConstructors())
+ .flatMap(constructor -> stream(constructor.getAnnotations())),
+ Origin.CONSTRUCTOR);
+ List<AnnotationTypeOrigin> constructorParameterAnnotations =
+ extractTestParameterAnnotations(
+ stream(testClass.getJavaClass().getConstructors())
+ .flatMap(
+ constructor ->
+ stream(constructor.getParameterAnnotations()).flatMap(Stream::of)),
+ Origin.CONSTRUCTOR_PARAMETER);
+
+ checkDuplicatedClassAndFieldAnnotations(
+ constructorAnnotations, classAnnotations, fieldAnnotations);
+
+ checkDuplicatedFieldsAnnotations(methodAnnotations, fieldAnnotations);
+
+ checkState(
+ constructorAnnotations.stream().distinct().count() == constructorAnnotations.size(),
+ "Annotations should not be duplicated on the constructor.");
+
+ checkState(
+ classAnnotations.stream().distinct().count() == 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);
+ }
+
+ cachedAnnotationTypeOrigins =
+ Stream.of(
+ // The order matters, since it will determine which annotation processor is
+ // called first.
+ classAnnotations.stream(),
+ fieldAnnotations.stream(),
+ constructorAnnotations.stream(),
+ constructorParameterAnnotations.stream(),
+ methodAnnotations.stream(),
+ parameterAnnotations.stream())
+ .flatMap(x -> x)
+ .distinct()
+ .collect(toImmutableList());
+ }
+
+ Set<Origin> originsToFilterBy =
+ ImmutableSet.<Origin>builder().add(firstOrigin).add(otherOrigins).build();
+ return cachedAnnotationTypeOrigins.stream()
+ .filter(annotationTypeOrigin -> originsToFilterBy.contains(annotationTypeOrigin.origin()))
+ .collect(toImmutableList());
+ }
+
+ 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 (fieldAnnotations.stream().distinct().count() != fieldAnnotations.size()) {
+ List<Class<? extends Annotation>> methodOrFieldAnnotations =
+ Stream.concat(methodAnnotations.stream(), fieldAnnotations.stream().distinct())
+ .map(AnnotationTypeOrigin::annotationType)
+ .collect(toCollection(ArrayList::new));
+
+ checkState(
+ methodOrFieldAnnotations.stream().distinct().count() == 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 =
+ classAnnotations.stream()
+ .map(AnnotationTypeOrigin::annotationType)
+ .collect(toImmutableSet());
+
+ ImmutableSet<Class<? extends Annotation>> uniqueFieldAnnotations =
+ fieldAnnotations.stream()
+ .map(AnnotationTypeOrigin::annotationType)
+ .collect(toImmutableSet());
+ ImmutableSet<Class<? extends Annotation>> uniqueConstructorAnnotations =
+ constructorAnnotations.stream()
+ .map(AnnotationTypeOrigin::annotationType)
+ .collect(toImmutableSet());
+
+ 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");
+ }
+
+ /** Returns a list of annotation types that are a {@link TestParameterAnnotation}. */
+ private List<AnnotationTypeOrigin> extractTestParameterAnnotations(
+ Stream<Annotation> annotations, Origin origin) {
+ return annotations
+ .map(Annotation::annotationType)
+ .filter(annotationType -> annotationType.isAnnotationPresent(TestParameterAnnotation.class))
+ .map(annotationType -> AnnotationTypeOrigin.create(annotationType, origin))
+ .collect(toCollection(ArrayList::new));
+ }
+
+ @Override
+ public ValidationResult validateConstructor(TestClass testClass, List<Throwable> errorsReturned) {
+ if (testClass.getJavaClass().getConstructors().length != 1) {
+ errorsReturned.add(
+ new IllegalStateException("Test class should have exactly one public constructor"));
+ return ValidationResult.HANDLED;
+ }
+ Constructor<?> constructor = testClass.getOnlyConstructor();
+ Class<?>[] parameterTypes = constructor.getParameterTypes();
+ if (parameterTypes.length == 0) {
+ return ValidationResult.NOT_HANDLED;
+ }
+ // The constructor has parameters, they must be injected by a TestParameterAnnotation
+ // annotation.
+ Annotation[][] parameterAnnotations = constructor.getParameterAnnotations();
+ validateMethodOrConstructorParameters(
+ removeOverrides(
+ getAnnotationTypeOrigins(
+ Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER),
+ testClass.getJavaClass()),
+ testClass,
+ errorsReturned,
+ constructor,
+ parameterTypes,
+ parameterAnnotations);
+
+ return ValidationResult.HANDLED;
+ }
+
+ @Override
+ public ValidationResult validateTestMethod(
+ TestClass testClass, FrameworkMethod testMethod, List<Throwable> errorsReturned) {
+ Class<?>[] methodParameterTypes = testMethod.getMethod().getParameterTypes();
+ if (methodParameterTypes.length == 0) {
+ return ValidationResult.NOT_HANDLED;
+ } else {
+ Method method = testMethod.getMethod();
+ // The method has parameters, they must be injected by a TestParameterAnnotation annotation.
+ testMethod.validatePublicVoid(false /* isStatic */, errorsReturned);
+ Annotation[][] parametersAnnotations = method.getParameterAnnotations();
+ validateMethodOrConstructorParameters(
+ getAnnotationTypeOrigins(Origin.CLASS, Origin.METHOD, Origin.METHOD_PARAMETER),
+ testClass,
+ errorsReturned,
+ method,
+ methodParameterTypes,
+ parametersAnnotations);
+ return ValidationResult.HANDLED;
+ }
+ }
+
+ private void validateMethodOrConstructorParameters(
+ List<AnnotationTypeOrigin> annotationTypeOrigins,
+ TestClass testClass,
+ List<Throwable> errors,
+ AnnotatedElement methodOrConstructor,
+ Class<?>[] parameterTypes,
+ Annotation[][] parametersAnnotations) {
+ 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) {
+ List<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.getJavaClass(),
+ 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)));
+ }
+ }
+ }
+
+ @Override
+ public Optional<Statement> createStatement(
+ TestClass testClass,
+ FrameworkMethod frameworkMethod,
+ Object testObject,
+ Optional<Statement> statement) {
+ if (frameworkMethod.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, InvokeParameterizedMethod would be invoked 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).
+ || frameworkMethod.getAnnotation(TestParameters.class) != null) {
+ return statement;
+ } else {
+ return Optional.of(new InvokeParameterizedMethod(frameworkMethod, testObject));
+ }
+ }
+
+ /**
+ * 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> processTest(Class<?> testClass, TestInfo originalTest) {
+ List<List<TestParameterValue>> parameterValuesForMethod =
+ getParameterValuesForMethod(originalTest.getMethod());
+
+ 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<TestParameterValue> testParameterValues = parameterValuesForMethod.get(parametersIndex);
+ testInfos.add(
+ originalTest
+ .withExtraParameters(
+ testParameterValues.stream()
+ .map(
+ param ->
+ TestInfoParameter.create(
+ param.toTestNameString(), param.value(), param.valueIndex()))
+ .collect(toImmutableList()))
+ .withExtraAnnotation(
+ TestIndexHolderFactory.create(
+ /* methodIndex= */ strictIndexOf(
+ getMethodsIncludingParents(testClass), originalTest.getMethod()),
+ parametersIndex,
+ testClass.getName())));
+ }
+
+ return testInfos.build();
+ }
+
+ private List<List<TestParameterValue>> getParameterValuesForMethod(Method method) {
+ try {
+ return parameterValuesCache.get(
+ method,
+ () -> {
+ List<List<TestParameterValue>> testParameterValuesList =
+ getAnnotationValuesForUsedAnnotationTypes(testClass.getJavaClass(), method);
+
+ return Lists.cartesianProduct(testParameterValuesList).stream()
+ .filter(
+ // Skip tests based on the annotations' {@link Validator#shouldSkip} return
+ // value.
+ testParameterValues ->
+ testParameterValues.stream()
+ .noneMatch(
+ testParameterValue ->
+ callShouldSkip(
+ testParameterValue.annotationTypeOrigin().annotationType(),
+ testParameterValues)))
+ .collect(toImmutableList());
+ });
+ } catch (ExecutionException | UncheckedExecutionException e) {
+ Throwables.throwIfUnchecked(e.getCause());
+ throw new RuntimeException(e);
+ }
+ }
+
+ private List<TestParameterValue> getParameterValuesForTest(TestIndexHolder testIndexHolder) {
+ 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 =
+ getMethodsIncludingParents(testClass.getJavaClass()).get(testIndexHolder.methodIndex());
+ return getParameterValuesForMethod(testMethod).get(testIndexHolder.parametersIndex());
+ }
+
+ /**
+ * Returns the list of annotation index for all annotations defined in a given test method and its
+ * class.
+ */
+ private ImmutableList<List<TestParameterValue>> getAnnotationValuesForUsedAnnotationTypes(
+ Class<?> testClass, Method method) {
+ ImmutableList<AnnotationTypeOrigin> annotationTypes =
+ Stream.of(
+ getAnnotationTypeOrigins(Origin.CLASS).stream(),
+ getAnnotationTypeOrigins(Origin.FIELD).stream(),
+ getAnnotationTypeOrigins(Origin.CONSTRUCTOR).stream(),
+ getAnnotationTypeOrigins(Origin.CONSTRUCTOR_PARAMETER).stream(),
+ getAnnotationTypeOrigins(Origin.METHOD).stream(),
+ getAnnotationTypeOrigins(Origin.METHOD_PARAMETER).stream()
+ .sorted(annotationComparator(method.getParameterAnnotations())))
+ .flatMap(x -> x)
+ .collect(toImmutableList());
+
+ return removeOverrides(annotationTypes, testClass, method).stream()
+ .map(
+ annotationTypeOrigin ->
+ getAnnotationFromParametersOrTestOrClass(annotationTypeOrigin, method, testClass))
+ .filter(l -> !l.isEmpty())
+ .flatMap(List::stream)
+ .collect(toImmutableList());
+ }
+
+ private Comparator<AnnotationTypeOrigin> annotationComparator(
+ Annotation[][] parameterAnnotations) {
+ ImmutableList<String> annotationOrdering =
+ stream(parameterAnnotations)
+ .flatMap(Arrays::stream)
+ .map(Annotation::annotationType)
+ .map(Class::getName)
+ .collect(toImmutableList());
+ return Comparator.comparingInt(o -> annotationOrdering.indexOf(o.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(
+ annotationTypeOrigins.stream()
+ .filter(
+ annotationTypeOrigin -> {
+ switch (annotationTypeOrigin.origin()) {
+ case FIELD: // Fall through.
+ case CLASS:
+ return getAnnotationListWithType(
+ method.getAnnotations(), annotationTypeOrigin.annotationType())
+ .isEmpty();
+ default:
+ return true;
+ }
+ })
+ .collect(toCollection(ArrayList::new)),
+ testClass);
+ }
+
+ /** @see #removeOverrides(List, Class) */
+ private List<AnnotationTypeOrigin> removeOverrides(
+ List<AnnotationTypeOrigin> annotationTypeOrigins, Class<?> testClass) {
+ return annotationTypeOrigins.stream()
+ .filter(
+ annotationTypeOrigin -> {
+ switch (annotationTypeOrigin.origin()) {
+ case FIELD: // Fall through.
+ case CLASS:
+ return getAnnotationListWithType(
+ getOnlyConstructor(testClass).getAnnotations(),
+ annotationTypeOrigin.annotationType())
+ .isEmpty()
+ && getAnnotationListWithType(
+ getOnlyConstructor(testClass).getParameterAnnotations(),
+ annotationTypeOrigin.annotationType())
+ .isEmpty();
+ default:
+ return true;
+ }
+ })
+ .collect(toCollection(ArrayList::new));
+ }
+
+ /**
+ * 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<TestParameterValue>> getAnnotationFromParametersOrTestOrClass(
+ AnnotationTypeOrigin annotationTypeOrigin, Method method, Class<?> testClass) {
+ Origin origin = annotationTypeOrigin.origin();
+ Class<? extends Annotation> annotationType = annotationTypeOrigin.annotationType();
+ if (origin == Origin.CONSTRUCTOR_PARAMETER) {
+ Constructor<?> constructor = getOnlyConstructor(testClass);
+ List<AnnotationWithMetadata> annotations =
+ getAnnotationWithMetadataListWithType(constructor, annotationType);
+
+ if (!annotations.isEmpty()) {
+ return toTestParameterValueList(annotations, origin);
+ }
+ } else if (origin == Origin.CONSTRUCTOR) {
+ Annotation annotation = getOnlyConstructor(testClass).getAnnotation(annotationType);
+ if (annotation != null) {
+ return ImmutableList.of(
+ TestParameterValue.create(AnnotationWithMetadata.withoutMetadata(annotation), origin));
+ }
+
+ } else if (origin == Origin.METHOD_PARAMETER) {
+ List<AnnotationWithMetadata> annotations =
+ getAnnotationWithMetadataListWithType(method, annotationType);
+ if (!annotations.isEmpty()) {
+ return toTestParameterValueList(annotations, origin);
+ }
+ } else if (origin == Origin.METHOD) {
+ if (method.isAnnotationPresent(annotationType)) {
+ return ImmutableList.of(
+ TestParameterValue.create(
+ AnnotationWithMetadata.withoutMetadata(method.getAnnotation(annotationType)),
+ origin));
+ }
+ } else if (origin == Origin.FIELD) {
+ List<AnnotationWithMetadata> annotations =
+ streamWithParents(testClass)
+ .flatMap(c -> stream(c.getDeclaredFields()))
+ .flatMap(
+ field ->
+ getAnnotationListWithType(field.getAnnotations(), annotationType).stream()
+ .map(
+ annotation ->
+ AnnotationWithMetadata.withMetadata(
+ annotation, field.getType(), field.getName())))
+ .collect(toCollection(ArrayList::new));
+ if (!annotations.isEmpty()) {
+ return toTestParameterValueList(annotations, origin);
+ }
+ } else if (origin == Origin.CLASS) {
+ Annotation annotation = testClass.getAnnotation(annotationType);
+ if (annotation != null) {
+ return ImmutableList.of(
+ TestParameterValue.create(AnnotationWithMetadata.withoutMetadata(annotation), origin));
+ }
+ }
+ return ImmutableList.of();
+ }
+
+ private static ImmutableList<List<TestParameterValue>> toTestParameterValueList(
+ List<AnnotationWithMetadata> annotationWithMetadatas, Origin origin) {
+ return annotationWithMetadatas.stream()
+ .map(annotationWithMetadata -> TestParameterValue.create(annotationWithMetadata, origin))
+ .collect(toImmutableList());
+ }
+
+ private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType(
+ Method callable, Class<? extends Annotation> annotationType) {
+ try {
+ return getAnnotationWithMetadataListWithType(callable.getParameters(), annotationType);
+ } catch (NoSuchMethodError ignored) {
+ return getAnnotationWithMetadataListWithType(
+ callable.getParameterTypes(), callable.getParameterAnnotations(), annotationType);
+ }
+ }
+
+ private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType(
+ Constructor<?> callable, Class<? extends Annotation> annotationType) {
+ try {
+ return getAnnotationWithMetadataListWithType(callable.getParameters(), annotationType);
+ } catch (NoSuchMethodError ignored) {
+ return getAnnotationWithMetadataListWithType(
+ callable.getParameterTypes(), callable.getParameterAnnotations(), annotationType);
+ }
+ }
+
+ // 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) {
+ return stream(parameters)
+ .map(
+ parameter -> {
+ Annotation annotation = parameter.getAnnotation(annotationType);
+ return annotation == null
+ ? null
+ : parameter.isNamePresent()
+ ? AnnotationWithMetadata.withMetadata(
+ annotation, parameter.getType(), parameter.getName())
+ : AnnotationWithMetadata.withMetadata(annotation, parameter.getType());
+ })
+ .filter(Objects::nonNull)
+ .collect(toImmutableList());
+ }
+
+ private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType(
+ Class<?>[] parameterTypes,
+ Annotation[][] annotations,
+ Class<? extends Annotation> annotationType) {
+ 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]));
+ }
+ }
+ }
+ return resultBuilder.build();
+ }
+
+ private ImmutableList<Annotation> getAnnotationListWithType(
+ Annotation[][] parameterAnnotations, Class<? extends Annotation> annotationType) {
+ return stream(parameterAnnotations)
+ .flatMap(Stream::of)
+ .filter(annotation -> annotation.annotationType().equals(annotationType))
+ .collect(toImmutableList());
+ }
+
+ private ImmutableList<Annotation> getAnnotationListWithType(
+ Annotation[] annotations, Class<? extends Annotation> annotationType) {
+ return stream(annotations)
+ .filter(annotation -> annotation.annotationType().equals(annotationType))
+ .collect(toImmutableList());
+ }
+
+ private static Constructor<?> getOnlyConstructor(Class<?> testClass) {
+ Constructor<?>[] constructors = testClass.getConstructors();
+ checkState(
+ constructors.length == 1,
+ "a single public constructor is required for class %s",
+ testClass);
+ return constructors[0];
+ }
+
+ @Override
+ public Optional<Object> createTest(
+ TestClass testClass, FrameworkMethod method, Optional<Object> test) {
+ TestIndexHolder testIndexHolder = method.getAnnotation(TestIndexHolder.class);
+ if (testIndexHolder == null) {
+ return test;
+ }
+ try {
+ List<TestParameterValue> testParameterValues = getParameterValuesForTest(testIndexHolder);
+
+ Object testObject;
+ if (test.isPresent()) {
+ testObject = test.get();
+ } else {
+ Constructor<?> constructor = testClass.getOnlyConstructor();
+ Class<?>[] parameterTypes = constructor.getParameterTypes();
+ if (parameterTypes.length == 0) {
+ testObject = constructor.newInstance();
+ } else {
+ // The constructor has parameters, they must be injected by a TestParameterAnnotation
+ // annotation.
+ Annotation[][] parameterAnnotations = constructor.getParameterAnnotations();
+ Object[] arguments = new Object[parameterTypes.length];
+ List<Class<? extends Annotation>> processedAnnotationTypes = new ArrayList<>();
+ List<TestParameterValue> parameterValuesForConstructor =
+ filterByOrigin(
+ testParameterValues,
+ Origin.CLASS,
+ Origin.CONSTRUCTOR,
+ Origin.CONSTRUCTOR_PARAMETER);
+ for (int i = 0; i < arguments.length; i++) {
+ // Initialize each parameter value from the corresponding TestParameterAnnotation value.
+ arguments[i] =
+ getParameterValue(
+ parameterValuesForConstructor,
+ parameterTypes[i],
+ parameterAnnotations[i],
+ processedAnnotationTypes);
+ }
+ testObject = constructor.newInstance(arguments);
+ }
+ }
+ // Do not include {@link Origin#METHOD_PARAMETER} nor {@link Origin#CONSTRUCTOR_PARAMETER}
+ // annotations.
+ List<TestParameterValue> testParameterValuesForFieldInjection =
+ filterByOrigin(testParameterValues, Origin.CLASS, Origin.FIELD, Origin.METHOD);
+ // The annotationType corresponding to the annotationIndex, e.g ColorParameter.class
+ // in the example above.
+ List<TestParameterValue> remainingTestParameterValuesForFieldInjection =
+ new ArrayList<>(testParameterValuesForFieldInjection);
+ for (Field declaredField :
+ streamWithParents(testObject.getClass())
+ .flatMap(c -> stream(c.getDeclaredFields()))
+ .collect(toImmutableList())) {
+ for (TestParameterValue testParameterValue :
+ remainingTestParameterValuesForFieldInjection) {
+ if (declaredField.isAnnotationPresent(
+ testParameterValue.annotationTypeOrigin().annotationType())) {
+ declaredField.setAccessible(true);
+ declaredField.set(testObject, testParameterValue.value());
+ remainingTestParameterValuesForFieldInjection.remove(testParameterValue);
+ break;
+ }
+ }
+ }
+ return Optional.of(testObject);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Returns an {@link TestParameterValue} list that contains only the values originating from one
+ * of the {@code origins}.
+ */
+ private static ImmutableList<TestParameterValue> filterByOrigin(
+ List<TestParameterValue> testParameterValues, Origin... origins) {
+ Set<Origin> originsToFilterBy = ImmutableSet.copyOf(origins);
+ return testParameterValues.stream()
+ .filter(
+ testParameterValue ->
+ originsToFilterBy.contains(testParameterValue.annotationTypeOrigin().origin()))
+ .collect(toImmutableList());
+ }
+
+ /**
+ * 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 annotationTypeOrigins.stream()
+ .filter(annotationTypeOrigin -> originList.contains(annotationTypeOrigin.origin()))
+ .collect(toImmutableList());
+ }
+
+ @Override
+ public Statement processStatement(Statement originalStatement, Description finalTestDescription) {
+ TestIndexHolder testIndexHolder = finalTestDescription.getAnnotation(TestIndexHolder.class);
+ if (testIndexHolder == null) {
+ return originalStatement;
+ }
+ List<TestParameterValue> testParameterValues = getParameterValuesForTest(testIndexHolder);
+
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ for (TestParameterValue testParameterValue : testParameterValues) {
+ callBefore(
+ testParameterValue.annotationTypeOrigin().annotationType(),
+ testParameterValue.value());
+ }
+ try {
+ originalStatement.evaluate();
+ } finally {
+ // In reverse order.
+ for (TestParameterValue testParameterValue : Lists.reverse(testParameterValues)) {
+ callAfter(
+ testParameterValue.annotationTypeOrigin().annotationType(),
+ testParameterValue.value());
+ }
+ }
+ }
+ };
+ }
+
+ /**
+ * Class to invoke the test method if it has parameters, and they need to be injected from the
+ * TestParameterAnnotation values.
+ */
+ private class InvokeParameterizedMethod extends Statement {
+
+ private final FrameworkMethod frameworkMethod;
+ private final Object testObject;
+ private final List<TestParameterValue> testParameterValues;
+
+ public InvokeParameterizedMethod(FrameworkMethod frameworkMethod, Object testObject) {
+ this.frameworkMethod = frameworkMethod;
+ this.testObject = testObject;
+ TestIndexHolder testIndexHolder = frameworkMethod.getAnnotation(TestIndexHolder.class);
+ checkState(testIndexHolder != null);
+ testParameterValues =
+ filterByOrigin(
+ getParameterValuesForTest(testIndexHolder),
+ Origin.CLASS,
+ Origin.METHOD,
+ Origin.METHOD_PARAMETER);
+ }
+
+ @Override
+ public void evaluate() throws Throwable {
+ Class<?>[] parameterTypes = frameworkMethod.getMethod().getParameterTypes();
+ Annotation[][] parametersAnnotations = frameworkMethod.getMethod().getParameterAnnotations();
+ Object[] parameterValues = new Object[parameterTypes.length];
+
+ List<Class<? extends Annotation>> processedAnnotationTypes = new ArrayList<>();
+ // Initialize each parameter value from the corresponding TestParameterAnnotation value.
+ for (int i = 0; i < parameterTypes.length; i++) {
+ parameterValues[i] =
+ getParameterValue(
+ testParameterValues,
+ parameterTypes[i],
+ parametersAnnotations[i],
+ processedAnnotationTypes);
+ }
+ frameworkMethod.invokeExplosively(testObject, parameterValues);
+ }
+ }
+
+ /** Returns a {@link TestParameterAnnotation}'s value for a method or constructor parameter. */
+ private Object getParameterValue(
+ List<TestParameterValue> testParameterValues,
+ Class<?> methodParameterType,
+ Annotation[] parameterAnnotations,
+ List<Class<? extends Annotation>> processedAnnotationTypes) {
+ List<Class<? extends Annotation>> iteratedAnnotationTypes = new ArrayList<>();
+ for (TestParameterValue 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.value();
+ }
+ iteratedAnnotationTypes.add(annotationType);
+ }
+ }
+ }
+ // If no annotation matches, use the method parameter type.
+ for (TestParameterValue 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.value();
+ }
+ }
+ 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 getMethodsIncludingParents(testClass)} */
+ int methodIndex();
+
+ /**
+ * The index of the set of parameters to run the test method with in the list produced by {@link
+ * #getParameterValuesForMethod(Method)}.
+ */
+ 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() {}
+ }
+
+ /** Invokes the {@link TestParameterProcessor#before} method of an annotation. */
+ private static void callBefore(
+ Class<? extends Annotation> annotationType, Object annotationValue) {
+ TestParameterAnnotation annotation =
+ annotationType.getAnnotation(TestParameterAnnotation.class);
+ Class<? extends TestParameterProcessor> processor = annotation.processor();
+ try {
+ processor.getConstructor().newInstance().before(annotationValue);
+ } catch (Exception e) {
+ throw new RuntimeException("Unexpected exception while invoking processor " + processor, e);
+ }
+ }
+
+ /** Invokes the {@link TestParameterProcessor#after} method of an annotation. */
+ private static void callAfter(
+ Class<? extends Annotation> annotationType, Object annotationValue) {
+ TestParameterAnnotation annotation =
+ annotationType.getAnnotation(TestParameterAnnotation.class);
+ Class<? extends TestParameterProcessor> processor = annotation.processor();
+ try {
+ processor.getConstructor().newInstance().after(annotationValue);
+ } catch (Exception e) {
+ throw new RuntimeException("Unexpected exception while invoking processor " + processor, e);
+ }
+ }
+
+ /**
+ * Returns whether the test should be skipped according to the {@code annotationType}'s {@link
+ * TestParameterValidator} and the current list of {@link TestParameterValue}.
+ */
+ private static boolean callShouldSkip(
+ Class<? extends Annotation> annotationType, List<TestParameterValue> 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<TestParameterValue> testParameterValues;
+ private final Set<Object> valueList;
+
+ public ValidatorContext(List<TestParameterValue> testParameterValues) {
+ this.testParameterValues = testParameterValues;
+ this.valueList = testParameterValues.stream().map(TestParameterValue::value).collect(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(TestParameterValue::value);
+ }
+
+ @Override
+ public List<Object> getSpecifiedValues(Class<? extends Annotation> testParameter) {
+ return getParameter(testParameter)
+ .transform(TestParameterValue::specifiedValues)
+ .or(ImmutableList.of());
+ }
+
+ private Optional<TestParameterValue> getParameter(Class<? extends Annotation> testParameter) {
+ return Optional.fromNullable(
+ testParameterValues.stream()
+ .filter(value -> value.annotationTypeOrigin().annotationType().equals(testParameter))
+ .findAny()
+ .orElse(null));
+ }
+ }
+
+ /**
+ * 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, java.util.Optional.ofNullable(paramClass.orNull()));
+ } 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<Class<? extends Annotation>> getTestParameterAnnotations(
+ List<AnnotationTypeOrigin> annotationTypeOrigins,
+ final Class<?> testClass,
+ AnnotatedElement methodOrConstructor) {
+ return annotationTypeOrigins.stream()
+ .map(AnnotationTypeOrigin::annotationType)
+ .filter(
+ annotationType ->
+ testClass.isAnnotationPresent(annotationType)
+ || methodOrConstructor.isAnnotationPresent(annotationType))
+ .collect(toImmutableList());
+ }
+
+ 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> getMethodsIncludingParents(Class<?> clazz) {
+ ImmutableList.Builder<Method> resultBuilder = ImmutableList.builder();
+ while (clazz != null) {
+ resultBuilder.add(clazz.getMethods());
+ clazz = clazz.getSuperclass();
+ }
+ return resultBuilder.build();
+ }
+
+ private static Stream<Class<?>> streamWithParents(Class<?> clazz) {
+ Stream.Builder<Class<?>> resultBuilder = Stream.builder();
+
+ Class<?> currentClass = clazz;
+ while (currentClass != null) {
+ resultBuilder.add(currentClass);
+ currentClass = currentClass.getSuperclass();
+ }
+
+ return resultBuilder.build();
+ }
+
+ // Immutable collectors are re-implemented here because they are missing from the Android
+ // collection library.
+ private static <E> Collector<E, ?, ImmutableList<E>> toImmutableList() {
+ return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf);
+ }
+
+ private static <E> Collector<E, ?, ImmutableSet<E>> toImmutableSet() {
+ return Collectors.collectingAndThen(Collectors.toList(), ImmutableSet::copyOf);
+ }
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java
new file mode 100644
index 0000000..dd6c63f
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java
@@ -0,0 +1,36 @@
+/*
+ * 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 java.util.List;
+import org.junit.runners.model.InitializationError;
+
+/**
+ * A JUnit 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 (as opposed to {@link
+ * org.junit.runners.Parameterized} where each test case in a test class is invoked with the exact
+ * same set of parameters).
+ */
+public final class TestParameterInjector extends PluggableTestRunner {
+
+ public TestParameterInjector(Class<?> testClass) throws InitializationError {
+ super(testClass);
+ }
+
+ @Override
+ protected List<TestMethodProcessor> createTestMethodProcessorList() {
+ return TestMethodProcessors.createNewParameterizedProcessorsWithLegacyFeatures(getTestClass());
+ }
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterProcessor.java
new file mode 100644
index 0000000..efa4951
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterProcessor.java
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+/**
+ * Interface which allows {@link TestParameterAnnotation} annotations to run arbitrary code before
+ * and after test execution.
+ *
+ * <p>When multiple TestParameterAnnotation processors exist for a single test, they are executed in
+ * declaration order, starting with annotations defined at the class, field, method, and finally
+ * parameter level.
+ */
+interface TestParameterProcessor {
+ /** Executes code in the context of a running test statement before the statement starts. */
+ void before(Object testParameterValue);
+
+ /** Executes code in the context of a running test statement after the statement completes. */
+ void after(Object testParameterValue);
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java
new file mode 100644
index 0000000..3733833
--- /dev/null
+++ b/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/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java
new file mode 100644
index 0000000..6c398aa
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java
@@ -0,0 +1,52 @@
+/*
+ * 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 java.lang.annotation.Annotation;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * 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.
+ */
+ List<Object> provideValues(Annotation annotation, Optional<Class<?>> 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/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java
new file mode 100644
index 0000000..5207ec6
--- /dev/null
+++ b/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/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java
new file mode 100644
index 0000000..b7ee544
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java
@@ -0,0 +1,208 @@
+/*
+ * 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.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.collect.ImmutableList;
+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 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 use
+ * other types of parameterization, such as {@linkplain TestParameter @TestParameter}.
+ *
+ * <p>See {@link #value()} for simple examples.
+ */
+@Retention(RUNTIME)
+@Target({CONSTRUCTOR, METHOD})
+public @interface TestParameters {
+
+ /**
+ * Array of stringified set of parameters in YAML format. Each element 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. Parameter types that are supported:
+ *
+ * <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.
+ *
+ * <p><b>Examples</b>
+ *
+ * <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: {name: 'Hermione'}, expectedResultType: SUCCESS}",
+ * "{updateRequest: {name: '---'}, expectedResultType: FAILURE}",
+ * })
+ * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... }
+ * </pre>
+ */
+ String[] value() 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>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() {
+ checkState(name != null, "This set of parameters needs a name (%s)", parametersMap);
+ 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();
+ }
+ }
+}
diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
new file mode 100644
index 0000000..7796db0
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
@@ -0,0 +1,426 @@
+/*
+ * 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.util.Arrays.stream;
+
+import com.google.auto.value.AutoAnnotation;
+import com.google.common.base.Optional;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+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.testing.junit.testparameterinjector.TestInfo.TestInfoParameter;
+import com.google.testing.junit.testparameterinjector.TestParameters.DefaultTestParametersValuesProvider;
+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.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Parameter;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.runner.Description;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.Statement;
+import org.junit.runners.model.TestClass;
+
+/** {@code TestMethodProcessor} implementation for supporting {@link TestParameters}. */
+@SuppressWarnings("AndroidJdkLibsChecker") // Parameter is not available on old Android SDKs.
+class TestParametersMethodProcessor implements TestMethodProcessor {
+
+ private final TestClass testClass;
+
+ private final LoadingCache<Object, ImmutableList<TestParametersValues>>
+ parameterValuesByConstructorOrMethodCache =
+ CacheBuilder.newBuilder()
+ .maximumSize(1000)
+ .build(
+ CacheLoader.from(
+ methodOrConstructor ->
+ (methodOrConstructor instanceof Constructor)
+ ? toParameterValuesList(
+ methodOrConstructor,
+ ((Constructor<?>) methodOrConstructor)
+ .getAnnotation(TestParameters.class),
+ ((Constructor<?>) methodOrConstructor).getParameters())
+ : toParameterValuesList(
+ methodOrConstructor,
+ ((Method) methodOrConstructor)
+ .getAnnotation(TestParameters.class),
+ ((Method) methodOrConstructor).getParameters())));
+
+ public TestParametersMethodProcessor(TestClass testClass) {
+ this.testClass = testClass;
+ }
+
+ @Override
+ public ValidationResult validateConstructor(TestClass testClass, List<Throwable> exceptions) {
+ if (testClass.getOnlyConstructor().isAnnotationPresent(TestParameters.class)) {
+ try {
+ // This method throws an exception if there is a validation error
+ getConstructorParameters();
+ } catch (Throwable t) {
+ exceptions.add(t);
+ }
+ return ValidationResult.HANDLED;
+ } else {
+ return ValidationResult.NOT_HANDLED;
+ }
+ }
+
+ @Override
+ public ValidationResult validateTestMethod(
+ TestClass testClass, FrameworkMethod testMethod, List<Throwable> exceptions) {
+ if (testMethod.getMethod().isAnnotationPresent(TestParameters.class)) {
+ try {
+ // This method throws an exception if there is a validation error
+ getMethodParameters(testMethod.getMethod());
+ } catch (Throwable t) {
+ exceptions.add(t);
+ }
+ return ValidationResult.HANDLED;
+ } else {
+ return ValidationResult.NOT_HANDLED;
+ }
+ }
+
+ @Override
+ public List<TestInfo> processTest(Class<?> clazz, TestInfo originalTest) {
+ boolean constructorIsParameterized =
+ testClass.getOnlyConstructor().isAnnotationPresent(TestParameters.class);
+ boolean methodIsParameterized =
+ originalTest.getMethod().isAnnotationPresent(TestParameters.class);
+
+ if (!constructorIsParameterized && !methodIsParameterized) {
+ return ImmutableList.of(originalTest);
+ }
+
+ ImmutableList.Builder<TestInfo> testInfos = ImmutableList.builder();
+
+ ImmutableList<Optional<TestParametersValues>> constructorParametersList =
+ getConstructorParametersOrSingleAbsentElement();
+ 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(
+ Stream.of(
+ constructorParameters
+ .transform(
+ param ->
+ TestInfoParameter.create(
+ param.name(),
+ param.parametersMap(),
+ constructorParametersIndexCopy))
+ .orNull(),
+ methodParameters
+ .transform(
+ param ->
+ TestInfoParameter.create(
+ param.name(),
+ param.parametersMap(),
+ methodParametersIndexCopy))
+ .orNull())
+ .filter(Objects::nonNull)
+ .collect(toImmutableList()))
+ .withExtraAnnotation(
+ TestIndexHolderFactory.create(
+ constructorParametersIndex, methodParametersIndex)));
+ }
+ }
+ return testInfos.build();
+ }
+
+ private ImmutableList<Optional<TestParametersValues>>
+ getConstructorParametersOrSingleAbsentElement() {
+ return testClass.getOnlyConstructor().isAnnotationPresent(TestParameters.class)
+ ? getConstructorParameters().stream().map(Optional::of).collect(toImmutableList())
+ : ImmutableList.of(Optional.absent());
+ }
+
+ private ImmutableList<Optional<TestParametersValues>> getMethodParametersOrSingleAbsentElement(
+ Method method) {
+ return method.isAnnotationPresent(TestParameters.class)
+ ? getMethodParameters(method).stream().map(Optional::of).collect(toImmutableList())
+ : ImmutableList.of(Optional.absent());
+ }
+
+ @Override
+ public Statement processStatement(Statement originalStatement, Description finalTestDescription) {
+ return originalStatement;
+ }
+
+ @Override
+ public Optional<Object> createTest(
+ TestClass testClass, FrameworkMethod method, Optional<Object> test) {
+ if (testClass.getOnlyConstructor().isAnnotationPresent(TestParameters.class)) {
+ ImmutableList<TestParametersValues> parameterValuesList = getConstructorParameters();
+ TestParametersValues parametersValues =
+ parameterValuesList.get(
+ method.getAnnotation(TestIndexHolder.class).constructorParametersIndex());
+
+ try {
+ Constructor<?> constructor = testClass.getOnlyConstructor();
+ return Optional.of(
+ constructor.newInstance(
+ toParameterArray(
+ parametersValues, testClass.getOnlyConstructor().getParameters())));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ } else {
+ return test;
+ }
+ }
+
+ @Override
+ public Optional<Statement> createStatement(
+ TestClass testClass,
+ FrameworkMethod method,
+ Object testObject,
+ Optional<Statement> statement) {
+ if (method.getMethod().isAnnotationPresent(TestParameters.class)) {
+ ImmutableList<TestParametersValues> parameterValuesList =
+ getMethodParameters(method.getMethod());
+ TestParametersValues parametersValues =
+ parameterValuesList.get(
+ method.getAnnotation(TestIndexHolder.class).methodParametersIndex());
+
+ return Optional.of(
+ new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ method.invokeExplosively(
+ testObject,
+ toParameterArray(parametersValues, method.getMethod().getParameters()));
+ }
+ });
+ } else {
+ return statement;
+ }
+ }
+
+ private ImmutableList<TestParametersValues> getConstructorParameters() {
+ return parameterValuesByConstructorOrMethodCache.getUnchecked(testClass.getOnlyConstructor());
+ }
+
+ private ImmutableList<TestParametersValues> getMethodParameters(Method method) {
+ return parameterValuesByConstructorOrMethodCache.getUnchecked(method);
+ }
+
+ private static ImmutableList<TestParametersValues> toParameterValuesList(
+ Object methodOrConstructor, TestParameters annotation, Parameter[] invokableParameters) {
+ 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 on annotation %s",
+ annotation);
+ checkState(
+ valueIsSet || valuesProviderIsSet,
+ "Either value or valuesProvider must be set on annotation %s",
+ annotation);
+
+ ImmutableList<Parameter> parametersList = ImmutableList.copyOf(invokableParameters);
+ checkState(
+ parametersList.stream().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.",
+ methodOrConstructor);
+ if (valueIsSet) {
+ return stream(annotation.value())
+ .map(yamlMap -> toParameterValues(yamlMap, parametersList))
+ .collect(toImmutableList());
+ } else {
+ return toParameterValuesList(annotation.valuesProvider(), parametersList);
+ }
+ }
+
+ private static ImmutableList<TestParametersValues> toParameterValuesList(
+ Class<? extends TestParametersValuesProvider> valuesProvider, List<Parameter> parameters) {
+ try {
+ Constructor<? extends TestParametersValuesProvider> constructor =
+ valuesProvider.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ return constructor.newInstance().provideValues().stream()
+ .peek(values -> validateThatValuesMatchParameters(values, parameters))
+ .collect(toImmutableList());
+ } 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 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) {
+ Object yamlMapObject = ParameterValueParsing.parseYamlStringToObject(yamlString);
+ checkState(
+ yamlMapObject instanceof Map,
+ "Cannot map YAML string '%s' to parameters because it is not a mapping",
+ yamlString);
+ @SuppressWarnings("unchecked")
+ Map<String, Object> yamlMap = (Map<String, Object>) 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());
+
+ return TestParametersValues.builder()
+ .name(yamlString)
+ .addParameters(
+ Maps.transformEntries(
+ yamlMap,
+ (parameterName, parsedYaml) ->
+ ParameterValueParsing.parseYamlObjectToJavaType(
+ parsedYaml,
+ TypeToken.of(parametersByName.get(parameterName).getParameterizedType()))))
+ .build();
+ }
+
+ private static Object[] toParameterArray(
+ TestParametersValues parametersValues, Parameter[] parameters) {
+ return stream(parameters)
+ .map(parameter -> parametersValues.parametersMap().get(parameter.getName()))
+ .toArray();
+ }
+
+ // Immutable collectors are re-implemented here because they are missing from the Android
+ // collection library.
+ private static <E> Collector<E, ?, ImmutableList<E>> toImmutableList() {
+ return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf);
+ }
+
+ /**
+ * 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/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java
new file mode 100644
index 0000000..0f19466
--- /dev/null
+++ b/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.protobuf.ByteString;
+
+import org.junit.Ignore;
+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}",
+ })
+ @Ignore("b/195657808 @TestParameters is not supported on Android")
+ 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"),
+ 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),
+
+ 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);
+ }
+
+ private enum TestEnum {
+ AAA,
+ BBB;
+ }
+}
diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java
new file mode 100644
index 0000000..686b152
--- /dev/null
+++ b/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.ImmutableList;
+import java.util.List;
+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 int ruleInvocationCount = 0;
+
+ public static class TestAndMethodRule implements MethodRule, TestRule {
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ ruleInvocationCount++;
+ return base;
+ }
+
+ @Override
+ public Statement apply(Statement base, FrameworkMethod method, Object target) {
+ ruleInvocationCount++;
+ return base;
+ }
+ }
+
+ @RunWith(PluggableTestRunner.class)
+ public static class PluggableTestRunnerTestClass {
+
+ @Rule public TestAndMethodRule rule = new TestAndMethodRule();
+
+ @Test
+ public void test() {
+ // no-op
+ }
+ }
+
+ @Test
+ public void ruleThatIsBothTestRuleAndMethodRuleIsInvokedOnceOnly() throws Exception {
+ PluggableTestRunner.run(
+ new PluggableTestRunner(PluggableTestRunnerTestClass.class) {
+ @Override
+ protected List<TestMethodProcessor> createTestMethodProcessorList() {
+ return ImmutableList.of();
+ }
+ });
+
+ assertThat(ruleInvocationCount).isEqualTo(1);
+ }
+}
diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java
new file mode 100644
index 0000000..ae817f6
--- /dev/null
+++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java
@@ -0,0 +1,249 @@
+/*
+ * 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"),
+ /* 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(
+ /* name= */ "short", /* value= */ 1, /* indexInValueSource= */ 1),
+ TestInfoParameter.create(
+ /* name= */ "shorter", /* value= */ null, /* indexInValueSource= */ 3)),
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* name= */ "short", /* value= */ 1, /* indexInValueSource= */ 1),
+ TestInfoParameter.create(
+ /* name= */ "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(
+ /* name= */ "short", /* value= */ 1, /* indexInValueSource= */ 0),
+ TestInfoParameter.create(
+ /* name= */ "shorter", /* value= */ null, /* indexInValueSource= */ 0)),
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* name= */ "short", /* value= */ 1, /* indexInValueSource= */ 0),
+ TestInfoParameter.create(
+ /* name= */ "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(
+ /* name= */ "shorter", /* value= */ null, /* indexInValueSource= */ 0)),
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* name= */ "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"
+ + " 000000000000000000000000000000000000000000000000000000000000...]")
+ .inOrder();
+ }
+
+ @Test
+ public void shortenNamesIfNecessary_tooManyParameters() throws Exception {
+ TestInfo testInfoWithManyParams =
+ fakeTestInfo(
+ IntStream.range(0, 50)
+ .mapToObj(
+ i ->
+ TestInfoParameter.create(
+ /* name= */ "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(
+ /* name= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1),
+ TestInfoParameter.create(
+ /* name= */ "bbb", /* value= */ null, /* indexInValueSource= */ 3)),
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* name= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1),
+ TestInfoParameter.create(
+ /* name= */ "ccc", /* value= */ 1, /* indexInValueSource= */ 0)));
+
+ ImmutableList<TestInfo> result = TestInfo.deduplicateTestNames(givenTestInfos);
+
+ assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder();
+ }
+
+ @Test
+ public void deduplicateTestNames_duplicateParameterNamesWithDifferentTypes() throws Exception {
+ ImmutableList<TestInfo> result =
+ TestInfo.deduplicateTestNames(
+ ImmutableList.of(
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* name= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1),
+ TestInfoParameter.create(
+ /* name= */ "null", /* value= */ null, /* indexInValueSource= */ 3)),
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* name= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1),
+ TestInfoParameter.create(
+ /* name= */ "null", /* value= */ "null", /* indexInValueSource= */ 0)),
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* name= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1),
+ TestInfoParameter.create(
+ /* name= */ "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(
+ /* name= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 0),
+ TestInfoParameter.create(
+ /* name= */ "bbb", /* value= */ 1, /* indexInValueSource= */ 0)),
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* name= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 0),
+ TestInfoParameter.create(
+ /* name= */ "bbb", /* value= */ 1, /* indexInValueSource= */ 1)),
+ fakeTestInfo(
+ TestInfoParameter.create(
+ /* name= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 0),
+ TestInfoParameter.create(
+ /* name= */ "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"), /* 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/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
new file mode 100644
index 0000000..51a328d
--- /dev/null
+++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
@@ -0,0 +1,1077 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.junit.runner.notification.Failure;
+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 {
+
+ private static List<TestEnum> testedParameters;
+
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE})
+ TestEnum enumParameter;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ public void test() {
+ testedParameters.add(enumParameter);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO, TestEnum.THREE);
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class MultipleAllEnumValuesAnnotationClass {
+
+ private static List<String> testedParameters;
+
+ @TestParameter TestEnum enumParameter1;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ public void test(@TestParameter TestEnum enumParameter2) {
+ testedParameters.add(enumParameter1 + ":" + enumParameter2);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters).hasSize(TestEnum.values().length * TestEnum.values().length);
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_FOR_ALL_PLACEMENTS_ONLY)
+ public static class SingleParameterAnnotationClass {
+
+ private static List<TestEnum> testedParameters;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE})
+ public void test(TestEnum enumParameter) {
+ testedParameters.add(enumParameter);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO, TestEnum.THREE);
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class SingleAnnotatedParameterAnnotationClass {
+
+ private static List<TestEnum> testedParameters;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ public void test(
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) TestEnum enumParameter) {
+ testedParameters.add(enumParameter);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO, TestEnum.THREE);
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class AnnotatedSuperclassParameter {
+
+ private static List<Object> testedParameters;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ public void test(
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) Object enumParameter) {
+ testedParameters.add(enumParameter);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO, TestEnum.THREE);
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class DuplicatedAnnotatedParameterAnnotationClass {
+
+ private static List<ImmutableList<TestEnum>> testedParameters;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ public void test(
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) TestEnum enumParameter,
+ @EnumParameter({TestEnum.FOUR, TestEnum.FIVE}) TestEnum enumParameter2) {
+ testedParameters.add(ImmutableList.of(enumParameter, enumParameter2));
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters)
+ .containsExactly(
+ ImmutableList.of(TestEnum.ONE, TestEnum.FOUR),
+ ImmutableList.of(TestEnum.ONE, TestEnum.FIVE),
+ ImmutableList.of(TestEnum.TWO, TestEnum.FOUR),
+ ImmutableList.of(TestEnum.TWO, TestEnum.FIVE),
+ ImmutableList.of(TestEnum.THREE, TestEnum.FOUR),
+ ImmutableList.of(TestEnum.THREE, TestEnum.FIVE));
+ }
+ }
+
+ @ClassTestResult(Result.FAILURE)
+ public static class SingleAnnotatedParameterAnnotationClassWithMissingValue {
+
+ private static List<TestEnum> testedParameters;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ public void test(@EnumParameter TestEnum enumParameter) {
+ testedParameters.add(enumParameter);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO, TestEnum.THREE);
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_FOR_ALL_PLACEMENTS_ONLY)
+ public static class MultipleAnnotationTestClass {
+
+ private static List<TestEnum> testedParameters;
+
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO})
+ TestEnum enumParameter;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ @EnumParameter({TestEnum.THREE})
+ public void parameterized() {
+ testedParameters.add(enumParameter);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters).containsExactly(TestEnum.THREE);
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class TooLongTestNamesShortened {
+
+ @Rule public TestName testName = new TestName();
+
+ private static List<String> allTestNames;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ allTestNames = new ArrayList<>();
+ }
+
+ @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) {
+ allTestNames.add(testName.getMethodName());
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(allTestNames)
+ .containsExactly(
+ "test1[1.ABC]",
+ "test1[2.This is a very long string (240 characters) that would normally cause"
+ + " Sponge+Tin to exceed the...]");
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class DuplicateTestNames {
+
+ @Rule public TestName testName = new TestName();
+
+ private static List<String> allTestNames;
+ private static List<Object> allTestParameterValues;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ allTestNames = new ArrayList<>();
+ allTestParameterValues = new ArrayList<>();
+ }
+
+ @Test
+ public void test1(@TestParameter({"ABC", "ABC"}) String testString) {
+ allTestNames.add(testName.getMethodName());
+ allTestParameterValues.add(testString);
+ }
+
+ private static final class Test2Provider implements TestParameterValuesProvider {
+ @Override
+ public List<Object> provideValues() {
+ return newArrayList(123, "123", "null", null);
+ }
+ }
+
+ @Test
+ public void test2(@TestParameter(valuesProvider = Test2Provider.class) Object testObject) {
+ allTestNames.add(testName.getMethodName());
+ allTestParameterValues.add(testObject);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(allTestNames)
+ .containsExactly(
+ "test1[1.ABC]",
+ "test1[2.ABC]",
+ "test2[123 (Integer)]",
+ "test2[123 (String)]",
+ "test2[null (String)]",
+ "test2[null (null reference)]");
+ assertThat(allTestParameterValues).containsExactly("ABC", "ABC", 123, "123", "null", null);
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class DuplicateFieldAnnotationTestClass {
+
+ private static List<String> testedParameters;
+
+ @TestParameter({"foo", "bar"})
+ String stringParameter;
+
+ @TestParameter({"baz", "qux"})
+ String stringParameter2;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ public void test() {
+ testedParameters.add(stringParameter + ":" + stringParameter2);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters).containsExactly("foo:baz", "foo:qux", "bar:baz", "bar:qux");
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class DuplicateIdenticalFieldAnnotationTestClass {
+
+ private static List<String> testedParameters;
+
+ @TestParameter({"foo", "bar"})
+ String stringParameter;
+
+ @TestParameter({"foo", "bar"})
+ String stringParameter2;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ public void test() {
+ testedParameters.add(stringParameter + ":" + stringParameter2);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters).containsExactly("foo:foo", "foo:bar", "bar:foo", "bar:bar");
+ }
+ }
+
+ @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 {
+
+ private static List<TestEnum> testedParameters;
+
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE})
+ TestEnum enumParameter;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ public void test() {
+ testedParameters.add(enumParameter);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO, TestEnum.THREE);
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class MultipleAnnotationTestClassWithAnnotation {
+
+ private static List<String> testedParameters;
+
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE})
+ TestEnum enumParameter;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ public void parameterized(@TestParameter({"foo", "bar"}) String stringParameter) {
+ testedParameters.add(stringParameter + ":" + enumParameter);
+ }
+
+ @Test
+ public void nonParameterized() {
+ testedParameters.add("none:" + enumParameter);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters)
+ .containsExactly(
+ "none:ONE",
+ "none:TWO",
+ "none:THREE",
+ "foo:ONE",
+ "foo:TWO",
+ "foo:THREE",
+ "bar:ONE",
+ "bar:TWO",
+ "bar:THREE");
+ }
+ }
+
+ public abstract static class BaseClassWithAnnotations {
+ @Rule public TestName testName = new TestName();
+
+ static List<String> allTestNames;
+
+ @TestParameter boolean boolInBase;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ allTestNames = new ArrayList<>();
+ }
+
+ @Before
+ public void setUp() {
+ assertThat(allTestNames).doesNotContain(testName.getMethodName());
+ }
+
+ @After
+ public void tearDown() {
+ assertThat(allTestNames).contains(testName.getMethodName());
+ }
+
+ @Test
+ public void testInBase(@TestParameter({"ONE", "TWO"}) TestEnum enumInBase) {
+ allTestNames.add(testName.getMethodName());
+ }
+
+ @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) {
+ allTestNames.add(testName.getMethodName());
+ }
+
+ @Override
+ public void abstractTestInBase() {
+ allTestNames.add(testName.getMethodName());
+ }
+
+ @Override
+ public void overridableTestInBase() {
+ allTestNames.add(testName.getMethodName());
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(allTestNames)
+ .containsExactly(
+ "testInBase[boolInChild=false,boolInBase=false,ONE]",
+ "testInBase[boolInChild=false,boolInBase=false,TWO]",
+ "testInBase[boolInChild=false,boolInBase=true,ONE]",
+ "testInBase[boolInChild=false,boolInBase=true,TWO]",
+ "testInBase[boolInChild=true,boolInBase=false,ONE]",
+ "testInBase[boolInChild=true,boolInBase=false,TWO]",
+ "testInBase[boolInChild=true,boolInBase=true,ONE]",
+ "testInBase[boolInChild=true,boolInBase=true,TWO]",
+ "testInChild[boolInChild=false,boolInBase=false,TWO]",
+ "testInChild[boolInChild=false,boolInBase=false,THREE]",
+ "testInChild[boolInChild=false,boolInBase=true,TWO]",
+ "testInChild[boolInChild=false,boolInBase=true,THREE]",
+ "testInChild[boolInChild=true,boolInBase=false,TWO]",
+ "testInChild[boolInChild=true,boolInBase=false,THREE]",
+ "testInChild[boolInChild=true,boolInBase=true,TWO]",
+ "testInChild[boolInChild=true,boolInBase=true,THREE]",
+ "abstractTestInBase[boolInChild=false,boolInBase=false]",
+ "abstractTestInBase[boolInChild=false,boolInBase=true]",
+ "abstractTestInBase[boolInChild=true,boolInBase=false]",
+ "abstractTestInBase[boolInChild=true,boolInBase=true]",
+ "overridableTestInBase[boolInChild=false,boolInBase=false]",
+ "overridableTestInBase[boolInChild=false,boolInBase=true]",
+ "overridableTestInBase[boolInChild=true,boolInBase=false]",
+ "overridableTestInBase[boolInChild=true,boolInBase=true]");
+ }
+ }
+
+ @Retention(RUNTIME)
+ @TestParameterAnnotation(validator = TestEnumValidator.class, processor = TestEnumProcessor.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);
+ }
+ }
+
+ public static class TestEnumProcessor implements TestParameterProcessor {
+
+ static List<Object> beforeCalls = new ArrayList<>();
+ static List<Object> afterCalls = new ArrayList<>();
+
+ static void init() {
+ beforeCalls.clear();
+ afterCalls.clear();
+ }
+
+ static TestEnum currentValue;
+
+ @Override
+ public void before(Object testParameterValue) {
+ beforeCalls.add(testParameterValue);
+ currentValue = (TestEnum) testParameterValue;
+ }
+
+ @Override
+ public void after(Object testParameterValue) {
+ afterCalls.add(testParameterValue);
+ currentValue = null;
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class MethodEvaluatorClass {
+
+ private static List<TestEnum> testedParameters;
+
+ @Test
+ public void test(
+ @EnumEvaluatorParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) TestEnum value) {
+ if (value == TestEnum.THREE) {
+ fail();
+ } else {
+ testedParameters.add(value);
+ }
+ }
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @BeforeClass
+ public static void init() {
+ TestEnumProcessor.init();
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO);
+ assertThat(TestEnumProcessor.beforeCalls).containsExactly(TestEnum.ONE, TestEnum.TWO);
+ assertThat(TestEnumProcessor.afterCalls).containsExactly(TestEnum.ONE, TestEnum.TWO);
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class FieldEvaluatorClass {
+
+ private static List<TestEnum> testedParameters;
+
+ @EnumEvaluatorParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE})
+ TestEnum value;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ public void test() {
+ if (value == TestEnum.THREE) {
+ fail();
+ } else {
+ testedParameters.add(value);
+ }
+ }
+
+ @BeforeClass
+ public static void init() {
+ TestEnumProcessor.init();
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO);
+ assertThat(TestEnumProcessor.beforeCalls).containsExactly(TestEnum.ONE, TestEnum.TWO);
+ assertThat(TestEnumProcessor.afterCalls).containsExactly(TestEnum.ONE, TestEnum.TWO);
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_FOR_ALL_PLACEMENTS_ONLY)
+ @EnumEvaluatorParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE})
+ public static class ClassEvaluatorClass {
+
+ private static List<TestEnum> testedParameters;
+
+ public ClassEvaluatorClass() {}
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ public void test() {
+ if (TestEnumProcessor.currentValue == TestEnum.THREE) {
+ fail();
+ } else {
+ testedParameters.add(TestEnumProcessor.currentValue);
+ }
+ }
+
+ @BeforeClass
+ public static void init() {
+ TestEnumProcessor.init();
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO);
+ assertThat(TestEnumProcessor.beforeCalls).containsExactly(TestEnum.ONE, TestEnum.TWO);
+ assertThat(TestEnumProcessor.afterCalls).containsExactly(TestEnum.ONE, TestEnum.TWO);
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class ConstructorClass {
+
+ private static List<TestEnum> testedParameters;
+ final TestEnum enumParameter;
+
+ public ConstructorClass(
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) TestEnum enumParameter) {
+ this.enumParameter = enumParameter;
+ }
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ public void test() {
+ testedParameters.add(enumParameter);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO, TestEnum.THREE);
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_FOR_ALL_PLACEMENTS_ONLY)
+ public static class MethodFieldOverrideClass {
+
+ private static List<TestEnum> testedParameters;
+
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO})
+ TestEnum enumParameter;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE})
+ public void test() {
+ testedParameters.add(enumParameter);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO, TestEnum.THREE);
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_FOR_ALL_PLACEMENTS_ONLY)
+ @EnumEvaluatorParameter({TestEnum.ONE})
+ public static class MethodClassOverrideClass {
+
+ private static List<TestEnum> testedParameters;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ @EnumEvaluatorParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE})
+ public void test() {
+ testedParameters.add(TestEnumProcessor.currentValue);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO);
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_FOR_ALL_PLACEMENTS_ONLY)
+ public static class ErrorDuplicatedConstructorMethodAnnotation {
+
+ private static List<String> testedParameters;
+ final TestEnum enumParameter;
+
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE})
+ public ErrorDuplicatedConstructorMethodAnnotation(TestEnum enumParameter) {
+ this.enumParameter = enumParameter;
+ }
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO})
+ public void test(TestEnum otherParameter) {
+ testedParameters.add(enumParameter + ":" + otherParameter);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters)
+ .containsExactly("ONE:ONE", "ONE:TWO", "TWO:ONE", "TWO:TWO", "THREE:ONE", "THREE:TWO");
+ }
+ }
+
+ @ClassTestResult(Result.FAILURE)
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE})
+ public static class ErrorDuplicatedClassFieldAnnotation {
+
+ private static List<TestEnum> testedParameters;
+
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO})
+ TestEnum enumParameter;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ public void test() {
+ testedParameters.add(enumParameter);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO);
+ }
+ }
+
+ @ClassTestResult(Result.FAILURE)
+ public static class ErrorNonStaticProviderClass {
+
+ @Test
+ public void test(@TestParameter(valuesProvider = NonStaticProvider.class) int i) {}
+
+ @SuppressWarnings("ClassCanBeStatic")
+ class NonStaticProvider implements TestParameterValuesProvider {
+ @Override
+ public List<?> provideValues() {
+ return ImmutableList.of();
+ }
+ }
+ }
+
+ 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 {
+
+ @EnumAParameter EnumA enumA;
+ @EnumBParameter EnumB enumB;
+ @EnumCParameter EnumC enumC;
+
+ private static List<List<Object>> testedParameters;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ public void test() {
+ testedParameters.add(ImmutableList.of(enumA, enumB, enumC));
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ // Only 3 tests should have been sufficient to cover all cases.
+ assertThat(testedParameters).hasSize(3);
+ assertAllEnumsAreIncluded(EnumA.values());
+ assertAllEnumsAreIncluded(EnumB.values());
+ assertAllEnumsAreIncluded(EnumC.values());
+ }
+
+ private static <T extends Enum<T>> void assertAllEnumsAreIncluded(Enum<T>[] values) {
+ Set<Enum<T>> enumSet = new HashSet<>(Arrays.asList(values));
+ for (List<Object> enumList : testedParameters) {
+ enumSet.removeAll(enumList);
+ }
+ assertThat(enumSet).isEmpty();
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class TestNamesTest {
+
+ @Rule public TestName name = new TestName();
+
+ @TestParameter("8")
+ long fieldParam;
+
+ @Test
+ public void withPrimitives(
+ @TestParameter("true") boolean param1, @TestParameter("2") int param2) {
+ assertThat(name.getMethodName())
+ .isEqualTo("withPrimitives[fieldParam=8,param1=true,param2=2]");
+ }
+
+ @Test
+ public void withString(@TestParameter("AAA") String param1) {
+ assertThat(name.getMethodName()).isEqualTo("withString[fieldParam=8,AAA]");
+ }
+
+ @Test
+ public void withEnum(@EnumParameter(TestEnum.TWO) TestEnum param1) {
+ assertThat(name.getMethodName()).isEqualTo("withEnum[fieldParam=8,TWO]");
+ }
+ }
+
+ @ClassTestResult(Result.SUCCESS_ALWAYS)
+ public static class MethodNameContainsOrderedParameterNames {
+
+ @Rule public TestName name = new TestName();
+
+ @Test
+ public void pretest(@TestParameter({"a", "b"}) String foo) {}
+
+ @Test
+ public void test(
+ @EnumParameter({TestEnum.ONE, TestEnum.TWO}) TestEnum e, @TestParameter({"c"}) String foo) {
+ assertThat(name.getMethodName()).isEqualTo("test[" + e.name() + "," + foo + "]");
+ }
+ }
+
+ @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
+ @Ignore("b/195657808 @TestParameters is not supported on Android")
+ public void test() throws Exception {
+ List<Failure> failures;
+ switch (result) {
+ case SUCCESS_ALWAYS:
+ failures =
+ PluggableTestRunner.run(
+ newTestRunnerWithParameterizedSupport(
+ TestParameterAnnotationMethodProcessor::forAllAnnotationPlacements));
+ assertThat(failures).isEmpty();
+
+ failures =
+ PluggableTestRunner.run(
+ newTestRunnerWithParameterizedSupport(
+ TestParameterAnnotationMethodProcessor::onlyForFieldsAndParameters));
+ assertThat(failures).isEmpty();
+ break;
+
+ case SUCCESS_FOR_ALL_PLACEMENTS_ONLY:
+ failures =
+ PluggableTestRunner.run(
+ newTestRunnerWithParameterizedSupport(
+ TestParameterAnnotationMethodProcessor::forAllAnnotationPlacements));
+ assertThat(failures).isEmpty();
+
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ PluggableTestRunner.run(
+ newTestRunnerWithParameterizedSupport(
+ TestParameterAnnotationMethodProcessor::onlyForFieldsAndParameters)));
+ break;
+
+ case FAILURE:
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ PluggableTestRunner.run(
+ newTestRunnerWithParameterizedSupport(
+ TestParameterAnnotationMethodProcessor::forAllAnnotationPlacements)));
+ assertThrows(
+ IllegalStateException.class,
+ () ->
+ PluggableTestRunner.run(
+ newTestRunnerWithParameterizedSupport(
+ TestParameterAnnotationMethodProcessor::onlyForFieldsAndParameters)));
+ break;
+ }
+ }
+
+ private PluggableTestRunner newTestRunnerWithParameterizedSupport(
+ Function<TestClass, TestMethodProcessor> processor) throws Exception {
+ return new PluggableTestRunner(testClass) {
+ @Override
+ protected List<TestMethodProcessor> createTestMethodProcessorList() {
+ return ImmutableList.of(processor.apply(getTestClass()));
+ }
+ };
+ }
+}
diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java
new file mode 100644
index 0000000..b16d5e1
--- /dev/null
+++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java
@@ -0,0 +1,211 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.common.base.CharMatcher;
+import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider;
+import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runner.notification.Failure;
+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 {
+ private static List<TestEnum> testedParameters;
+
+ @TestParameter TestEnum enumParameter;
+
+ @BeforeClass
+ public static void initializeStaticFields() {
+ assertWithMessage("Expecting this class to be run only once").that(testedParameters).isNull();
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ public void test() {
+ testedParameters.add(enumParameter);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO, TestEnum.THREE);
+ }
+ }
+
+ @RunAsTest
+ public static class AnnotatedConstructorParameter {
+ private static List<TestEnum> testedParameters;
+
+ private final TestEnum enumParameter;
+
+ public AnnotatedConstructorParameter(@TestParameter TestEnum enumParameter) {
+ this.enumParameter = enumParameter;
+ }
+
+ @BeforeClass
+ public static void initializeStaticFields() {
+ assertWithMessage("Expecting this class to be run only once").that(testedParameters).isNull();
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ public void test() {
+ testedParameters.add(enumParameter);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO, TestEnum.THREE);
+ }
+ }
+
+ @RunAsTest
+ public static class MultipleAnnotatedParameters {
+ private static List<String> testedParameters;
+
+ @BeforeClass
+ public static void initializeStaticFields() {
+ assertWithMessage("Expecting this class to be run only once").that(testedParameters).isNull();
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ public void test(
+ @TestParameter TestEnum enumParameterA,
+ @TestParameter({"TWO", "THREE"}) TestEnum enumParameterB,
+ @TestParameter({"!!binary 'ZGF0YQ=='", "data2"}) byte[] bytes) {
+ testedParameters.add(
+ String.format("%s:%s:%s", enumParameterA, enumParameterB, new String(bytes)));
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters)
+ .containsExactly(
+ "ONE:TWO:data",
+ "ONE:THREE:data",
+ "TWO:TWO:data",
+ "TWO:THREE:data",
+ "THREE:TWO:data",
+ "THREE:THREE:data",
+ "ONE:TWO:data2",
+ "ONE:THREE:data2",
+ "TWO:TWO:data2",
+ "TWO:THREE:data2",
+ "THREE:TWO:data2",
+ "THREE:THREE:data2");
+ }
+ }
+
+ @RunAsTest
+ public static class WithValuesProvider {
+ private static List<Object> testedParameters;
+
+ @BeforeClass
+ public static void initializeStaticFields() {
+ assertWithMessage("Expecting this class to be run only once").that(testedParameters).isNull();
+ testedParameters = new ArrayList<>();
+ }
+
+ @Test
+ public void stringTest(
+ @TestParameter(valuesProvider = TestStringProvider.class) String string) {
+ testedParameters.add(string);
+ }
+
+ @Test
+ public void charMatcherTest(
+ @TestParameter(valuesProvider = CharMatcherProvider.class) CharMatcher charMatcher) {
+ testedParameters.add(charMatcher);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testedParameters)
+ .containsExactly(
+ "A", "B", null, CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace());
+ }
+
+ private static final class TestStringProvider implements TestParameterValuesProvider {
+ @Override
+ public List<String> provideValues() {
+ return newArrayList("A", "B", null);
+ }
+ }
+
+ private static final class CharMatcherProvider implements TestParameterValuesProvider {
+ @Override
+ public List<CharMatcher> provideValues() {
+ 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 {
+ List<Failure> failures =
+ PluggableTestRunner.run(
+ new PluggableTestRunner(testClass) {
+ @Override
+ protected List<TestMethodProcessor> createTestMethodProcessorList() {
+ return TestMethodProcessors.createNewParameterizedProcessorsWithLegacyFeatures(
+ getTestClass());
+ }
+ });
+
+ assertThat(failures).isEmpty();
+ }
+}
diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java
new file mode 100644
index 0000000..b27ce7a
--- /dev/null
+++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java
@@ -0,0 +1,474 @@
+/*
+ * 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 java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.common.collect.ImmutableList;
+import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValues;
+import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValuesProvider;
+import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.junit.runner.notification.Failure;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+@Ignore("b/195657808 @TestParameters is not supported on Android")
+public class TestParametersMethodProcessorTest {
+
+ @Retention(RUNTIME)
+ @interface RunAsTest {}
+
+ public enum TestEnum {
+ ONE,
+ TWO,
+ THREE;
+ }
+
+ @RunAsTest
+ public static class SimpleMethodAnnotation {
+ @Rule public TestName testName = new TestName();
+
+ private static Map<String, String> testNameToStringifiedParametersMap;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testNameToStringifiedParametersMap = new LinkedHashMap<>();
+ }
+
+ @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}",
+ })
+ @Ignore("b/195657808 @TestParameters is not supported on Android")
+ public void test(TestEnum testEnum, long testLong, boolean testBoolean, String testString) {
+ testNameToStringifiedParametersMap.put(
+ testName.getMethodName(),
+ String.format("%s,%s,%s,%s", testEnum, testLong, testBoolean, testString));
+ }
+
+ @Test
+ @TestParameters({
+ "{testString: ABC}",
+ "{testString: 'This is a very long string (240 characters) that would normally cause"
+ + " Sponge+Tin to exceed the filename limit of 255 characters."
+ + " ================================================================================="
+ + "=============='}"
+ })
+ @Ignore("b/195657808 @TestParameters is not supported on Android")
+ public void test2_withLongNames(String testString) {
+ testNameToStringifiedParametersMap.put(testName.getMethodName(), testString);
+ }
+
+ @Test
+ @TestParameters({
+ "{testEnums: [ONE, TWO, THREE], testLongs: [11, 4], testBooleans: [false, true],"
+ + " testStrings: [ABC, '123']}",
+ "{testEnums: [TWO],\ntestLongs: [22],\ntestBooleans: [true],\r\n\r\n testStrings: ['DEF']}",
+ "{testEnums: [], testLongs: [], testBooleans: [], testStrings: []}",
+ })
+ @Ignore("b/195657808 @TestParameters is not supported on Android")
+ public void test3_withRepeatedParams(
+ List<TestEnum> testEnums,
+ List<Long> testLongs,
+ List<Boolean> testBooleans,
+ List<String> testStrings) {
+ testNameToStringifiedParametersMap.put(
+ testName.getMethodName(),
+ String.format("%s,%s,%s,%s", testEnums, testLongs, testBooleans, testStrings));
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testNameToStringifiedParametersMap)
+ .containsExactly(
+ "test[{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}]",
+ "ONE,11,false,ABC",
+ "test[{testEnum: TWO, testLong: 22, testBoolean: true, testString: 'DEF'}]",
+ "TWO,22,true,DEF",
+ "test[{testEnum: null, testLong: 33, testBoolean: false, testString: null}]",
+ "null,33,false,null",
+ "test2_withLongNames[1.{testString: ABC}]",
+ "ABC",
+ "test2_withLongNames[2.{testString: 'This is a very long string (240 characters)"
+ + " that would normally cause Sponge+Tin...]",
+ "This is a very long string (240 characters) that would normally cause Sponge+Tin to"
+ + " exceed the filename limit of 255 characters."
+ + " ================================================================================="
+ + "==============",
+ "test3_withRepeatedParams[1.{testEnums: [ONE, TWO, THREE], testLongs: [11, 4],"
+ + " testBooleans: [false, true], testStrings: [...]",
+ "[ONE, TWO, THREE],[11, 4],[false, true],[ABC, 123]",
+ "test3_withRepeatedParams[2.{testEnums: [TWO], testLongs: [22], testBooleans: [true],"
+ + " testStrings: ['DEF']}]",
+ "[TWO],[22],[true],[DEF]",
+ "test3_withRepeatedParams[3.{testEnums: [], testLongs: [], testBooleans: [],"
+ + " testStrings: []}]",
+ "[],[],[],[]");
+ }
+ }
+
+ @RunAsTest
+ public static class SimpleConstructorAnnotation {
+
+ @Rule public TestName testName = new TestName();
+
+ private static Map<String, String> testNameToStringifiedParametersMap;
+
+ 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;
+ }
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testNameToStringifiedParametersMap = new LinkedHashMap<>();
+ }
+
+ @Test
+ public void test1() {
+ testNameToStringifiedParametersMap.put(
+ testName.getMethodName(),
+ String.format("%s,%s,%s,%s", testEnum, testLong, testBoolean, testString));
+ }
+
+ @Test
+ public void test2() {
+ testNameToStringifiedParametersMap.put(
+ testName.getMethodName(),
+ String.format("%s,%s,%s,%s", testEnum, testLong, testBoolean, testString));
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testNameToStringifiedParametersMap)
+ .containsExactly(
+ "test1[{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}]",
+ "ONE,11,false,ABC",
+ "test1[{testEnum: TWO, testLong: 22, testBoolean: true, testString: DEF}]",
+ "TWO,22,true,DEF",
+ "test1[{testEnum: null, testLong: 33, testBoolean: false, testString: null}]",
+ "null,33,false,null",
+ "test2[{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}]",
+ "ONE,11,false,ABC",
+ "test2[{testEnum: TWO, testLong: 22, testBoolean: true, testString: DEF}]",
+ "TWO,22,true,DEF",
+ "test2[{testEnum: null, testLong: 33, testBoolean: false, testString: null}]",
+ "null,33,false,null");
+ }
+ }
+
+ @RunAsTest
+ public static class ConstructorAnnotationWithProvider {
+
+ @Rule public TestName testName = new TestName();
+
+ private static Map<String, TestEnum> testNameToParameterMap;
+
+ private final TestEnum testEnum;
+
+ @TestParameters(valuesProvider = TestEnumValuesProvider.class)
+ public ConstructorAnnotationWithProvider(TestEnum testEnum) {
+ this.testEnum = testEnum;
+ }
+
+ @BeforeClass
+ public static void resetStaticState() {
+ testNameToParameterMap = new LinkedHashMap<>();
+ }
+
+ @Test
+ public void test1() {
+ testNameToParameterMap.put(testName.getMethodName(), testEnum);
+ }
+
+ @Test
+ public void test2() {
+ testNameToParameterMap.put(testName.getMethodName(), testEnum);
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(testNameToParameterMap)
+ .containsExactly(
+ "test1[one]", TestEnum.ONE,
+ "test1[two]", TestEnum.TWO,
+ "test1[null-case]", null,
+ "test2[one]", TestEnum.ONE,
+ "test2[two]", TestEnum.TWO,
+ "test2[null-case]", null);
+ }
+
+ 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());
+ }
+ }
+ }
+
+ public abstract static class BaseClassWithMethodAnnotation {
+ @Rule public TestName testName = new TestName();
+
+ static List<String> allTestNames;
+
+ @BeforeClass
+ public static void resetStaticState() {
+ allTestNames = new ArrayList<>();
+ }
+
+ @Before
+ public void setUp() {
+ assertThat(allTestNames).doesNotContain(testName.getMethodName());
+ }
+
+ @After
+ public void tearDown() {
+ assertThat(allTestNames).contains(testName.getMethodName());
+ }
+
+ @Test
+ @TestParameters({"{testEnum: ONE}", "{testEnum: TWO}"})
+ public void testInBase(TestEnum testEnum) {
+ allTestNames.add(testName.getMethodName());
+ }
+ }
+
+ @RunAsTest
+ public static class AnnotationInheritedFromBaseClass extends BaseClassWithMethodAnnotation {
+
+ @Test
+ @TestParameters({"{testEnum: TWO}", "{testEnum: THREE}"})
+ public void testInChild(TestEnum testEnum) {
+ allTestNames.add(testName.getMethodName());
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(allTestNames)
+ .containsExactly(
+ "testInBase[{testEnum: ONE}]",
+ "testInBase[{testEnum: TWO}]",
+ "testInChild[{testEnum: TWO}]",
+ "testInChild[{testEnum: THREE}]");
+ }
+ }
+
+ @RunAsTest
+ public static class MixedWithTestParameterMethodAnnotation {
+ @Rule public TestName testName = new TestName();
+
+ private static List<String> allTestNames;
+ private static List<String> testNamesThatInvokedBefore;
+ private static List<String> testNamesThatInvokedAfter;
+
+ @TestParameters({"{testEnum: ONE}", "{testEnum: TWO}"})
+ public MixedWithTestParameterMethodAnnotation(TestEnum testEnum) {}
+
+ @BeforeClass
+ public static void resetStaticState() {
+ allTestNames = new ArrayList<>();
+ testNamesThatInvokedBefore = new ArrayList<>();
+ testNamesThatInvokedAfter = new ArrayList<>();
+ }
+
+ @Before
+ public void setUp() {
+ assertThat(allTestNames).doesNotContain(testName.getMethodName());
+ testNamesThatInvokedBefore.add(testName.getMethodName());
+ }
+
+ @After
+ public void tearDown() {
+ assertThat(allTestNames).contains(testName.getMethodName());
+ testNamesThatInvokedAfter.add(testName.getMethodName());
+ }
+
+ @Test
+ public void test1(@TestParameter TestEnum testEnum) {
+ assertThat(testNamesThatInvokedBefore).contains(testName.getMethodName());
+ allTestNames.add(testName.getMethodName());
+ }
+
+ @Test
+ @TestParameters({"{testString: ABC}", "{testString: DEF}"})
+ public void test2(String testString) {
+ allTestNames.add(testName.getMethodName());
+ }
+
+ @Test
+ @TestParameters({
+ "{testString: ABC}",
+ "{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) {
+ allTestNames.add(testName.getMethodName());
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(allTestNames)
+ .containsExactly(
+ "test1[{testEnum: ONE},ONE]",
+ "test1[{testEnum: ONE},TWO]",
+ "test1[{testEnum: ONE},THREE]",
+ "test1[{testEnum: TWO},ONE]",
+ "test1[{testEnum: TWO},TWO]",
+ "test1[{testEnum: TWO},THREE]",
+ "test2[{testEnum: ONE},{testString: ABC}]",
+ "test2[{testEnum: ONE},{testString: DEF}]",
+ "test2[{testEnum: TWO},{testString: ABC}]",
+ "test2[{testEnum: TWO},{testString: DEF}]",
+ "test3_withLongNames[{testEnum: ONE},1.{testString: ABC}]",
+ "test3_withLongNames[{testEnum: ONE},2.{testString: 'This is a very long string"
+ + " (240 characters) that would normally caus...]",
+ "test3_withLongNames[{testEnum: TWO},1.{testString: ABC}]",
+ "test3_withLongNames[{testEnum: TWO},2.{testString: 'This is a very long string"
+ + " (240 characters) that would normally caus...]");
+
+ assertThat(testNamesThatInvokedBefore).containsExactlyElementsIn(allTestNames).inOrder();
+ assertThat(testNamesThatInvokedAfter).containsExactlyElementsIn(allTestNames).inOrder();
+ }
+ }
+
+ @RunAsTest
+ public static class MixedWithTestParameterFieldAnnotation {
+ @Rule public TestName testName = new TestName();
+
+ private static List<String> allTestNames;
+
+ @TestParameter TestEnum testEnumA;
+
+ @TestParameters({"{testEnumB: ONE}", "{testEnumB: TWO}"})
+ public MixedWithTestParameterFieldAnnotation(TestEnum testEnumB) {}
+
+ @BeforeClass
+ public static void resetStaticState() {
+ allTestNames = new ArrayList<>();
+ }
+
+ @Before
+ public void setUp() {
+ assertThat(allTestNames).doesNotContain(testName.getMethodName());
+ }
+
+ @After
+ public void tearDown() {
+ assertThat(allTestNames).contains(testName.getMethodName());
+ }
+
+ @Test
+ @TestParameters({"{testString: ABC}", "{testString: DEF}"})
+ @Ignore("b/195657808 @TestParameters is not supported on Android")
+ public void test1(String testString) {
+ allTestNames.add(testName.getMethodName());
+ }
+
+ @AfterClass
+ public static void completedAllParameterizedTests() {
+ assertThat(allTestNames)
+ .containsExactly(
+ "test1[{testEnumB: ONE},{testString: ABC},ONE]",
+ "test1[{testEnumB: ONE},{testString: ABC},TWO]",
+ "test1[{testEnumB: ONE},{testString: ABC},THREE]",
+ "test1[{testEnumB: ONE},{testString: DEF},ONE]",
+ "test1[{testEnumB: ONE},{testString: DEF},TWO]",
+ "test1[{testEnumB: ONE},{testString: DEF},THREE]",
+ "test1[{testEnumB: TWO},{testString: ABC},ONE]",
+ "test1[{testEnumB: TWO},{testString: ABC},TWO]",
+ "test1[{testEnumB: TWO},{testString: ABC},THREE]",
+ "test1[{testEnumB: TWO},{testString: DEF},ONE]",
+ "test1[{testEnumB: TWO},{testString: DEF},TWO]",
+ "test1[{testEnumB: TWO},{testString: DEF},THREE]");
+ }
+ }
+
+ @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})
+ .collect(toImmutableList());
+ }
+
+ private final Class<?> testClass;
+
+ public TestParametersMethodProcessorTest(String name, Class<?> testClass) {
+ this.testClass = testClass;
+ }
+
+ @Test
+ public void test() throws Exception {
+ List<Failure> failures = PluggableTestRunner.run(newTestRunner());
+ assertThat(failures).isEmpty();
+ }
+
+ private PluggableTestRunner newTestRunner() throws Exception {
+ return new PluggableTestRunner(testClass) {
+ @Override
+ protected List<TestMethodProcessor> createTestMethodProcessorList() {
+ return TestMethodProcessors.createNewParameterizedProcessorsWithLegacyFeatures(
+ getTestClass());
+ }
+ };
+ }
+}