Snap for 7862349 from 57d56d332e0f4eecec125f8f9d9917ad311bb70c to t-keystone-qcom-release

Change-Id: I3551c87939e226fc7e0fff7c2e9c75b6c231deb0
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/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..86fb534
--- /dev/null
+++ b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java
@@ -0,0 +1,440 @@
+/*
+ * 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.
+   *
+   * <p>Deterministic means that the order will not change, even when tests are added/removed or
+   * between releases.
+   *
+   * @deprecated Override {@link #sortTestMethods} with preferred sorting strategy.
+   */
+  @Deprecated
+  protected boolean shouldSortTestMethodsDeterministically() {
+    return false; // Don't sort methods by default
+  }
+
+  /**
+   * Sort test methods (across different TestMethodProcessors).
+   *
+   * <p>This should be deterministic. The order should not change, even when tests are added/removed
+   * or between releases.
+   */
+  protected Stream<FrameworkMethod> sortTestMethods(Stream<FrameworkMethod> methods) {
+    if (!shouldSortTestMethodsDeterministically()) {
+      return methods;
+    }
+
+    return methods.sorted(
+            comparing((FrameworkMethod method) -> method.getName().hashCode())
+                    .thenComparing(FrameworkMethod::getName));
+  }
+
+  /**
+   * Returns classes used as annotations to indicate test methods.
+   *
+   * <p>Defaults to {@link Test}.
+   */
+  protected ImmutableList<Class<? extends Annotation>> getSupportedTestAnnotations() {
+    return ImmutableList.of(Test.class);
+  }
+
+  /**
+   * {@link TestRule}s that will be executed 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 =
+            getSupportedTestAnnotations().stream()
+                    .flatMap(annotation -> getTestClass().getAnnotatedMethods(annotation).stream())
+                    .flatMap(method -> processMethod(method).stream());
+
+    processedMethods = sortTestMethods(processedMethods);
+
+    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 =
+            getSupportedTestAnnotations().stream()
+                    .flatMap(annotation -> getTestClass().getAnnotatedMethods(annotation).stream())
+                    .collect(Collectors.toList());
+    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..13561ff
--- /dev/null
+++ b/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.Comparator.comparing;
+
+import com.google.common.collect.ImmutableList;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+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 {
+  @Retention(RetentionPolicy.RUNTIME)
+  private static @interface CustomTest {}
+
+  private static int ruleInvocationCount = 0;
+  private static int testMethodInvocationCount = 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 TestAndMethodRuleTestClass {
+
+    @Rule public TestAndMethodRule rule = new TestAndMethodRule();
+
+    @Test
+    public void test() {
+      // no-op
+    }
+  }
+
+  @Test
+  public void ruleThatIsBothTestRuleAndMethodRuleIsInvokedOnceOnly() throws Exception {
+    PluggableTestRunner.run(
+        new PluggableTestRunner(TestAndMethodRuleTestClass.class) {
+          @Override
+          protected List<TestMethodProcessor> createTestMethodProcessorList() {
+            return ImmutableList.of();
+          }
+        });
+
+    assertThat(ruleInvocationCount).isEqualTo(1);
+  }
+
+  @RunWith(PluggableTestRunner.class)
+  public static class CustomTestAnnotationTestClass {
+    @SuppressWarnings("JUnit4TestNotRun")
+    @CustomTest
+    public void customTestAnnotatedTest() {
+      testMethodInvocationCount++;
+    }
+
+    @Test
+    public void testAnnotatedTest() {
+      testMethodInvocationCount++;
+    }
+  }
+
+  @Test
+  public void testMarkedWithCustomClassIsInvoked() throws Exception {
+    testMethodInvocationCount = 0;
+    PluggableTestRunner.run(
+            new PluggableTestRunner(CustomTestAnnotationTestClass.class) {
+              @Override
+              protected List<TestMethodProcessor> createTestMethodProcessorList() {
+                return ImmutableList.of();
+              }
+
+              @Override
+              protected ImmutableList<Class<? extends Annotation>> getSupportedTestAnnotations() {
+                return ImmutableList.of(Test.class, CustomTest.class);
+              }
+            });
+
+    assertThat(testMethodInvocationCount).isEqualTo(2);
+  }
+
+  private static final List<String> testOrder = new ArrayList<>();
+
+  @RunWith(PluggableTestRunner.class)
+  public static class SortedPluggableTestRunnerTestClass {
+    @Test
+    public void a() {
+      testOrder.add("a");
+    }
+
+    @Test
+    public void b() {
+      testOrder.add("b");
+    }
+
+    @Test
+    public void c() {
+      testOrder.add("c");
+    }
+  }
+
+  @Test
+  public void testsAreSortedCorrectly() throws Exception {
+    testOrder.clear();
+    PluggableTestRunner.run(
+            new PluggableTestRunner(SortedPluggableTestRunnerTestClass.class) {
+              @Override
+              protected List<TestMethodProcessor> createTestMethodProcessorList() {
+                return ImmutableList.of();
+              }
+
+              @Override
+              protected Stream<FrameworkMethod> sortTestMethods(Stream<FrameworkMethod> methods) {
+                return methods.sorted(comparing(FrameworkMethod::getName).reversed());
+              }
+            });
+    assertThat(testOrder).containsExactly("c", "b", "a");
+  }
+
+}
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());
+      }
+    };
+  }
+}