From 2ab5697681b163b2eaf5a716297bc5f436dd2cc5 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Thu, 25 Feb 2021 13:33:01 +0000 Subject: First commit: Add first version of code. --- .gitignore | 1 + CONTRIBUTING.md | 29 + LICENSE | 202 +++ README.md | 240 ++++ pom.xml | 182 +++ .../BaseTestParameterValidator.java | 83 ++ .../ParameterValueParsing.java | 221 ++++ .../ParameterizedTestMethodProcessor.java | 237 ++++ .../testparameterinjector/PluggableTestRunner.java | 414 ++++++ .../testparameterinjector/ProtoValueParsing.java | 25 + .../junit/testparameterinjector/TestInfo.java | 75 ++ .../testparameterinjector/TestMethodProcessor.java | 100 ++ .../TestMethodProcessors.java | 54 + .../junit/testparameterinjector/TestParameter.java | 214 ++++ .../TestParameterAnnotation.java | 266 ++++ .../TestParameterAnnotationMethodProcessor.java | 1346 ++++++++++++++++++++ .../TestParameterInjector.java | 68 + .../TestParameterProcessor.java | 32 + .../TestParameterValidator.java | 69 + .../TestParameterValueProvider.java | 53 + .../testparameterinjector/TestParameterValues.java | 28 + .../testparameterinjector/TestParameters.java | 212 +++ .../TestParametersMethodProcessor.java | 413 ++++++ .../ParameterValueParsingTest.java | 112 ++ .../PluggableTestRunnerTest.java | 74 ++ ...TestParameterAnnotationMethodProcessorTest.java | 982 ++++++++++++++ .../testparameterinjector/TestParameterTest.java | 205 +++ .../TestParametersMethodProcessorTest.java | 465 +++++++ 28 files changed, 6402 insertions(+) create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessors.java create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestParameterProcessor.java create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java create mode 100644 src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java create mode 100644 src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java create mode 100644 src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java create mode 100644 src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java create mode 100644 src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ 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 + 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/README.md b/README.md new file mode 100644 index 0000000..e5f303e --- /dev/null +++ b/README.md @@ -0,0 +1,240 @@ +TestParameterInjector +===================== + +## Introduction + +Parameterized tests are a great way to avoid code duplication between tests and +promote high test coverage for data-driven tests. + +## 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 } +} +``` + +## 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; +``` + +#### 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 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 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..43cecb7 --- /dev/null +++ b/pom.xml @@ -0,0 +1,182 @@ + + + + + 4.0.0 + + org.sonatype.oss + oss-parent + 7 + + + com.google.testparameterinjector + test-parameter-injector + 1.0-SNAPSHOT + + TestParameterInjector + + + A simple yet powerful parameterized test runner. + + + https://github.com/google/testparameterinjector + + 2021 + + + Google Inc. + http://www.google.com/ + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + jnyman + Jens Nyman + jnyman@google.com + Google Inc. + http://www.google.com/ + + owner + developer + + +1 + + + sergebeauchamp + Serge Beauchamp + sergebeauchamp@google.com + Google Inc. + http://www.google.com/ + + developer + + +0 + + + + + http://github.com/google/testparameterinjector/ + scm:git:git://github.com/google/testparameterinjector.git + scm:git:ssh://git@github.com/google/testparameterinjector.git + + + + GitHub Issues + http://github.com/google/testparameterinjector/issues + + + + 3.0.3 + + + + UTF-8 + + + + + + com.google.auto.value + auto-value-annotations + 1.7.4 + + + com.google.auto.value + auto-value + 1.7.4 + + + com.google.code.findbugs + jsr305 + 3.0.2 + + + com.google.guava + guava + 30.1-jre + + + com.google.protobuf + protobuf-java-util + 3.14.0 + + + junit + junit + 4.13.2 + + + org.yaml + snakeyaml + 1.27 + + + + + com.google.truth + truth + 1.1.2 + test + + + + + + + + maven-javadoc-plugin + 3.2.0 + + + maven-jar-plugin + 3.2.0 + + + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + 1.8 + 1.8 + true + + -parameters + + + + + maven-source-plugin + 3.2.1 + + + maven-surefire-plugin + 2.22.2 + + + + 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> 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 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 parameter : parameters) { + List 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 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>> 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..5c94fb9 --- /dev/null +++ b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java @@ -0,0 +1,221 @@ +/* + * 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.MessageLite; +import java.lang.reflect.ParameterizedType; +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 > Enum parseEnum(String str, Class enumType) { + return Enum.valueOf((Class) 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; + } + + YamlValueTransfomer yamlValueTransfomer = + new YamlValueTransfomer(parsedYaml, javaType.getRawType()); + + yamlValueTransfomer + .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); + + yamlValueTransfomer.ifJavaType(Boolean.class).supportParsedType(Boolean.class, identity()); + + yamlValueTransfomer.ifJavaType(Integer.class).supportParsedType(Integer.class, identity()); + + yamlValueTransfomer + .ifJavaType(Long.class) + .supportParsedType(Long.class, identity()) + .supportParsedType(Integer.class, Integer::longValue); + + yamlValueTransfomer + .ifJavaType(Float.class) + .supportParsedType(Float.class, identity()) + .supportParsedType(Double.class, Double::floatValue) + .supportParsedType(Integer.class, Integer::floatValue); + + yamlValueTransfomer + .ifJavaType(Double.class) + .supportParsedType(Double.class, identity()) + .supportParsedType(Integer.class, Integer::doubleValue) + .supportParsedType(Long.class, Long::doubleValue); + + yamlValueTransfomer + .ifJavaType(Enum.class) + .supportParsedType( + String.class, str -> ParameterValueParsing.parseEnum(str, javaType.getRawType())); + + yamlValueTransfomer + .ifJavaType(MessageLite.class) + .supportParsedType(String.class, str -> parseTextprotoMessage(str, javaType.getRawType())) + .supportParsedType( + Map.class, + map -> + getProtoValueParser() + .parseProtobufMessage((Map) map, javaType.getRawType())); + + // Added mainly for protocol buffer parsing + yamlValueTransfomer + .ifJavaType(List.class) + .supportParsedType( + List.class, + list -> + Lists.transform( + list, + e -> + parseYamlObjectToJavaType( + e, getGenericParameterType(javaType, /* parameterIndex= */ 0)))); + yamlValueTransfomer + .ifJavaType(Map.class) + .supportParsedType( + Map.class, + map -> + Maps.transformValues( + map, + v -> + parseYamlObjectToJavaType( + v, getGenericParameterType(javaType, /* parameterIndex= */ 1)))); + + return yamlValueTransfomer.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 YamlValueTransfomer { + private final Object parsedYaml; + private final Class javaType; + @Nullable private Object transformedJavaValue; + + YamlValueTransfomer(Object parsedYaml, Class javaType) { + this.parsedYaml = parsedYaml; + this.javaType = javaType; + } + + SupportedJavaType ifJavaType(Class 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 { + + private final Class supportedJavaType; + + private SupportedJavaType(Class supportedJavaType) { + this.supportedJavaType = supportedJavaType; + } + + @SuppressWarnings("unchecked") + SupportedJavaType supportParsedType( + Class parsedYamlType, Function 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..459bccc --- /dev/null +++ b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java @@ -0,0 +1,237 @@ +/* + * 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 java.lang.annotation.Annotation; +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. + * + *

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

The {@link Parameters} annotated method can return either a {@code Collection} or a + * {@code Collection}. + * + *

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> 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 testNamePattern; + + ParameterizedTestMethodProcessor(TestClass testClass) { + Optional 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.>of((Iterable) parameters); + } else if (parameters instanceof Object[]) { + parametersForAllTests = + Optional.>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 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 errorsReturned) { + return ValidationResult.NOT_HANDLED; + } + + @Override + public List processTest(Class testClass, TestInfo originalTest) { + if (parametersForAllTests.isPresent()) { + ImmutableList.Builder 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 testName = MessageFormat.format(namePattern, parametersForOneTest); + tests.add( + TestInfo.create( + originalTest.getMethod(), + originalTest.getName() + "[" + testName + "]", + updateAnnotationList(originalTest, testIndex))); + testIndex++; + } + return tests.build(); + } + return ImmutableList.of(originalTest); + } + + @Override + public Statement processStatement(Statement originalStatement, Description finalTestDescription) { + return originalStatement; + } + + @Override + public Optional createTest( + TestClass testClass, FrameworkMethod method, Optional test) { + if (parametersForAllTests.isPresent()) { + Object[] testParameters = + getTestParameters(method.getAnnotation(TestIndexHolder.class).testIndex()); + try { + Constructor constructor = testClass.getOnlyConstructor(); + return Optional.of(constructor.newInstance(testParameters)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return test; + } + + @Override + public Optional createStatement( + TestClass testClass, + FrameworkMethod method, + Object testObject, + Optional statement) { + return statement; + } + + /** + * Stores into the annotation list of a test method the {@code testIndex} required to identify + * which parameter should be used for this test instance. + */ + private ImmutableList updateAnnotationList( + TestInfo originalTest, final int testIndex) { + Annotation parameterHolder = TestIndexHolderFactory.create(testIndex); + return new ImmutableList.Builder() + .addAll(originalTest.getAnnotations()) + .add(parameterHolder) + .build(); + } + + /** + * 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 getParametersMethod(TestClass testClass) { + List 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..f919de9 --- /dev/null +++ b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java @@ -0,0 +1,414 @@ +/* + * 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. + * + *

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

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 currentTestInfo = new ThreadLocal<>(); + + private ImmutableList testRules; + private List 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 createTestMethodProcessorList(); + + /** + * This method is run to perform optional additional operations on the test instance, right after + * it was created. + */ + protected void finalizeCreatedTestInstance(Object testInstance) { + // Do nothing by default + } + + /** + * If true, all test methods (across different TestMethodProcessors) will be sorted in a + * deterministic way by their test name. + * + *

Deterministic means that the order will not change, even when tests are added/removed or + * between releases. + */ + protected boolean shouldSortTestMethodsDeterministically() { + return false; // Don't sort methods by default + } + + /** + * {@link TestRule}s that will be executed after the ones defined in the test class (but still + * before all {@link MethodRule}s). This is meant to be overridden by subclasses. + */ + protected List 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 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 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 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 run(PluggableTestRunner testRunner) throws Exception { + final ImmutableList.Builder 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 computeTestMethods() { + Stream processedMethods = + super.computeTestMethods().stream().flatMap(method -> processMethod(method).stream()); + + if (shouldSortTestMethodsDeterministically()) { + processedMethods = + processedMethods.sorted( + comparing((FrameworkMethod method) -> method.getName().hashCode()) + .thenComparing(FrameworkMethod::getName)); + } + + return processedMethods.collect(toImmutableList()); + } + + /** Implementation of a JUnit FrameworkMethod where the name and annotation list is overridden. */ + private static class OverriddenFrameworkMethod extends FrameworkMethod { + + private final TestInfo testInfo; + + public OverriddenFrameworkMethod(Method method, TestInfo testInfo) { + super(method); + this.testInfo = testInfo; + } + + public TestInfo getTestInfo() { + return testInfo; + } + + @Override + public String getName() { + return testInfo.getName(); + } + + @Override + public Annotation[] getAnnotations() { + List annotations = testInfo.getAnnotations(); + return annotations.toArray(new Annotation[0]); + } + + @Override + public T getAnnotation(final Class 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 processMethod(FrameworkMethod initialMethod) { + ImmutableList methods = ImmutableList.of(initialMethod); + for (final TestMethodProcessor testMethodProcessor : getTestMethodProcessors()) { + methods = + methods.stream() + .flatMap( + method -> { + TestInfo originalTest = + TestInfo.create( + method.getMethod(), + method.getName(), + ImmutableList.copyOf(method.getAnnotations())); + List processedTests = + testMethodProcessor.processTest( + getTestClass().getJavaClass(), originalTest); + + return processedTests.stream() + .map( + processedTest -> + new OverriddenFrameworkMethod(method.getMethod(), processedTest)); + }) + .collect(toImmutableList()); + } + return methods; + } + + // 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 = 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 testRules = + Stream.of( + getTestRulesForProcessors().stream(), + getInnerTestRules().stream(), + getTestRules(target).stream(), + getOuterTestRules().stream()) + .flatMap(x -> x) + .collect(toImmutableList()); + + Iterable 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 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 errorsReturned) { + for (TestMethodProcessor testMethodProcessor : getTestMethodProcessors()) { + if (testMethodProcessor.validateConstructor(getTestClass(), errorsReturned) + == ValidationResult.HANDLED) { + return; + } + } + super.validateZeroArgConstructor(errorsReturned); + } + + @Override + protected final void validateTestMethods(List list) { + List testMethods = getTestClass().getAnnotatedMethods(Test.class); + for (FrameworkMethod testMethod : testMethods) { + boolean isHandled = false; + for (TestMethodProcessor testMethodProcessor : getTestMethodProcessors()) { + if (testMethodProcessor.validateTestMethod(getTestClass(), testMethod, list) + == ValidationResult.HANDLED) { + isHandled = true; + break; + } + } + if (!isHandled) { + testMethod.validatePublicVoidNoArg(false /* isStatic */, list); + } + } + } + + // Fix for ParentRunner bug: + // Overriding this method because a superclass (ParentRunner) is calling this in its constructor + // and then throwing an InitializationError that doesn't have any of the causes in the exception + // message. + @Override + protected final void collectInitializationErrors(List 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 annotation, boolean isStatic, List errors) { + super.validatePublicVoidNoArgMethods(annotation, isStatic, errors); + } + + private synchronized List getTestMethodProcessors() { + if (testMethodProcessors == null) { + testMethodProcessors = createTestMethodProcessorList(); + } + return testMethodProcessors; + } + + private synchronized ImmutableList 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 Collector> 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 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..daf6b9a --- /dev/null +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java @@ -0,0 +1,75 @@ +/* + * 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.stream.Collectors.toList; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.List; +import java.util.function.Function; +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. + * + *

See b/168325767 for the reason behind this. tl;dr the name is put into a Unix file with max + * 255 characters. The surrounding constant characters take up 31 characters. The max is reduced + * by an additional 24 characters to account for future changes. + */ + static final int MAX_TEST_NAME_LENGTH = 200; + + public abstract Method getMethod(); + + public abstract String getName(); + + public abstract ImmutableList getAnnotations(); + + @Nullable + public T getAnnotation(Class annotationClass) { + for (Annotation annotation : getAnnotations()) { + if (annotationClass.isInstance(annotation)) { + return annotationClass.cast(annotation); + } + } + return null; + } + + private TestInfo withName(String otherName) { + return TestInfo.create(getMethod(), otherName, getAnnotations()); + } + + public static TestInfo create(Method method, String name, List annotations) { + return new AutoValue_TestInfo(method, name, ImmutableList.copyOf(annotations)); + } + + static ImmutableList shortenNamesIfNecessary( + List testInfos, Function shorterNameFunction) { + if (testInfos.stream().anyMatch(i -> i.getName().length() > MAX_TEST_NAME_LENGTH)) { + return ImmutableList.copyOf( + testInfos.stream() + .map(testInfo -> testInfo.withName(shorterNameFunction.apply(testInfo))) + .collect(toList())); + } else { + return ImmutableList.copyOf(testInfos); + } + } +} 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..34996be --- /dev/null +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java @@ -0,0 +1,100 @@ +/* + * 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. + * + *

Note: Implementations of this interface are expected to be immutable, i.e. they no longer + * change after construction. + */ +/* copybara:strip_begin(advanced usage) */ public /* copybara:strip_end */ +interface TestMethodProcessor { + + /** Allows to transform the test information (name and annotations). */ + List 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. + *

The default implementation should return {@code test}. + */ + Optional createTest(TestClass testClass, FrameworkMethod method, Optional 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. + *

The default implementation should return {@code statement}. + */ + Optional createStatement( + TestClass testClass, + FrameworkMethod method, + Object testObject, + Optional 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 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 errorsReturned); + + /** + * Whether the constructor or method validation has been handled or not. + * + *

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

Note that this includes support for {@link org.junit.runners.Parameterized}. + */ + public static ImmutableList + 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: + * + *

    + *
  • No support for {@link org.junit.runners.Parameterized} + *
  • No support for class and method-level parameters, except for @TestParameters + *
+ */ + public static ImmutableList 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..0325d4d --- /dev/null +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java @@ -0,0 +1,214 @@ +/* + * 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.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Test parameter annotation that defines the values that a single parameter can have. + * + *

For enums and booleans, the values can be automatically derived as all possible values: + * + *

+ * {@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 }
+ * 
+ * + *

The values can be explicitly defined as a parsed string: + * + *

+ * 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)]
+ * }
+ * 
+ * + *

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

Types that are supported: + * + *

    + *
  • String: No parsing happens + *
  • boolean: Specified as YAML boolean + *
  • long and int: Specified as YAML integer + *
  • float and double: Specified as YAML floating point or integer + *
  • Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()} + *
  • Protobuf messages: Specified as a YAML mapping or as textproto string: + *
      + *
    • As YAML mapping: The mapping keys are the proto field names and their values are + * parsed in the same way as the parameter values + *
    • Textproto string: Formatted according to go/textformat-spec + *
    + *
  • + *
+ * + *

For dynamic sets of parameters or parameter types that are not supported here, use {@link + * #valuesProvider()} and leave this field empty. + * + *

For examples, see {@link TestParameter}. + */ + String[] value() default {}; + + /** + * Sets a provider that will return a list of parameter values. + * + *

If this field is set, {@link #value()} must be empty and vice versa. + * + *

Example

+ * + *
+   * {@literal @}Test
+   * public void matchesAllOf_throwsOnNull(
+   *     {@literal @}TestParameter(valuesProvider = CharMatcherProvider.class)
+   *         CharMatcher charMatcher) {
+   *   assertThrows(NullPointerException.class, () -> charMatcher.matchesAllOf(null));
+   * }
+   *
+   * private static final class CharMatcherProvider implements TestParameterValuesProvider {
+   *   {@literal @}Override
+   *   public List provideValues() {
+   *     return ImmutableList.of(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace());
+   *   }
+   * }
+   * 
+ */ + Class 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 provideValues() { + return ImmutableList.of(); + } + } + + /** Implementation of this parameter annotation. */ + final class InternalImplementationOfThisParameter implements TestParameterValueProvider { + @Override + public List provideValues( + Annotation uncastAnnotation, Optional> 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 annotationType, Optional> 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; + } else if (Enum.class.isAssignableFrom(parameterClass)) { + return 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 getValuesFromProvider( + Class valuesProvider) { + try { + Constructor constructor = + valuesProvider.getDeclaredConstructor(); + constructor.setAccessible(true); + return new ArrayList<>(constructor.newInstance().provideValues()); + } 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..7c06181 --- /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. + * + *

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: + * + *

{@code
+ * @RunWith(ParameterizedTestRunner.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();
+ *     }
+ * }
+ * }
+ * + *

An alternative is to use a method parameter for injection: + * + *

{@code
+ * @RunWith(ParameterizedTestRunner.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();
+ *     }
+ * }
+ * }
+ * + *

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

{@code
+ * @RunWith(ParameterizedTestRunner.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();
+ *     }
+ * }
+ * }
+ * + *

Class constructors can also be annotated with @TestParameterAnnotation annotations, as shown + * below: + * + *

{@code
+ * @RunWith(ParameterizedTestRunner.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() {...}
+ * }
+ * }
+ * + *

Each field that needs to be injected from a parameter requires its dedicated distinct + * annotation. + * + *

If the same annotation is defined both on the class and method, the method parameter values + * take precedence. + * + *

If the same annotation is defined both on the class and constructor, the constructor parameter + * values take precedence. + * + *

Annotations cannot be duplicated between the constructor or constructor parameters and a + * method or method parameter. + * + *

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 validator() default DefaultValidator.class; + + /** + * Specifies a processor for the parameter to invoke arbitrary code before and after the test + * statement's execution. + */ + Class processor() default DefaultProcessor.class; + + /** Specifies a value provider for the parameter to provide the values to test. */ + Class 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 provideValues(Annotation annotation, Optional> 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 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 annotationType, Optional> 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 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..c268e3d --- /dev/null +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -0,0 +1,1346 @@ +/* + * 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.RetentionPolicy.RUNTIME; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; +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 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.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 + */ +/* copybara:strip_begin(advanced usage) */ public /* copybara:strip_end */ +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 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 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> 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 paramName(); + + public static ImmutableList create( + AnnotationWithMetadata annotationWithMetadata, Origin origin) { + List 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 specifiedValues.stream() + .map( + value -> + new AutoValue_TestParameterAnnotationMethodProcessor_TestParameterValue( + AnnotationTypeOrigin.create( + annotationWithMetadata.annotation().annotationType(), origin), + value, + 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 getTestParameterValue( + TestInfo testInfo, Class annotationType) { + return getTestParameterValues(testInfo).getValue(annotationType); + } + + private static List getParametersAnnotationValues( + AnnotationWithMetadata annotationWithMetadata) { + Annotation annotation = annotationWithMetadata.annotation(); + TestParameterAnnotation testParameter = + annotation.annotationType().getAnnotation(TestParameterAnnotation.class); + Class valueProvider = testParameter.valueProvider(); + try { + return valueProvider + .getConstructor() + .newInstance() + .provideValues( + annotation, + java.util.Optional.ofNullable(annotationWithMetadata.paramClass().orNull())); + } catch (Exception e) { + throw new RuntimeException( + "Unexpected exception while invoking value provider " + valueProvider, e); + } + } + + private static Predicate matches(Class 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 annotationType(); + + /** Where the annotation was declared. */ + abstract Origin origin(); + + public static AnnotationTypeOrigin create( + Class 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> 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 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 withoutMetadata(Annotation annotation) { + return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( + annotation, Optional.absent(), Optional.absent()); + } + } + + private final TestClass testClass; + private final boolean onlyForFieldsAndParameters; + private volatile ImmutableList cachedAnnotationTypeOrigins; + private final Cache>> 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: + * + *
    + *
  • At a method / constructor parameter + *
  • At a field + *
  • At a method / constructor on the class + *
  • At the test class + *
+ */ + 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. + * + *

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 getAnnotationTypeOrigins( + Origin firstOrigin, Origin... otherOrigins) { + if (cachedAnnotationTypeOrigins == null) { + // Collect all annotations used in declared fields and methods that have themselves a + // @TestParameterAnnotation annotation. + List fieldAnnotations = + extractTestParameterAnnotations( + streamWithParents(testClass.getJavaClass()) + .flatMap(c -> stream(c.getDeclaredFields())) + .flatMap(field -> stream(field.getAnnotations())), + Origin.FIELD); + List methodAnnotations = + extractTestParameterAnnotations( + stream(testClass.getJavaClass().getMethods()) + .flatMap(method -> stream(method.getAnnotations())), + Origin.METHOD); + List parameterAnnotations = + extractTestParameterAnnotations( + stream(testClass.getJavaClass().getMethods()) + .flatMap(method -> stream(method.getParameterAnnotations()).flatMap(Stream::of)), + Origin.METHOD_PARAMETER); + List classAnnotations = + extractTestParameterAnnotations( + stream(testClass.getJavaClass().getAnnotations()), Origin.CLASS); + List constructorAnnotations = + extractTestParameterAnnotations( + stream(testClass.getJavaClass().getConstructors()) + .flatMap(constructor -> stream(constructor.getAnnotations())), + Origin.CONSTRUCTOR); + List 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 originsToFilterBy = + ImmutableSet.builder().add(firstOrigin).add(otherOrigins).build(); + return cachedAnnotationTypeOrigins.stream() + .filter(annotationTypeOrigin -> originsToFilterBy.contains(annotationTypeOrigin.origin())) + .collect(toImmutableList()); + } + + private void checkDuplicatedFieldsAnnotations( + List methodAnnotations, List 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> 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 constructorAnnotations, + List classAnnotations, + List fieldAnnotations) { + ImmutableSet> classAnnotationTypes = + classAnnotations.stream() + .map(AnnotationTypeOrigin::annotationType) + .collect(toImmutableSet()); + + ImmutableSet> uniqueFieldAnnotations = + fieldAnnotations.stream() + .map(AnnotationTypeOrigin::annotationType) + .collect(toImmutableSet()); + ImmutableSet> 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 extractTestParameterAnnotations( + Stream 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 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 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 annotationTypeOrigins, + TestClass testClass, + List 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> 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 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 createStatement( + TestClass testClass, + FrameworkMethod frameworkMethod, + Object testObject, + Optional 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. + * + *

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)}). + * + *

For multiple annotations (say, {@code @TestParameter("foo", "bar")} and + * {@code @ColorParameter({BLUE, WHITE})}), it will generate the following result: + * + *

    + *
  • ("foo", BLUE) + *
  • ("foo", WHITE) + *
  • ("bar", BLUE) + *
  • ("bar", WHITE) + *
  • + *
+ * + * corresponding to the cartesian product of both annotations. + */ + @Override + public List processTest(Class testClass, TestInfo originalTest) { + List> parameterValuesForMethod = + getParameterValuesForMethod(originalTest.getMethod()); + + if (parameterValuesForMethod.equals(ImmutableList.of(ImmutableList.of()))) { + // This test is not parameterized + return ImmutableList.of(originalTest); + } + + ImmutableList.Builder testInfos = ImmutableList.builder(); + for (int parametersIndex = 0; + parametersIndex < parameterValuesForMethod.size(); + ++parametersIndex) { + List testParameterValues = parameterValuesForMethod.get(parametersIndex); + String testNameSuffix = getTestNameSuffix(testParameterValues); + testInfos.add( + TestInfo.create( + originalTest.getMethod(), + appendParametersToTestName(originalTest.getName(), testNameSuffix), + ImmutableList.builder() + .addAll(originalTest.getAnnotations()) + .add( + TestIndexHolderFactory.create( + /* methodIndex= */ ImmutableList.copyOf(testClass.getMethods()) + .indexOf(originalTest.getMethod()), + parametersIndex, + testClass.getName())) + .build())); + } + + return TestInfo.shortenNamesIfNecessary( + testInfos.build(), + testInfo -> + appendParametersToTestName( + originalTest.getName(), + String.valueOf( + testInfo.getAnnotation(TestIndexHolder.class).parametersIndex() + 1))); + } + + /** + * Returns the suffix of the test given the {@code testParameterValues} that will be appended to + * the test name inside bracket, e.g. "testname[suffix]". + */ + private static String getTestNameSuffix(List testParameterValues) { + return testParameterValues.stream() + .map( + value -> { + Class annotationType = + value.annotationTypeOrigin().annotationType(); + String namePattern = + annotationType.getAnnotation(TestParameterAnnotation.class).name(); + if (value.paramName().isPresent() + && value.paramClass().isPresent() + && namePattern.equals("{0}") + && Primitives.unwrap(value.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", value.paramName().get(), value.value()); + } else { + return MessageFormat.format(namePattern, value.value()); + } + }) + .map(string -> string.trim().replaceAll("\\s+", " ")) + .collect(joining(",")); + } + + /** + * Appends the given suffix to the given test name in brackets. If the original test name already + * has brackets, the suffix is inserted in the existing brackets instead. + */ + private static String appendParametersToTestName(String originalTestName, String testNameSuffix) { + if (originalTestName.endsWith("]")) { + return String.format( + "%s,%s]", originalTestName.substring(0, originalTestName.length() - 1), testNameSuffix); + } else { + return String.format("%s[%s]", originalTestName, testNameSuffix); + } + } + + private List> getParameterValuesForMethod(Method method) { + try { + return parameterValuesCache.get( + method, + () -> { + List> 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 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 = testClass.getJavaClass().getMethods()[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> getAnnotationValuesForUsedAnnotationTypes( + Class testClass, Method method) { + ImmutableList 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 annotationComparator( + Annotation[][] parameterAnnotations) { + ImmutableList 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}. + * + *

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 removeOverrides( + List 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 removeOverrides( + List 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. + * + *

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> getAnnotationFromParametersOrTestOrClass( + AnnotationTypeOrigin annotationTypeOrigin, Method method, Class testClass) { + Origin origin = annotationTypeOrigin.origin(); + Class annotationType = annotationTypeOrigin.annotationType(); + if (origin == Origin.CONSTRUCTOR_PARAMETER) { + Constructor constructor = getOnlyConstructor(testClass); + List annotations = + getAnnotationWithMetadataListWithType(constructor.getParameters(), 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 annotations = + getAnnotationWithMetadataListWithType(method.getParameters(), 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 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> toTestParameterValueList( + List annotationWithMetadatas, Origin origin) { + return annotationWithMetadatas.stream() + .map(annotationWithMetadata -> TestParameterValue.create(annotationWithMetadata, origin)) + .collect(toImmutableList()); + } + + // Parameter is not available on old Android SDKs, and isn't desugared. Many (most?) Android tests + // will run against a more recent Java SDK, so this will work fine. If it proves problematic for + // users trying to run, say, emulator tests, it would be possible to just not provide parameter + // names on Android. + @SuppressWarnings("AndroidJdkLibsChecker") + private static ImmutableList getAnnotationWithMetadataListWithType( + Parameter[] parameters, Class annotationType) { + return stream(parameters) + .map( + parameter -> { + Annotation annotation = parameter.getAnnotation(annotationType); + return annotation == null + ? null + : AnnotationWithMetadata.withMetadata( + annotation, parameter.getType(), parameter.getName()); + }) + .filter(Objects::nonNull) + .filter( + annotationWithMetadata -> + annotationWithMetadata.annotation().annotationType().equals(annotationType)) + .collect(toImmutableList()); + } + + private ImmutableList getAnnotationListWithType( + Annotation[][] parameterAnnotations, Class annotationType) { + return stream(parameterAnnotations) + .flatMap(Stream::of) + .filter(annotation -> annotation.annotationType().equals(annotationType)) + .collect(toImmutableList()); + } + + private ImmutableList getAnnotationListWithType( + Annotation[] annotations, Class 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 createTest( + TestClass testClass, FrameworkMethod method, Optional test) { + TestIndexHolder testIndexHolder = method.getAnnotation(TestIndexHolder.class); + if (testIndexHolder == null) { + return test; + } + try { + List 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> processedAnnotationTypes = new ArrayList<>(); + List 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 testParameterValuesForFieldInjection = + filterByOrigin(testParameterValues, Origin.CLASS, Origin.FIELD, Origin.METHOD); + // The annotationType corresponding to the annotationIndex, e.g ColorParameter.class + // in the example above. + List 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 filterByOrigin( + List testParameterValues, Origin... origins) { + Set 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 filterAnnotationTypeOriginsByOrigin( + List annotationTypeOrigins, Origin... origins) { + List 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 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 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> 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 testParameterValues, + Class methodParameterType, + Annotation[] parameterAnnotations, + List> processedAnnotationTypes) { + List> 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 annotationType = + testParameterValue.annotationTypeOrigin().annotationType(); + if (parameterAnnotation.annotationType().equals(annotationType)) { + // If multiple annotations exist, ensure that the proper one is selected. + // For instance, for: + // + // test(@FooParameter(1,2) Foo foo, @FooParameter(3,4) Foo bar) {} + // + // 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 the {@code testClass.getMethods()} */ + 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 annotationType, Object annotationValue) { + TestParameterAnnotation annotation = + annotationType.getAnnotation(TestParameterAnnotation.class); + Class 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 annotationType, Object annotationValue) { + TestParameterAnnotation annotation = + annotationType.getAnnotation(TestParameterAnnotation.class); + Class 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 annotationType, List testParameterValues) { + TestParameterAnnotation annotation = + annotationType.getAnnotation(TestParameterAnnotation.class); + Class 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 testParameterValues; + private final Set valueList; + + public ValidatorContext(List testParameterValues) { + this.testParameterValues = testParameterValues; + this.valueList = testParameterValues.stream().map(TestParameterValue::value).collect(toSet()); + } + + @Override + public boolean has(Class testParameter, Object value) { + return getValue(testParameter).transform(value::equals).or(false); + } + + @Override + public , U extends Enum> boolean has(T value1, U value2) { + return valueList.contains(value1) && valueList.contains(value2); + } + + @Override + public Optional getValue(Class testParameter) { + return getParameter(testParameter).transform(TestParameterValue::value); + } + + @Override + public List getSpecifiedValues(Class testParameter) { + return getParameter(testParameter) + .transform(TestParameterValue::specifiedValues) + .or(ImmutableList.of()); + } + + private Optional getParameter(Class 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 annotationType, Optional> paramClass) { + TestParameterAnnotation testParameter = + annotationType.getAnnotation(TestParameterAnnotation.class); + Class 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> getTestParameterAnnotations( + List annotationTypeOrigins, + final Class testClass, + AnnotatedElement methodOrConstructor) { + return annotationTypeOrigins.stream() + .map(AnnotationTypeOrigin::annotationType) + .filter( + annotationType -> + testClass.isAnnotationPresent(annotationType) + || methodOrConstructor.isAnnotationPresent(annotationType)) + .collect(toImmutableList()); + } + + private static Stream> streamWithParents(Class clazz) { + Stream.Builder> 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 Collector> toImmutableList() { + return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf); + } + + private static Collector> 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..44aceaa --- /dev/null +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.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.collect.ImmutableList; +import java.util.List; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.InitializationError; +import org.junit.runners.model.Statement; + +/** + * 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 getInnerTestRules() { + return ImmutableList.of(new TestNamePrinterRule()); + } + + @Override + protected List createTestMethodProcessorList() { + return TestMethodProcessors.createNewParameterizedProcessorsWithLegacyFeatures(getTestClass()); + } + + /** A {@link TestRule} that prints the current test name before and after the test. */ + private static final class TestNamePrinterRule implements TestRule { + + @Override + public Statement apply(final Statement originalStatement, final Description testDescription) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + String testName = + testDescription.getTestClass().getSimpleName() + + "." + + testDescription.getMethodName(); + System.out.println("\n\nBeginning test: " + testName); + try { + originalStatement.evaluate(); + } finally { + System.out.println("\nEnd of test: " + testName); + } + } + }; + } + } +} 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..1be53d0 --- /dev/null +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterProcessor.java @@ -0,0 +1,32 @@ +/* + * 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. + * + *

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. + */ +/* copybara:strip_begin(advanced usage) */ public /* copybara:strip_end */ +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..2f9b5c7 --- /dev/null +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java @@ -0,0 +1,69 @@ +/* + * 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. + */ +/* copybara:strip_begin(advanced usage) */ public /* copybara:strip_end */ +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 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. + */ + , U extends Enum> boolean has(T value1, U value2); + + /** + * Returns all the current test value for a given {@link TestParameterAnnotation} annotated + * annotation. + */ + Optional getValue(Class testParameter); + + /** + * Returns all the values specified for a given {@link TestParameterAnnotation} annotated + * annotation in the test. + * + *

For example, if the test annotates '@Foo(a,b,c)', getSpecifiedValues(Foo.class) will + * return [a,b,c]. + */ + List getSpecifiedValues(Class testParameter); + } + + /** + * Returns whether the test should be skipped based on the annotations' values. + * + *

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

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..40a8b47 --- /dev/null +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java @@ -0,0 +1,53 @@ +/* + * 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. + */ +/* copybara:strip_begin(advanced usage) */ public /* copybara:strip_end */ +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 provideValues(Annotation annotation, Optional> 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 annotationType, Optional> 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..5b7767d --- /dev/null +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java @@ -0,0 +1,28 @@ +/* + * 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. */ +/* copybara:strip_begin(advanced usage) */ public /* copybara:strip_end */ +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 getValue(Class 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..98907a7 --- /dev/null +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java @@ -0,0 +1,212 @@ +/* + * 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. + * + *

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

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

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

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: + * + *

    + *
  • YAML primitives: + *
      + *
    • String: Specified as YAML string + *
    • boolean: Specified as YAML boolean + *
    • long and int: Specified as YAML integer + *
    • float and double: Specified as YAML floating point or integer + *
    + *
  • + *
  • Parsed types: + *
      + *
    • Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()} + *
    • Protobuf messages: Specified as a YAML mapping or as textproto string: + *
        + *
      • As YAML mapping: The mapping keys are the proto field names and their values + * are parsed in the same way as the parameter values + *
      • Textproto string: A YAML string formatted according to go/textformat-spec + *
      + *
    + *
  • + *
+ * + *

For dynamic sets of parameters or parameter types that are not supported here, use {@link + * #valuesProvider()} and leave this field empty. + * + *

Examples

+ * + *
+   * {@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) { ... }
+   * 
+ */ + 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. + * + *

If this field is set, {@link #value()} must be empty and vice versa. + * + *

Example

+ * + *
+   * {@literal @}Test
+   * {@literal @}TestParameters(valuesProvider = IsAdultValueProvider.class)
+   * public void personIsAdult(int age, boolean expectIsAdult) { ... }
+   *
+   * private static final class IsAdultValueProvider implements TestParametersValuesProvider {
+   *   {@literal @}Override public List 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()
+   *     );
+   *   }
+   * }
+   * 
+ */ + Class valuesProvider() default + DefaultTestParametersValuesProvider.class; + + /** Interface for custom providers of test parameter values. */ + interface TestParametersValuesProvider { + List 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. + * + *

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 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 parametersMap = new LinkedHashMap<>(); + + /** + * Sets a name for this set of parameters that will be used for describing this test. + * + *

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; + 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 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 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..0334c6e --- /dev/null +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java @@ -0,0 +1,413 @@ +/* + * 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.TestParameters.DefaultTestParametersValuesProvider; +import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValues; +import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValuesProvider; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.List; +import java.util.Map; +import java.util.stream.Collector; +import java.util.stream.Collectors; +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> + 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 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 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 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 testInfos = ImmutableList.builder(); + + ImmutableList> constructorParametersList = + getConstructorParametersOrSingleAbsentElement(); + ImmutableList> methodParametersList = + getMethodParametersOrSingleAbsentElement(originalTest.getMethod()); + for (int constructorParametersIndex = 0; + constructorParametersIndex < constructorParametersList.size(); + ++constructorParametersIndex) { + Optional constructorParameters = + constructorParametersList.get(constructorParametersIndex); + + for (int methodParametersIndex = 0; + methodParametersIndex < methodParametersList.size(); + ++methodParametersIndex) { + Optional methodParameters = + methodParametersList.get(methodParametersIndex); + testInfos.add( + TestInfo.create( + originalTest.getMethod(), + getTestName(originalTest, constructorParameters, methodParameters), + new ImmutableList.Builder() + .addAll(originalTest.getAnnotations()) + .add( + TestIndexHolderFactory.create( + constructorParametersIndex, methodParametersIndex)) + .build())); + } + } + return TestInfo.shortenNamesIfNecessary( + testInfos.build(), + testInfo -> { + TestIndexHolder annotation = testInfo.getAnnotation(TestIndexHolder.class); + return maybeAppendToTestName( + maybeAppendToTestName( + originalTest.getName(), + maybeParameterIndexString( + annotation.constructorParametersIndex(), constructorParametersList)), + maybeParameterIndexString(annotation.methodParametersIndex(), methodParametersList)); + }); + } + + private static Optional maybeParameterIndexString( + int index, ImmutableList> parameterList) { + return parameterList.get(index).transform(p -> String.valueOf(index + 1)); + } + + private ImmutableList> + getConstructorParametersOrSingleAbsentElement() { + return testClass.getOnlyConstructor().isAnnotationPresent(TestParameters.class) + ? getConstructorParameters().stream().map(Optional::of).collect(toImmutableList()) + : ImmutableList.of(Optional.absent()); + } + + private ImmutableList> getMethodParametersOrSingleAbsentElement( + Method method) { + return method.isAnnotationPresent(TestParameters.class) + ? getMethodParameters(method).stream().map(Optional::of).collect(toImmutableList()) + : ImmutableList.of(Optional.absent()); + } + + private static String getTestName( + TestInfo originalTest, + Optional constructorParameters, + Optional methodParameters) { + return maybeAppendParametersToTestName( + maybeAppendParametersToTestName(originalTest.getName(), constructorParameters), + methodParameters); + } + + private static String maybeAppendParametersToTestName( + String originalTestName, Optional parameters) { + return maybeAppendToTestName( + originalTestName, parameters.transform(p -> p.name().replaceAll("\\s+", " "))); + } + + private static String maybeAppendToTestName( + String originalTestName, Optional maybeSuffix) { + if (!maybeSuffix.isPresent()) { + return originalTestName; + } else { + String suffixName = maybeSuffix.get(); + if (originalTestName.endsWith("]")) { + return String.format( + "%s,%s]", originalTestName.substring(0, originalTestName.length() - 1), suffixName); + } else { + return String.format("%s[%s]", originalTestName, suffixName); + } + } + } + + @Override + public Statement processStatement(Statement originalStatement, Description finalTestDescription) { + return originalStatement; + } + + @Override + public Optional createTest( + TestClass testClass, FrameworkMethod method, Optional test) { + if (testClass.getOnlyConstructor().isAnnotationPresent(TestParameters.class)) { + ImmutableList 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 createStatement( + TestClass testClass, + FrameworkMethod method, + Object testObject, + Optional statement) { + if (method.getMethod().isAnnotationPresent(TestParameters.class)) { + ImmutableList 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 getConstructorParameters() { + return parameterValuesByConstructorOrMethodCache.getUnchecked(testClass.getOnlyConstructor()); + } + + private ImmutableList getMethodParameters(Method method) { + return parameterValuesByConstructorOrMethodCache.getUnchecked(method); + } + + private static ImmutableList 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 parametersList = ImmutableList.copyOf(invokableParameters); + checkState( + parametersList.stream().allMatch(Parameter::isNamePresent), + "" + + "Parameter name is not present for method or constructor: %s. Please ensure that" + + " this test was built with the -parameters compiler option", + methodOrConstructor); + if (valueIsSet) { + return stream(annotation.value()) + .map(yamlMap -> toParameterValues(yamlMap, parametersList)) + .collect(toImmutableList()); + } else { + return toParameterValuesList(annotation.valuesProvider(), parametersList); + } + } + + private static ImmutableList toParameterValuesList( + Class valuesProvider, List parameters) { + try { + Constructor constructor = + valuesProvider.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance().provideValues().stream() + .peek(values -> validateThatValuesMatchParameters(values, parameters)) + .collect(toImmutableList()); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + } + + private static void validateThatValuesMatchParameters( + TestParametersValues testParametersValues, List parameters) { + ImmutableMap 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 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 yamlMap = (Map) yamlMapObject; + + ImmutableMap 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 Collector> 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..9d412ad --- /dev/null +++ b/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java @@ -0,0 +1,112 @@ +/* + * 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 org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class ParameterValueParsingTest { + + @Test + public void parseEnum_success() throws Exception { + Enum result = ParameterValueParsing.parseEnum("BBB", TestEnum.class); + + assertThat(result).isEqualTo(TestEnum.BBB); + } + + @Test + @TestParameters({ + "{yamlString: '{a: b, c: 15}', valid: true}", + "{yamlString: '{a: b c: 15', valid: false}", + "{yamlString: 'a: b c: 15', valid: false}", + }) + public void isValidYamlString_success(String yamlString, boolean valid) throws Exception { + boolean result = ParameterValueParsing.isValidYamlString(yamlString); + + assertThat(result).isEqualTo(valid); + } + + enum ParseYamlValueToJavaTypeCases { + STRING_TO_STRING( + /* yamlString= */ "abc", /* javaClass= */ String.class, /* expectedResult= */ "abc"), + BOOLEAN_TO_STRING( + /* yamlString= */ "true", /* javaClass= */ String.class, /* expectedResult= */ "true"), + INT_TO_STRING( + /* yamlString= */ "123", /* javaClass= */ String.class, /* expectedResult= */ "123"), + LONG_TO_STRING( + /* yamlString= */ "442147483648", + /* javaClass= */ String.class, + /* expectedResult= */ "442147483648"), + 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); + + final String yamlString; + final Class javaClass; + final Object expectedResult; + + ParseYamlValueToJavaTypeCases(String yamlString, Class javaClass, Object expectedResult) { + this.yamlString = yamlString; + this.javaClass = javaClass; + this.expectedResult = expectedResult; + } + } + + @Test + public void parseYamlStringToJavaType_success( + @TestParameter ParseYamlValueToJavaTypeCases parseYamlValueToJavaTypeCases) throws Exception { + Object result = + ParameterValueParsing.parseYamlStringToJavaType( + parseYamlValueToJavaTypeCases.yamlString, parseYamlValueToJavaTypeCases.javaClass); + + assertThat(result).isEqualTo(parseYamlValueToJavaTypeCases.expectedResult); + } + + private enum TestEnum { + AAA, + BBB; + } +} diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java new file mode 100644 index 0000000..686b152 --- /dev/null +++ b/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.MethodRule; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.Statement; + +@RunWith(JUnit4.class) +public class PluggableTestRunnerTest { + + private static int ruleInvocationCount = 0; + + public static class TestAndMethodRule implements MethodRule, TestRule { + + @Override + public Statement apply(Statement base, Description description) { + ruleInvocationCount++; + return base; + } + + @Override + public Statement apply(Statement base, FrameworkMethod method, Object target) { + ruleInvocationCount++; + return base; + } + } + + @RunWith(PluggableTestRunner.class) + public static class PluggableTestRunnerTestClass { + + @Rule public TestAndMethodRule rule = new TestAndMethodRule(); + + @Test + public void test() { + // no-op + } + } + + @Test + public void ruleThatIsBothTestRuleAndMethodRuleIsInvokedOnceOnly() throws Exception { + PluggableTestRunner.run( + new PluggableTestRunner(PluggableTestRunnerTestClass.class) { + @Override + protected List createTestMethodProcessorList() { + return ImmutableList.of(); + } + }); + + assertThat(ruleInvocationCount).isEqualTo(1); + } +} 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..a9142ea --- /dev/null +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java @@ -0,0 +1,982 @@ +/* + * 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 static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +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.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. + */ +// TODO(sergebeauchamp): Test error handling edge cases. +@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 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 MultipleAllEnumValueseAnnotationClass { + + private static List 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 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 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 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> 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 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 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 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]", "test1[2]"); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class DuplicateFieldAnnotationTestClass { + + private static List 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 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 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 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 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()); + } + } + + @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()); + } + + @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]"); + } + } + + @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 beforeCalls = new ArrayList<>(); + static List 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 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 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 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 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 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 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 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 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); + } + } + + 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>> 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> 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 > void assertAllEnumsAreIncluded(Enum[] values) { + Set> enumSet = new HashSet<>(Arrays.asList(values)); + for (List 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 parameters() { + return Arrays.stream(TestParameterAnnotationMethodProcessorTest.class.getClasses()) + .filter(cls -> cls.isAnnotationPresent(ClassTestResult.class)) + .map( + cls -> + new Object[] { + cls.getSimpleName(), cls, cls.getAnnotation(ClassTestResult.class).value() + }) + .collect(toImmutableList()); + } + + private final Class testClass; + private final Result result; + + public TestParameterAnnotationMethodProcessorTest( + String name, Class testClass, Result result) { + this.testClass = testClass; + this.result = result; + } + + @Test + public void test() throws Exception { + List 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 processor) throws Exception { + return new PluggableTestRunner(testClass) { + @Override + protected List 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..e6649c4 --- /dev/null +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java @@ -0,0 +1,205 @@ +/* + * 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 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 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 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 TestEnum enumParameterB) { + testedParameters.add(String.format("%s:%s", enumParameterA, enumParameterB)); + } + + @AfterClass + public static void completedAllParameterizedTests() { + assertThat(testedParameters) + .containsExactly( + "ONE:ONE", + "ONE:TWO", + "ONE:THREE", + "TWO:ONE", + "TWO:TWO", + "TWO:THREE", + "THREE:ONE", + "THREE:TWO", + "THREE:THREE"); + } + } + + @RunAsTest + public static class WithValuesProvider { + private static List 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 provideValues() { + return newArrayList("A", "B", null); + } + } + + private static final class CharMatcherProvider implements TestParameterValuesProvider { + @Override + public List provideValues() { + return newArrayList(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace()); + } + } + } + + @Parameters(name = "{0}") + public static Collection 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 failures = + PluggableTestRunner.run( + new PluggableTestRunner(testClass) { + @Override + protected List 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..3e583cf --- /dev/null +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java @@ -0,0 +1,465 @@ +/* + * 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.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) +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 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}", + }) + 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." + + " =================================================================================" + + "=============='}" + }) + 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: []}", + }) + public void test3_withRepeatedParams( + List testEnums, + List testLongs, + List testBooleans, + List 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]", + "ABC", + "test2_withLongNames[2]", + "This is a very long string (240 characters) that would normally cause Sponge+Tin to" + + " exceed the filename limit of 255 characters." + + " =================================================================================" + + "==============", + "test3_withRepeatedParams[{testEnums: [ONE, TWO, THREE], testLongs: [11, 4]," + + " testBooleans: [false, true], testStrings: [ABC, '123']}]", + "[ONE, TWO, THREE],[11, 4],[false, true],[ABC, 123]", + "test3_withRepeatedParams[{testEnums: [TWO], testLongs: [22], testBooleans: [true]," + + " testStrings: ['DEF']}]", + "[TWO],[22],[true],[DEF]", + "test3_withRepeatedParams[{testEnums: [], testLongs: [], testBooleans: []," + + " testStrings: []}]", + "[],[],[],[]"); + } + } + + @RunAsTest + public static class SimpleConstructorAnnotation { + + @Rule public TestName testName = new TestName(); + + private static Map 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 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 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 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 allTestNames; + private static List testNamesThatInvokedBefore; + private static List 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[1,1]", + "test3_withLongNames[1,2]", + "test3_withLongNames[2,1]", + "test3_withLongNames[2,2]"); + + assertThat(testNamesThatInvokedBefore).containsExactlyElementsIn(allTestNames).inOrder(); + assertThat(testNamesThatInvokedAfter).containsExactlyElementsIn(allTestNames).inOrder(); + } + } + + @RunAsTest + public static class MixedWithTestParameterFieldAnnotation { + @Rule public TestName testName = new TestName(); + + private static List 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}"}) + 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 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 failures = PluggableTestRunner.run(newTestRunner()); + assertThat(failures).isEmpty(); + } + + private PluggableTestRunner newTestRunner() throws Exception { + return new PluggableTestRunner(testClass) { + @Override + protected List createTestMethodProcessorList() { + return TestMethodProcessors.createNewParameterizedProcessorsWithLegacyFeatures( + getTestClass()); + } + }; + } +} -- cgit v1.2.3 From 7a05459bef7e51d90564e22e408fb5744475616a Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Thu, 25 Feb 2021 13:39:51 +0000 Subject: Remove redundant compiler arguments --- pom.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pom.xml b/pom.xml index 43cecb7..36fa5a7 100644 --- a/pom.xml +++ b/pom.xml @@ -164,9 +164,6 @@ 1.8 1.8 true - - -parameters - -- cgit v1.2.3 From 156aef001dec74a2f863bd1158e1c40d76072dbc Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Thu, 25 Feb 2021 16:59:21 +0000 Subject: Remove google-internal code --- .../testing/junit/testparameterinjector/TestMethodProcessor.java | 1 - .../google/testing/junit/testparameterinjector/TestParameter.java | 7 ------- .../TestParameterAnnotationMethodProcessor.java | 1 - .../junit/testparameterinjector/TestParameterProcessor.java | 1 - .../junit/testparameterinjector/TestParameterValidator.java | 1 - .../junit/testparameterinjector/TestParameterValueProvider.java | 1 - .../testing/junit/testparameterinjector/TestParameterValues.java | 1 - .../google/testing/junit/testparameterinjector/TestParameters.java | 6 ------ 8 files changed, 19 deletions(-) diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java index 34996be..880327f 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java @@ -28,7 +28,6 @@ import org.junit.runners.model.TestClass; *

Note: Implementations of this interface are expected to be immutable, i.e. they no longer * change after construction. */ -/* copybara:strip_begin(advanced usage) */ public /* copybara:strip_end */ interface TestMethodProcessor { /** Allows to transform the test information (name and annotations). */ diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java index 0325d4d..a79b8f5 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java @@ -78,13 +78,6 @@ public @interface TestParameter { *

  • long and int: Specified as YAML integer *
  • float and double: Specified as YAML floating point or integer *
  • Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()} - *
  • Protobuf messages: Specified as a YAML mapping or as textproto string: - *
      - *
    • As YAML mapping: The mapping keys are the proto field names and their values are - * parsed in the same way as the parameter values - *
    • Textproto string: Formatted according to go/textformat-spec - *
    - *
  • * * *

    For dynamic sets of parameters or parameter types that are not supported here, use {@link diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index c268e3d..ab37238 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -66,7 +66,6 @@ import org.junit.runners.model.TestClass; * * @see TestParameterAnnotation */ -/* copybara:strip_begin(advanced usage) */ public /* copybara:strip_end */ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { /** diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterProcessor.java index 1be53d0..efa4951 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterProcessor.java @@ -22,7 +22,6 @@ package com.google.testing.junit.testparameterinjector; * declaration order, starting with annotations defined at the class, field, method, and finally * parameter level. */ -/* copybara:strip_begin(advanced usage) */ public /* copybara:strip_end */ interface TestParameterProcessor { /** Executes code in the context of a running test statement before the statement starts. */ void before(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 index 2f9b5c7..3733833 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java @@ -22,7 +22,6 @@ 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. */ -/* copybara:strip_begin(advanced usage) */ public /* copybara:strip_end */ interface TestParameterValidator { /** diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java index 40a8b47..6c398aa 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java @@ -22,7 +22,6 @@ import java.util.Optional; * Interface which allows {@link TestParameterAnnotation} annotations to provide the values to test * in a dynamic way. */ -/* copybara:strip_begin(advanced usage) */ public /* copybara:strip_end */ interface TestParameterValueProvider { /** diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java index 5b7767d..5207ec6 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java @@ -18,7 +18,6 @@ import com.google.common.base.Optional; import java.lang.annotation.Annotation; /** Interface to retrieve the {@link TestParameterAnnotation} values for a test. */ -/* copybara:strip_begin(advanced usage) */ public /* copybara:strip_end */ interface TestParameterValues { /** * Returns a {@link TestParameterAnnotation} value for the current test as specified by {@code diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java index 98907a7..723aa7e 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java @@ -66,12 +66,6 @@ public @interface TestParameters { *

  • Parsed types: *
      *
    • Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()} - *
    • Protobuf messages: Specified as a YAML mapping or as textproto string: - *
        - *
      • As YAML mapping: The mapping keys are the proto field names and their values - * are parsed in the same way as the parameter values - *
      • Textproto string: A YAML string formatted according to go/textformat-spec - *
      *
    *
  • * -- cgit v1.2.3 From 077e0cf55e25ab169f963096681ebec1fb18727f Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 26 Feb 2021 08:21:15 +0000 Subject: Replace obsolete sonatype parent by --- pom.xml | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 36fa5a7..13c98a8 100644 --- a/pom.xml +++ b/pom.xml @@ -19,11 +19,6 @@ 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"> 4.0.0 - - org.sonatype.oss - oss-parent - 7 - com.google.testparameterinjector test-parameter-injector @@ -54,7 +49,7 @@ - jnyman + nymanjens Jens Nyman jnyman@google.com Google Inc. @@ -88,6 +83,18 @@ GitHub Issues http://github.com/google/testparameterinjector/issues + + + sonatype-nexus-snapshots + Sonatype Nexus Snapshots + https://oss.sonatype.org/content/repositories/snapshots/ + + + sonatype-nexus-staging + Nexus Release Repository + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + 3.0.3 @@ -176,4 +183,54 @@ + + + + sonatype-oss-release + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.2.0 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.1 + + + sign-artifacts + verify + + sign + + + + + + + + -- cgit v1.2.3 From e1195ac6eb7ba83242938deea2fac3ced4bb3e73 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 26 Feb 2021 08:22:49 +0000 Subject: Change version on the main branch to HEAD-SNAPSHOT. Relase branches will get the actual version numbers --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 13c98a8..d656a6a 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ com.google.testparameterinjector test-parameter-injector - 1.0-SNAPSHOT + HEAD-SNAPSHOT TestParameterInjector -- cgit v1.2.3 From 7eadbc8e083557915523862cf14ec63fca9a41f0 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 26 Feb 2021 08:30:00 +0000 Subject: Remove AutoValue from the runtime dependencies. See https://github.com/google/auto/blob/master/value/userguide/index.md#with-maven for more info --- pom.xml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index d656a6a..8d8dc16 100644 --- a/pom.xml +++ b/pom.xml @@ -110,11 +110,7 @@ com.google.auto.value auto-value-annotations 1.7.4 - - - com.google.auto.value - auto-value - 1.7.4 + provided com.google.code.findbugs @@ -170,7 +166,14 @@ 1.8 1.8 1.8 - true + true + + + com.google.auto.value + auto-value + 1.7.4 + + -- cgit v1.2.3 From 57cdf6bb8d426bc2ffb7fb738737d4b1ac8ef7dd Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 26 Feb 2021 08:35:51 +0000 Subject: Change protobuf-java-util to protobuf-java, which has all we need --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8d8dc16..ac62649 100644 --- a/pom.xml +++ b/pom.xml @@ -124,7 +124,7 @@ com.google.protobuf - protobuf-java-util + protobuf-java 3.14.0 -- cgit v1.2.3 From ee4a1992e8a5a1ee9046c57038fef40af351ffc7 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 26 Feb 2021 09:00:38 +0000 Subject: Add ajurkowski as developer to .pom file. --- pom.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pom.xml b/pom.xml index ac62649..96877b7 100644 --- a/pom.xml +++ b/pom.xml @@ -71,6 +71,17 @@ +0 + + ajurkowski + Alex Jurkowski + ajurkowski@google.com + Google Inc. + http://www.google.com/ + + developer + + -6 + -- cgit v1.2.3 From 7064f2c2bbfacd9a173d4206fc53499dec08c4a2 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 26 Feb 2021 09:12:11 +0000 Subject: Remove obsolete TODO --- .../TestParameterAnnotationMethodProcessorTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java index a9142ea..45095de 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java @@ -47,7 +47,6 @@ import org.junit.runners.model.TestClass; * Test class to test the PluggableTestRunner test harness works with {@link * TestParameterAnnotation}s. */ -// TODO(sergebeauchamp): Test error handling edge cases. @RunWith(Parameterized.class) public class TestParameterAnnotationMethodProcessorTest { -- cgit v1.2.3 From fb57734f3974053ecb7788bf2344404ea32630b9 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 26 Feb 2021 10:04:49 +0000 Subject: Fix maven-javadoc-plugin error: Specify the java version And remove a redundant plugin --- pom.xml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 96877b7..3ba9863 100644 --- a/pom.xml +++ b/pom.xml @@ -161,10 +161,6 @@ - - maven-javadoc-plugin - 3.2.0 - maven-jar-plugin 3.2.0 @@ -219,6 +215,9 @@ org.apache.maven.plugins maven-javadoc-plugin + + 8 + 3.2.0 -- cgit v1.2.3 From 41f4eb841c8e265abbe4ad29751ac3f423a3c7f1 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 26 Feb 2021 10:26:12 +0000 Subject: Fix javadoc parse errors --- .../google/testing/junit/testparameterinjector/TestParameter.java | 6 +++--- .../google/testing/junit/testparameterinjector/TestParameters.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java index a79b8f5..14d1daf 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java @@ -92,19 +92,19 @@ public @interface TestParameter { * *

    If this field is set, {@link #value()} must be empty and vice versa. * - *

    Example

    + *

    Example * *

        * {@literal @}Test
        * public void matchesAllOf_throwsOnNull(
        *     {@literal @}TestParameter(valuesProvider = CharMatcherProvider.class)
        *         CharMatcher charMatcher) {
    -   *   assertThrows(NullPointerException.class, () -> charMatcher.matchesAllOf(null));
    +   *   assertThrows(NullPointerException.class, () -> charMatcher.matchesAllOf(null));
        * }
        *
        * private static final class CharMatcherProvider implements TestParameterValuesProvider {
        *   {@literal @}Override
    -   *   public List provideValues() {
    +   *   public {@literal List} provideValues() {
        *     return ImmutableList.of(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace());
        *   }
        * }
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java
    index 723aa7e..355997e 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java
    @@ -73,7 +73,7 @@ public @interface TestParameters {
        * 

    For dynamic sets of parameters or parameter types that are not supported here, use {@link * #valuesProvider()} and leave this field empty. * - *

    Examples

    + *

    Examples * *

        * {@literal @}Test
    @@ -99,7 +99,7 @@ public @interface TestParameters {
        *
        * 

    If this field is set, {@link #value()} must be empty and vice versa. * - *

    Example

    + *

    Example * *

        * {@literal @}Test
    @@ -107,7 +107,7 @@ public @interface TestParameters {
        * public void personIsAdult(int age, boolean expectIsAdult) { ... }
        *
        * private static final class IsAdultValueProvider implements TestParametersValuesProvider {
    -   *   {@literal @}Override public List provideValues() {
    +   *   {@literal @}Override public {@literal List} provideValues() {
        *     return ImmutableList.of(
        *       TestParametersValues.builder()
        *         .name("teenager")
    -- 
    cgit v1.2.3
    
    
    From 99e892ac628e7c02796b6e69b0e8c33e5dfe0ab4 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Fri, 26 Feb 2021 20:10:06 +0000
    Subject: Remove scope:provided from AutoValue dependency to avoid problems
     with some build tools
    
    ---
     pom.xml | 1 -
     1 file changed, 1 deletion(-)
    
    diff --git a/pom.xml b/pom.xml
    index 3ba9863..bc3e5be 100644
    --- a/pom.xml
    +++ b/pom.xml
    @@ -121,7 +121,6 @@
           com.google.auto.value
           auto-value-annotations
           1.7.4
    -      provided
         
         
           com.google.code.findbugs
    -- 
    cgit v1.2.3
    
    
    From aa553172ae5609c23aa926c108a5b5bd6e5690b5 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Wed, 3 Mar 2021 14:00:27 +0000
    Subject: README: Fix typo in package name
    
    ---
     README.md | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/README.md b/README.md
    index e5f303e..3224771 100644
    --- a/README.md
    +++ b/README.md
    @@ -11,8 +11,8 @@ promote high test coverage for data-driven tests.
     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;
    +import com.google.testing.junit.testparameterinjector.TestParameterInjector;
    +import com.google.testing.junit.testparameterinjector.TestParameter;
     
     @RunWith(TestParameterInjector.class)
     public class MyTest {
    -- 
    cgit v1.2.3
    
    
    From 7a50ffa385eda8d440aaa3589e30e08139fd6be5 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Wed, 3 Mar 2021 14:35:45 +0000
    Subject: Remove the test rule that prints the test name
    
    ---
     .../TestParameterInjector.java                     | 32 ----------------------
     1 file changed, 32 deletions(-)
    
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java
    index 44aceaa..dd6c63f 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java
    @@ -14,12 +14,8 @@
     
     package com.google.testing.junit.testparameterinjector;
     
    -import com.google.common.collect.ImmutableList;
     import java.util.List;
    -import org.junit.rules.TestRule;
    -import org.junit.runner.Description;
     import org.junit.runners.model.InitializationError;
    -import org.junit.runners.model.Statement;
     
     /**
      * A JUnit test runner which knows how to instantiate and run test classes where each test case may
    @@ -33,36 +29,8 @@ public final class TestParameterInjector extends PluggableTestRunner {
         super(testClass);
       }
     
    -  @Override
    -  protected List getInnerTestRules() {
    -    return ImmutableList.of(new TestNamePrinterRule());
    -  }
    -
       @Override
       protected List createTestMethodProcessorList() {
         return TestMethodProcessors.createNewParameterizedProcessorsWithLegacyFeatures(getTestClass());
       }
    -
    -  /** A {@link TestRule} that prints the current test name before and after the test. */
    -  private static final class TestNamePrinterRule implements TestRule {
    -
    -    @Override
    -    public Statement apply(final Statement originalStatement, final Description testDescription) {
    -      return new Statement() {
    -        @Override
    -        public void evaluate() throws Throwable {
    -          String testName =
    -              testDescription.getTestClass().getSimpleName()
    -                  + "."
    -                  + testDescription.getMethodName();
    -          System.out.println("\n\nBeginning test: " + testName);
    -          try {
    -            originalStatement.evaluate();
    -          } finally {
    -            System.out.println("\nEnd of test: " + testName);
    -          }
    -        }
    -      };
    -    }
    -  }
     }
    -- 
    cgit v1.2.3
    
    
    From 4f66547bb52429f7635cc88b2e40e39bf9d84bd9 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Fri, 5 Mar 2021 10:33:06 +0000
    Subject: Improve README: Add maven dependency and elaborate on introduction
    
    ---
     README.md | 24 ++++++++++++++++++++++++
     1 file changed, 24 insertions(+)
    
    diff --git a/README.md b/README.md
    index 3224771..842c060 100644
    --- a/README.md
    +++ b/README.md
    @@ -3,9 +3,18 @@ TestParameterInjector
     
     ## 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.
    +
     ## Getting started
     
     To start using `TestParameterInjector` right away, copy the following snippet:
    @@ -31,6 +40,21 @@ public class MyTest {
     }
     ```
     
    +And add the following dependency to your `.pom` file:
    +
    +```xml
    +
    +  com.google.testparameterinjector
    +  test-parameter-injector
    +  1.0-rc1
    +
    +```
    +
    +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
    -- 
    cgit v1.2.3
    
    
    From a39f340c55de3e589a2299f4b5be09b9ac9c11c5 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Fri, 5 Mar 2021 10:33:08 +0000
    Subject: Add Maven tip on how to add -parameters
    
    ---
     .../TestParametersMethodProcessor.java                      | 13 ++++++++++++-
     1 file changed, 12 insertions(+), 1 deletion(-)
    
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
    index 0334c6e..daf5a36 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
    @@ -290,7 +290,18 @@ class TestParametersMethodProcessor implements TestMethodProcessor {
             parametersList.stream().allMatch(Parameter::isNamePresent),
             ""
                 + "Parameter name is not present for method or constructor: %s.  Please ensure that"
    -            + " this test was built with the -parameters compiler option",
    +            + " this test was built with the -parameters compiler option.\n"
    +            + "\n"
    +            + "In Maven, you do this by adding true to the"
    +            + " maven-compiler-plugin's configuration. For example:\n"
    +            + "\n"
    +            + "\n"
    +            + "  maven-compiler-plugin\n"
    +            + "  3.8.1\n"
    +            + "  \n"
    +            + "    true\n"
    +            + "  \n"
    +            + "",
             methodOrConstructor);
         if (valueIsSet) {
           return stream(annotation.value())
    -- 
    cgit v1.2.3
    
    
    From 9df04afe1e6e49e91c0b12c7dbca6973c547bc82 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Tue, 9 Mar 2021 21:14:13 +0000
    Subject: Bump version to v1.0 in README.md
    
    ---
     README.md | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/README.md b/README.md
    index 842c060..06b2db5 100644
    --- a/README.md
    +++ b/README.md
    @@ -46,7 +46,7 @@ And add the following dependency to your `.pom` file:
     
       com.google.testparameterinjector
       test-parameter-injector
    -  1.0-rc1
    +  1.0
     
     ```
     
    -- 
    cgit v1.2.3
    
    
    From 98786b940357b29fd4ce5eaea47b91d954126d93 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Thu, 11 Mar 2021 09:35:57 +0000
    Subject: Fix typo in TestParameterAnnotationMethodProcessorTest
    
    ---
     .../TestParameterAnnotationMethodProcessorTest.java                     | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
    index 45095de..c30733b 100644
    --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
    +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
    @@ -106,7 +106,7 @@ public class TestParameterAnnotationMethodProcessorTest {
       }
     
       @ClassTestResult(Result.SUCCESS_ALWAYS)
    -  public static class MultipleAllEnumValueseAnnotationClass {
    +  public static class MultipleAllEnumValuesAnnotationClass {
     
         private static List testedParameters;
     
    -- 
    cgit v1.2.3
    
    
    From 5c32be62e07e877c5d413dee070c9f116182e3b3 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Thu, 11 Mar 2021 10:10:12 +0000
    Subject: Improve the instructions for exposing parameter names to the Java
     runtime:
    
    - Replace  by , because the former doesn't work sometimes
    - Include all tags from the top level, so it can be easily copy-pasted into a vanilla pom.xml
    - Mention that 'mvn clean' needs to be run
    ---
     .../TestParametersMethodProcessor.java             | 28 +++++++++++++++-------
     1 file changed, 19 insertions(+), 9 deletions(-)
    
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
    index daf5a36..e27b09b 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
    @@ -289,19 +289,29 @@ class TestParametersMethodProcessor implements TestMethodProcessor {
         checkState(
             parametersList.stream().allMatch(Parameter::isNamePresent),
             ""
    -            + "Parameter name is not present for method or constructor: %s.  Please ensure that"
    -            + " this test was built with the -parameters compiler option.\n"
    +            + "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 true to the"
                 + " maven-compiler-plugin's configuration. For example:\n"
                 + "\n"
    -            + "\n"
    -            + "  maven-compiler-plugin\n"
    -            + "  3.8.1\n"
    -            + "  \n"
    -            + "    true\n"
    -            + "  \n"
    -            + "",
    +            + "\n"
    +            + "  \n"
    +            + "    \n"
    +            + "      org.apache.maven.plugins\n"
    +            + "      maven-compiler-plugin\n"
    +            + "      3.8.1\n"
    +            + "      \n"
    +            + "        \n"
    +            + "          -parameters\n"
    +            + "        \n"
    +            + "      \n"
    +            + "    \n"
    +            + "  \n"
    +            + "\n"
    +            + "\n"
    +            + "Don't forget to run `mvn clean` after making this change.",
             methodOrConstructor);
         if (valueIsSet) {
           return stream(annotation.value())
    -- 
    cgit v1.2.3
    
    
    From fe5b4349ac613cb2887b75ae53040adf12c83624 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Mon, 29 Mar 2021 12:58:29 +0100
    Subject: Add support for ByteString and byte[]
    
    ---
     README.md                                          |  3 ++
     .../ParameterValueParsing.java                     | 42 ++++++++++++++--------
     .../junit/testparameterinjector/TestParameter.java |  2 ++
     .../testparameterinjector/TestParameters.java      |  2 ++
     .../ParameterValueParsingTest.java                 | 23 +++++++++++-
     .../testparameterinjector/TestParameterTest.java   | 28 +++++++++------
     6 files changed, 73 insertions(+), 27 deletions(-)
    
    diff --git a/README.md b/README.md
    index 06b2db5..e0dd333 100644
    --- a/README.md
    +++ b/README.md
    @@ -118,6 +118,9 @@ The following examples show most of the supported types. See the `@TestParameter
     @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;
     ```
     
     #### Multiple parameters: All combinations are run
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java
    index 5c94fb9..624ee9b 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java
    @@ -23,8 +23,10 @@ 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;
    @@ -68,10 +70,10 @@ final class ParameterValueParsing {
           return null;
         }
     
    -    YamlValueTransfomer yamlValueTransfomer =
    -        new YamlValueTransfomer(parsedYaml, javaType.getRawType());
    +    YamlValueTransformer yamlValueTransformer =
    +        new YamlValueTransformer(parsedYaml, javaType.getRawType());
     
    -    yamlValueTransfomer
    +    yamlValueTransformer
             .ifJavaType(String.class)
             .supportParsedType(String.class, identity())
             // Also support other primitives because it's easy to accidentally write e.g. a number when
    @@ -81,33 +83,33 @@ final class ParameterValueParsing {
             .supportParsedType(Long.class, Object::toString)
             .supportParsedType(Double.class, Object::toString);
     
    -    yamlValueTransfomer.ifJavaType(Boolean.class).supportParsedType(Boolean.class, identity());
    +    yamlValueTransformer.ifJavaType(Boolean.class).supportParsedType(Boolean.class, identity());
     
    -    yamlValueTransfomer.ifJavaType(Integer.class).supportParsedType(Integer.class, identity());
    +    yamlValueTransformer.ifJavaType(Integer.class).supportParsedType(Integer.class, identity());
     
    -    yamlValueTransfomer
    +    yamlValueTransformer
             .ifJavaType(Long.class)
             .supportParsedType(Long.class, identity())
             .supportParsedType(Integer.class, Integer::longValue);
     
    -    yamlValueTransfomer
    +    yamlValueTransformer
             .ifJavaType(Float.class)
             .supportParsedType(Float.class, identity())
             .supportParsedType(Double.class, Double::floatValue)
             .supportParsedType(Integer.class, Integer::floatValue);
     
    -    yamlValueTransfomer
    +    yamlValueTransformer
             .ifJavaType(Double.class)
             .supportParsedType(Double.class, identity())
             .supportParsedType(Integer.class, Integer::doubleValue)
             .supportParsedType(Long.class, Long::doubleValue);
     
    -    yamlValueTransfomer
    +    yamlValueTransformer
             .ifJavaType(Enum.class)
             .supportParsedType(
                 String.class, str -> ParameterValueParsing.parseEnum(str, javaType.getRawType()));
     
    -    yamlValueTransfomer
    +    yamlValueTransformer
             .ifJavaType(MessageLite.class)
             .supportParsedType(String.class, str -> parseTextprotoMessage(str, javaType.getRawType()))
             .supportParsedType(
    @@ -116,8 +118,18 @@ final class ParameterValueParsing {
                     getProtoValueParser()
                         .parseProtobufMessage((Map) 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
    -    yamlValueTransfomer
    +    yamlValueTransformer
             .ifJavaType(List.class)
             .supportParsedType(
                 List.class,
    @@ -127,7 +139,7 @@ final class ParameterValueParsing {
                         e ->
                             parseYamlObjectToJavaType(
                                 e, getGenericParameterType(javaType, /* parameterIndex= */ 0))));
    -    yamlValueTransfomer
    +    yamlValueTransformer
             .ifJavaType(Map.class)
             .supportParsedType(
                 Map.class,
    @@ -138,7 +150,7 @@ final class ParameterValueParsing {
                             parseYamlObjectToJavaType(
                                 v, getGenericParameterType(javaType, /* parameterIndex= */ 1))));
     
    -    return yamlValueTransfomer.transformedJavaValue();
    +    return yamlValueTransformer.transformedJavaValue();
       }
     
       private static TypeToken getGenericParameterType(TypeToken typeToken, int parameterIndex) {
    @@ -151,12 +163,12 @@ final class ParameterValueParsing {
         return TypeToken.of(parameterizedType.getActualTypeArguments()[parameterIndex]);
       }
     
    -  private static final class YamlValueTransfomer {
    +  private static final class YamlValueTransformer {
         private final Object parsedYaml;
         private final Class javaType;
         @Nullable private Object transformedJavaValue;
     
    -    YamlValueTransfomer(Object parsedYaml, Class javaType) {
    +    YamlValueTransformer(Object parsedYaml, Class javaType) {
           this.parsedYaml = parsedYaml;
           this.javaType = javaType;
         }
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
    index 14d1daf..f2ee847 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
    @@ -78,6 +78,8 @@ public @interface TestParameter {
        *   
  • long and int: Specified as YAML integer *
  • float and double: Specified as YAML floating point or integer *
  • Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()} + *
  • Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML bytes + * (example: "!!binary 'ZGF0YQ=='") * * *

    For dynamic sets of parameters or parameter types that are not supported here, use {@link diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java index 355997e..7e6bbc0 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java @@ -66,6 +66,8 @@ public @interface TestParameters { *

  • Parsed types: *
      *
    • Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()} + *
    • Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML + * bytes (example: "!!binary 'ZGF0YQ=='") *
    *
  • * diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java index 9d412ad..2f36632 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java @@ -16,6 +16,7 @@ package com.google.testing.junit.testparameterinjector; import static com.google.common.truth.Truth.assertThat; +import com.google.protobuf.ByteString; import org.junit.Test; import org.junit.runner.RunWith; @@ -82,7 +83,27 @@ public class ParameterValueParsingTest { STRING_TO_ENUM( /* yamlString= */ "AAA", /* javaClass= */ TestEnum.class, - /* expectedResult= */ TestEnum.AAA); + /* 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; diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java index e6649c4..b16d5e1 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java @@ -110,23 +110,29 @@ public class TestParameterTest { @Test public void test( - @TestParameter TestEnum enumParameterA, @TestParameter TestEnum enumParameterB) { - testedParameters.add(String.format("%s:%s", enumParameterA, enumParameterB)); + @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:ONE", - "ONE:TWO", - "ONE:THREE", - "TWO:ONE", - "TWO:TWO", - "TWO:THREE", - "THREE:ONE", - "THREE:TWO", - "THREE:THREE"); + "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"); } } -- cgit v1.2.3 From f02d67f39082abe6adefd7af93f1b28156a657b3 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Mon, 29 Mar 2021 13:08:34 +0100 Subject: Bump version to v1.1 in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e0dd333..6bcb5df 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ And add the following dependency to your `.pom` file: com.google.testparameterinjector test-parameter-injector - 1.0 + 1.1 ``` -- cgit v1.2.3 From dbbf883152a842dc3fbc8ffcb27e6ae8f25eb7f8 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 2 Apr 2021 18:35:18 +0100 Subject: Add link to blogpost to README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 6bcb5df..0ac029b 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ 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: -- cgit v1.2.3 From 7a62c43af8c4f10f1451ec84cd21c2ba509dae82 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 2 Apr 2021 18:35:29 +0100 Subject: Fix outdated references in javadoc --- .../junit/testparameterinjector/TestParameterAnnotation.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java index 7c06181..a859a4f 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java @@ -41,7 +41,7 @@ import java.util.Optional; * example: * *
    {@code
    - * @RunWith(ParameterizedTestRunner.class)
    + * @RunWith(TestParameterInjector.class)
      * public class ColorTest {
      *     @Retention(RUNTIME)
      *     @Target({TYPE, METHOD, FIELD})
    @@ -62,7 +62,7 @@ import java.util.Optional;
      * 

    An alternative is to use a method parameter for injection: * *

    {@code
    - * @RunWith(ParameterizedTestRunner.class)
    + * @RunWith(TestParameterInjector.class)
      * public class ColorTest {
      *     @Retention(RUNTIME)
      *     @Target({TYPE, METHOD, FIELD})
    @@ -84,7 +84,7 @@ import java.util.Optional;
      * same @TestParameterAnnotation annotation.
      *
      * 
    {@code
    - * @RunWith(ParameterizedTestRunner.class)
    + * @RunWith(TestParameterInjector.class)
      * public class ColorTest {
      *     @Retention(RUNTIME)
      *     @Target({TYPE, METHOD, FIELD})
    @@ -105,7 +105,7 @@ import java.util.Optional;
      * below:
      *
      * 
    {@code
    - * @RunWith(ParameterizedTestRunner.class)
    + * @RunWith(TestParameterInjector.class)
      * public class ColorTest {
      *     @Retention(RUNTIME)
      *     @Target({TYPE, METHOD, FIELD})
    -- 
    cgit v1.2.3
    
    
    From df0f254caba795afe8cbc4dad73996225b4eed72 Mon Sep 17 00:00:00 2001
    From: Jake Wharton 
    Date: Wed, 14 Apr 2021 17:25:26 -0400
    Subject: Add Parameter wrapper to support older Android versions
    
    ---
     .../testparameterinjector/ParameterWrapper.java    | 130 +++++++++++++++++++++
     .../TestParameterAnnotationMethodProcessor.java    |  12 +-
     2 files changed, 133 insertions(+), 9 deletions(-)
     create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/ParameterWrapper.java
    
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterWrapper.java b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterWrapper.java
    new file mode 100644
    index 0000000..b5f3972
    --- /dev/null
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterWrapper.java
    @@ -0,0 +1,130 @@
    +/*
    + * 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.lang.reflect.Constructor;
    +import java.lang.reflect.Method;
    +import java.lang.reflect.Parameter;
    +import javax.annotation.Nullable;
    +
    +abstract class ParameterWrapper {
    +  @SuppressWarnings("AndroidJdkLibsChecker") // j.l.r.Parameter is not available on old Android SDKs.
    +  static ParameterWrapper[] get(Constructor constructor) {
    +    try {
    +      return Java8.create(constructor.getParameters());
    +    } catch (NoSuchMethodError ignored) {
    +      return Legacy.create(constructor);
    +    }
    +  }
    +
    +  @SuppressWarnings("AndroidJdkLibsChecker") // j.l.r.Parameter is not available on old Android SDKs.
    +  static ParameterWrapper[] get(Method method) {
    +    try {
    +      return Java8.create(method.getParameters());
    +    } catch (NoSuchMethodError ignored) {
    +      return Legacy.create(method);
    +    }
    +  }
    +
    +  abstract  @Nullable T getAnnotation(Class annotationType);
    +
    +  abstract Class getType();
    +
    +  abstract String getName();
    +
    +  @SuppressWarnings("AndroidJdkLibsChecker") // j.l.r.Parameter is not available on old Android SDKs.
    +  private static final class Java8 extends ParameterWrapper {
    +    static ParameterWrapper[] create(Parameter[] parameters) {
    +      ParameterWrapper[] array = new ParameterWrapper[parameters.length];
    +      for (int i = 0; i < parameters.length; i++) {
    +        array[i] = new Java8(parameters[i]);
    +      }
    +      return array;
    +    }
    +
    +    private final Parameter parameter;
    +
    +    private Java8(Parameter parameter) {
    +      this.parameter = parameter;
    +    }
    +
    +    @Override
    +     @Nullable T getAnnotation(Class annotationType) {
    +      return parameter.getAnnotation(annotationType);
    +    }
    +
    +    @Override
    +    Class getType() {
    +      return parameter.getType();
    +    }
    +
    +    @Override
    +    String getName() {
    +      return parameter.getName();
    +    }
    +  }
    +
    +  private static final class Legacy extends ParameterWrapper {
    +    static ParameterWrapper[] create(Constructor constructor) {
    +      return create(constructor.getParameterAnnotations(), constructor.getParameterTypes());
    +    }
    +
    +    static ParameterWrapper[] create(Method method) {
    +      return create(method.getParameterAnnotations(), method.getParameterTypes());
    +    }
    +
    +    private static ParameterWrapper[] create(Annotation[][] annotations, Class[] types) {
    +      assert annotations.length == types.length;
    +      ParameterWrapper[] array = new ParameterWrapper[annotations.length];
    +      for (int i = 0; i < annotations.length; i++) {
    +        // Per j.l.r.Parameter.getName(), "argN" is the synthetic name format used when a class
    +        //  file does not actually contain parameter name information.
    +        array[i] = new Legacy(annotations[i], types[i], "arg" + i);
    +      }
    +      return array;
    +    }
    +
    +    private final Annotation[] annotations;
    +    private final Class type;
    +    private final String name;
    +
    +    private Legacy(Annotation[] annotations, Class type, String name) {
    +      this.annotations = annotations;
    +      this.type = type;
    +      this.name = name;
    +    }
    +
    +    @Override
    +     @Nullable T getAnnotation(Class annotationType) {
    +      for (Annotation annotation : annotations) {
    +        if (annotation.annotationType().equals(annotationType)) {
    +          return annotationType.cast(annotation);
    +        }
    +      }
    +      return null;
    +    }
    +
    +    @Override
    +    Class getType() {
    +      return type;
    +    }
    +
    +    @Override
    +    String getName() {
    +      return name;
    +    }
    +  }
    +}
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    index ab37238..29ad11f 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    @@ -40,7 +40,6 @@ 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;
    @@ -842,7 +841,7 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
         if (origin == Origin.CONSTRUCTOR_PARAMETER) {
           Constructor constructor = getOnlyConstructor(testClass);
           List annotations =
    -          getAnnotationWithMetadataListWithType(constructor.getParameters(), annotationType);
    +          getAnnotationWithMetadataListWithType(ParameterWrapper.get(constructor), annotationType);
     
           if (!annotations.isEmpty()) {
             return toTestParameterValueList(annotations, origin);
    @@ -856,7 +855,7 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
     
         } else if (origin == Origin.METHOD_PARAMETER) {
           List annotations =
    -          getAnnotationWithMetadataListWithType(method.getParameters(), annotationType);
    +          getAnnotationWithMetadataListWithType(ParameterWrapper.get(method), annotationType);
           if (!annotations.isEmpty()) {
             return toTestParameterValueList(annotations, origin);
           }
    @@ -899,13 +898,8 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
             .collect(toImmutableList());
       }
     
    -  // Parameter is not available on old Android SDKs, and isn't desugared. Many (most?) Android tests
    -  // will run against a more recent Java SDK, so this will work fine. If it proves problematic for
    -  // users trying to run, say, emulator tests, it would be possible to just not provide parameter
    -  // names on Android.
    -  @SuppressWarnings("AndroidJdkLibsChecker")
       private static ImmutableList getAnnotationWithMetadataListWithType(
    -      Parameter[] parameters, Class annotationType) {
    +      ParameterWrapper[] parameters, Class annotationType) {
         return stream(parameters)
             .map(
                 parameter -> {
    -- 
    cgit v1.2.3
    
    
    From 5b1a597b6e641a0351f6ada7d1d3c67128bfd65f Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Fri, 16 Apr 2021 15:24:57 +0100
    Subject: Replace ParameterWrapper by extra static factory methods for
     AnnotationWithMetadata
    
    ---
     .../testparameterinjector/ParameterWrapper.java    | 130 ---------------------
     .../TestParameterAnnotationMethodProcessor.java    |  58 ++++++++-
     2 files changed, 52 insertions(+), 136 deletions(-)
     delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/ParameterWrapper.java
    
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterWrapper.java b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterWrapper.java
    deleted file mode 100644
    index b5f3972..0000000
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterWrapper.java
    +++ /dev/null
    @@ -1,130 +0,0 @@
    -/*
    - * 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.lang.reflect.Constructor;
    -import java.lang.reflect.Method;
    -import java.lang.reflect.Parameter;
    -import javax.annotation.Nullable;
    -
    -abstract class ParameterWrapper {
    -  @SuppressWarnings("AndroidJdkLibsChecker") // j.l.r.Parameter is not available on old Android SDKs.
    -  static ParameterWrapper[] get(Constructor constructor) {
    -    try {
    -      return Java8.create(constructor.getParameters());
    -    } catch (NoSuchMethodError ignored) {
    -      return Legacy.create(constructor);
    -    }
    -  }
    -
    -  @SuppressWarnings("AndroidJdkLibsChecker") // j.l.r.Parameter is not available on old Android SDKs.
    -  static ParameterWrapper[] get(Method method) {
    -    try {
    -      return Java8.create(method.getParameters());
    -    } catch (NoSuchMethodError ignored) {
    -      return Legacy.create(method);
    -    }
    -  }
    -
    -  abstract  @Nullable T getAnnotation(Class annotationType);
    -
    -  abstract Class getType();
    -
    -  abstract String getName();
    -
    -  @SuppressWarnings("AndroidJdkLibsChecker") // j.l.r.Parameter is not available on old Android SDKs.
    -  private static final class Java8 extends ParameterWrapper {
    -    static ParameterWrapper[] create(Parameter[] parameters) {
    -      ParameterWrapper[] array = new ParameterWrapper[parameters.length];
    -      for (int i = 0; i < parameters.length; i++) {
    -        array[i] = new Java8(parameters[i]);
    -      }
    -      return array;
    -    }
    -
    -    private final Parameter parameter;
    -
    -    private Java8(Parameter parameter) {
    -      this.parameter = parameter;
    -    }
    -
    -    @Override
    -     @Nullable T getAnnotation(Class annotationType) {
    -      return parameter.getAnnotation(annotationType);
    -    }
    -
    -    @Override
    -    Class getType() {
    -      return parameter.getType();
    -    }
    -
    -    @Override
    -    String getName() {
    -      return parameter.getName();
    -    }
    -  }
    -
    -  private static final class Legacy extends ParameterWrapper {
    -    static ParameterWrapper[] create(Constructor constructor) {
    -      return create(constructor.getParameterAnnotations(), constructor.getParameterTypes());
    -    }
    -
    -    static ParameterWrapper[] create(Method method) {
    -      return create(method.getParameterAnnotations(), method.getParameterTypes());
    -    }
    -
    -    private static ParameterWrapper[] create(Annotation[][] annotations, Class[] types) {
    -      assert annotations.length == types.length;
    -      ParameterWrapper[] array = new ParameterWrapper[annotations.length];
    -      for (int i = 0; i < annotations.length; i++) {
    -        // Per j.l.r.Parameter.getName(), "argN" is the synthetic name format used when a class
    -        //  file does not actually contain parameter name information.
    -        array[i] = new Legacy(annotations[i], types[i], "arg" + i);
    -      }
    -      return array;
    -    }
    -
    -    private final Annotation[] annotations;
    -    private final Class type;
    -    private final String name;
    -
    -    private Legacy(Annotation[] annotations, Class type, String name) {
    -      this.annotations = annotations;
    -      this.type = type;
    -      this.name = name;
    -    }
    -
    -    @Override
    -     @Nullable T getAnnotation(Class annotationType) {
    -      for (Annotation annotation : annotations) {
    -        if (annotation.annotationType().equals(annotationType)) {
    -          return annotationType.cast(annotation);
    -        }
    -      }
    -      return null;
    -    }
    -
    -    @Override
    -    Class getType() {
    -      return type;
    -    }
    -
    -    @Override
    -    String getName() {
    -      return name;
    -    }
    -  }
    -}
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    index 29ad11f..a706c63 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    @@ -14,6 +14,7 @@
     
     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;
    @@ -40,6 +41,7 @@ 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;
    @@ -841,7 +843,7 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
         if (origin == Origin.CONSTRUCTOR_PARAMETER) {
           Constructor constructor = getOnlyConstructor(testClass);
           List annotations =
    -          getAnnotationWithMetadataListWithType(ParameterWrapper.get(constructor), annotationType);
    +          getAnnotationWithMetadataListWithType(constructor, annotationType);
     
           if (!annotations.isEmpty()) {
             return toTestParameterValueList(annotations, origin);
    @@ -855,7 +857,7 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
     
         } else if (origin == Origin.METHOD_PARAMETER) {
           List annotations =
    -          getAnnotationWithMetadataListWithType(ParameterWrapper.get(method), annotationType);
    +          getAnnotationWithMetadataListWithType(method, annotationType);
           if (!annotations.isEmpty()) {
             return toTestParameterValueList(annotations, origin);
           }
    @@ -899,7 +901,31 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
       }
     
       private static ImmutableList getAnnotationWithMetadataListWithType(
    -      ParameterWrapper[] parameters, Class annotationType) {
    +      Method callable, Class annotationType) {
    +    try {
    +      return getAnnotationWithMetadataListWithType(callable.getParameters(), annotationType);
    +    } catch (NoSuchMethodError ignored) {
    +      return getAnnotationWithMetadataListWithType(
    +          callable.getParameterTypes(), callable.getParameterAnnotations(), annotationType);
    +    }
    +  }
    +
    +  private static ImmutableList getAnnotationWithMetadataListWithType(
    +      Constructor callable, Class 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 getAnnotationWithMetadataListWithType(
    +      Parameter[] parameters, Class annotationType) {
         return stream(parameters)
             .map(
                 parameter -> {
    @@ -910,12 +936,32 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
                           annotation, parameter.getType(), parameter.getName());
                 })
             .filter(Objects::nonNull)
    -        .filter(
    -            annotationWithMetadata ->
    -                annotationWithMetadata.annotation().annotationType().equals(annotationType))
             .collect(toImmutableList());
       }
     
    +  private static ImmutableList getAnnotationWithMetadataListWithType(
    +      Class[] parameterTypes,
    +      Annotation[][] annotations,
    +      Class annotationType) {
    +    checkArgument(parameterTypes.length == annotations.length);
    +
    +    ImmutableList.Builder resultBuilder = ImmutableList.builder();
    +    for (int i = 0; i < annotations.length; i++) {
    +      Class parameterType = parameterTypes[i];
    +      // Per j.l.r.Parameter.getName(), "argN" is the synthetic name format used when a class
    +      //  file does not actually contain parameter name information.
    +      String parameterName = "arg" + i;
    +
    +      for (Annotation annotation : annotations[i]) {
    +        if (annotation.annotationType().equals(annotationType)) {
    +          resultBuilder.add(
    +              AnnotationWithMetadata.withMetadata(annotation, parameterType, parameterName));
    +        }
    +      }
    +    }
    +    return resultBuilder.build();
    +  }
    +
       private ImmutableList getAnnotationListWithType(
           Annotation[][] parameterAnnotations, Class annotationType) {
         return stream(parameterAnnotations)
    -- 
    cgit v1.2.3
    
    
    From cc6299c4db72b9cb0bfe05c44fc0123003096d25 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Wed, 21 Apr 2021 14:52:03 +0100
    Subject: Don't use the parameter name if it's not explicitly provided by the
     compiler
    
    ---
     .../TestParameterAnnotationMethodProcessor.java       | 19 ++++++++++---------
     1 file changed, 10 insertions(+), 9 deletions(-)
    
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    index a706c63..1735e4b 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    @@ -247,6 +247,11 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
               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());
    @@ -932,8 +937,10 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
                   Annotation annotation = parameter.getAnnotation(annotationType);
                   return annotation == null
                       ? null
    -                  : AnnotationWithMetadata.withMetadata(
    -                      annotation, parameter.getType(), parameter.getName());
    +                  : parameter.isNamePresent()
    +                      ? AnnotationWithMetadata.withMetadata(
    +                          annotation, parameter.getType(), parameter.getName())
    +                      : AnnotationWithMetadata.withMetadata(annotation, parameter.getType());
                 })
             .filter(Objects::nonNull)
             .collect(toImmutableList());
    @@ -947,15 +954,9 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
     
         ImmutableList.Builder resultBuilder = ImmutableList.builder();
         for (int i = 0; i < annotations.length; i++) {
    -      Class parameterType = parameterTypes[i];
    -      // Per j.l.r.Parameter.getName(), "argN" is the synthetic name format used when a class
    -      //  file does not actually contain parameter name information.
    -      String parameterName = "arg" + i;
    -
           for (Annotation annotation : annotations[i]) {
             if (annotation.annotationType().equals(annotationType)) {
    -          resultBuilder.add(
    -              AnnotationWithMetadata.withMetadata(annotation, parameterType, parameterName));
    +          resultBuilder.add(AnnotationWithMetadata.withMetadata(annotation, parameterTypes[i]));
             }
           }
         }
    -- 
    cgit v1.2.3
    
    
    From d037598523107f5ec09a966c6dd385bda569882d Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Wed, 21 Apr 2021 14:55:36 +0100
    Subject: Bump version to v1.2 in README.md
    
    ---
     README.md | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/README.md b/README.md
    index 0ac029b..c528c06 100644
    --- a/README.md
    +++ b/README.md
    @@ -50,7 +50,7 @@ And add the following dependency to your `.pom` file:
     
       com.google.testparameterinjector
       test-parameter-injector
    -  1.1
    +  1.2
     
     ```
     
    -- 
    cgit v1.2.3
    
    
    From 579c2a81d49b0355bfa09bcc13a354ac7b2dc8b4 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Fri, 23 Apr 2021 15:15:59 +0100
    Subject: Treat 'null' as a magic string that results in a null value
    
    This is alrady the case for protos and other YAML-parsed values, but not for strings/enums
    
    This fixes https://github.com/google/TestParameterInjector/issues/4
    ---
     .../com/google/testing/junit/testparameterinjector/TestParameter.java | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
    index f2ee847..a128a79 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
    @@ -180,9 +180,9 @@ public @interface TestParameter {
     
         private static Object parseStringValue(String value, Class parameterClass) {
           if (parameterClass.equals(String.class)) {
    -        return value;
    +        return value.equals("null") ? null : value;
           } else if (Enum.class.isAssignableFrom(parameterClass)) {
    -        return ParameterValueParsing.parseEnum(value, 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);
    -- 
    cgit v1.2.3
    
    
    From 0deb9cb35784cec9aa0b7b1b25e403961bffdc29 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Fri, 23 Apr 2021 15:23:40 +0100
    Subject: Bump version to v1.3 in README.md
    
    ---
     README.md | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/README.md b/README.md
    index c528c06..99995e6 100644
    --- a/README.md
    +++ b/README.md
    @@ -50,7 +50,7 @@ And add the following dependency to your `.pom` file:
     
       com.google.testparameterinjector
       test-parameter-injector
    -  1.2
    +  1.3
     
     ```
     
    -- 
    cgit v1.2.3
    
    
    From b8bb940ee77ca93ba6db457c2b939358764372d0 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Mon, 26 Apr 2021 12:34:27 +0100
    Subject: Document that the null string is always parsed to the null reference
    
    ---
     README.md | 2 ++
     1 file changed, 2 insertions(+)
    
    diff --git a/README.md b/README.md
    index 99995e6..c1b4ccf 100644
    --- a/README.md
    +++ b/README.md
    @@ -127,6 +127,8 @@ The following examples show most of the supported types. See the `@TestParameter
     @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
    -- 
    cgit v1.2.3
    
    
    From 56b52f75ac6bcbf80f4b9d255dc2769fb8758388 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Fri, 21 May 2021 16:23:02 +0100
    Subject: Add changelog.
    
    This fixes https://github.com/google/TestParameterInjector/issues/7.
    ---
     CHANGELOG.md | 13 +++++++++++++
     1 file changed, 13 insertions(+)
     create mode 100644 CHANGELOG.md
    
    diff --git a/CHANGELOG.md b/CHANGELOG.md
    new file mode 100644
    index 0000000..b59e511
    --- /dev/null
    +++ b/CHANGELOG.md
    @@ -0,0 +1,13 @@
    +## 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[]`
    -- 
    cgit v1.2.3
    
    
    From d28fcdfb6bdd3a33a0a750d4b551d938f88d7f2d Mon Sep 17 00:00:00 2001
    From: Jake Wharton 
    Date: Sat, 22 May 2021 00:15:42 -0400
    Subject: Add GitHub Actions to build and deploy site
    
    This will deploy "main" under docs/latest/ and deploy tags to docs/1.x/ on the "site" branch.
    ---
     .github/dependabot.yaml        |  7 +++++++
     .github/workflows/build.yaml   | 33 +++++++++++++++++++++++++++++++++
     .github/workflows/release.yaml | 29 +++++++++++++++++++++++++++++
     3 files changed, 69 insertions(+)
     create mode 100644 .github/dependabot.yaml
     create mode 100644 .github/workflows/build.yaml
     create mode 100644 .github/workflows/release.yaml
    
    diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml
    new file mode 100644
    index 0000000..81bae9a
    --- /dev/null
    +++ b/.github/dependabot.yaml
    @@ -0,0 +1,7 @@
    +version: 2
    +
    +updates:
    +  - package-ecosystem: "github-actions"
    +    directory: "/"
    +    schedule:
    +      interval: "daily"
    diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
    new file mode 100644
    index 0000000..213acc6
    --- /dev/null
    +++ b/.github/workflows/build.yaml
    @@ -0,0 +1,33 @@
    +name: build
    +
    +on:
    +  pull_request: {}
    +  push:
    +    branches:
    +      - '**'
    +    tags-ignore:
    +      - '**'
    +
    +jobs:
    +  build:
    +    runs-on: ubuntu-latest
    +
    +    steps:
    +      - uses: actions/checkout@v2.3.4
    +
    +      - uses: actions/setup-java@v2
    +        with:
    +          distribution: 'zulu'
    +          java-version: 11
    +
    +      - run: mvn verify javadoc:javadoc
    +
    +      - name: Deploy docs to website
    +        if: ${{ github.ref == 'refs/heads/main' }}
    +        uses: JamesIves/github-pages-deploy-action@releases/v3
    +        with:
    +          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    +          BRANCH: site
    +          FOLDER: target/site/apidocs
    +          TARGET_FOLDER: docs/latest/
    +          CLEAN: true
    diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
    new file mode 100644
    index 0000000..6d68197
    --- /dev/null
    +++ b/.github/workflows/release.yaml
    @@ -0,0 +1,29 @@
    +name: release
    +
    +on:
    +  push:
    +    tags:
    +      - '**'
    +
    +jobs:
    +  release:
    +    runs-on: ubuntu-latest
    +
    +    steps:
    +      - uses: actions/checkout@v2.3.4
    +
    +      - uses: actions/setup-java@v2
    +        with:
    +          distribution: 'zulu'
    +          java-version: 11
    +
    +      - run: mvn javadoc:javadoc
    +
    +      - name: Deploy docs to website
    +        uses: JamesIves/github-pages-deploy-action@releases/v3
    +        with:
    +          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    +          BRANCH: site
    +          FOLDER: target/site/apidocs
    +          TARGET_FOLDER: docs/1.x/
    +          CLEAN: true
    -- 
    cgit v1.2.3
    
    
    From f72fce9d19c595f3ba0abed9c9f4b64d3d620418 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Sat, 22 May 2021 10:22:37 +0100
    Subject: Add Javadoc link to README
    
    ---
     README.md | 2 ++
     1 file changed, 2 insertions(+)
    
    diff --git a/README.md b/README.md
    index c1b4ccf..d09ea80 100644
    --- a/README.md
    +++ b/README.md
    @@ -1,6 +1,8 @@
     TestParameterInjector
     =====================
     
    +[Javadoc](https://google.github.io/TestParameterInjector/docs/latest/)
    +
     ## Introduction
     
     `TestParameterInjector` is a JUnit4 test runner that runs its test methods for
    -- 
    cgit v1.2.3
    
    
    From 7c1de22a6f95823d5c977bdb134b31597d56782b Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Sat, 22 May 2021 10:25:17 +0100
    Subject: Improve Javadoc link in README
    
    ---
     README.md | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/README.md b/README.md
    index d09ea80..767809a 100644
    --- a/README.md
    +++ b/README.md
    @@ -1,7 +1,7 @@
     TestParameterInjector
     =====================
     
    -[Javadoc](https://google.github.io/TestParameterInjector/docs/latest/)
    +[Link to Javadoc.](https://google.github.io/TestParameterInjector/docs/latest/)
     
     ## Introduction
     
    -- 
    cgit v1.2.3
    
    
    From 32edf6e32e11ec56c46e05b7db49cd6fd622c2b5 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Tue, 25 May 2021 10:14:59 +0100
    Subject: TestParameterInjector: Throw a descriptive error message when a
     provider class should be static.
    
    New exception:
    
    java.lang.IllegalStateException: Could not find a no-arg constructor for CharMatcherProvider, probably because it is a not-static inner class. You can fix this by making CharMatcherProvider static.
    ---
     .../junit/testparameterinjector/TestParameter.java       | 15 +++++++++++++++
     .../TestParameterAnnotationMethodProcessor.java          |  2 +-
     .../TestParametersMethodProcessor.java                   | 15 +++++++++++++++
     .../TestParameterAnnotationMethodProcessorTest.java      | 16 ++++++++++++++++
     4 files changed, 47 insertions(+), 1 deletion(-)
    
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
    index a128a79..6725d16 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
    @@ -29,6 +29,7 @@ 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;
    @@ -201,6 +202,20 @@ public @interface TestParameter {
                 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/TestParameterAnnotationMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    index 1735e4b..94614f1 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    @@ -174,7 +174,7 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
               .provideValues(
                   annotation,
                   java.util.Optional.ofNullable(annotationWithMetadata.paramClass().orNull()));
    -    } catch (Exception e) {
    +    } catch (ReflectiveOperationException e) {
           throw new RuntimeException(
               "Unexpected exception while invoking value provider " + valueProvider, e);
         }
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
    index e27b09b..0a3368e 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
    @@ -35,6 +35,7 @@ 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;
    @@ -331,6 +332,20 @@ class TestParametersMethodProcessor implements TestMethodProcessor {
           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);
         }
    diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
    index c30733b..490b734 100644
    --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
    +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
    @@ -21,6 +21,7 @@ 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;
    @@ -777,6 +778,21 @@ public class TestParameterAnnotationMethodProcessorTest {
         }
       }
     
    +  @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
    -- 
    cgit v1.2.3
    
    
    From 7fb29c9f5086be48cc12dc7e5703e810ccbde2c2 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Tue, 25 May 2021 10:16:20 +0100
    Subject: TestParameterInjector: Support overridden test methods
    
    ---
     CHANGELOG.md                                       |  5 ++++
     .../TestParameterAnnotationMethodProcessor.java    | 24 +++++++++++++++----
     ...TestParameterAnnotationMethodProcessorTest.java | 28 +++++++++++++++++++++-
     3 files changed, 52 insertions(+), 5 deletions(-)
    
    diff --git a/CHANGELOG.md b/CHANGELOG.md
    index b59e511..946e593 100644
    --- a/CHANGELOG.md
    +++ b/CHANGELOG.md
    @@ -1,3 +1,8 @@
    +## 1.4
    +
    +- Bugfix: Run test methods declared in a base class (instead of throwing an
    +  exception)
    +
     ## 1.3
     
     - Treat 'null' as a magic string that results in a null value
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    index 94614f1..23aedcc 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    @@ -647,8 +647,8 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
                       .addAll(originalTest.getAnnotations())
                       .add(
                           TestIndexHolderFactory.create(
    -                          /* methodIndex= */ ImmutableList.copyOf(testClass.getMethods())
    -                              .indexOf(originalTest.getMethod()),
    +                          /* methodIndex= */ strictIndexOf(
    +                              getMethodsIncludingParents(testClass), originalTest.getMethod()),
                               parametersIndex,
                               testClass.getName()))
                       .build()));
    @@ -740,7 +740,8 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
                 + " class that this runner is handling (%s)",
             testIndexHolder.testClassName(),
             testClass.getName());
    -    Method testMethod = testClass.getJavaClass().getMethods()[testIndexHolder.methodIndex()];
    +    Method testMethod =
    +        getMethodsIncludingParents(testClass.getJavaClass()).get(testIndexHolder.methodIndex());
         return getParameterValuesForMethod(testMethod).get(testIndexHolder.parametersIndex());
       }
     
    @@ -1209,7 +1210,7 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
       @Retention(RUNTIME)
       @interface TestIndexHolder {
     
    -    /** The index of the test method in the {@code testClass.getMethods()} */
    +    /** The index of the test method in {@code getMethodsIncludingParents(testClass)} */
         int methodIndex();
     
         /**
    @@ -1362,6 +1363,21 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
             .collect(toImmutableList());
       }
     
    +  private  int strictIndexOf(List haystack, T needle) {
    +    int index = haystack.indexOf(needle);
    +    checkArgument(index >= 0, "Could not find '%s' in %s", needle, haystack);
    +    return index;
    +  }
    +
    +  private ImmutableList getMethodsIncludingParents(Class clazz) {
    +    ImmutableList.Builder resultBuilder = ImmutableList.builder();
    +    while (clazz != null) {
    +      resultBuilder.add(clazz.getMethods());
    +      clazz = clazz.getSuperclass();
    +    }
    +    return resultBuilder.build();
    +  }
    +
       private static Stream> streamWithParents(Class clazz) {
         Stream.Builder> resultBuilder = Stream.builder();
     
    diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
    index 490b734..e16dd28 100644
    --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
    +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
    @@ -472,6 +472,14 @@ public class TestParameterAnnotationMethodProcessorTest {
         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)
    @@ -484,6 +492,16 @@ public class TestParameterAnnotationMethodProcessorTest {
           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)
    @@ -503,7 +521,15 @@ public class TestParameterAnnotationMethodProcessorTest {
                   "testInChild[boolInChild=true,boolInBase=false,TWO]",
                   "testInChild[boolInChild=true,boolInBase=false,THREE]",
                   "testInChild[boolInChild=true,boolInBase=true,TWO]",
    -              "testInChild[boolInChild=true,boolInBase=true,THREE]");
    +              "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]");
         }
       }
     
    -- 
    cgit v1.2.3
    
    
    From cff1233bb7a8acd79a0e0ae3b3663911e41dfa30 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Tue, 25 May 2021 10:32:25 +0100
    Subject: Add license headers to the newly added YAML files
    
    ---
     .github/dependabot.yaml        | 14 ++++++++++++++
     .github/workflows/build.yaml   | 14 ++++++++++++++
     .github/workflows/release.yaml | 14 ++++++++++++++
     3 files changed, 42 insertions(+)
    
    diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml
    index 81bae9a..f262838 100644
    --- a/.github/dependabot.yaml
    +++ b/.github/dependabot.yaml
    @@ -1,3 +1,17 @@
    +# Copyright 2021 Google Inc.
    +#
    +# Licensed under the Apache License, Version 2.0 (the "License"); you may not
    +# use this file except in compliance with the License. You may obtain a copy of
    +# the License at
    +#
    +#     http://www.apache.org/licenses/LICENSE-2.0
    +#
    +# Unless required by applicable law or agreed to in writing, software
    +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    +# License for the specific language governing permissions and limitations under
    +# the License.
    +
     version: 2
     
     updates:
    diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
    index 213acc6..579478e 100644
    --- a/.github/workflows/build.yaml
    +++ b/.github/workflows/build.yaml
    @@ -1,3 +1,17 @@
    +# Copyright 2021 Google Inc.
    +#
    +# Licensed under the Apache License, Version 2.0 (the "License"); you may not
    +# use this file except in compliance with the License. You may obtain a copy of
    +# the License at
    +#
    +#     http://www.apache.org/licenses/LICENSE-2.0
    +#
    +# Unless required by applicable law or agreed to in writing, software
    +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    +# License for the specific language governing permissions and limitations under
    +# the License.
    +
     name: build
     
     on:
    diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
    index 6d68197..c95bf04 100644
    --- a/.github/workflows/release.yaml
    +++ b/.github/workflows/release.yaml
    @@ -1,3 +1,17 @@
    +# Copyright 2021 Google Inc.
    +#
    +# Licensed under the Apache License, Version 2.0 (the "License"); you may not
    +# use this file except in compliance with the License. You may obtain a copy of
    +# the License at
    +#
    +#     http://www.apache.org/licenses/LICENSE-2.0
    +#
    +# Unless required by applicable law or agreed to in writing, software
    +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    +# License for the specific language governing permissions and limitations under
    +# the License.
    +
     name: release
     
     on:
    -- 
    cgit v1.2.3
    
    
    From 7636fd72d08c80f5786693e78548a83a120d746b Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Fri, 18 Jun 2021 15:38:47 +0100
    Subject: TestParameterInjector: Add a test with duplicate test names
    
    ---
     ...TestParameterAnnotationMethodProcessorTest.java | 43 ++++++++++++++++++++++
     1 file changed, 43 insertions(+)
    
    diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
    index e16dd28..f8f4de4 100644
    --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
    +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
    @@ -15,6 +15,7 @@
     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;
    @@ -302,6 +303,48 @@ public class TestParameterAnnotationMethodProcessorTest {
         }
       }
     
    +  @ClassTestResult(Result.SUCCESS_ALWAYS)
    +  public static class DuplicateTestNames {
    +
    +    @Rule public TestName testName = new TestName();
    +
    +    private static List allTestNames;
    +    private static List 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 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[ABC]", "test1[ABC]", "test2[123]", "test2[123]", "test2[null]", "test2[null]");
    +      assertThat(allTestParameterValues).containsExactly("ABC", "ABC", 123, "123", "null", null);
    +    }
    +  }
    +
       @ClassTestResult(Result.SUCCESS_ALWAYS)
       public static class DuplicateFieldAnnotationTestClass {
     
    -- 
    cgit v1.2.3
    
    
    From cc4317b5cc3bd8ff0053105de594ca55fb71e748 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Mon, 21 Jun 2021 14:32:03 +0100
    Subject: Store parameter values in TestInfo class so it makes
     combining/shortening/deduplicating easier.
    
    Note: This is a pure refactor. No behavioral change should be noticable.
    ---
     .../ParameterizedTestMethodProcessor.java          | 23 ++-----
     .../testparameterinjector/PluggableTestRunner.java | 34 +++++-----
     .../junit/testparameterinjector/TestInfo.java      | 46 ++++++++++++-
     .../TestParameterAnnotationMethodProcessor.java    | 78 ++++++++++------------
     .../testparameterinjector/TestParameters.java      |  2 +-
     .../TestParametersMethodProcessor.java             | 36 +++-------
     6 files changed, 108 insertions(+), 111 deletions(-)
    
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java
    index 459bccc..ab49ca2 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java
    @@ -20,7 +20,6 @@ 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 java.lang.annotation.Annotation;
     import java.lang.annotation.Retention;
     import java.lang.annotation.RetentionPolicy;
     import java.lang.reflect.Constructor;
    @@ -137,12 +136,11 @@ class ParameterizedTestMethodProcessor implements TestMethodProcessor {
               parametersForOneTest = new Object[] {parameters};
             }
             String namePattern = testNamePattern.get().replace("{index}", Integer.toString(testIndex));
    -        String testName = MessageFormat.format(namePattern, parametersForOneTest);
    +        String testParametersString = MessageFormat.format(namePattern, parametersForOneTest);
             tests.add(
    -            TestInfo.create(
    -                originalTest.getMethod(),
    -                originalTest.getName() + "[" + testName + "]",
    -                updateAnnotationList(originalTest, testIndex)));
    +            originalTest
    +                .withExtraParameters(ImmutableList.of(testParametersString))
    +                .withExtraAnnotation(TestIndexHolderFactory.create(testIndex)));
             testIndex++;
           }
           return tests.build();
    @@ -180,19 +178,6 @@ class ParameterizedTestMethodProcessor implements TestMethodProcessor {
         return statement;
       }
     
    -  /**
    -   * Stores into the annotation list of a test method the {@code testIndex} required to identify
    -   * which parameter should be used for this test instance.
    -   */
    -  private ImmutableList updateAnnotationList(
    -      TestInfo originalTest, final int testIndex) {
    -    Annotation parameterHolder = TestIndexHolderFactory.create(testIndex);
    -    return new ImmutableList.Builder()
    -        .addAll(originalTest.getAnnotations())
    -        .add(parameterHolder)
    -        .build();
    -  }
    -
       /**
        * 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.
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java
    index f919de9..1e477d0 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java
    @@ -205,29 +205,25 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner {
       }
     
       private ImmutableList processMethod(FrameworkMethod initialMethod) {
    -    ImmutableList methods = ImmutableList.of(initialMethod);
    +    ImmutableList testInfos =
    +        ImmutableList.of(
    +            TestInfo.createWithoutParameters(
    +                initialMethod.getMethod(), ImmutableList.copyOf(initialMethod.getAnnotations())));
    +
         for (final TestMethodProcessor testMethodProcessor : getTestMethodProcessors()) {
    -      methods =
    -          methods.stream()
    +      testInfos =
    +          testInfos.stream()
                   .flatMap(
    -                  method -> {
    -                    TestInfo originalTest =
    -                        TestInfo.create(
    -                            method.getMethod(),
    -                            method.getName(),
    -                            ImmutableList.copyOf(method.getAnnotations()));
    -                    List processedTests =
    -                        testMethodProcessor.processTest(
    -                            getTestClass().getJavaClass(), originalTest);
    -
    -                    return processedTests.stream()
    -                        .map(
    -                            processedTest ->
    -                                new OverriddenFrameworkMethod(method.getMethod(), processedTest));
    -                  })
    +                  lastTestInfo ->
    +                      testMethodProcessor
    +                          .processTest(getTestClass().getJavaClass(), lastTestInfo)
    +                          .stream())
                   .collect(toImmutableList());
         }
    -    return methods;
    +
    +    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
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java
    index daf6b9a..e54d7e5 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java
    @@ -14,6 +14,7 @@
     
     package com.google.testing.junit.testparameterinjector;
     
    +import static java.util.stream.Collectors.joining;
     import static java.util.stream.Collectors.toList;
     
     import com.google.auto.value.AutoValue;
    @@ -41,6 +42,8 @@ abstract class TestInfo {
     
       public abstract String getName();
     
    +  abstract ImmutableList getParameterNames();
    +
       public abstract ImmutableList getAnnotations();
     
       @Nullable
    @@ -53,12 +56,40 @@ abstract class TestInfo {
         return null;
       }
     
    +  TestInfo withExtraParameters(List parameterNames) {
    +    ImmutableList newParameterNames =
    +        ImmutableList.builder()
    +            .addAll(this.getParameterNames())
    +            .addAll(parameterNames)
    +            .build();
    +    return new AutoValue_TestInfo(
    +        getMethod(),
    +        TestInfo.getDefaultName(getMethod(), newParameterNames),
    +        newParameterNames,
    +        getAnnotations());
    +  }
    +
    +  TestInfo withExtraAnnotation(Annotation annotation) {
    +    ImmutableList newAnnotations =
    +        ImmutableList.builder().addAll(this.getAnnotations()).add(annotation).build();
    +    return new AutoValue_TestInfo(getMethod(), getName(), getParameterNames(), newAnnotations);
    +  }
    +
       private TestInfo withName(String otherName) {
    -    return TestInfo.create(getMethod(), otherName, getAnnotations());
    +    return new AutoValue_TestInfo(getMethod(), otherName, getParameterNames(), getAnnotations());
       }
     
    -  public static TestInfo create(Method method, String name, List annotations) {
    -    return new AutoValue_TestInfo(method, name, ImmutableList.copyOf(annotations));
    +  public static TestInfo legacyCreate(Method method, String name, List annotations) {
    +    return new AutoValue_TestInfo(
    +        method, name, /* parameterNames= */ ImmutableList.of(), ImmutableList.copyOf(annotations));
    +  }
    +
    +  static TestInfo createWithoutParameters(Method method, List annotations) {
    +    return new AutoValue_TestInfo(
    +        method,
    +        getDefaultName(method, /* parameterNames= */ ImmutableList.of()),
    +        /* parameterNames= */ ImmutableList.of(),
    +        ImmutableList.copyOf(annotations));
       }
     
       static ImmutableList shortenNamesIfNecessary(
    @@ -72,4 +103,13 @@ abstract class TestInfo {
           return ImmutableList.copyOf(testInfos);
         }
       }
    +
    +  private static String getDefaultName(Method testMethod, List parameterNames) {
    +    if (parameterNames.isEmpty()) {
    +      return testMethod.getName();
    +    } else {
    +      return String.format(
    +          "%s[%s]", testMethod.getName(), parameterNames.stream().collect(joining(",")));
    +    }
    +  }
     }
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    index 23aedcc..de608af 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    @@ -19,7 +19,6 @@ 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.joining;
     import static java.util.stream.Collectors.toCollection;
     import static java.util.stream.Collectors.toSet;
     
    @@ -110,6 +109,29 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
          */
         abstract Optional paramName();
     
    +    /**
    +     * Returns a String that represents this value and is fit for use in a test name (between
    +     * brackets).
    +     */
    +    String toTestNameString() {
    +      Class 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 create(
             AnnotationWithMetadata annotationWithMetadata, Origin origin) {
           List specifiedValues = getParametersAnnotationValues(annotationWithMetadata);
    @@ -638,20 +660,18 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
             parametersIndex < parameterValuesForMethod.size();
             ++parametersIndex) {
           List testParameterValues = parameterValuesForMethod.get(parametersIndex);
    -      String testNameSuffix = getTestNameSuffix(testParameterValues);
           testInfos.add(
    -          TestInfo.create(
    -              originalTest.getMethod(),
    -              appendParametersToTestName(originalTest.getName(), testNameSuffix),
    -              ImmutableList.builder()
    -                  .addAll(originalTest.getAnnotations())
    -                  .add(
    -                      TestIndexHolderFactory.create(
    -                          /* methodIndex= */ strictIndexOf(
    -                              getMethodsIncludingParents(testClass), originalTest.getMethod()),
    -                          parametersIndex,
    -                          testClass.getName()))
    -                  .build()));
    +          originalTest
    +              .withExtraParameters(
    +                  testParameterValues.stream()
    +                      .map(TestParameterValue::toTestNameString)
    +                      .collect(toImmutableList()))
    +              .withExtraAnnotation(
    +                  TestIndexHolderFactory.create(
    +                      /* methodIndex= */ strictIndexOf(
    +                          getMethodsIncludingParents(testClass), originalTest.getMethod()),
    +                      parametersIndex,
    +                      testClass.getName())));
         }
     
         return TestInfo.shortenNamesIfNecessary(
    @@ -663,36 +683,6 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
                         testInfo.getAnnotation(TestIndexHolder.class).parametersIndex() + 1)));
       }
     
    -  /**
    -   * Returns the suffix of the test given the {@code testParameterValues} that will be appended to
    -   * the test name inside bracket, e.g. "testname[suffix]".
    -   */
    -  private static String getTestNameSuffix(List testParameterValues) {
    -    return testParameterValues.stream()
    -        .map(
    -            value -> {
    -              Class annotationType =
    -                  value.annotationTypeOrigin().annotationType();
    -              String namePattern =
    -                  annotationType.getAnnotation(TestParameterAnnotation.class).name();
    -              if (value.paramName().isPresent()
    -                  && value.paramClass().isPresent()
    -                  && namePattern.equals("{0}")
    -                  && Primitives.unwrap(value.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", value.paramName().get(), value.value());
    -              } else {
    -                return MessageFormat.format(namePattern, value.value());
    -              }
    -            })
    -        .map(string -> string.trim().replaceAll("\\s+", " "))
    -        .collect(joining(","));
    -  }
    -
       /**
        * Appends the given suffix to the given test name in brackets. If the original test name already
        * has brackets, the suffix is inserted in the existing brackets instead.
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java
    index 7e6bbc0..b7ee544 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java
    @@ -169,7 +169,7 @@ public @interface TestParameters {
            * name of the resulting test will be "personIsAdult[teenager]".
            */
           public Builder name(String name) {
    -        this.name = name;
    +        this.name = name.replaceAll("\\s+", " ");
             return this;
           }
     
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
    index 0a3368e..ddbed2c 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
    @@ -30,7 +30,6 @@ import com.google.common.reflect.TypeToken;
     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.Annotation;
     import java.lang.annotation.Retention;
     import java.lang.annotation.RetentionPolicy;
     import java.lang.reflect.Constructor;
    @@ -39,8 +38,10 @@ 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;
    @@ -135,15 +136,15 @@ class TestParametersMethodProcessor implements TestMethodProcessor {
             Optional methodParameters =
                 methodParametersList.get(methodParametersIndex);
             testInfos.add(
    -            TestInfo.create(
    -                originalTest.getMethod(),
    -                getTestName(originalTest, constructorParameters, methodParameters),
    -                new ImmutableList.Builder()
    -                    .addAll(originalTest.getAnnotations())
    -                    .add(
    -                        TestIndexHolderFactory.create(
    -                            constructorParametersIndex, methodParametersIndex))
    -                    .build()));
    +            originalTest
    +                .withExtraParameters(
    +                    Stream.of(constructorParameters.orNull(), methodParameters.orNull())
    +                        .filter(Objects::nonNull)
    +                        .map(TestParametersValues::name)
    +                        .collect(toImmutableList()))
    +                .withExtraAnnotation(
    +                    TestIndexHolderFactory.create(
    +                        constructorParametersIndex, methodParametersIndex)));
           }
         }
         return TestInfo.shortenNamesIfNecessary(
    @@ -178,21 +179,6 @@ class TestParametersMethodProcessor implements TestMethodProcessor {
             : ImmutableList.of(Optional.absent());
       }
     
    -  private static String getTestName(
    -      TestInfo originalTest,
    -      Optional constructorParameters,
    -      Optional methodParameters) {
    -    return maybeAppendParametersToTestName(
    -        maybeAppendParametersToTestName(originalTest.getName(), constructorParameters),
    -        methodParameters);
    -  }
    -
    -  private static String maybeAppendParametersToTestName(
    -      String originalTestName, Optional parameters) {
    -    return maybeAppendToTestName(
    -        originalTestName, parameters.transform(p -> p.name().replaceAll("\\s+", " ")));
    -  }
    -
       private static String maybeAppendToTestName(
           String originalTestName, Optional maybeSuffix) {
         if (!maybeSuffix.isPresent()) {
    -- 
    cgit v1.2.3
    
    
    From 8ef745882ed8c19596216879f0c335356270ef32 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Tue, 22 Jun 2021 20:44:57 +0100
    Subject: Shorten long test names by truncating parameter names.
    
    This now happens after all processors have run, as to get a full picture of the test name length. This should improve the clarity of the test names because:
    
    - All test parameters are taken into account
    - The snippet allows at least the short parameter names to be shown.
    ---
     .../ParameterizedTestMethodProcessor.java          |   6 +-
     .../testparameterinjector/PluggableTestRunner.java |   2 +
     .../junit/testparameterinjector/TestInfo.java      | 151 ++++++++++++++----
     .../TestParameterAnnotationMethodProcessor.java    |  40 ++---
     .../TestParametersMethodProcessor.java             |  57 +++----
     .../junit/testparameterinjector/TestInfoTest.java  | 171 +++++++++++++++++++++
     ...TestParameterAnnotationMethodProcessorTest.java |   6 +-
     .../TestParametersMethodProcessorTest.java         |  23 +--
     8 files changed, 360 insertions(+), 96 deletions(-)
     create mode 100644 src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java
    
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java
    index ab49ca2..dbafc6a 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java
    @@ -20,6 +20,7 @@ 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;
    @@ -139,7 +140,10 @@ class ParameterizedTestMethodProcessor implements TestMethodProcessor {
             String testParametersString = MessageFormat.format(namePattern, parametersForOneTest);
             tests.add(
                 originalTest
    -                .withExtraParameters(ImmutableList.of(testParametersString))
    +                .withExtraParameters(
    +                    ImmutableList.of(
    +                        TestInfoParameter.create(
    +                            testParametersString, parametersForOneTest, testIndex)))
                     .withExtraAnnotation(TestIndexHolderFactory.create(testIndex)));
             testIndex++;
           }
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java
    index 1e477d0..76d6c43 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java
    @@ -221,6 +221,8 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner {
                   .collect(toImmutableList());
         }
     
    +    testInfos = TestInfo.shortenNamesIfNecessary(testInfos);
    +
         return testInfos.stream()
             .map(testInfo -> new OverriddenFrameworkMethod(testInfo.getMethod(), testInfo))
             .collect(toImmutableList());
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java
    index e54d7e5..76b4069 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java
    @@ -14,15 +14,22 @@
     
     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.toList;
    +import static java.util.stream.Collectors.toSet;
     
     import com.google.auto.value.AutoValue;
    +import com.google.common.annotations.VisibleForTesting;
     import com.google.common.collect.ImmutableList;
     import java.lang.annotation.Annotation;
     import java.lang.reflect.Method;
     import java.util.List;
    -import java.util.function.Function;
    +import java.util.Set;
    +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). */
    @@ -38,11 +45,14 @@ abstract class TestInfo {
        */
       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 abstract String getName();
     
    -  abstract ImmutableList getParameterNames();
    +  abstract ImmutableList getParameters();
     
       public abstract ImmutableList getAnnotations();
     
    @@ -56,60 +66,149 @@ abstract class TestInfo {
         return null;
       }
     
    -  TestInfo withExtraParameters(List parameterNames) {
    -    ImmutableList newParameterNames =
    -        ImmutableList.builder()
    -            .addAll(this.getParameterNames())
    -            .addAll(parameterNames)
    +  TestInfo withExtraParameters(List parameters) {
    +    ImmutableList newParameters =
    +        ImmutableList.builder()
    +            .addAll(this.getParameters())
    +            .addAll(parameters)
                 .build();
         return new AutoValue_TestInfo(
             getMethod(),
    -        TestInfo.getDefaultName(getMethod(), newParameterNames),
    -        newParameterNames,
    +        TestInfo.getDefaultName(getMethod(), newParameters),
    +        newParameters,
             getAnnotations());
       }
     
       TestInfo withExtraAnnotation(Annotation annotation) {
         ImmutableList newAnnotations =
             ImmutableList.builder().addAll(this.getAnnotations()).add(annotation).build();
    -    return new AutoValue_TestInfo(getMethod(), getName(), getParameterNames(), newAnnotations);
    +    return new AutoValue_TestInfo(getMethod(), getName(), getParameters(), newAnnotations);
       }
     
    -  private TestInfo withName(String otherName) {
    -    return new AutoValue_TestInfo(getMethod(), otherName, getParameterNames(), getAnnotations());
    +  @VisibleForTesting
    +  TestInfo withName(String otherName) {
    +    return new AutoValue_TestInfo(getMethod(), otherName, getParameters(), getAnnotations());
       }
     
       public static TestInfo legacyCreate(Method method, String name, List annotations) {
         return new AutoValue_TestInfo(
    -        method, name, /* parameterNames= */ ImmutableList.of(), ImmutableList.copyOf(annotations));
    +        method, name, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations));
       }
     
       static TestInfo createWithoutParameters(Method method, List annotations) {
         return new AutoValue_TestInfo(
             method,
    -        getDefaultName(method, /* parameterNames= */ ImmutableList.of()),
    -        /* parameterNames= */ ImmutableList.of(),
    +        getDefaultName(method, /* parameters= */ ImmutableList.of()),
    +        /* parameters= */ ImmutableList.of(),
             ImmutableList.copyOf(annotations));
       }
     
    -  static ImmutableList shortenNamesIfNecessary(
    -      List testInfos, Function shorterNameFunction) {
    -    if (testInfos.stream().anyMatch(i -> i.getName().length() > MAX_TEST_NAME_LENGTH)) {
    -      return ImmutableList.copyOf(
    -          testInfos.stream()
    -              .map(testInfo -> testInfo.withName(shorterNameFunction.apply(testInfo)))
    -              .collect(toList()));
    +  static ImmutableList shortenNamesIfNecessary(List 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 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.withName(
    +                        String.format(
    +                            "%s[%s]",
    +                            info.getMethod().getName(),
    +                            IntStream.range(0, numberOfParameters)
    +                                .mapToObj(
    +                                    parameterIndex ->
    +                                        parameterIndicesThatNeedUpdate.contains(parameterIndex)
    +                                            ? getShortenedName(
    +                                                info.getParameters().get(parameterIndex),
    +                                                getMaxCharactersPerParameter(
    +                                                    info, numberOfParameters))
    +                                            : info.getParameters().get(parameterIndex).getName())
    +                                .collect(joining(",")))))
    +            .collect(toImmutableList());
    +      }
         } else {
           return ImmutableList.copyOf(testInfos);
         }
       }
     
    -  private static String getDefaultName(Method testMethod, List parameterNames) {
    -    if (parameterNames.isEmpty()) {
    +  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);
    +  }
    +
    +  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 String getDefaultName(Method testMethod, List parameters) {
    +    if (parameters.isEmpty()) {
           return testMethod.getName();
         } else {
           return String.format(
    -          "%s[%s]", testMethod.getName(), parameterNames.stream().collect(joining(",")));
    +          "%s[%s]",
    +          testMethod.getName(),
    +          parameters.stream().map(TestInfoParameter::getName).collect(joining(",")));
    +    }
    +  }
    +
    +  private static  Collector> 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();
    +
    +    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/TestParameterAnnotationMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    index de608af..4380f57 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java
    @@ -33,6 +33,7 @@ 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;
    @@ -53,6 +54,7 @@ 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;
    @@ -90,6 +92,9 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
         @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).
    @@ -139,13 +144,14 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
               !specifiedValues.isEmpty(),
               "The number of parameter values should not be 0"
                   + ", otherwise the parameter would cause the test to be skipped.");
    -      return specifiedValues.stream()
    -          .map(
    -              value ->
    +      return IntStream.range(0, specifiedValues.size())
    +          .mapToObj(
    +              valueIndex ->
                       new AutoValue_TestParameterAnnotationMethodProcessor_TestParameterValue(
                           AnnotationTypeOrigin.create(
                               annotationWithMetadata.annotation().annotationType(), origin),
    -                      value,
    +                      specifiedValues.get(valueIndex),
    +                      valueIndex,
                           new ArrayList<>(specifiedValues),
                           annotationWithMetadata.paramClass(),
                           annotationWithMetadata.paramName()))
    @@ -664,7 +670,10 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
               originalTest
                   .withExtraParameters(
                       testParameterValues.stream()
    -                      .map(TestParameterValue::toTestNameString)
    +                      .map(
    +                          param ->
    +                              TestInfoParameter.create(
    +                                  param.toTestNameString(), param.value(), param.valueIndex()))
                           .collect(toImmutableList()))
                   .withExtraAnnotation(
                       TestIndexHolderFactory.create(
    @@ -674,26 +683,7 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor {
                           testClass.getName())));
         }
     
    -    return TestInfo.shortenNamesIfNecessary(
    -        testInfos.build(),
    -        testInfo ->
    -            appendParametersToTestName(
    -                originalTest.getName(),
    -                String.valueOf(
    -                    testInfo.getAnnotation(TestIndexHolder.class).parametersIndex() + 1)));
    -  }
    -
    -  /**
    -   * Appends the given suffix to the given test name in brackets. If the original test name already
    -   * has brackets, the suffix is inserted in the existing brackets instead.
    -   */
    -  private static String appendParametersToTestName(String originalTestName, String testNameSuffix) {
    -    if (originalTestName.endsWith("]")) {
    -      return String.format(
    -          "%s,%s]", originalTestName.substring(0, originalTestName.length() - 1), testNameSuffix);
    -    } else {
    -      return String.format("%s[%s]", originalTestName, testNameSuffix);
    -    }
    +    return testInfos.build();
       }
     
       private List> getParameterValuesForMethod(Method method) {
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
    index ddbed2c..7796db0 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java
    @@ -27,6 +27,7 @@ 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;
    @@ -135,34 +136,39 @@ class TestParametersMethodProcessor implements TestMethodProcessor {
               ++methodParametersIndex) {
             Optional 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.orNull(), methodParameters.orNull())
    +                    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)
    -                        .map(TestParametersValues::name)
                             .collect(toImmutableList()))
                     .withExtraAnnotation(
                         TestIndexHolderFactory.create(
                             constructorParametersIndex, methodParametersIndex)));
           }
         }
    -    return TestInfo.shortenNamesIfNecessary(
    -        testInfos.build(),
    -        testInfo -> {
    -          TestIndexHolder annotation = testInfo.getAnnotation(TestIndexHolder.class);
    -          return maybeAppendToTestName(
    -              maybeAppendToTestName(
    -                  originalTest.getName(),
    -                  maybeParameterIndexString(
    -                      annotation.constructorParametersIndex(), constructorParametersList)),
    -              maybeParameterIndexString(annotation.methodParametersIndex(), methodParametersList));
    -        });
    -  }
    -
    -  private static Optional maybeParameterIndexString(
    -      int index, ImmutableList> parameterList) {
    -    return parameterList.get(index).transform(p -> String.valueOf(index + 1));
    +    return testInfos.build();
       }
     
       private ImmutableList>
    @@ -179,21 +185,6 @@ class TestParametersMethodProcessor implements TestMethodProcessor {
             : ImmutableList.of(Optional.absent());
       }
     
    -  private static String maybeAppendToTestName(
    -      String originalTestName, Optional maybeSuffix) {
    -    if (!maybeSuffix.isPresent()) {
    -      return originalTestName;
    -    } else {
    -      String suffixName = maybeSuffix.get();
    -      if (originalTestName.endsWith("]")) {
    -        return String.format(
    -            "%s,%s]", originalTestName.substring(0, originalTestName.length() - 1), suffixName);
    -      } else {
    -        return String.format("%s[%s]", originalTestName, suffixName);
    -      }
    -    }
    -  }
    -
       @Override
       public Statement processStatement(Statement originalStatement, Description finalTestDescription) {
         return originalStatement;
    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..40ce142
    --- /dev/null
    +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java
    @@ -0,0 +1,171 @@
    +/*
    + * 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 result = TestInfo.shortenNamesIfNecessary(ImmutableList.of());
    +
    +    assertThat(result).isEmpty();
    +  }
    +
    +  @Test
    +  public void shortenNamesIfNecessary_noParameters() throws Exception {
    +    ImmutableList givenTestInfos = ImmutableList.of(fakeTestInfo());
    +
    +    ImmutableList result = TestInfo.shortenNamesIfNecessary(givenTestInfos);
    +
    +    assertThat(result).containsExactlyElementsIn(givenTestInfos);
    +  }
    +
    +  @Test
    +  public void shortenNamesIfNecessary_veryLongTestMethodName_noParameters() throws Exception {
    +    ImmutableList givenTestInfos =
    +        ImmutableList.of(
    +            TestInfo.createWithoutParameters(
    +                getClass()
    +                    .getMethod(
    +                        "unusedMethodThatHasAVeryLongNameForTest000000000000000000000000000000000"
    +                            + "000000000000000000000000000000000000000000000000000000000000000000"
    +                            + "000000000000000000000000000000000000000000000000000000000000000000"
    +                            + "000000000000000000000000"),
    +                /* annotations= */ ImmutableList.of()));
    +
    +    ImmutableList result = TestInfo.shortenNamesIfNecessary(givenTestInfos);
    +
    +    assertThat(result).containsExactlyElementsIn(givenTestInfos);
    +  }
    +
    +  @Test
    +  public void shortenNamesIfNecessary_noShorteningNeeded() throws Exception {
    +    ImmutableList 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 result = TestInfo.shortenNamesIfNecessary(givenTestInfos);
    +
    +    assertThat(result).containsExactlyElementsIn(givenTestInfos);
    +  }
    +
    +  @Test
    +  public void shortenNamesIfNecessary_singleParameterTooLong_twoParameters() throws Exception {
    +    ImmutableList 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...]");
    +  }
    +
    +  @Test
    +  public void shortenNamesIfNecessary_singleParameterTooLong_onlyParameter() throws Exception {
    +    ImmutableList 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...]");
    +  }
    +
    +  @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 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]");
    +  }
    +
    +  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 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
    index f8f4de4..96f2d13 100644
    --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
    +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
    @@ -299,7 +299,11 @@ public class TestParameterAnnotationMethodProcessorTest {
     
         @AfterClass
         public static void completedAllParameterizedTests() {
    -      assertThat(allTestNames).containsExactly("test1[1]", "test1[2]");
    +      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...]");
         }
       }
     
    diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java
    index 3e583cf..6121d49 100644
    --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java
    +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java
    @@ -114,20 +114,21 @@ public class TestParametersMethodProcessorTest {
                   "TWO,22,true,DEF",
                   "test[{testEnum: null, testLong: 33, testBoolean: false, testString: null}]",
                   "null,33,false,null",
    -              "test2_withLongNames[1]",
    +              "test2_withLongNames[1.{testString: ABC}]",
                   "ABC",
    -              "test2_withLongNames[2]",
    +              "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[{testEnums: [ONE, TWO, THREE], testLongs: [11, 4],"
    -                  + " testBooleans: [false, true], testStrings: [ABC, '123']}]",
    +              "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[{testEnums: [TWO], testLongs: [22], testBooleans: [true],"
    +              "test3_withRepeatedParams[2.{testEnums: [TWO], testLongs: [22], testBooleans: [true],"
                       + " testStrings: ['DEF']}]",
                   "[TWO],[22],[true],[DEF]",
    -              "test3_withRepeatedParams[{testEnums: [], testLongs: [], testBooleans: [],"
    +              "test3_withRepeatedParams[3.{testEnums: [], testLongs: [], testBooleans: [],"
                       + " testStrings: []}]",
                   "[],[],[],[]");
         }
    @@ -372,10 +373,12 @@ public class TestParametersMethodProcessorTest {
                   "test2[{testEnum: ONE},{testString: DEF}]",
                   "test2[{testEnum: TWO},{testString: ABC}]",
                   "test2[{testEnum: TWO},{testString: DEF}]",
    -              "test3_withLongNames[1,1]",
    -              "test3_withLongNames[1,2]",
    -              "test3_withLongNames[2,1]",
    -              "test3_withLongNames[2,2]");
    +              "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();
    -- 
    cgit v1.2.3
    
    
    From 2b6a926905bafdce79afcdfb56f4264c4a881616 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Tue, 22 Jun 2021 20:55:21 +0100
    Subject: Stop storing the whole test name as a separate field.
    
    ---
     .../junit/testparameterinjector/TestInfo.java      | 89 ++++++++++++----------
     1 file changed, 47 insertions(+), 42 deletions(-)
    
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java
    index 76b4069..3efd622 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java
    @@ -21,12 +21,12 @@ import static java.util.stream.Collectors.joining;
     import static java.util.stream.Collectors.toSet;
     
     import com.google.auto.value.AutoValue;
    -import com.google.common.annotations.VisibleForTesting;
     import com.google.common.collect.ImmutableList;
     import java.lang.annotation.Annotation;
     import java.lang.reflect.Method;
     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;
    @@ -50,7 +50,16 @@ abstract class TestInfo {
     
       public abstract Method getMethod();
     
    -  public abstract String getName();
    +  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 getParameters();
     
    @@ -67,40 +76,50 @@ abstract class TestInfo {
       }
     
       TestInfo withExtraParameters(List parameters) {
    -    ImmutableList newParameters =
    +    return new AutoValue_TestInfo(
    +        getMethod(),
             ImmutableList.builder()
                 .addAll(this.getParameters())
                 .addAll(parameters)
    -            .build();
    -    return new AutoValue_TestInfo(
    -        getMethod(),
    -        TestInfo.getDefaultName(getMethod(), newParameters),
    -        newParameters,
    +            .build(),
             getAnnotations());
       }
     
       TestInfo withExtraAnnotation(Annotation annotation) {
         ImmutableList newAnnotations =
             ImmutableList.builder().addAll(this.getAnnotations()).add(annotation).build();
    -    return new AutoValue_TestInfo(getMethod(), getName(), getParameters(), newAnnotations);
    +    return new AutoValue_TestInfo(getMethod(), getParameters(), newAnnotations);
       }
     
    -  @VisibleForTesting
    -  TestInfo withName(String otherName) {
    -    return new AutoValue_TestInfo(getMethod(), otherName, getParameters(), getAnnotations());
    +  /**
    +   * 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 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 annotations) {
         return new AutoValue_TestInfo(
    -        method, name, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations));
    +        method, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations));
       }
     
       static TestInfo createWithoutParameters(Method method, List annotations) {
         return new AutoValue_TestInfo(
    -        method,
    -        getDefaultName(method, /* parameters= */ ImmutableList.of()),
    -        /* parameters= */ ImmutableList.of(),
    -        ImmutableList.copyOf(annotations));
    +        method, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations));
       }
     
       static ImmutableList shortenNamesIfNecessary(List testInfos) {
    @@ -130,20 +149,13 @@ abstract class TestInfo {
             return testInfos.stream()
                 .map(
                     info ->
    -                    info.withName(
    -                        String.format(
    -                            "%s[%s]",
    -                            info.getMethod().getName(),
    -                            IntStream.range(0, numberOfParameters)
    -                                .mapToObj(
    -                                    parameterIndex ->
    -                                        parameterIndicesThatNeedUpdate.contains(parameterIndex)
    -                                            ? getShortenedName(
    -                                                info.getParameters().get(parameterIndex),
    -                                                getMaxCharactersPerParameter(
    -                                                    info, numberOfParameters))
    -                                            : info.getParameters().get(parameterIndex).getName())
    -                                .collect(joining(",")))))
    +                    info.withUpdatedParameterNames(
    +                        (parameter, parameterIndex) ->
    +                            parameterIndicesThatNeedUpdate.contains(parameterIndex)
    +                                ? getShortenedName(
    +                                    parameter,
    +                                    getMaxCharactersPerParameter(info, numberOfParameters))
    +                                : info.getParameters().get(parameterIndex).getName()))
                 .collect(toImmutableList());
           }
         } else {
    @@ -176,17 +188,6 @@ abstract class TestInfo {
         }
       }
     
    -  private static String getDefaultName(Method testMethod, List parameters) {
    -    if (parameters.isEmpty()) {
    -      return testMethod.getName();
    -    } else {
    -      return String.format(
    -          "%s[%s]",
    -          testMethod.getName(),
    -          parameters.stream().map(TestInfoParameter::getName).collect(joining(",")));
    -    }
    -  }
    -
       private static  Collector> toImmutableList() {
         return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf);
       }
    @@ -205,6 +206,10 @@ abstract class TestInfo {
          */
         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(
    -- 
    cgit v1.2.3
    
    
    From c361514c8692349716d964b3b1a5bdae3827788a Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Tue, 22 Jun 2021 21:09:51 +0100
    Subject: Bugfix: Make sure there are never any duplicate test names.
    
    Reason: The other test name is ignored by the framework, even if the actual values were different.
    ---
     CHANGELOG.md                                       |  3 +
     .../testparameterinjector/PluggableTestRunner.java |  2 +-
     .../junit/testparameterinjector/TestInfo.java      | 89 ++++++++++++++++++++++
     .../junit/testparameterinjector/TestInfoTest.java  | 88 +++++++++++++++++++--
     ...TestParameterAnnotationMethodProcessorTest.java |  7 +-
     5 files changed, 182 insertions(+), 7 deletions(-)
    
    diff --git a/CHANGELOG.md b/CHANGELOG.md
    index 946e593..c2104df 100644
    --- a/CHANGELOG.md
    +++ b/CHANGELOG.md
    @@ -2,6 +2,9 @@
     
     - 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 
     
     ## 1.3
     
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java
    index 76d6c43..2c9a199 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java
    @@ -221,7 +221,7 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner {
                   .collect(toImmutableList());
         }
     
    -    testInfos = TestInfo.shortenNamesIfNecessary(testInfos);
    +    testInfos = TestInfo.deduplicateTestNames(TestInfo.shortenNamesIfNecessary(testInfos));
     
         return testInfos.stream()
             .map(testInfo -> new OverriddenFrameworkMethod(testInfo.getMethod(), testInfo))
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java
    index 3efd622..7d16b6e 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java
    @@ -22,8 +22,11 @@ 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;
    @@ -174,6 +177,16 @@ abstract class TestInfo {
             MAX_PARAMETER_NAME_LENGTH - 3);
       }
     
    +  static ImmutableList deduplicateTestNames(List 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) {
    @@ -188,6 +201,82 @@ abstract class TestInfo {
         }
       }
     
    +  private static ImmutableList maybeAddTypesIfDuplicate(List testInfos) {
    +    Multimap testNameToInfo =
    +        MultimapBuilder.linkedHashKeys().arrayListValues().build();
    +    for (TestInfo testInfo : testInfos) {
    +      testNameToInfo.put(testInfo.getName(), testInfo);
    +    }
    +
    +    return testNameToInfo.keySet().stream()
    +        .flatMap(
    +            testName -> {
    +              Collection 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 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 deduplicateWithNumberPrefixes(
    +      ImmutableList 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  Collector> toImmutableList() {
         return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf);
       }
    diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java
    index 40ce142..ae817f6 100644
    --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java
    +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java
    @@ -42,7 +42,7 @@ public class TestInfoTest {
     
         ImmutableList result = TestInfo.shortenNamesIfNecessary(givenTestInfos);
     
    -    assertThat(result).containsExactlyElementsIn(givenTestInfos);
    +    assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder();
       }
     
       @Test
    @@ -60,7 +60,7 @@ public class TestInfoTest {
     
         ImmutableList result = TestInfo.shortenNamesIfNecessary(givenTestInfos);
     
    -    assertThat(result).containsExactlyElementsIn(givenTestInfos);
    +    assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder();
       }
     
       @Test
    @@ -80,7 +80,7 @@ public class TestInfoTest {
     
         ImmutableList result = TestInfo.shortenNamesIfNecessary(givenTestInfos);
     
    -    assertThat(result).containsExactlyElementsIn(givenTestInfos);
    +    assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder();
       }
     
       @Test
    @@ -108,7 +108,8 @@ public class TestInfoTest {
             .containsExactly(
                 "toLowerCase[short,1.shorter]",
                 "toLowerCase[short,2.very long parameter name for test "
    -                + "0000000000000000000000000000000000000000000000000000...]");
    +                + "0000000000000000000000000000000000000000000000000000...]")
    +        .inOrder();
       }
     
       @Test
    @@ -132,7 +133,8 @@ public class TestInfoTest {
             .containsExactly(
                 "toLowerCase[1.shorter]",
                 "toLowerCase[2.very long parameter name for test"
    -                + " 000000000000000000000000000000000000000000000000000000000000...]");
    +                + " 000000000000000000000000000000000000000000000000000000000000...]")
    +        .inOrder();
       }
     
       @Test
    @@ -155,6 +157,82 @@ public class TestInfoTest {
                     + "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 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 result = TestInfo.deduplicateTestNames(givenTestInfos);
    +
    +    assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder();
    +  }
    +
    +  @Test
    +  public void deduplicateTestNames_duplicateParameterNamesWithDifferentTypes() throws Exception {
    +    ImmutableList 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 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(
    diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
    index 96f2d13..ec77dc2 100644
    --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
    +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java
    @@ -344,7 +344,12 @@ public class TestParameterAnnotationMethodProcessorTest {
         public static void completedAllParameterizedTests() {
           assertThat(allTestNames)
               .containsExactly(
    -              "test1[ABC]", "test1[ABC]", "test2[123]", "test2[123]", "test2[null]", "test2[null]");
    +              "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);
         }
       }
    -- 
    cgit v1.2.3
    
    
    From acb188b5e8c37e749c872dafa8007bc36f02920d Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Fri, 2 Jul 2021 17:44:47 +0100
    Subject: Change protobuf dependency to protobuf-lite.
    
    Fixes https://github.com/google/TestParameterInjector/issues/10
    ---
     pom.xml | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/pom.xml b/pom.xml
    index bc3e5be..b753189 100644
    --- a/pom.xml
    +++ b/pom.xml
    @@ -134,8 +134,8 @@
         
         
           com.google.protobuf
    -      protobuf-java
    -      3.14.0
    +      protobuf-lite
    +      3.0.1
         
         
           junit
    -- 
    cgit v1.2.3
    
    
    From e65d6bebdba9df211b258fae996fe34b6eadb787 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Mon, 5 Jul 2021 10:06:21 +0100
    Subject: Bump verstion to 1.4 and update CHANGELOG
    
    ---
     CHANGELOG.md | 3 ++-
     README.md    | 2 +-
     2 files changed, 3 insertions(+), 2 deletions(-)
    
    diff --git a/CHANGELOG.md b/CHANGELOG.md
    index c2104df..ebe26a6 100644
    --- a/CHANGELOG.md
    +++ b/CHANGELOG.md
    @@ -4,7 +4,8 @@
       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 
    +- Duplicate test names are given a suffix for deduplication
    +- Replaced dependency on `protobuf-java` by a dependency on `protobuf-javalite`
     
     ## 1.3
     
    diff --git a/README.md b/README.md
    index 767809a..7af215b 100644
    --- a/README.md
    +++ b/README.md
    @@ -52,7 +52,7 @@ And add the following dependency to your `.pom` file:
     
       com.google.testparameterinjector
       test-parameter-injector
    -  1.3
    +  1.4
     
     ```
     
    -- 
    cgit v1.2.3
    
    
    From 504f7b83bfca3ec702e7e17d16f2797df36a6250 Mon Sep 17 00:00:00 2001
    From: Jens Nyman 
    Date: Mon, 6 Sep 2021 09:00:42 +0100
    Subject: Internal refactor: Add PluggableTestRunner.sortTestMethods() and
     getSupportedTestAnnotations()
    
    ---
     .../testparameterinjector/PluggableTestRunner.java | 46 +++++++++---
     .../PluggableTestRunnerTest.java                   | 84 +++++++++++++++++++++-
     2 files changed, 119 insertions(+), 11 deletions(-)
    
    diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java
    index 2c9a199..3b2144a 100644
    --- a/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java
    +++ b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java
    @@ -83,15 +83,43 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner {
     
       /**
        * If true, all test methods (across different TestMethodProcessors) will be sorted in a
    -   * deterministic way by their test name.
    +   * deterministic way.
        *
        * 

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

    This should be deterministic. The order should not change, even when tests are added/removed + * or between releases. + */ + protected Stream sortTestMethods(Stream 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. + * + *

    Defaults to {@link Test}. + */ + protected ImmutableList> 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. @@ -146,14 +174,11 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { @Override protected final ImmutableList computeTestMethods() { Stream processedMethods = - super.computeTestMethods().stream().flatMap(method -> processMethod(method).stream()); + getSupportedTestAnnotations().stream() + .flatMap(annotation -> getTestClass().getAnnotatedMethods(annotation).stream()) + .flatMap(method -> processMethod(method).stream()); - if (shouldSortTestMethodsDeterministically()) { - processedMethods = - processedMethods.sorted( - comparing((FrameworkMethod method) -> method.getName().hashCode()) - .thenComparing(FrameworkMethod::getName)); - } + processedMethods = sortTestMethods(processedMethods); return processedMethods.collect(toImmutableList()); } @@ -324,7 +349,10 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { @Override protected final void validateTestMethods(List list) { - List testMethods = getTestClass().getAnnotatedMethods(Test.class); + List testMethods = + getSupportedTestAnnotations().stream() + .flatMap(annotation -> getTestClass().getAnnotatedMethods(annotation).stream()) + .collect(Collectors.toList()); for (FrameworkMethod testMethod : testMethods) { boolean isHandled = false; for (TestMethodProcessor testMethodProcessor : getTestMethodProcessors()) { diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java index 686b152..7662bd9 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java @@ -15,9 +15,15 @@ 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; @@ -30,8 +36,11 @@ 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 { @@ -49,7 +58,7 @@ public class PluggableTestRunnerTest { } @RunWith(PluggableTestRunner.class) - public static class PluggableTestRunnerTestClass { + public static class TestAndMethodRuleTestClass { @Rule public TestAndMethodRule rule = new TestAndMethodRule(); @@ -62,7 +71,7 @@ public class PluggableTestRunnerTest { @Test public void ruleThatIsBothTestRuleAndMethodRuleIsInvokedOnceOnly() throws Exception { PluggableTestRunner.run( - new PluggableTestRunner(PluggableTestRunnerTestClass.class) { + new PluggableTestRunner(TestAndMethodRuleTestClass.class) { @Override protected List createTestMethodProcessorList() { return ImmutableList.of(); @@ -71,4 +80,75 @@ public class PluggableTestRunnerTest { 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 createTestMethodProcessorList() { + return ImmutableList.of(); + } + + @Override + protected ImmutableList> getSupportedTestAnnotations() { + return ImmutableList.of(Test.class, CustomTest.class); + } + }); + + assertThat(testMethodInvocationCount).isEqualTo(2); + } + + private static final List 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 createTestMethodProcessorList() { + return ImmutableList.of(); + } + + @Override + protected Stream sortTestMethods(Stream methods) { + return methods.sorted(comparing(FrameworkMethod::getName).reversed()); + } + }); + assertThat(testOrder).containsExactly("c", "b", "a"); + } } -- cgit v1.2.3 From 8abfc2f337f1565d603157c706c62d9e3aab4e51 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 7 Sep 2021 10:20:23 +0100 Subject: Make map parsing work for integer keys --- .../ParameterValueParsing.java | 24 ++++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java index 624ee9b..5420189 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java @@ -18,9 +18,9 @@ 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 static java.util.stream.Collectors.toMap; 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; @@ -141,18 +141,24 @@ final class ParameterValueParsing { e, getGenericParameterType(javaType, /* parameterIndex= */ 0)))); yamlValueTransformer .ifJavaType(Map.class) - .supportParsedType( - Map.class, - map -> - Maps.transformValues( - map, - v -> - parseYamlObjectToJavaType( - v, getGenericParameterType(javaType, /* parameterIndex= */ 1)))); + .supportParsedType(Map.class, map -> parseYamlMapToJavaMap(map, javaType)); return yamlValueTransformer.transformedJavaValue(); } + private static Map parseYamlMapToJavaMap(Map map, TypeToken javaType) { + return map.entrySet().stream() + .collect( + toMap( + entry -> + parseYamlObjectToJavaType( + entry.getKey(), getGenericParameterType(javaType, /* parameterIndex= */ 0)), + entry -> + parseYamlObjectToJavaType( + entry.getValue(), + getGenericParameterType(javaType, /* parameterIndex= */ 1)))); + } + private static TypeToken getGenericParameterType(TypeToken typeToken, int parameterIndex) { checkArgument( typeToken.getType() instanceof ParameterizedType, -- cgit v1.2.3 From c518bf3ad309f3b80fe6765dda3013aea6ecd93f Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Thu, 9 Sep 2021 08:33:17 +0100 Subject: Test name size limits: Remove the limit on the max length of a single parameter --- CHANGELOG.md | 5 +++++ .../testing/junit/testparameterinjector/TestInfo.java | 19 ++++--------------- .../junit/testparameterinjector/TestInfoTest.java | 3 ++- .../TestParameterAnnotationMethodProcessorTest.java | 3 ++- .../TestParametersMethodProcessorTest.java | 13 +++++++------ 5 files changed, 20 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebe26a6..07580f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.5 + +- Test names with very long parameter strings are abbreviated differentily: In + some cases, more characters are allowed. + ## 1.4 - Bugfix: Run test methods declared in a base class (instead of throwing an diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java index 7d16b6e..5074ffc 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java @@ -16,7 +16,6 @@ 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; @@ -48,9 +47,6 @@ abstract class TestInfo { */ 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() { @@ -126,12 +122,7 @@ abstract class TestInfo { } static ImmutableList shortenNamesIfNecessary(List 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))) { + if (testInfos.stream().anyMatch(info -> info.getName().length() > MAX_TEST_NAME_LENGTH)) { int numberOfParameters = testInfos.get(0).getParameters().size(); if (numberOfParameters == 0) { @@ -170,11 +161,9 @@ abstract class TestInfo { 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); + + // Subtract 4 characters to leave place for joining commas and the parameter index. + return maxLengthOfAllParameters / numberOfParameters - 4; } static ImmutableList deduplicateTestNames(List testInfos) { diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java index ae817f6..e04a52a 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java @@ -133,7 +133,8 @@ public class TestInfoTest { .containsExactly( "toLowerCase[1.shorter]", "toLowerCase[2.very long parameter name for test" - + " 000000000000000000000000000000000000000000000000000000000000...]") + + " 000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "00000000000000000000000000000000000000000000000000000000000000000000...]") .inOrder(); } diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java index ec77dc2..bedde44 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java @@ -303,7 +303,8 @@ public class TestParameterAnnotationMethodProcessorTest { .containsExactly( "test1[1.ABC]", "test1[2.This is a very long string (240 characters) that would normally cause" - + " Sponge+Tin to exceed the...]"); + + " Sponge+Tin to exceed the filename limit of 255 characters." + + " =========================================================...]"); } } diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java index 6121d49..8a2e20c 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java @@ -116,19 +116,20 @@ public class TestParametersMethodProcessorTest { "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...]", + "test2_withLongNames[2.{testString: 'This is a very long string (240 characters) that" + + " would normally cause Sponge+Tin to exceed the filename limit of 255" + + " characters. =============================...]", "This is a very long string (240 characters) that would normally cause Sponge+Tin to" + " exceed the filename limit of 255 characters." + " =================================================================================" + "==============", - "test3_withRepeatedParams[1.{testEnums: [ONE, TWO, THREE], testLongs: [11, 4]," - + " testBooleans: [false, true], testStrings: [...]", + "test3_withRepeatedParams[{testEnums: [ONE, TWO, THREE], testLongs: [11, 4]," + + " testBooleans: [false, true], testStrings: [ABC, '123']}]", "[ONE, TWO, THREE],[11, 4],[false, true],[ABC, 123]", - "test3_withRepeatedParams[2.{testEnums: [TWO], testLongs: [22], testBooleans: [true]," + "test3_withRepeatedParams[{testEnums: [TWO], testLongs: [22], testBooleans: [true]," + " testStrings: ['DEF']}]", "[TWO],[22],[true],[DEF]", - "test3_withRepeatedParams[3.{testEnums: [], testLongs: [], testBooleans: []," + "test3_withRepeatedParams[{testEnums: [], testLongs: [], testBooleans: []," + " testStrings: []}]", "[],[],[],[]"); } -- cgit v1.2.3 From 951936c1cf3759c4769092b89d4236dfd5eaf5e8 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Thu, 9 Sep 2021 10:29:44 +0100 Subject: Don't assume all YAML maps have String keys (integer keys are also allowed) --- .../testparameterinjector/TestParametersMethodProcessor.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java index 7796db0..32bc22a 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java @@ -366,8 +366,7 @@ class TestParametersMethodProcessor implements TestMethodProcessor { yamlMapObject instanceof Map, "Cannot map YAML string '%s' to parameters because it is not a mapping", yamlString); - @SuppressWarnings("unchecked") - Map yamlMap = (Map) yamlMapObject; + Map yamlMap = (Map) yamlMapObject; ImmutableMap parametersByName = Maps.uniqueIndex(parameters, Parameter::getName); @@ -377,11 +376,14 @@ class TestParametersMethodProcessor implements TestMethodProcessor { yamlString, parametersByName.keySet()); + @SuppressWarnings("unchecked") + Map checkedYamlMap = (Map) yamlMap; + return TestParametersValues.builder() .name(yamlString) .addParameters( Maps.transformEntries( - yamlMap, + checkedYamlMap, (parameterName, parsedYaml) -> ParameterValueParsing.parseYamlObjectToJavaType( parsedYaml, -- cgit v1.2.3 From 64d2bbf38ce67d6dfae50df2cc8f3c24e6f2c4ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Oct 2021 01:28:40 +0000 Subject: Bump actions/checkout from 2.3.4 to 2.3.5 Bumps [actions/checkout](https://github.com/actions/checkout) from 2.3.4 to 2.3.5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2.3.4...v2.3.5) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yaml | 2 +- .github/workflows/release.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 579478e..030ae90 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.3.4 + - uses: actions/checkout@v2.3.5 - uses: actions/setup-java@v2 with: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c95bf04..348a66d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.3.4 + - uses: actions/checkout@v2.3.5 - uses: actions/setup-java@v2 with: -- cgit v1.2.3 From d2219b56dd4f01ae0b1911ebe393294a2caccbda Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 26 Oct 2021 12:27:25 +0100 Subject: TestParameterInjector: Simplification: Use Executable to combine constructors and methods. --- .../TestParametersMethodProcessor.java | 26 ++++++---------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java index 32bc22a..0f445f7 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java @@ -34,6 +34,7 @@ import com.google.testing.junit.testparameterinjector.TestParameters.TestParamet import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; @@ -54,24 +55,11 @@ class TestParametersMethodProcessor implements TestMethodProcessor { private final TestClass testClass; - private final LoadingCache> + private final LoadingCache> 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()))); + .build(CacheLoader.from(TestParametersMethodProcessor::toParameterValuesList)); public TestParametersMethodProcessor(TestClass testClass) { this.testClass = testClass; @@ -248,8 +236,8 @@ class TestParametersMethodProcessor implements TestMethodProcessor { return parameterValuesByConstructorOrMethodCache.getUnchecked(method); } - private static ImmutableList toParameterValuesList( - Object methodOrConstructor, TestParameters annotation, Parameter[] invokableParameters) { + private static ImmutableList toParameterValuesList(Executable executable) { + TestParameters annotation = executable.getAnnotation(TestParameters.class); boolean valueIsSet = annotation.value().length > 0; boolean valuesProviderIsSet = !annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class); @@ -263,7 +251,7 @@ class TestParametersMethodProcessor implements TestMethodProcessor { "Either value or valuesProvider must be set on annotation %s", annotation); - ImmutableList parametersList = ImmutableList.copyOf(invokableParameters); + ImmutableList parametersList = ImmutableList.copyOf(executable.getParameters()); checkState( parametersList.stream().allMatch(Parameter::isNamePresent), "" @@ -290,7 +278,7 @@ class TestParametersMethodProcessor implements TestMethodProcessor { + "\n" + "\n" + "Don't forget to run `mvn clean` after making this change.", - methodOrConstructor); + executable); if (valueIsSet) { return stream(annotation.value()) .map(yamlMap -> toParameterValues(yamlMap, parametersList)) -- cgit v1.2.3 From 9187e4bb4772e74552671e394a723f786aefaa38 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 27 Oct 2021 10:03:02 +0100 Subject: Extract logic that checks for @TestParameters annotation in separate method. This is in preparation for follow-up work that allows @TestParamters to be repeatable. This change can lead to a repeated version of the annotation to be present instead. --- .../TestParametersMethodProcessor.java | 29 ++++++++++++++-------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java index 0f445f7..b5e173c 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java @@ -67,7 +67,7 @@ class TestParametersMethodProcessor implements TestMethodProcessor { @Override public ValidationResult validateConstructor(TestClass testClass, List exceptions) { - if (testClass.getOnlyConstructor().isAnnotationPresent(TestParameters.class)) { + if (hasRelevantAnnotation(testClass.getOnlyConstructor())) { try { // This method throws an exception if there is a validation error getConstructorParameters(); @@ -83,7 +83,7 @@ class TestParametersMethodProcessor implements TestMethodProcessor { @Override public ValidationResult validateTestMethod( TestClass testClass, FrameworkMethod testMethod, List exceptions) { - if (testMethod.getMethod().isAnnotationPresent(TestParameters.class)) { + if (hasRelevantAnnotation(testMethod.getMethod())) { try { // This method throws an exception if there is a validation error getMethodParameters(testMethod.getMethod()); @@ -98,10 +98,8 @@ class TestParametersMethodProcessor implements TestMethodProcessor { @Override public List processTest(Class clazz, TestInfo originalTest) { - boolean constructorIsParameterized = - testClass.getOnlyConstructor().isAnnotationPresent(TestParameters.class); - boolean methodIsParameterized = - originalTest.getMethod().isAnnotationPresent(TestParameters.class); + boolean constructorIsParameterized = hasRelevantAnnotation(testClass.getOnlyConstructor()); + boolean methodIsParameterized = hasRelevantAnnotation(originalTest.getMethod()); if (!constructorIsParameterized && !methodIsParameterized) { return ImmutableList.of(originalTest); @@ -161,14 +159,14 @@ class TestParametersMethodProcessor implements TestMethodProcessor { private ImmutableList> getConstructorParametersOrSingleAbsentElement() { - return testClass.getOnlyConstructor().isAnnotationPresent(TestParameters.class) + return hasRelevantAnnotation(testClass.getOnlyConstructor()) ? getConstructorParameters().stream().map(Optional::of).collect(toImmutableList()) : ImmutableList.of(Optional.absent()); } private ImmutableList> getMethodParametersOrSingleAbsentElement( Method method) { - return method.isAnnotationPresent(TestParameters.class) + return hasRelevantAnnotation(method) ? getMethodParameters(method).stream().map(Optional::of).collect(toImmutableList()) : ImmutableList.of(Optional.absent()); } @@ -181,7 +179,7 @@ class TestParametersMethodProcessor implements TestMethodProcessor { @Override public Optional createTest( TestClass testClass, FrameworkMethod method, Optional test) { - if (testClass.getOnlyConstructor().isAnnotationPresent(TestParameters.class)) { + if (hasRelevantAnnotation(testClass.getOnlyConstructor())) { ImmutableList parameterValuesList = getConstructorParameters(); TestParametersValues parametersValues = parameterValuesList.get( @@ -207,7 +205,7 @@ class TestParametersMethodProcessor implements TestMethodProcessor { FrameworkMethod method, Object testObject, Optional statement) { - if (method.getMethod().isAnnotationPresent(TestParameters.class)) { + if (hasRelevantAnnotation(method.getMethod())) { ImmutableList parameterValuesList = getMethodParameters(method.getMethod()); TestParametersValues parametersValues = @@ -379,6 +377,17 @@ class TestParametersMethodProcessor implements TestMethodProcessor { .build(); } + // Note: We're not using the Executable interface here because it isn't supported by Java 7 and + // this code is called even if only @TestParameter is used. In other places, Executable is usable + // because @TestParameters only works for Java 8 anyway. + private static boolean hasRelevantAnnotation(Constructor executable) { + return executable.isAnnotationPresent(TestParameters.class); + } + + private static boolean hasRelevantAnnotation(Method executable) { + return executable.isAnnotationPresent(TestParameters.class); + } + private static Object[] toParameterArray( TestParametersValues parametersValues, Parameter[] parameters) { return stream(parameters) -- cgit v1.2.3 From 28fbbc1b1fb50a492ebc02dc670a653e7a030a4c Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 27 Oct 2021 16:28:19 +0100 Subject: Propagate IllegalStateExceptions directly and test its error messages. --- .../TestParametersMethodProcessor.java | 34 ++++++-- .../TestParametersMethodProcessorTest.java | 96 ++++++++++++++++------ 2 files changed, 100 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java index b5e173c..a3432bb 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java @@ -19,6 +19,7 @@ import static java.util.Arrays.stream; import com.google.auto.value.AutoAnnotation; import com.google.common.base.Optional; +import com.google.common.base.Throwables; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; @@ -27,6 +28,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.google.common.primitives.Primitives; import com.google.common.reflect.TypeToken; +import com.google.common.util.concurrent.UncheckedExecutionException; import com.google.testing.junit.testparameterinjector.TestInfo.TestInfoParameter; import com.google.testing.junit.testparameterinjector.TestParameters.DefaultTestParametersValuesProvider; import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValues; @@ -38,6 +40,7 @@ import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; @@ -227,11 +230,25 @@ class TestParametersMethodProcessor implements TestMethodProcessor { } private ImmutableList getConstructorParameters() { - return parameterValuesByConstructorOrMethodCache.getUnchecked(testClass.getOnlyConstructor()); + try { + return parameterValuesByConstructorOrMethodCache.getUnchecked(testClass.getOnlyConstructor()); + } catch (UncheckedExecutionException e) { + // Rethrow IllegalStateException because they can be caused by user mistakes and the user + // doesn't need to know that the caching layer is in between. + Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); + throw e; + } } private ImmutableList getMethodParameters(Method method) { - return parameterValuesByConstructorOrMethodCache.getUnchecked(method); + try { + return parameterValuesByConstructorOrMethodCache.getUnchecked(method); + } catch (UncheckedExecutionException e) { + // Rethrow IllegalStateException because they can be caused by user mistakes and the user + // doesn't need to know that the caching layer is in between. + Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); + throw e; + } } private static ImmutableList toParameterValuesList(Executable executable) { @@ -242,12 +259,15 @@ class TestParametersMethodProcessor implements TestMethodProcessor { checkState( !(valueIsSet && valuesProviderIsSet), - "It is not allowed to specify both value and valuesProvider on annotation %s", - annotation); + "It is not allowed to specify both value and valuesProvider in @TestParameters(value=%s," + + " valuesProvider=%s) on %s()", + Arrays.toString(annotation.value()), + annotation.valuesProvider().getSimpleName(), + executable.getName()); checkState( valueIsSet || valuesProviderIsSet, - "Either value or valuesProvider must be set on annotation %s", - annotation); + "Either a value or a valuesProvider must be set in @TestParameters on %s()", + executable.getName()); ImmutableList parametersList = ImmutableList.copyOf(executable.getParameters()); checkState( @@ -276,7 +296,7 @@ class TestParametersMethodProcessor implements TestMethodProcessor { + "\n" + "\n" + "Don't forget to run `mvn clean` after making this change.", - executable); + executable.getName()); if (valueIsSet) { return stream(annotation.value()) .map(yamlMap -> toParameterValues(yamlMap, parametersList)) diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java index 8a2e20c..928a108 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java @@ -15,8 +15,11 @@ package com.google.testing.junit.testparameterinjector; import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.truth.OptionalSubject.optionals; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValues; @@ -28,6 +31,7 @@ import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -44,7 +48,9 @@ import org.junit.runners.Parameterized.Parameters; public class TestParametersMethodProcessorTest { @Retention(RUNTIME) - @interface RunAsTest {} + @interface RunAsTest { + String failsWithMessage() default ""; + } public enum TestEnum { ONE, @@ -52,6 +58,16 @@ public class TestParametersMethodProcessorTest { THREE; } + private static final class TestEnumValuesProvider implements TestParametersValuesProvider { + @Override + public List 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()); + } + } + @RunAsTest public static class SimpleMethodAnnotation { @Rule public TestName testName = new TestName(); @@ -238,25 +254,6 @@ public class TestParametersMethodProcessorTest { "test2[two]", TestEnum.TWO, "test2[null-case]", null); } - - private static final class TestEnumValuesProvider implements TestParametersValuesProvider { - @Override - public List 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 { @@ -437,26 +434,79 @@ public class TestParametersMethodProcessorTest { } } + @RunAsTest( + failsWithMessage = + "Either a value or a valuesProvider must be set in @TestParameters on test1()") + public static class InvalidTestBecauseEmptyAnnotation { + @Test + @TestParameters + public void test1() {} + } + + @RunAsTest( + failsWithMessage = + "Either a value or a valuesProvider must be set in @TestParameters on" + + " com.google.testing.junit.testparameterinjector.TestParametersMethodProcessorTest" + + "$InvalidTestBecauseEmptyAnnotationOnConstructor()") + public static class InvalidTestBecauseEmptyAnnotationOnConstructor { + @TestParameters + public InvalidTestBecauseEmptyAnnotationOnConstructor() {} + + @Test + public void test1() {} + } + + @RunAsTest( + failsWithMessage = + "It is not allowed to specify both value and valuesProvider in" + + " @TestParameters(value=[{testEnum: ONE}], valuesProvider=TestEnumValuesProvider)" + + " on test1()") + public static class InvalidTestBecauseCombiningValueWithProvider { + @Test + @TestParameters(value = "{testEnum: ONE}", valuesProvider = TestEnumValuesProvider.class) + public void test1(TestEnum testEnum) {} + } + @Parameters(name = "{0}") public static Collection parameters() { return Arrays.stream(TestParametersMethodProcessorTest.class.getClasses()) .filter(cls -> cls.isAnnotationPresent(RunAsTest.class)) - .map(cls -> new Object[] {cls.getSimpleName(), cls}) + .map( + cls -> + new Object[] { + cls.getSimpleName(), cls, cls.getAnnotation(RunAsTest.class).failsWithMessage() + }) .collect(toImmutableList()); } private final Class testClass; + private final Optional maybeFailureMessage; - public TestParametersMethodProcessorTest(String name, Class testClass) { + public TestParametersMethodProcessorTest( + String name, Class testClass, String failsWithMessage) { this.testClass = testClass; + this.maybeFailureMessage = + failsWithMessage.isEmpty() ? Optional.empty() : Optional.of(failsWithMessage); } @Test - public void test() throws Exception { + public void test_success() throws Exception { + assume().about(optionals()).that(maybeFailureMessage).isEmpty(); + List failures = PluggableTestRunner.run(newTestRunner()); assertThat(failures).isEmpty(); } + @Test + public void test_failure() throws Exception { + assume().about(optionals()).that(maybeFailureMessage).isPresent(); + + IllegalStateException exception = + assertThrows(IllegalStateException.class, () -> PluggableTestRunner.run(newTestRunner())); + + assertThat(exception).hasMessageThat().isEqualTo(maybeFailureMessage.get()); + } + private PluggableTestRunner newTestRunner() throws Exception { return new PluggableTestRunner(testClass) { @Override -- cgit v1.2.3 From c19c2aad0af35778a26711ab3171598b542053c5 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 27 Oct 2021 16:30:08 +0100 Subject: Make @TestParameters repeatable --- .../testparameterinjector/TestParameters.java | 85 ++++++++---- .../TestParametersMethodProcessor.java | 152 ++++++++++++++------- .../TestParametersMethodProcessorTest.java | 106 ++++++++++---- 3 files changed, 242 insertions(+), 101 deletions(-) diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java index b7ee544..becdc5c 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java @@ -22,6 +22,7 @@ import static java.util.Collections.unmodifiableMap; import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.util.LinkedHashMap; @@ -30,29 +31,72 @@ 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. + * Annotation that can be placed (repeatedly) on @Test-methods or a test constructor to indicate the + * sets of parameters that it should be invoked with. * *

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

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

    Note: If this annotation is used in a test class, the other methods in that class can still + * use other types of parameterization, such as {@linkplain TestParameter @TestParameter}. * *

    See {@link #value()} for simple examples. */ @Retention(RUNTIME) @Target({CONSTRUCTOR, METHOD}) +@Repeatable(TestParameters.RepeatedTestParameters.class) public @interface TestParameters { /** - * Array of stringified set of parameters in YAML format. Each element corresponds to a single - * invocation of a test method. + * Specifies one or more stringified sets of parameters in YAML format. Each set corresponds to a + * single invocation of a test method. * *

    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: + * type if possible. See yaml.org for the YAML syntax and the section below on the supported + * parameter types. + * + *

    There are two distinct ways of using this annotation: repeated vs single: + * + *

    Recommended usage: Separate annotation per parameter set

    + * + * This approach uses multiple @TestParameters annotations, one for each set of parameters, for + * example: + * + *
    +   * {@literal @}Test
    +   * {@literal @}TestParameters("{age: 17, expectIsAdult: false}")
    +   * {@literal @}TestParameters("{age: 22, expectIsAdult: true}")
    +   * public void personIsAdult(int age, boolean expectIsAdult) { ... }
    +   *
    +   * {@literal @}Test
    +   * {@literal @}TestParameters("{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}")
    +   * {@literal @}TestParameters("{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}")
    +   * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... }
    +   * 
    + * + *

    Old discouraged usage: Single annotation with all parameter sets

    + * + * This approach uses a single @TestParameter annotation for all parameter sets, for example: + * + *
    +   * {@literal @}Test
    +   * {@literal @}TestParameters({
    +   *   "{age: 17, expectIsAdult: false}",
    +   *   "{age: 22, expectIsAdult: true}",
    +   * })
    +   * public void personIsAdult(int age, boolean expectIsAdult) { ... }
    +   *
    +   * {@literal @}Test
    +   * {@literal @}TestParameters({
    +   *   "{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}",
    +   *   "{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}",
    +   * })
    +   * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... }
    +   * 
    + * + *

    Supported parameter types

    * *
      *
    • YAML primitives: @@ -74,24 +118,6 @@ public @interface TestParameters { * *

      For dynamic sets of parameters or parameter types that are not supported here, use {@link * #valuesProvider()} and leave this field empty. - * - *

      Examples - * - *

      -   * {@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) { ... }
      -   * 
      */ String[] value() default {}; @@ -205,4 +231,13 @@ public @interface TestParameters { return ImmutableList.of(); } } + + /** + * Holder annotation for multiple @TestParameters annotations. This should never be used directly. + */ + @Retention(RUNTIME) + @Target({CONSTRUCTOR, METHOD}) + @interface RepeatedTestParameters { + TestParameters[] value(); + } } diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java index a3432bb..26e52b9 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java @@ -15,6 +15,7 @@ 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.util.Arrays.stream; import com.google.auto.value.AutoAnnotation; @@ -31,6 +32,7 @@ import com.google.common.reflect.TypeToken; import com.google.common.util.concurrent.UncheckedExecutionException; import com.google.testing.junit.testparameterinjector.TestInfo.TestInfoParameter; import com.google.testing.junit.testparameterinjector.TestParameters.DefaultTestParametersValuesProvider; +import com.google.testing.junit.testparameterinjector.TestParameters.RepeatedTestParameters; import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValues; import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValuesProvider; import java.lang.annotation.Retention; @@ -252,57 +254,51 @@ class TestParametersMethodProcessor implements TestMethodProcessor { } private static ImmutableList toParameterValuesList(Executable executable) { - TestParameters annotation = executable.getAnnotation(TestParameters.class); - boolean valueIsSet = annotation.value().length > 0; - boolean valuesProviderIsSet = - !annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class); - - checkState( - !(valueIsSet && valuesProviderIsSet), - "It is not allowed to specify both value and valuesProvider in @TestParameters(value=%s," - + " valuesProvider=%s) on %s()", - Arrays.toString(annotation.value()), - annotation.valuesProvider().getSimpleName(), - executable.getName()); - checkState( - valueIsSet || valuesProviderIsSet, - "Either a value or a valuesProvider must be set in @TestParameters on %s()", - executable.getName()); - + checkParameterNamesArePresent(executable); ImmutableList parametersList = ImmutableList.copyOf(executable.getParameters()); - 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 true to the" - + " maven-compiler-plugin's configuration. For example:\n" - + "\n" - + "\n" - + " \n" - + " \n" - + " org.apache.maven.plugins\n" - + " maven-compiler-plugin\n" - + " 3.8.1\n" - + " \n" - + " \n" - + " -parameters\n" - + " \n" - + " \n" - + " \n" - + " \n" - + "\n" - + "\n" - + "Don't forget to run `mvn clean` after making this change.", - executable.getName()); - if (valueIsSet) { - return stream(annotation.value()) - .map(yamlMap -> toParameterValues(yamlMap, parametersList)) + + if (executable.isAnnotationPresent(TestParameters.class)) { + checkState( + !executable.isAnnotationPresent(RepeatedTestParameters.class), + "Unexpected situation: Both @TestParameters and @RepeatedTestParameters annotating the" + + " same method"); + TestParameters annotation = executable.getAnnotation(TestParameters.class); + boolean valueIsSet = annotation.value().length > 0; + boolean valuesProviderIsSet = + !annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class); + + checkState( + !(valueIsSet && valuesProviderIsSet), + "It is not allowed to specify both value and valuesProvider in @TestParameters(value=%s," + + " valuesProvider=%s) on %s()", + Arrays.toString(annotation.value()), + annotation.valuesProvider().getSimpleName(), + executable.getName()); + checkState( + valueIsSet || valuesProviderIsSet, + "Either a value or a valuesProvider must be set in @TestParameters on %s()", + executable.getName()); + + if (valueIsSet) { + return stream(annotation.value()) + .map(yamlMap -> toParameterValues(yamlMap, parametersList)) + .collect(toImmutableList()); + } else { + return toParameterValuesList(annotation.valuesProvider(), parametersList); + } + } else { // Not annotated with @TestParameters + verify( + executable.isAnnotationPresent(RepeatedTestParameters.class), + "This method should only be called for executables with at least one relevant" + + " annotation"); + + return stream(executable.getAnnotation(RepeatedTestParameters.class).value()) + .map( + annotation -> + toParameterValues( + validateAndGetSingleValueFromRepeatedAnnotation(annotation, executable), + parametersList)) .collect(toImmutableList()); - } else { - return toParameterValuesList(annotation.valuesProvider(), parametersList); } } @@ -334,6 +330,58 @@ class TestParametersMethodProcessor implements TestMethodProcessor { } } + private static void checkParameterNamesArePresent(Executable executable) { + checkState( + stream(executable.getParameters()).allMatch(Parameter::isNamePresent), + "" + + "No parameter name could be found for %s, which likely means that parameter names" + + " aren't available at runtime. Please ensure that the this test was built with the" + + " -parameters compiler option.\n" + + "\n" + + "In Maven, you do this by adding true to the" + + " maven-compiler-plugin's configuration. For example:\n" + + "\n" + + "\n" + + " \n" + + " \n" + + " org.apache.maven.plugins\n" + + " maven-compiler-plugin\n" + + " 3.8.1\n" + + " \n" + + " \n" + + " -parameters\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + "\n" + + "Don't forget to run `mvn clean` after making this change.", + executable.getName()); + } + + private static String validateAndGetSingleValueFromRepeatedAnnotation( + TestParameters annotation, Executable executable) { + checkState( + annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class), + "Setting a valuesProvider is not supported for methods/constructors with" + + " multiple @TestParameters annotations on %s()", + executable.getName()); + checkState( + annotation.value().length > 0, + "Either a value or a valuesProvider must be set in @TestParameters on %s()", + executable.getName()); + checkState( + annotation.value().length == 1, + "When specifying more than one @TestParameter for a method/constructor, each annotation" + + " must have exactly one value. Instead, got %s values on %s(): %s", + annotation.value().length, + executable.getName(), + Arrays.toString(annotation.value())); + + return annotation.value()[0]; + } + private static void validateThatValuesMatchParameters( TestParametersValues testParametersValues, List parameters) { ImmutableMap parametersByName = @@ -401,11 +449,13 @@ class TestParametersMethodProcessor implements TestMethodProcessor { // this code is called even if only @TestParameter is used. In other places, Executable is usable // because @TestParameters only works for Java 8 anyway. private static boolean hasRelevantAnnotation(Constructor executable) { - return executable.isAnnotationPresent(TestParameters.class); + return executable.isAnnotationPresent(TestParameters.class) + || executable.isAnnotationPresent(RepeatedTestParameters.class); } private static boolean hasRelevantAnnotation(Method executable) { - return executable.isAnnotationPresent(TestParameters.class); + return executable.isAnnotationPresent(TestParameters.class) + || executable.isAnnotationPresent(RepeatedTestParameters.class); } private static Object[] toParameterArray( diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java index 928a108..e817005 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java @@ -79,37 +79,47 @@ public class TestParametersMethodProcessorTest { testNameToStringifiedParametersMap = new LinkedHashMap<>(); } + @Test + @TestParameters("{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}") + @TestParameters("{testEnum: TWO,\ntestLong: 22,\ntestBoolean: true,\r\n\r\n testString: 'DEF'}") + @TestParameters("{testEnum: null, testLong: 33, testBoolean: false, testString: null}") + public void test(TestEnum testEnum, long testLong, boolean testBoolean, String testString) { + testNameToStringifiedParametersMap.put( + testName.getMethodName(), + String.format("%s,%s,%s,%s", testEnum, testLong, testBoolean, testString)); + } + @Test @TestParameters({ "{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}", "{testEnum: TWO,\ntestLong: 22,\ntestBoolean: true,\r\n\r\n testString: 'DEF'}", "{testEnum: null, testLong: 33, testBoolean: false, testString: null}", }) - public void test(TestEnum testEnum, long testLong, boolean testBoolean, String testString) { + public void test_singleAnnotation( + 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." - + " =================================================================================" - + "=============='}" - }) + @TestParameters("{testString: ABC}") + @TestParameters( + "{testString: 'This is a very long string (240 characters) that would normally cause" + + " Sponge+Tin to exceed the filename limit of 255 characters." + + " =================================================================================" + + "=============='}") public void test2_withLongNames(String testString) { 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: []}", - }) + @TestParameters( + "{testEnums: [ONE, TWO, THREE], testLongs: [11, 4], testBooleans: [false, true]," + + " testStrings: [ABC, '123']}") + @TestParameters( + "{testEnums: [TWO],\ntestLongs: [22],\ntestBooleans: [true],\r\n\r\n testStrings: ['DEF']}") + @TestParameters("{testEnums: [], testLongs: [], testBooleans: [], testStrings: []}") public void test3_withRepeatedParams( List testEnums, List testLongs, @@ -130,6 +140,15 @@ public class TestParametersMethodProcessorTest { "TWO,22,true,DEF", "test[{testEnum: null, testLong: 33, testBoolean: false, testString: null}]", "null,33,false,null", + "test_singleAnnotation[{testEnum: ONE, testLong: 11, testBoolean: false, testString:" + + " ABC}]", + "ONE,11,false,ABC", + "test_singleAnnotation[{testEnum: TWO, testLong: 22, testBoolean: true, testString:" + + " 'DEF'}]", + "TWO,22,true,DEF", + "test_singleAnnotation[{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" @@ -277,7 +296,8 @@ public class TestParametersMethodProcessorTest { } @Test - @TestParameters({"{testEnum: ONE}", "{testEnum: TWO}"}) + @TestParameters("{testEnum: ONE}") + @TestParameters("{testEnum: TWO}") public void testInBase(TestEnum testEnum) { allTestNames.add(testName.getMethodName()); } @@ -311,7 +331,8 @@ public class TestParametersMethodProcessorTest { private static List testNamesThatInvokedBefore; private static List testNamesThatInvokedAfter; - @TestParameters({"{testEnum: ONE}", "{testEnum: TWO}"}) + @TestParameters("{testEnum: ONE}") + @TestParameters("{testEnum: TWO}") public MixedWithTestParameterMethodAnnotation(TestEnum testEnum) {} @BeforeClass @@ -340,19 +361,19 @@ public class TestParametersMethodProcessorTest { } @Test - @TestParameters({"{testString: ABC}", "{testString: DEF}"}) + @TestParameters("{testString: ABC}") + @TestParameters("{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." - + " =================================================================================" - + "=============='}" - }) + @TestParameters("{testString: ABC}") + @TestParameters( + "{testString: 'This is a very long string (240 characters) that would normally cause" + + " Sponge+Tin to exceed the filename limit of 255 characters." + + " =================================================================================" + + "=============='}") public void test3_withLongNames(String testString) { allTestNames.add(testName.getMethodName()); } @@ -391,7 +412,8 @@ public class TestParametersMethodProcessorTest { @TestParameter TestEnum testEnumA; - @TestParameters({"{testEnumB: ONE}", "{testEnumB: TWO}"}) + @TestParameters("{testEnumB: ONE}") + @TestParameters("{testEnumB: TWO}") public MixedWithTestParameterFieldAnnotation(TestEnum testEnumB) {} @BeforeClass @@ -462,11 +484,45 @@ public class TestParametersMethodProcessorTest { + " @TestParameters(value=[{testEnum: ONE}], valuesProvider=TestEnumValuesProvider)" + " on test1()") public static class InvalidTestBecauseCombiningValueWithProvider { + @Test @TestParameters(value = "{testEnum: ONE}", valuesProvider = TestEnumValuesProvider.class) public void test1(TestEnum testEnum) {} } + @RunAsTest( + failsWithMessage = + "Either a value or a valuesProvider must be set in @TestParameters on test1()") + public static class InvalidTestBecauseRepeatedAnnotationIsEmpty { + @Test + @TestParameters(value = "{testEnum: ONE}") + @TestParameters + public void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "When specifying more than one @TestParameter for a method/constructor, each annotation" + + " must have exactly one value. Instead, got 2 values on test1(): [{testEnum: TWO}," + + " {testEnum: THREE}]") + public static class InvalidTestBecauseRepeatedAnnotationHasMultipleValues { + @Test + @TestParameters(value = "{testEnum: ONE}") + @TestParameters(value = {"{testEnum: TWO}", "{testEnum: THREE}"}) + public void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "Setting a valuesProvider is not supported for methods/constructors with" + + " multiple @TestParameters annotations on test1()") + public static class InvalidTestBecauseRepeatedAnnotationHasProvider { + @Test + @TestParameters(valuesProvider = TestEnumValuesProvider.class) + @TestParameters(valuesProvider = TestEnumValuesProvider.class) + public void test1(TestEnum testEnum) {} + } + @Parameters(name = "{0}") public static Collection parameters() { return Arrays.stream(TestParametersMethodProcessorTest.class.getClasses()) -- cgit v1.2.3 From cd4eace1ba9fe536b9250a74a451acd04181bb66 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 27 Oct 2021 17:28:52 +0100 Subject: Fix for failing build: Remove dependency on OptionalSubject --- .../testparameterinjector/TestParametersMethodProcessorTest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java index e817005..aff5dd7 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java @@ -15,7 +15,6 @@ package com.google.testing.junit.testparameterinjector; import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.truth.OptionalSubject.optionals; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.TruthJUnit.assume; import static java.lang.annotation.RetentionPolicy.RUNTIME; @@ -547,7 +546,7 @@ public class TestParametersMethodProcessorTest { @Test public void test_success() throws Exception { - assume().about(optionals()).that(maybeFailureMessage).isEmpty(); + assume().that(maybeFailureMessage.isPresent()).isFalse(); List failures = PluggableTestRunner.run(newTestRunner()); assertThat(failures).isEmpty(); @@ -555,7 +554,7 @@ public class TestParametersMethodProcessorTest { @Test public void test_failure() throws Exception { - assume().about(optionals()).that(maybeFailureMessage).isPresent(); + assume().that(maybeFailureMessage.isPresent()).isTrue(); IllegalStateException exception = assertThrows(IllegalStateException.class, () -> PluggableTestRunner.run(newTestRunner())); -- cgit v1.2.3 From 2af80a726fb0276942ad7a648e8a24b89613acd9 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 27 Oct 2021 17:30:54 +0100 Subject: Repeated @TestParameters: Add documentation and update CHANGELOG --- CHANGELOG.md | 18 ++++++++++++++++++ README.md | 6 ++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07580f9..69c491b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ ## 1.5 +- `@TestParameters` can now also be used as a repeated annotation: + +```java +// Newly added and recommended for new code +@Test +@TestParameters("{age: 17, expectIsAdult: false}") +@TestParameters("{age: 22, expectIsAdult: true}") +public void withRepeatedAnnotation(int age,boolean expectIsAdult){...} + +// The old way of using @TestParameters is still supported +@Test +@TestParameters({ + "{age: 17, expectIsAdult: false}", + "{age: 22, expectIsAdult: true}", +}) +public void withSingleAnnotation(int age,boolean expectIsAdult){...} +``` + - Test names with very long parameter strings are abbreviated differentily: In some cases, more characters are allowed. diff --git a/README.md b/README.md index 7af215b..7dbafa2 100644 --- a/README.md +++ b/README.md @@ -207,10 +207,8 @@ mappings: ```java @Test -@TestParameters({ - "{age: 17, expectIsAdult: false}", - "{age: 22, expectIsAdult: true}", -}) +@TestParameters("{age: 17, expectIsAdult: false}") +@TestParameters("{age: 22, expectIsAdult: true}") public void personIsAdult(int age, boolean expectIsAdult) { ... } ``` -- cgit v1.2.3 From b5c8d083fc747986cb11c6c8387e99163fbbf633 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 29 Oct 2021 08:57:46 +0100 Subject: Roll back documentation change for repeated @TestParameters, because it is using an unreleased feature and will thus confuse clients using the latest release. --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7af215b..7dbafa2 100644 --- a/README.md +++ b/README.md @@ -207,10 +207,8 @@ mappings: ```java @Test -@TestParameters({ - "{age: 17, expectIsAdult: false}", - "{age: 22, expectIsAdult: true}", -}) +@TestParameters("{age: 17, expectIsAdult: false}") +@TestParameters("{age: 22, expectIsAdult: true}") public void personIsAdult(int age, boolean expectIsAdult) { ... } ``` -- cgit v1.2.3 From ea88d36e510bf0a33e34a71372eb4d7577b21cd1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Nov 2021 01:16:13 +0000 Subject: Bump actions/checkout from 2.3.5 to 2.4.0 Bumps [actions/checkout](https://github.com/actions/checkout) from 2.3.5 to 2.4.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2.3.5...v2.4.0) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yaml | 2 +- .github/workflows/release.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 030ae90..5759fb4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.3.5 + - uses: actions/checkout@v2.4.0 - uses: actions/setup-java@v2 with: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 348a66d..79ab019 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.3.5 + - uses: actions/checkout@v2.4.0 - uses: actions/setup-java@v2 with: -- cgit v1.2.3 From 8fa942efcb92390967b42b96aa826b09f4ee53e4 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 3 Nov 2021 09:48:19 +0000 Subject: Allow setting a custom name for @TestParameters. --- CHANGELOG.md | 13 ++++++++-- .../testparameterinjector/TestParameters.java | 12 ++++++++++ .../TestParametersMethodProcessor.java | 16 +++++++++---- .../TestParametersMethodProcessorTest.java | 28 +++++++++++++++++++++- 4 files changed, 62 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69c491b..50bc3a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ @Test @TestParameters("{age: 17, expectIsAdult: false}") @TestParameters("{age: 22, expectIsAdult: true}") -public void withRepeatedAnnotation(int age,boolean expectIsAdult){...} +public void withRepeatedAnnotation(int age, boolean expectIsAdult){...} // The old way of using @TestParameters is still supported @Test @@ -15,7 +15,16 @@ public void withRepeatedAnnotation(int age,boolean expectIsAdult){...} "{age: 17, expectIsAdult: false}", "{age: 22, expectIsAdult: true}", }) -public void withSingleAnnotation(int age,boolean expectIsAdult){...} +public void withSingleAnnotation(int age, boolean expectIsAdult){...} +``` + +- `@TestParameters` supports setting a custom test name: + +```java +@Test +@TestParameters(customName = "teenager", value = "{age: 17, expectIsAdult: false}") +@TestParameters(customName = "young adult", value = "{age: 22, expectIsAdult: true}") +public void personIsAdult(int age, boolean expectIsAdult){...} ``` - Test names with very long parameter strings are abbreviated differentily: In diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java index becdc5c..d58e470 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java @@ -121,6 +121,18 @@ public @interface TestParameters { */ String[] value() default {}; + /** + * Overrides the name of the parameter set that is used in the test name. + * + *

      This can only be set if {@link #value()} has exactly one element. If not set, the YAML + * string in {@link #value()} is used in the test name. + * + *

      For example: If this name is set to "young adult", then the test name might be + * "personIsAdult[young adult]" where the default might have been "personIsAdult[{age: 17, + * expectIsAdult: false}]". + */ + String customName() default ""; + /** * Sets a provider that will return a list of parameter sets. Each element in the returned list * corresponds to a single invocation of a test method. diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java index 26e52b9..076ea1c 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java @@ -278,10 +278,17 @@ class TestParametersMethodProcessor implements TestMethodProcessor { valueIsSet || valuesProviderIsSet, "Either a value or a valuesProvider must be set in @TestParameters on %s()", executable.getName()); + if (!annotation.customName().isEmpty()) { + checkState( + annotation.value().length == 1, + "Setting @TestParameters.customName is only allowed if there is exactly one YAML string" + + " in @TestParameters.value (on %s())", + executable.getName()); + } if (valueIsSet) { return stream(annotation.value()) - .map(yamlMap -> toParameterValues(yamlMap, parametersList)) + .map(yamlMap -> toParameterValues(yamlMap, parametersList, annotation.customName())) .collect(toImmutableList()); } else { return toParameterValuesList(annotation.valuesProvider(), parametersList); @@ -297,7 +304,8 @@ class TestParametersMethodProcessor implements TestMethodProcessor { annotation -> toParameterValues( validateAndGetSingleValueFromRepeatedAnnotation(annotation, executable), - parametersList)) + parametersList, + annotation.customName())) .collect(toImmutableList()); } } @@ -414,7 +422,7 @@ class TestParametersMethodProcessor implements TestMethodProcessor { } private static TestParametersValues toParameterValues( - String yamlString, List parameters) { + String yamlString, List parameters, String maybeCustomName) { Object yamlMapObject = ParameterValueParsing.parseYamlStringToObject(yamlString); checkState( yamlMapObject instanceof Map, @@ -434,7 +442,7 @@ class TestParametersMethodProcessor implements TestMethodProcessor { Map checkedYamlMap = (Map) yamlMap; return TestParametersValues.builder() - .name(yamlString) + .name(maybeCustomName.isEmpty() ? yamlString : maybeCustomName) .addParameters( Maps.transformEntries( checkedYamlMap, diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java index aff5dd7..6043b92 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java @@ -129,6 +129,14 @@ public class TestParametersMethodProcessorTest { String.format("%s,%s,%s,%s", testEnums, testLongs, testBooleans, testStrings)); } + @Test + @TestParameters(customName = "custom1", value = "{testEnum: ONE}") + @TestParameters("{testEnum: TWO}") + @TestParameters(customName = "custom3", value = "{testEnum: THREE}") + public void test4_withCustomName(TestEnum testEnum) { + testNameToStringifiedParametersMap.put(testName.getMethodName(), String.valueOf(testEnum)); + } + @AfterClass public static void completedAllParameterizedTests() { assertThat(testNameToStringifiedParametersMap) @@ -165,7 +173,13 @@ public class TestParametersMethodProcessorTest { "[TWO],[22],[true],[DEF]", "test3_withRepeatedParams[{testEnums: [], testLongs: [], testBooleans: []," + " testStrings: []}]", - "[],[],[],[]"); + "[],[],[],[]", + "test4_withCustomName[custom1]", + "ONE", + "test4_withCustomName[{testEnum: TWO}]", + "TWO", + "test4_withCustomName[custom3]", + "THREE"); } } @@ -522,6 +536,18 @@ public class TestParametersMethodProcessorTest { public void test1(TestEnum testEnum) {} } + @RunAsTest( + failsWithMessage = + "Setting @TestParameters.customName is only allowed if there is exactly one YAML string" + + " in @TestParameters.value (on test1())") + public static class InvalidTestBecauseNamedAnnotationHasMultipleValues { + @Test + @TestParameters( + customName = "custom", + value = {"{testEnum: TWO}", "{testEnum: THREE}"}) + public void test1(TestEnum testEnum) {} + } + @Parameters(name = "{0}") public static Collection parameters() { return Arrays.stream(TestParametersMethodProcessorTest.class.getClasses()) -- cgit v1.2.3 From ecdecf4ec289f4b1ff78d3f096833637fc98c27d Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 3 Nov 2021 09:49:12 +0000 Subject: Roll back documentation change for repeated @TestParameters, because it is using an unreleased feature and will thus confuse clients using the latest release. --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7dbafa2..7af215b 100644 --- a/README.md +++ b/README.md @@ -207,8 +207,10 @@ mappings: ```java @Test -@TestParameters("{age: 17, expectIsAdult: false}") -@TestParameters("{age: 22, expectIsAdult: true}") +@TestParameters({ + "{age: 17, expectIsAdult: false}", + "{age: 22, expectIsAdult: true}", +}) public void personIsAdult(int age, boolean expectIsAdult) { ... } ``` -- cgit v1.2.3 From cdad10dd56216414c780fe42fa9e479a69762eed Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 3 Nov 2021 10:07:33 +0000 Subject: Fix javadoc error in maven build: Use H4 instead of H2 headers Preceding header in javadoc is H3 --- .../google/testing/junit/testparameterinjector/TestParameters.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java index d58e470..bf71add 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java @@ -59,7 +59,7 @@ public @interface TestParameters { * *

      There are two distinct ways of using this annotation: repeated vs single: * - *

      Recommended usage: Separate annotation per parameter set

      + *

      Recommended usage: Separate annotation per parameter set

      * * This approach uses multiple @TestParameters annotations, one for each set of parameters, for * example: @@ -76,7 +76,7 @@ public @interface TestParameters { * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... } * * - *

      Old discouraged usage: Single annotation with all parameter sets

      + *

      Old discouraged usage: Single annotation with all parameter sets

      * * This approach uses a single @TestParameter annotation for all parameter sets, for example: * @@ -96,7 +96,7 @@ public @interface TestParameters { * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... } * * - *

      Supported parameter types

      + *

      Supported parameter types

      * *
        *
      • YAML primitives: -- cgit v1.2.3 From 4c437e700ec1805c5e6d9992be5c01acc05844d6 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Thu, 4 Nov 2021 21:01:09 +0000 Subject: Bump version to v1.5 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7af215b..2b87a58 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ And add the following dependency to your `.pom` file: com.google.testparameterinjector test-parameter-injector - 1.4 + 1.5 ``` -- cgit v1.2.3 From 9fb4faadcadba1e59a264277648919a4836e6747 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Thu, 4 Nov 2021 21:03:56 +0000 Subject: Documentation: Add example to add @TestParameters tests with a custom name --- README.md | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2b87a58..77c8b94 100644 --- a/README.md +++ b/README.md @@ -207,19 +207,40 @@ mappings: ```java @Test -@TestParameters({ - "{age: 17, expectIsAdult: false}", - "{age: 22, expectIsAdult: true}", -}) +@TestParameters("{age: 17, expectIsAdult: false}") +@TestParameters("{age: 22, expectIsAdult: true}") public void personIsAdult(int age, boolean expectIsAdult) { ... } ``` +which would generate the following tests: + +``` +MyTest#personIsAdult[{age: 17, expectIsAdult: false}] +MyTest#personIsAdult[{age: 22, expectIsAdult: true}] +``` + The string format supports the same types as `@TestParameter` (e.g. enums). See the `@TestParameters` javadoc for more info. `@TestParameters` works in the same way on the constructor, in which case all tests will be run for the given parameter sets. +> Tip: Consider setting a custom name if the YAML string is large: +> +> ```java +> @Test +> @TestParameters(customName = "teenager", value = "{age: 17, expectIsAdult: false}") +> @TestParameters(customName = "young adult", value = "{age: 22, expectIsAdult: true}") +> public void personIsAdult(int age, boolean expectIsAdult) { ... } +> ``` +> +> This will generate the following test names: +> +> ``` +> MyTest#personIsAdult[teenager] +> MyTest#personIsAdult[young adult] +> ``` + ## Advanced usage ### Dynamic parameter generation for `@TestParameter` -- cgit v1.2.3 From ab4ee9c615edbf4ca9a973c83eda51eba642fb11 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 5 Nov 2021 10:03:33 +0000 Subject: Replace

        by in @TestParameters. Reason: It seems to be impossible to satisfy the maven-javadoc-plugin constraints in both 'mvn -Psonatype-oss-release install' and 'mvn verify javadoc:javadoc' --- .../testing/junit/testparameterinjector/TestParameters.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java index bf71add..53af4df 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java @@ -59,9 +59,9 @@ public @interface TestParameters { * *

        There are two distinct ways of using this annotation: repeated vs single: * - *

        Recommended usage: Separate annotation per parameter set

        + *

        Recommended usage: Separate annotation per parameter set * - * This approach uses multiple @TestParameters annotations, one for each set of parameters, for + *

        This approach uses multiple @TestParameters annotations, one for each set of parameters, for * example: * *

        @@ -76,9 +76,9 @@ public @interface TestParameters {
            * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... }
            * 
        * - *

        Old discouraged usage: Single annotation with all parameter sets

        + *

        Old discouraged usage: Single annotation with all parameter sets * - * This approach uses a single @TestParameter annotation for all parameter sets, for example: + *

        This approach uses a single @TestParameter annotation for all parameter sets, for example: * *

            * {@literal @}Test
        @@ -96,7 +96,7 @@ public @interface TestParameters {
            * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... }
            * 
        * - *

        Supported parameter types

        + *

        Supported parameter types * *

          *
        • YAML primitives: -- cgit v1.2.3 From 625650b0d0d8603f6c892f8592f2053ff501d42c Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 5 Nov 2021 10:04:25 +0000 Subject: Document that @TestParameters needs the -parameters option to be enabled --- .../google/testing/junit/testparameterinjector/TestParameters.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java index 53af4df..9b1c5c9 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java @@ -42,6 +42,10 @@ import javax.annotation.Nullable; * use other types of parameterization, such as {@linkplain TestParameter @TestParameter}. * *

          See {@link #value()} for simple examples. + * + *

          Warning: This annotation can only be used if the compiled java code contains the parameter + * names. This is typically done by passing the {@code -parameters} option to the Java compiler, + * which requires using Java 8 or higher and may not be available on Android. */ @Retention(RUNTIME) @Target({CONSTRUCTOR, METHOD}) -- cgit v1.2.3 From 6e66e934444fa2a2bc47cf15799278f1d2e85eb8 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Mon, 8 Nov 2021 12:36:36 +0000 Subject: Support non-number floating point values This fixes https://github.com/google/TestParameterInjector/issues/14 --- .../testparameterinjector/ParameterValueParsing.java | 16 +++++++++++++--- .../testparameterinjector/ParameterValueParsingTest.java | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java index 5420189..f7c7cd6 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java @@ -96,13 +96,15 @@ final class ParameterValueParsing { .ifJavaType(Float.class) .supportParsedType(Float.class, identity()) .supportParsedType(Double.class, Double::floatValue) - .supportParsedType(Integer.class, Integer::floatValue); + .supportParsedType(Integer.class, Integer::floatValue) + .supportParsedType(String.class, Float::valueOf); yamlValueTransformer .ifJavaType(Double.class) .supportParsedType(Double.class, identity()) .supportParsedType(Integer.class, Integer::doubleValue) - .supportParsedType(Long.class, Long::doubleValue); + .supportParsedType(Long.class, Long::doubleValue) + .supportParsedType(String.class, Double::valueOf); yamlValueTransformer .ifJavaType(Enum.class) @@ -210,7 +212,15 @@ final class ParameterValueParsing { transformedJavaValue == null, "This case is already handled. This is a bug in" + " testparameterinjector.TestParametersMethodProcessor."); - transformedJavaValue = checkNotNull(transformation.apply((ParsedYamlT) parsedYaml)); + try { + transformedJavaValue = checkNotNull(transformation.apply((ParsedYamlT) parsedYaml)); + } catch (Exception e) { + throw new IllegalArgumentException( + String.format( + "Could not map YAML value %s (class = %s) to java class %s", + parsedYaml, parsedYaml.getClass(), javaType), + e); + } } } diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java index 2f36632..7b98707 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java @@ -75,6 +75,20 @@ public class ParameterValueParsingTest { /* yamlString= */ "442147483648", /* javaClass= */ Double.class, /* expectedResult= */ 442147483648.0), + NAN_TO_DOUBLE( + /* yamlString= */ "NaN", /* javaClass= */ Double.class, /* expectedResult= */ Double.NaN), + INFINITY_TO_DOUBLE( + /* yamlString= */ "Infinity", + /* javaClass= */ Double.class, + /* expectedResult= */ Double.POSITIVE_INFINITY), + POSITIVE_INFINITY_TO_DOUBLE( + /* yamlString= */ "+Infinity", + /* javaClass= */ Double.class, + /* expectedResult= */ Double.POSITIVE_INFINITY), + NEGATIVE_INFINITY_TO_DOUBLE( + /* yamlString= */ "-Infinity", + /* javaClass= */ Double.class, + /* expectedResult= */ Double.NEGATIVE_INFINITY), DOUBLE_TO_FLOAT( /* yamlString= */ "1.23", /* javaClass= */ Float.class, /* expectedResult= */ 1.23F), -- cgit v1.2.3 From 0dfad698ad6d4d36ebbf74e414301d645bb10683 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 10 Nov 2021 20:13:47 +0000 Subject: Bump version to v1.6 in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 77c8b94..c08aab8 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ And add the following dependency to your `.pom` file: com.google.testparameterinjector test-parameter-injector - 1.5 + 1.6 ``` -- cgit v1.2.3 From 555ae9ee1f292b3ae1e21fe2f659f7aec0fe8ccb Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 8 Dec 2021 14:07:52 +0000 Subject: Prepare TestParameterInjector codebase for JUnit5: Start making method processors JUnit4 independent See https://github.com/google/TestParameterInjector/issues/11. --- .../ParameterizedTestMethodProcessor.java | 35 ++--- .../testparameterinjector/PluggableTestRunner.java | 67 +++++---- .../testparameterinjector/TestMethodProcessor.java | 84 ++++++----- .../TestParameterAnnotationMethodProcessor.java | 165 ++++++++++----------- .../TestParametersMethodProcessor.java | 60 +++----- ...TestParameterAnnotationMethodProcessorTest.java | 37 +++-- .../testparameterinjector/TestParameterTest.java | 23 ++- .../TestParametersMethodProcessorTest.java | 24 ++- 8 files changed, 274 insertions(+), 221 deletions(-) diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java index dbafc6a..41bef2f 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java @@ -24,7 +24,9 @@ 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.lang.reflect.Method; import java.text.MessageFormat; +import java.util.ArrayList; import java.util.List; import org.junit.runner.Description; import org.junit.runners.Parameterized.Parameters; @@ -86,26 +88,21 @@ class ParameterizedTestMethodProcessor implements TestMethodProcessor { } @Override - public ValidationResult validateConstructor(TestClass testClass, List list) { + public ValidationResult validateConstructor(Constructor constructor) { 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( + return ValidationResult.validated( new IllegalStateException( "Mismatch constructor parameter count with values" + " returned by the @Parameters method")); - return ValidationResult.HANDLED; } + List errors = new ArrayList<>(); for (int i = 0; i < testParameters.length; i++) { if (!parameterTypes[i].isAssignableFrom(testParameters[i].getClass())) { - list.add( + errors.add( new IllegalStateException( String.format( "Mismatch constructor parameter type %s with value" @@ -113,15 +110,15 @@ class ParameterizedTestMethodProcessor implements TestMethodProcessor { parameterTypes[i], testParameters[i]))); } } - return ValidationResult.HANDLED; + return ValidationResult.validated(errors); + } else { + return ValidationResult.notValidated(); } - return ValidationResult.NOT_HANDLED; } @Override - public ValidationResult validateTestMethod( - TestClass testClass, FrameworkMethod testMethod, List errorsReturned) { - return ValidationResult.NOT_HANDLED; + public ValidationResult validateTestMethod(Method testMethod) { + return ValidationResult.notValidated(); } @Override @@ -174,12 +171,8 @@ class ParameterizedTestMethodProcessor implements TestMethodProcessor { } @Override - public Optional createStatement( - TestClass testClass, - FrameworkMethod method, - Object testObject, - Optional statement) { - return statement; + public Optional> maybeGetTestMethodParameters(TestInfo testInfo) { + return Optional.absent(); } /** diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java index 3b2144a..0139d34 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java @@ -281,16 +281,23 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { @Override protected final Statement methodInvoker(FrameworkMethod frameworkMethod, Object testObject) { - Optional statement = Optional.absent(); - for (TestMethodProcessor testMethodProcessor : getTestMethodProcessors()) { - statement = - testMethodProcessor.createStatement( - getTestClass(), frameworkMethod, testObject, statement); - } - if (statement.isPresent()) { - return statement.get(); + TestInfo testInfo = ((OverriddenFrameworkMethod) frameworkMethod).getTestInfo(); + Optional> maybeParameters = + getTestMethodProcessors().stream() + .map(processor -> processor.maybeGetTestMethodParameters(testInfo)) + .filter(Optional::isPresent) + .findFirst() + .orElse(Optional.absent()); + if (maybeParameters.isPresent()) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + frameworkMethod.invokeExplosively(testObject, maybeParameters.get().toArray()); + } + }; + } else { + return super.methodInvoker(frameworkMethod, testObject); } - return super.methodInvoker(frameworkMethod, testObject); } /** Modifies the statement with each {@link MethodRule} and {@link TestRule} */ @@ -338,32 +345,38 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { @Override protected final void validateZeroArgConstructor(List errorsReturned) { - for (TestMethodProcessor testMethodProcessor : getTestMethodProcessors()) { - if (testMethodProcessor.validateConstructor(getTestClass(), errorsReturned) - == ValidationResult.HANDLED) { - return; - } + ValidationResult validationResult = + getTestMethodProcessors().stream() + .map(processor -> processor.validateConstructor(getTestClass().getOnlyConstructor())) + .filter(ValidationResult::wasValidated) + .findFirst() + .orElse(ValidationResult.notValidated()); + + if (validationResult.wasValidated()) { + errorsReturned.addAll(validationResult.validationErrors()); + } else { + super.validateZeroArgConstructor(errorsReturned); } - super.validateZeroArgConstructor(errorsReturned); } @Override - protected final void validateTestMethods(List list) { + protected final void validateTestMethods(List errorsReturned) { List testMethods = getSupportedTestAnnotations().stream() .flatMap(annotation -> getTestClass().getAnnotatedMethods(annotation).stream()) - .collect(Collectors.toList()); + .collect(toImmutableList()); 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); + ValidationResult validationResult = + getTestMethodProcessors().stream() + .map(processor -> processor.validateTestMethod(testMethod.getMethod())) + .filter(ValidationResult::wasValidated) + .findFirst() + .orElse(ValidationResult.notValidated()); + + if (validationResult.wasValidated()) { + errorsReturned.addAll(validationResult.validationErrors()); + } else { + testMethod.validatePublicVoidNoArg(/* isStatic= */ false, errorsReturned); } } } diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java index 880327f..f9049ed 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java @@ -14,10 +14,16 @@ package com.google.testing.junit.testparameterinjector; +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.auto.value.AutoValue; import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Collection; 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; @@ -54,46 +60,54 @@ interface TestMethodProcessor { Optional createTest(TestClass testClass, FrameworkMethod method, Optional 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. - *

          The default implementation should return {@code statement}. + * If this processor can handle the given test, returns the parameters with which that method + * should be invoked. */ - Optional createStatement( - TestClass testClass, - FrameworkMethod method, - Object testObject, - Optional statement); + Optional> maybeGetTestMethodParameters(TestInfo testInfo); - /** - * 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 errorsReturned); + /** Optionally validates the given constructor. */ + ValidationResult validateConstructor(Constructor constructor); - /** - * 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 errorsReturned); + /** Optionally validates the given method. */ + ValidationResult validateTestMethod(Method testMethod); /** - * Whether the constructor or method validation has been handled or not. + * Value class that captures the result of a validating a single constructor or test method. * - *

          If the validation is not handled by a processor, it will be handled using the default {@link - * BlockJUnit4ClassRunner} validator. + *

          If the validation is not validated by any processor, it will be validated using the default + * validator. If a processor validates a constructor/test method, the remaining processors will + * *not* be called. */ - enum ValidationResult { - NOT_HANDLED, - HANDLED, + @AutoValue + abstract class ValidationResult { + + /** Returns true if the properties of the given constructor/test method were validated. */ + public abstract boolean wasValidated(); + + /** Returns the validation errors, if any. */ + public abstract ImmutableList validationErrors(); + + static ValidationResult notValidated() { + return of(/* wasValidated= */ false, /* validationErrors= */ ImmutableList.of()); + } + + static ValidationResult validated(Collection errors) { + return of(/* wasValidated= */ true, /* validationErrors= */ errors); + } + + static ValidationResult validated(Throwable error) { + return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of(error)); + } + + static ValidationResult valid() { + return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of()); + } + + private static ValidationResult of( + boolean wasValidated, Collection validationErrors) { + checkArgument(wasValidated || validationErrors.isEmpty()); + return new AutoValue_TestMethodProcessor_ValidationResult( + wasValidated, ImmutableList.copyOf(validationErrors)); + } } } diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 4380f57..0e9c1cc 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -41,6 +41,7 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; import java.text.MessageFormat; import java.util.ArrayList; @@ -478,63 +479,68 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { } @Override - public ValidationResult validateConstructor(TestClass testClass, List 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(); + public ValidationResult validateConstructor(Constructor constructor) { Class[] parameterTypes = constructor.getParameterTypes(); if (parameterTypes.length == 0) { - return ValidationResult.NOT_HANDLED; + return ValidationResult.notValidated(); } // 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; + return ValidationResult.validated( + validateMethodOrConstructorParameters( + removeOverrides( + getAnnotationTypeOrigins( + Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER), + testClass.getJavaClass()), + testClass, + constructor, + parameterTypes, + parameterAnnotations)); } @Override - public ValidationResult validateTestMethod( - TestClass testClass, FrameworkMethod testMethod, List errorsReturned) { - Class[] methodParameterTypes = testMethod.getMethod().getParameterTypes(); + public ValidationResult validateTestMethod(Method testMethod) { + Class[] methodParameterTypes = testMethod.getParameterTypes(); if (methodParameterTypes.length == 0) { - return ValidationResult.NOT_HANDLED; + return ValidationResult.notValidated(); } 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; + + List errors = new ArrayList<>(); + if (Modifier.isStatic(testMethod.getModifiers())) { + errors.add( + new Exception(String.format("Method %s() should not be static", testMethod.getName()))); + } + if (!Modifier.isPublic(testMethod.getModifiers())) { + errors.add( + new Exception(String.format("Method %s() should be public", testMethod.getName()))); + } + if (testMethod.getReturnType() != Void.TYPE) { + errors.add( + new Exception(String.format("Method %s() should return void", testMethod.getName()))); + } + Annotation[][] parametersAnnotations = testMethod.getParameterAnnotations(); + errors.addAll( + validateMethodOrConstructorParameters( + getAnnotationTypeOrigins(Origin.CLASS, Origin.METHOD, Origin.METHOD_PARAMETER), + testClass, + testMethod, + methodParameterTypes, + parametersAnnotations)); + + return ValidationResult.validated(errors); } } - private void validateMethodOrConstructorParameters( + private List validateMethodOrConstructorParameters( List annotationTypeOrigins, TestClass testClass, - List errors, AnnotatedElement methodOrConstructor, Class[] parameterTypes, Annotation[][] parametersAnnotations) { + List errors = new ArrayList<>(); + for (int parameterIndex = 0; parameterIndex < parameterTypes.length; parameterIndex++) { Class parameterType = parameterTypes[parameterIndex]; Annotation[] parameterAnnotations = parametersAnnotations[parameterIndex]; @@ -605,28 +611,50 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { parameterType.getName(), methodOrConstructor))); } } + return errors; } @Override - public Optional createStatement( - TestClass testClass, - FrameworkMethod frameworkMethod, - Object testObject, - Optional statement) { - if (frameworkMethod.getAnnotation(TestIndexHolder.class) == null + public Optional> maybeGetTestMethodParameters(TestInfo testInfo) { + Method testMethod = testInfo.getMethod(); + if (testInfo.getAnnotation(TestIndexHolder.class) == null // Explicitly skip @TestParameters annotated methods to ensure compatibility. // // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported) // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;). - // Without the @TestParameters check below, InvokeParameterizedMethod would be invoked for + // Without the @TestParameters check below, this class would try to find parameters for // these methods. When there are no method parameters, this is a no-op, but when the method // is annotated with @TestParameters, this throws an exception (because there are method // parameters that this processor has no values for - they are provided by the // @TestParameters processor). - || frameworkMethod.getAnnotation(TestParameters.class) != null) { - return statement; + || testMethod.isAnnotationPresent(TestParameters.class)) { + return Optional.absent(); } else { - return Optional.of(new InvokeParameterizedMethod(frameworkMethod, testObject)); + TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); + checkState(testIndexHolder != null); + List testParameterValues = + filterByOrigin( + getParameterValuesForTest(testIndexHolder), + Origin.CLASS, + Origin.METHOD, + Origin.METHOD_PARAMETER); + + Class[] parameterTypes = testMethod.getParameterTypes(); + Annotation[][] parametersAnnotations = testMethod.getParameterAnnotations(); + ArrayList parameterValues = + new ArrayList<>(/* initialCapacity= */ parameterTypes.length); + + List> processedAnnotationTypes = new ArrayList<>(); + for (int i = 0; i < parameterTypes.length; i++) { + parameterValues.add( + getParameterValue( + testParameterValues, + parameterTypes[i], + parametersAnnotations[i], + processedAnnotationTypes)); + } + + return Optional.of(parameterValues); } } @@ -1095,49 +1123,6 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { }; } - /** - * 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 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> 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 testParameterValues, diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java index 076ea1c..6255d68 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java @@ -17,6 +17,7 @@ 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.util.Arrays.stream; +import static java.util.stream.Collectors.toList; import com.google.auto.value.AutoAnnotation; import com.google.common.base.Optional; @@ -71,33 +72,32 @@ class TestParametersMethodProcessor implements TestMethodProcessor { } @Override - public ValidationResult validateConstructor(TestClass testClass, List exceptions) { - if (hasRelevantAnnotation(testClass.getOnlyConstructor())) { + public ValidationResult validateConstructor(Constructor constructor) { + if (hasRelevantAnnotation(constructor)) { try { // This method throws an exception if there is a validation error getConstructorParameters(); } catch (Throwable t) { - exceptions.add(t); + return ValidationResult.validated(t); } - return ValidationResult.HANDLED; + return ValidationResult.valid(); } else { - return ValidationResult.NOT_HANDLED; + return ValidationResult.notValidated(); } } @Override - public ValidationResult validateTestMethod( - TestClass testClass, FrameworkMethod testMethod, List exceptions) { - if (hasRelevantAnnotation(testMethod.getMethod())) { + public ValidationResult validateTestMethod(Method testMethod) { + if (hasRelevantAnnotation(testMethod)) { try { // This method throws an exception if there is a validation error - getMethodParameters(testMethod.getMethod()); + getMethodParameters(testMethod); } catch (Throwable t) { - exceptions.add(t); + return ValidationResult.validated(t); } - return ValidationResult.HANDLED; + return ValidationResult.valid(); } else { - return ValidationResult.NOT_HANDLED; + return ValidationResult.notValidated(); } } @@ -194,8 +194,8 @@ class TestParametersMethodProcessor implements TestMethodProcessor { Constructor constructor = testClass.getOnlyConstructor(); return Optional.of( constructor.newInstance( - toParameterArray( - parametersValues, testClass.getOnlyConstructor().getParameters()))); + toParameterList(parametersValues, testClass.getOnlyConstructor().getParameters()) + .toArray())); } catch (Exception e) { throw new RuntimeException(e); } @@ -205,29 +205,17 @@ class TestParametersMethodProcessor implements TestMethodProcessor { } @Override - public Optional createStatement( - TestClass testClass, - FrameworkMethod method, - Object testObject, - Optional statement) { - if (hasRelevantAnnotation(method.getMethod())) { - ImmutableList parameterValuesList = - getMethodParameters(method.getMethod()); + public Optional> maybeGetTestMethodParameters(TestInfo testInfo) { + Method testMethod = testInfo.getMethod(); + if (hasRelevantAnnotation(testMethod)) { + ImmutableList parameterValuesList = getMethodParameters(testMethod); 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())); - } - }); + testInfo.getAnnotation(TestIndexHolder.class).methodParametersIndex()); + + return Optional.of(toParameterList(parametersValues, testMethod.getParameters())); } else { - return statement; + return Optional.absent(); } } @@ -466,11 +454,11 @@ class TestParametersMethodProcessor implements TestMethodProcessor { || executable.isAnnotationPresent(RepeatedTestParameters.class); } - private static Object[] toParameterArray( + private static List toParameterList( TestParametersValues parametersValues, Parameter[] parameters) { return stream(parameters) .map(parameter -> parametersValues.parametersMap().get(parameter.getName())) - .toArray(); + .collect(toList()); } // Immutable collectors are re-implemented here because they are missing from the Android diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java index bedde44..fdc5d52 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java @@ -15,12 +15,15 @@ package com.google.testing.junit.testparameterinjector; import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.truth.Truth.assertThat; import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.util.stream.Collectors.joining; import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; +import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider; import java.lang.annotation.Annotation; @@ -1016,28 +1019,24 @@ public class TestParameterAnnotationMethodProcessorTest { @Test public void test() throws Exception { - List failures; switch (result) { case SUCCESS_ALWAYS: - failures = + assertNoFailures( PluggableTestRunner.run( newTestRunnerWithParameterizedSupport( - TestParameterAnnotationMethodProcessor::forAllAnnotationPlacements)); - assertThat(failures).isEmpty(); + TestParameterAnnotationMethodProcessor::forAllAnnotationPlacements))); - failures = + assertNoFailures( PluggableTestRunner.run( newTestRunnerWithParameterizedSupport( - TestParameterAnnotationMethodProcessor::onlyForFieldsAndParameters)); - assertThat(failures).isEmpty(); + TestParameterAnnotationMethodProcessor::onlyForFieldsAndParameters))); break; case SUCCESS_FOR_ALL_PLACEMENTS_ONLY: - failures = + assertNoFailures( PluggableTestRunner.run( newTestRunnerWithParameterizedSupport( - TestParameterAnnotationMethodProcessor::forAllAnnotationPlacements)); - assertThat(failures).isEmpty(); + TestParameterAnnotationMethodProcessor::forAllAnnotationPlacements))); assertThrows( IllegalStateException.class, @@ -1073,4 +1072,22 @@ public class TestParameterAnnotationMethodProcessorTest { } }; } + + private static void assertNoFailures(List failures) { + if (failures.size() == 1) { + throw new AssertionError(getOnlyElement(failures).getException()); + } else if (failures.size() > 1) { + throw new AssertionError( + String.format( + "Test failed unexpectedly:\n\n%s", + failures.stream() + .map( + f -> + String.format( + "<<%s>> %s", + f.getDescription(), + Throwables.getStackTraceAsString(f.getException()))) + .collect(joining("\n------------------------------------\n")))); + } + } } diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java index b16d5e1..efc5c4b 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java @@ -15,12 +15,15 @@ package com.google.testing.junit.testparameterinjector; import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.util.stream.Collectors.joining; import com.google.common.base.CharMatcher; +import com.google.common.base.Throwables; import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider; import java.lang.annotation.Retention; import java.util.ArrayList; @@ -206,6 +209,24 @@ public class TestParameterTest { } }); - assertThat(failures).isEmpty(); + assertNoFailures(failures); + } + + private static void assertNoFailures(List failures) { + if (failures.size() == 1) { + throw new AssertionError(getOnlyElement(failures).getException()); + } else if (failures.size() > 1) { + throw new AssertionError( + String.format( + "Test failed unexpectedly:\n\n%s", + failures.stream() + .map( + f -> + String.format( + "<<%s>> %s", + f.getDescription(), + Throwables.getStackTraceAsString(f.getException()))) + .collect(joining("\n------------------------------------\n")))); + } } } diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java index 6043b92..2db6bad 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java @@ -15,11 +15,14 @@ package com.google.testing.junit.testparameterinjector; import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.TruthJUnit.assume; import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.util.stream.Collectors.joining; import static org.junit.Assert.assertThrows; +import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValues; import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValuesProvider; @@ -575,7 +578,8 @@ public class TestParametersMethodProcessorTest { assume().that(maybeFailureMessage.isPresent()).isFalse(); List failures = PluggableTestRunner.run(newTestRunner()); - assertThat(failures).isEmpty(); + + assertNoFailures(failures); } @Test @@ -597,4 +601,22 @@ public class TestParametersMethodProcessorTest { } }; } + + private static void assertNoFailures(List failures) { + if (failures.size() == 1) { + throw new AssertionError(getOnlyElement(failures).getException()); + } else if (failures.size() > 1) { + throw new AssertionError( + String.format( + "Test failed unexpectedly:\n\n%s", + failures.stream() + .map( + f -> + String.format( + "<<%s>> %s", + f.getDescription(), + Throwables.getStackTraceAsString(f.getException()))) + .collect(joining("\n------------------------------------\n")))); + } + } } -- cgit v1.2.3 From 57c4927fa8c043a3adac7592f60cc29b046e239b Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 10 Dec 2021 14:21:35 +0000 Subject: Prepare TestParameterInjector codebase for JUnit5: Delete TestParameterProcessor (Google-internal feature) See https://github.com/google/TestParameterInjector/issues/11. --- .../TestParameterAnnotation.java | 15 ---- .../TestParameterAnnotationMethodProcessor.java | 53 +----------- .../TestParameterProcessor.java | 31 ------- ...TestParameterAnnotationMethodProcessorTest.java | 99 +--------------------- 4 files changed, 2 insertions(+), 196 deletions(-) delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestParameterProcessor.java diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java index a859a4f..8c04bc0 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java @@ -153,12 +153,6 @@ import java.util.Optional; /** Specifies a validator for the parameter to determine whether test should be skipped. */ Class validator() default DefaultValidator.class; - /** - * Specifies a processor for the parameter to invoke arbitrary code before and after the test - * statement's execution. - */ - Class processor() default DefaultProcessor.class; - /** Specifies a value provider for the parameter to provide the values to test. */ Class valueProvider() default DefaultValueProvider.class; @@ -171,15 +165,6 @@ import java.util.Optional; } } - /** 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. diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 0e9c1cc..1f9f5a9 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -1095,32 +1095,7 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { @Override public Statement processStatement(Statement originalStatement, Description finalTestDescription) { - TestIndexHolder testIndexHolder = finalTestDescription.getAnnotation(TestIndexHolder.class); - if (testIndexHolder == null) { - return originalStatement; - } - List 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()); - } - } - } - }; + return originalStatement; } /** Returns a {@link TestParameterAnnotation}'s value for a method or constructor parameter. */ @@ -1202,32 +1177,6 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { private TestIndexHolderFactory() {} } - /** Invokes the {@link TestParameterProcessor#before} method of an annotation. */ - private static void callBefore( - Class annotationType, Object annotationValue) { - TestParameterAnnotation annotation = - annotationType.getAnnotation(TestParameterAnnotation.class); - Class 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 annotationType, Object annotationValue) { - TestParameterAnnotation annotation = - annotationType.getAnnotation(TestParameterAnnotation.class); - Class 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}. diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterProcessor.java deleted file mode 100644 index efa4951..0000000 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterProcessor.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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. - * - *

          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/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java index fdc5d52..62299fc 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java @@ -590,7 +590,7 @@ public class TestParameterAnnotationMethodProcessorTest { } @Retention(RUNTIME) - @TestParameterAnnotation(validator = TestEnumValidator.class, processor = TestEnumProcessor.class) + @TestParameterAnnotation(validator = TestEnumValidator.class) public @interface EnumEvaluatorParameter { TestEnum[] value() default {}; } @@ -603,31 +603,6 @@ public class TestParameterAnnotationMethodProcessorTest { } } - public static class TestEnumProcessor implements TestParameterProcessor { - - static List beforeCalls = new ArrayList<>(); - static List 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 { @@ -648,16 +623,9 @@ public class TestParameterAnnotationMethodProcessorTest { 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); } } @@ -683,51 +651,9 @@ public class TestParameterAnnotationMethodProcessorTest { } } - @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 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); } } @@ -783,29 +709,6 @@ public class TestParameterAnnotationMethodProcessorTest { } } - @ClassTestResult(Result.SUCCESS_FOR_ALL_PLACEMENTS_ONLY) - @EnumEvaluatorParameter({TestEnum.ONE}) - public static class MethodClassOverrideClass { - - private static List 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 { -- cgit v1.2.3 From e35b9d9efcb0231efa2a0c74838a2043b0a286ef Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 15 Dec 2021 11:23:34 +0000 Subject: Prepare TestParameterInjector codebase for JUnit5: Make method processors JUnit4 independent See https://github.com/google/TestParameterInjector/issues/11. --- .../ExecutableValidationResult.java | 61 +++++++++ .../ParameterizedTestMethodProcessor.java | 56 ++++---- .../testparameterinjector/PluggableTestRunner.java | 88 ++++++------ .../testparameterinjector/TestMethodProcessor.java | 80 ++--------- .../TestParameterAnnotationMethodProcessor.java | 152 ++++++++++----------- .../TestParametersMethodProcessor.java | 47 +++---- 6 files changed, 240 insertions(+), 244 deletions(-) create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java b/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java new file mode 100644 index 0000000..5dc6344 --- /dev/null +++ b/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java @@ -0,0 +1,61 @@ +/* + * 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 com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import java.util.Collection; + +/** + * Value class that captures the result of a validating a single constructor or test method. + * + *

          If the validation is not validated by any processor, it will be validated using the default + * validator. If a processor validates a constructor/test method, the remaining processors will + * *not* be called. + */ +@AutoValue +abstract class ExecutableValidationResult { + + /** Returns true if the properties of the given constructor/test method were validated. */ + public abstract boolean wasValidated(); + + /** Returns the validation errors, if any. */ + public abstract ImmutableList validationErrors(); + + static ExecutableValidationResult notValidated() { + return of(/* wasValidated= */ false, /* validationErrors= */ ImmutableList.of()); + } + + static ExecutableValidationResult validated(Collection errors) { + return of(/* wasValidated= */ true, /* validationErrors= */ errors); + } + + static ExecutableValidationResult validated(Throwable error) { + return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of(error)); + } + + static ExecutableValidationResult valid() { + return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of()); + } + + private static ExecutableValidationResult of( + boolean wasValidated, Collection validationErrors) { + checkArgument(wasValidated || validationErrors.isEmpty()); + return new AutoValue_ExecutableValidationResult( + wasValidated, ImmutableList.copyOf(validationErrors)); + } +} diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java index 41bef2f..2895f48 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java @@ -27,11 +27,10 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.text.MessageFormat; import java.util.ArrayList; +import java.util.Arrays; 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; /** @@ -88,37 +87,37 @@ class ParameterizedTestMethodProcessor implements TestMethodProcessor { } @Override - public ValidationResult validateConstructor(Constructor constructor) { + public ExecutableValidationResult validateConstructor(Constructor constructor) { if (parametersForAllTests.isPresent()) { Class[] parameterTypes = constructor.getParameterTypes(); - Object[] testParameters = getTestParameters(0); + List testParameters = getTestParameters(0); - if (parameterTypes.length != testParameters.length) { - return ValidationResult.validated( + if (parameterTypes.length != testParameters.size()) { + return ExecutableValidationResult.validated( new IllegalStateException( "Mismatch constructor parameter count with values" + " returned by the @Parameters method")); } List errors = new ArrayList<>(); - for (int i = 0; i < testParameters.length; i++) { - if (!parameterTypes[i].isAssignableFrom(testParameters[i].getClass())) { + for (int i = 0; i < testParameters.size(); i++) { + if (!parameterTypes[i].isAssignableFrom(testParameters.get(i).getClass())) { errors.add( new IllegalStateException( String.format( "Mismatch constructor parameter type %s with value" + " returned by the @Parameters method: %s", - parameterTypes[i], testParameters[i]))); + parameterTypes[i], testParameters.get(i)))); } } - return ValidationResult.validated(errors); + return ExecutableValidationResult.validated(errors); } else { - return ValidationResult.notValidated(); + return ExecutableValidationResult.notValidated(); } } @Override - public ValidationResult validateTestMethod(Method testMethod) { - return ValidationResult.notValidated(); + public ExecutableValidationResult validateTestMethod(Method testMethod) { + return ExecutableValidationResult.notValidated(); } @Override @@ -150,24 +149,14 @@ class ParameterizedTestMethodProcessor implements TestMethodProcessor { } @Override - public Statement processStatement(Statement originalStatement, Description finalTestDescription) { - return originalStatement; - } - - @Override - public Optional createTest( - TestClass testClass, FrameworkMethod method, Optional test) { + public Optional> maybeGetConstructorParameters( + Constructor constructor, TestInfo testInfo) { if (parametersForAllTests.isPresent()) { - Object[] testParameters = - getTestParameters(method.getAnnotation(TestIndexHolder.class).testIndex()); - try { - Constructor constructor = testClass.getOnlyConstructor(); - return Optional.of(constructor.newInstance(testParameters)); - } catch (Exception e) { - throw new RuntimeException(e); - } + return Optional.of( + getTestParameters(testInfo.getAnnotation(TestIndexHolder.class).testIndex())); + } else { + return Optional.absent(); } - return test; } @Override @@ -175,6 +164,9 @@ class ParameterizedTestMethodProcessor implements TestMethodProcessor { return Optional.absent(); } + @Override + public void postProcessTestInstance(Object testInstance, TestInfo testInfo) {} + /** * 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. @@ -195,12 +187,12 @@ class ParameterizedTestMethodProcessor implements TestMethodProcessor { private TestIndexHolderFactory() {} } - private Object[] getTestParameters(int testIndex) { + private List getTestParameters(int testIndex) { Object parameters = Iterables.get(parametersForAllTests.get(), testIndex); if (parameters instanceof Object[]) { - return (Object[]) parameters; + return Arrays.asList((Object[]) parameters); } else { - return new Object[] {parameters}; + return Arrays.asList(parameters); } } diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java index 0139d34..3fbeeb2 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java @@ -23,8 +23,8 @@ 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.Constructor; import java.lang.reflect.Method; import java.util.List; import java.util.stream.Collector; @@ -60,7 +60,6 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { */ private static final ThreadLocal currentTestInfo = new ThreadLocal<>(); - private ImmutableList testRules; private List testMethodProcessors; protected PluggableTestRunner(Class klass) throws InitializationError { @@ -282,21 +281,26 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { @Override protected final Statement methodInvoker(FrameworkMethod frameworkMethod, Object testObject) { TestInfo testInfo = ((OverriddenFrameworkMethod) frameworkMethod).getTestInfo(); - Optional> maybeParameters = - getTestMethodProcessors().stream() - .map(processor -> processor.maybeGetTestMethodParameters(testInfo)) - .filter(Optional::isPresent) - .findFirst() - .orElse(Optional.absent()); - if (maybeParameters.isPresent()) { - return new Statement() { - @Override - public void evaluate() throws Throwable { - frameworkMethod.invokeExplosively(testObject, maybeParameters.get().toArray()); - } - }; - } else { + + if (testInfo.getMethod().getParameterTypes().length == 0) { return super.methodInvoker(frameworkMethod, testObject); + } else { + Optional> maybeParameters = + getTestMethodProcessors().stream() + .map(processor -> processor.maybeGetTestMethodParameters(testInfo)) + .filter(Optional::isPresent) + .findFirst() + .orElse(Optional.absent()); + if (maybeParameters.isPresent()) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + frameworkMethod.invokeExplosively(testObject, maybeParameters.get().toArray()); + } + }; + } else { + return super.methodInvoker(frameworkMethod, testObject); + } } } @@ -304,7 +308,6 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { private Statement withRules(FrameworkMethod method, Object target, Statement statement) { ImmutableList testRules = Stream.of( - getTestRulesForProcessors().stream(), getInnerTestRules().stream(), getTestRules(target).stream(), getOuterTestRules().stream()) @@ -330,13 +333,32 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { } private Object createTestForMethod(FrameworkMethod method) throws Exception { - Optional maybeTestInstance = Optional.absent(); + TestInfo testInfo = ((OverriddenFrameworkMethod) method).getTestInfo(); + Constructor constructor = getTestClass().getOnlyConstructor(); + + // Construct a test instance + Object testInstance; + if (constructor.getParameterTypes().length == 0) { + testInstance = createTest(); + } else { + List constructorParameters = + getTestMethodProcessors().stream() + .map(processor -> processor.maybeGetConstructorParameters(constructor, testInfo)) + .filter(Optional::isPresent) + .findFirst() + .get() + .get(); + try { + testInstance = constructor.newInstance(constructorParameters.toArray()); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + // Run all post processors on the newly created instance for (TestMethodProcessor testMethodProcessor : getTestMethodProcessors()) { - maybeTestInstance = testMethodProcessor.createTest(getTestClass(), method, maybeTestInstance); + testMethodProcessor.postProcessTestInstance(testInstance, testInfo); } - // If no processor created the test instance, fallback on the default implementation. - Object testInstance = - maybeTestInstance.isPresent() ? maybeTestInstance.get() : super.createTest(); finalizeCreatedTestInstance(testInstance); @@ -345,12 +367,12 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { @Override protected final void validateZeroArgConstructor(List errorsReturned) { - ValidationResult validationResult = + ExecutableValidationResult validationResult = getTestMethodProcessors().stream() .map(processor -> processor.validateConstructor(getTestClass().getOnlyConstructor())) - .filter(ValidationResult::wasValidated) + .filter(ExecutableValidationResult::wasValidated) .findFirst() - .orElse(ValidationResult.notValidated()); + .orElse(ExecutableValidationResult.notValidated()); if (validationResult.wasValidated()) { errorsReturned.addAll(validationResult.validationErrors()); @@ -366,12 +388,12 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { .flatMap(annotation -> getTestClass().getAnnotatedMethods(annotation).stream()) .collect(toImmutableList()); for (FrameworkMethod testMethod : testMethods) { - ValidationResult validationResult = + ExecutableValidationResult validationResult = getTestMethodProcessors().stream() .map(processor -> processor.validateTestMethod(testMethod.getMethod())) - .filter(ValidationResult::wasValidated) + .filter(ExecutableValidationResult::wasValidated) .findFirst() - .orElse(ValidationResult.notValidated()); + .orElse(ExecutableValidationResult.notValidated()); if (validationResult.wasValidated()) { errorsReturned.addAll(validationResult.validationErrors()); @@ -419,16 +441,6 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { return testMethodProcessors; } - private synchronized ImmutableList 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 diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java index f9049ed..cbe493b 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java @@ -14,19 +14,10 @@ package com.google.testing.junit.testparameterinjector; -import static com.google.common.base.Preconditions.checkArgument; - -import com.google.auto.value.AutoValue; import com.google.common.base.Optional; -import com.google.common.collect.ImmutableList; import java.lang.reflect.Constructor; import java.lang.reflect.Method; -import java.util.Collection; import java.util.List; -import org.junit.runner.Description; -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. @@ -40,74 +31,31 @@ interface TestMethodProcessor { List processTest(Class testClass, TestInfo originalTest); /** - * Allows to change the code executed during the test. + * If this processor can handle the given constructor, returns the parameters with which it should + * be invoked. * - * @param finalTestDescription the final description calculated taking into account this and all - * other test processors + *

          This method is never called for a parameterless constructor. */ - Statement processStatement(Statement originalStatement, Description finalTestDescription); + Optional> maybeGetConstructorParameters( + Constructor constructor, TestInfo testInfo); /** - * This method allows to transform the test object used for {@link #processStatement(Statement, - * Description)}. + * If this processor can handle the given test, returns the parameters with which {@code + * testInfo.getMethod()} should be invoked. * - * @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. - *

          The default implementation should return {@code test}. + *

          This method is never called for a parameterless {@code testInfo.getMethod()}. */ - Optional createTest(TestClass testClass, FrameworkMethod method, Optional test); + Optional> maybeGetTestMethodParameters(TestInfo testInfo); /** - * If this processor can handle the given test, returns the parameters with which that method - * should be invoked. + * Optionally process the test instance right after construction to ready it for the given test + * instance. */ - Optional> maybeGetTestMethodParameters(TestInfo testInfo); + void postProcessTestInstance(Object testInstance, TestInfo testInfo); /** Optionally validates the given constructor. */ - ValidationResult validateConstructor(Constructor constructor); + ExecutableValidationResult validateConstructor(Constructor constructor); /** Optionally validates the given method. */ - ValidationResult validateTestMethod(Method testMethod); - - /** - * Value class that captures the result of a validating a single constructor or test method. - * - *

          If the validation is not validated by any processor, it will be validated using the default - * validator. If a processor validates a constructor/test method, the remaining processors will - * *not* be called. - */ - @AutoValue - abstract class ValidationResult { - - /** Returns true if the properties of the given constructor/test method were validated. */ - public abstract boolean wasValidated(); - - /** Returns the validation errors, if any. */ - public abstract ImmutableList validationErrors(); - - static ValidationResult notValidated() { - return of(/* wasValidated= */ false, /* validationErrors= */ ImmutableList.of()); - } - - static ValidationResult validated(Collection errors) { - return of(/* wasValidated= */ true, /* validationErrors= */ errors); - } - - static ValidationResult validated(Throwable error) { - return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of(error)); - } - - static ValidationResult valid() { - return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of()); - } - - private static ValidationResult of( - boolean wasValidated, Collection validationErrors) { - checkArgument(wasValidated || validationErrors.isEmpty()); - return new AutoValue_TestMethodProcessor_ValidationResult( - wasValidated, ImmutableList.copyOf(validationErrors)); - } - } + ExecutableValidationResult validateTestMethod(Method testMethod); } diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 1f9f5a9..6a23746 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -58,9 +58,6 @@ 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; /** @@ -479,15 +476,15 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { } @Override - public ValidationResult validateConstructor(Constructor constructor) { + public ExecutableValidationResult validateConstructor(Constructor constructor) { Class[] parameterTypes = constructor.getParameterTypes(); if (parameterTypes.length == 0) { - return ValidationResult.notValidated(); + return ExecutableValidationResult.notValidated(); } // The constructor has parameters, they must be injected by a TestParameterAnnotation // annotation. Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); - return ValidationResult.validated( + return ExecutableValidationResult.validated( validateMethodOrConstructorParameters( removeOverrides( getAnnotationTypeOrigins( @@ -500,10 +497,10 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { } @Override - public ValidationResult validateTestMethod(Method testMethod) { + public ExecutableValidationResult validateTestMethod(Method testMethod) { Class[] methodParameterTypes = testMethod.getParameterTypes(); if (methodParameterTypes.length == 0) { - return ValidationResult.notValidated(); + return ExecutableValidationResult.notValidated(); } else { // The method has parameters, they must be injected by a TestParameterAnnotation annotation. @@ -529,7 +526,7 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { methodParameterTypes, parametersAnnotations)); - return ValidationResult.validated(errors); + return ExecutableValidationResult.validated(errors); } } @@ -614,6 +611,45 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { return errors; } + @Override + public Optional> maybeGetConstructorParameters( + Constructor constructor, TestInfo testInfo) { + if (testInfo.getAnnotation(TestIndexHolder.class) == null + // Explicitly skip @TestParameters annotated methods to ensure compatibility. + // + // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported) + // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;). + // Without the @TestParameters check below, this class would try to find parameters for + // these methods. When there are no method parameters, this is a no-op, but when the method + // is annotated with @TestParameters, this throws an exception (because there are method + // parameters that this processor has no values for - they are provided by the + // @TestParameters processor). + || constructor.isAnnotationPresent(TestParameters.class)) { + return Optional.absent(); + } else { + TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); + List testParameterValues = getParameterValuesForTest(testIndexHolder); + + Class[] parameterTypes = constructor.getParameterTypes(); + Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); + List parameterValues = new ArrayList<>(/* initialCapacity= */ parameterTypes.length); + List> processedAnnotationTypes = new ArrayList<>(); + List parameterValuesForConstructor = + filterByOrigin( + testParameterValues, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER); + for (int i = 0; i < parameterTypes.length; i++) { + // Initialize each parameter value from the corresponding TestParameterAnnotation value. + parameterValues.add( + getParameterValue( + parameterValuesForConstructor, + parameterTypes[i], + parameterAnnotations[i], + processedAnnotationTypes)); + } + return Optional.of(parameterValues); + } + } + @Override public Optional> maybeGetTestMethodParameters(TestInfo testInfo) { Method testMethod = testInfo.getMethod(); @@ -997,72 +1033,37 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { } @Override - public Optional createTest( - TestClass testClass, FrameworkMethod method, Optional test) { - TestIndexHolder testIndexHolder = method.getAnnotation(TestIndexHolder.class); - if (testIndexHolder == null) { - return test; - } + public void postProcessTestInstance(Object testInstance, TestInfo testInfo) { + TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); try { - List 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> processedAnnotationTypes = new ArrayList<>(); - List 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 testParameterValuesForFieldInjection = - filterByOrigin(testParameterValues, Origin.CLASS, Origin.FIELD, Origin.METHOD); - // The annotationType corresponding to the annotationIndex, e.g ColorParameter.class - // in the example above. - List 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; + if (testIndexHolder != null) { + List testParameterValues = getParameterValuesForTest(testIndexHolder); + + // Do not include {@link Origin#METHOD_PARAMETER} nor {@link Origin#CONSTRUCTOR_PARAMETER} + // annotations. + List testParameterValuesForFieldInjection = + filterByOrigin(testParameterValues, Origin.CLASS, Origin.FIELD, Origin.METHOD); + // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class + // in the example above. + List remainingTestParameterValuesForFieldInjection = + new ArrayList<>(testParameterValuesForFieldInjection); + for (Field declaredField : + streamWithParents(testInstance.getClass()) + .flatMap(c -> stream(c.getDeclaredFields())) + .collect(toImmutableList())) { + for (TestParameterValue testParameterValue : + remainingTestParameterValuesForFieldInjection) { + if (declaredField.isAnnotationPresent( + testParameterValue.annotationTypeOrigin().annotationType())) { + declaredField.setAccessible(true); + declaredField.set(testInstance, testParameterValue.value()); + remainingTestParameterValuesForFieldInjection.remove(testParameterValue); + break; + } } } } - return Optional.of(testObject); - } catch (Exception e) { + } catch (IllegalAccessException e) { throw new RuntimeException(e); } } @@ -1093,11 +1094,6 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { .collect(toImmutableList()); } - @Override - public Statement processStatement(Statement originalStatement, Description finalTestDescription) { - return originalStatement; - } - /** Returns a {@link TestParameterAnnotation}'s value for a method or constructor parameter. */ private Object getParameterValue( List testParameterValues, @@ -1106,7 +1102,7 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { List> processedAnnotationTypes) { List> iteratedAnnotationTypes = new ArrayList<>(); for (TestParameterValue testParameterValue : testParameterValues) { - // The annotationType corresponding to the annotationIndex, e.g ColorParameter.class + // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class // in the example above. for (Annotation parameterAnnotation : parameterAnnotations) { Class annotationType = @@ -1130,7 +1126,7 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { } // If no annotation matches, use the method parameter type. for (TestParameterValue testParameterValue : testParameterValues) { - // The annotationType corresponding to the annotationIndex, e.g ColorParameter.class + // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class // in the example above. if (methodParameterType.isAssignableFrom( getValueMethodReturnType( diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java index 6255d68..3e5e4c8 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java @@ -50,9 +50,6 @@ 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}. */ @@ -72,32 +69,32 @@ class TestParametersMethodProcessor implements TestMethodProcessor { } @Override - public ValidationResult validateConstructor(Constructor constructor) { + public ExecutableValidationResult validateConstructor(Constructor constructor) { if (hasRelevantAnnotation(constructor)) { try { // This method throws an exception if there is a validation error getConstructorParameters(); } catch (Throwable t) { - return ValidationResult.validated(t); + return ExecutableValidationResult.validated(t); } - return ValidationResult.valid(); + return ExecutableValidationResult.valid(); } else { - return ValidationResult.notValidated(); + return ExecutableValidationResult.notValidated(); } } @Override - public ValidationResult validateTestMethod(Method testMethod) { + public ExecutableValidationResult validateTestMethod(Method testMethod) { if (hasRelevantAnnotation(testMethod)) { try { // This method throws an exception if there is a validation error getMethodParameters(testMethod); } catch (Throwable t) { - return ValidationResult.validated(t); + return ExecutableValidationResult.validated(t); } - return ValidationResult.valid(); + return ExecutableValidationResult.valid(); } else { - return ValidationResult.notValidated(); + return ExecutableValidationResult.notValidated(); } } @@ -177,30 +174,17 @@ class TestParametersMethodProcessor implements TestMethodProcessor { } @Override - public Statement processStatement(Statement originalStatement, Description finalTestDescription) { - return originalStatement; - } - - @Override - public Optional createTest( - TestClass testClass, FrameworkMethod method, Optional test) { - if (hasRelevantAnnotation(testClass.getOnlyConstructor())) { + public Optional> maybeGetConstructorParameters( + Constructor constructor, TestInfo testInfo) { + if (hasRelevantAnnotation(constructor)) { ImmutableList parameterValuesList = getConstructorParameters(); TestParametersValues parametersValues = parameterValuesList.get( - method.getAnnotation(TestIndexHolder.class).constructorParametersIndex()); + testInfo.getAnnotation(TestIndexHolder.class).constructorParametersIndex()); - try { - Constructor constructor = testClass.getOnlyConstructor(); - return Optional.of( - constructor.newInstance( - toParameterList(parametersValues, testClass.getOnlyConstructor().getParameters()) - .toArray())); - } catch (Exception e) { - throw new RuntimeException(e); - } + return Optional.of(toParameterList(parametersValues, constructor.getParameters())); } else { - return test; + return Optional.absent(); } } @@ -219,6 +203,9 @@ class TestParametersMethodProcessor implements TestMethodProcessor { } } + @Override + public void postProcessTestInstance(Object testInstance, TestInfo testInfo) {} + private ImmutableList getConstructorParameters() { try { return parameterValuesByConstructorOrMethodCache.getUnchecked(testClass.getOnlyConstructor()); -- cgit v1.2.3 From e41e7284396e6bab8fc2652d156e9792773f6e8b Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 21 Dec 2021 15:38:29 +0000 Subject: Rename TestMethodProcessors to TestMethodProcessorList and make it a combining abstraction of all processors. See https://github.com/google/TestParameterInjector/issues/11. --- .../ParameterizedTestMethodProcessor.java | 2 +- .../testparameterinjector/PluggableTestRunner.java | 78 ++-------- .../testparameterinjector/TestMethodProcessor.java | 2 +- .../TestMethodProcessorList.java | 161 +++++++++++++++++++++ .../TestMethodProcessors.java | 54 ------- .../TestParameterAnnotationMethodProcessor.java | 5 +- .../TestParameterInjector.java | 6 +- .../TestParametersMethodProcessor.java | 2 +- .../PluggableTestRunnerTest.java | 12 +- ...TestParameterAnnotationMethodProcessorTest.java | 19 +-- .../testparameterinjector/TestParameterTest.java | 4 +- .../TestParametersMethodProcessorTest.java | 4 +- 12 files changed, 206 insertions(+), 143 deletions(-) create mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessors.java diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java index 2895f48..531224e 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java @@ -121,7 +121,7 @@ class ParameterizedTestMethodProcessor implements TestMethodProcessor { } @Override - public List processTest(Class testClass, TestInfo originalTest) { + public List calculateTestInfos(TestInfo originalTest) { if (parametersForAllTests.isPresent()) { ImmutableList.Builder tests = ImmutableList.builder(); int testIndex = 0; diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java index 3fbeeb2..d48c803 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java @@ -18,7 +18,6 @@ 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; @@ -60,17 +59,14 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { */ private static final ThreadLocal currentTestInfo = new ThreadLocal<>(); - private List testMethodProcessors; + private TestMethodProcessorList 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 createTestMethodProcessorList(); + /** Returns the TestMethodProcessorList to use. This is meant to be overridden by subclasses. */ + protected abstract TestMethodProcessorList createTestMethodProcessorList(); /** * This method is run to perform optional additional operations on the test instance, right after @@ -229,25 +225,7 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { } private ImmutableList processMethod(FrameworkMethod initialMethod) { - ImmutableList 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() + return getTestMethodProcessors().calculateTestInfos(initialMethod.getMethod()).stream() .map(testInfo -> new OverriddenFrameworkMethod(testInfo.getMethod(), testInfo)) .collect(toImmutableList()); } @@ -285,22 +263,13 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { if (testInfo.getMethod().getParameterTypes().length == 0) { return super.methodInvoker(frameworkMethod, testObject); } else { - Optional> maybeParameters = - getTestMethodProcessors().stream() - .map(processor -> processor.maybeGetTestMethodParameters(testInfo)) - .filter(Optional::isPresent) - .findFirst() - .orElse(Optional.absent()); - if (maybeParameters.isPresent()) { - return new Statement() { - @Override - public void evaluate() throws Throwable { - frameworkMethod.invokeExplosively(testObject, maybeParameters.get().toArray()); - } - }; - } else { - return super.methodInvoker(frameworkMethod, testObject); - } + List parameters = getTestMethodProcessors().getTestMethodParameters(testInfo); + return new Statement() { + @Override + public void evaluate() throws Throwable { + frameworkMethod.invokeExplosively(testObject, parameters.toArray()); + } + }; } } @@ -342,12 +311,7 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { testInstance = createTest(); } else { List constructorParameters = - getTestMethodProcessors().stream() - .map(processor -> processor.maybeGetConstructorParameters(constructor, testInfo)) - .filter(Optional::isPresent) - .findFirst() - .get() - .get(); + getTestMethodProcessors().getConstructorParameters(constructor, testInfo); try { testInstance = constructor.newInstance(constructorParameters.toArray()); } catch (IllegalAccessException e) { @@ -356,9 +320,7 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { } // Run all post processors on the newly created instance - for (TestMethodProcessor testMethodProcessor : getTestMethodProcessors()) { - testMethodProcessor.postProcessTestInstance(testInstance, testInfo); - } + getTestMethodProcessors().postProcessTestInstance(testInstance, testInfo); finalizeCreatedTestInstance(testInstance); @@ -368,11 +330,7 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { @Override protected final void validateZeroArgConstructor(List errorsReturned) { ExecutableValidationResult validationResult = - getTestMethodProcessors().stream() - .map(processor -> processor.validateConstructor(getTestClass().getOnlyConstructor())) - .filter(ExecutableValidationResult::wasValidated) - .findFirst() - .orElse(ExecutableValidationResult.notValidated()); + getTestMethodProcessors().validateConstructor(getTestClass().getOnlyConstructor()); if (validationResult.wasValidated()) { errorsReturned.addAll(validationResult.validationErrors()); @@ -389,11 +347,7 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { .collect(toImmutableList()); for (FrameworkMethod testMethod : testMethods) { ExecutableValidationResult validationResult = - getTestMethodProcessors().stream() - .map(processor -> processor.validateTestMethod(testMethod.getMethod())) - .filter(ExecutableValidationResult::wasValidated) - .findFirst() - .orElse(ExecutableValidationResult.notValidated()); + getTestMethodProcessors().validateTestMethod(testMethod.getMethod()); if (validationResult.wasValidated()) { errorsReturned.addAll(validationResult.validationErrors()); @@ -434,7 +388,7 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { super.validatePublicVoidNoArgMethods(annotation, isStatic, errors); } - private synchronized List getTestMethodProcessors() { + private synchronized TestMethodProcessorList getTestMethodProcessors() { if (testMethodProcessors == null) { testMethodProcessors = createTestMethodProcessorList(); } diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java index cbe493b..2558686 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java @@ -28,7 +28,7 @@ import java.util.List; interface TestMethodProcessor { /** Allows to transform the test information (name and annotations). */ - List processTest(Class testClass, TestInfo originalTest); + List calculateTestInfos(TestInfo originalTest); /** * If this processor can handle the given constructor, returns the parameters with which it should diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java new file mode 100644 index 0000000..4aa72d8 --- /dev/null +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java @@ -0,0 +1,161 @@ +/* + * 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.stream.Collectors.toList; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; +import org.junit.runners.model.TestClass; + +/** + * Combined version of all {@link TestMethodProcessor} implementations that this package supports. + */ +final class TestMethodProcessorList { + + private final ImmutableList testMethodProcessors; + + private TestMethodProcessorList(ImmutableList testMethodProcessors) { + this.testMethodProcessors = testMethodProcessors; + } + + /** + * Returns a TestMethodProcessorList that supports all features that this package supports. + * + *

          Note that this includes support for {@link org.junit.runners.Parameterized}. + */ + public static TestMethodProcessorList createNewParameterizedProcessorsWithLegacyFeatures( + TestClass testClass) { + return new TestMethodProcessorList( + ImmutableList.of( + new ParameterizedTestMethodProcessor(testClass), + new TestParametersMethodProcessor(testClass), + TestParameterAnnotationMethodProcessor.forAllAnnotationPlacements(testClass))); + } + + /** + * Returns a TestMethodProcessorList that supports all features that this package supports, except + * the following legacy features: + * + *

            + *
          • No support for {@link org.junit.runners.Parameterized} + *
          • No support for class and method-level parameters, except for @TestParameters + *
          + */ + public static TestMethodProcessorList createNewParameterizedProcessors(TestClass testClass) { + return new TestMethodProcessorList( + ImmutableList.of( + new TestParametersMethodProcessor(testClass), + TestParameterAnnotationMethodProcessor.onlyForFieldsAndParameters(testClass))); + } + + static TestMethodProcessorList empty() { + return new TestMethodProcessorList(ImmutableList.of()); + } + + /** + * Calculates the TestInfo instances for the given test method. Each TestInfo corresponds to a + * single test. + * + *

          The returned list always contains at least one element. If there is no parameterization, + * this would be the TestInfo for running the test method without parameters. + */ + public List calculateTestInfos(Method testMethod) { + List testInfos = + ImmutableList.of( + TestInfo.createWithoutParameters( + testMethod, ImmutableList.copyOf(testMethod.getAnnotations()))); + + for (final TestMethodProcessor testMethodProcessor : testMethodProcessors) { + testInfos = + testInfos.stream() + .flatMap( + lastTestInfo -> testMethodProcessor.calculateTestInfos(lastTestInfo).stream()) + .collect(toList()); + } + + testInfos = TestInfo.deduplicateTestNames(TestInfo.shortenNamesIfNecessary(testInfos)); + + return testInfos; + } + + /** + * Returns the parameters with which it should be invoked. + * + *

          This method is never called for a parameterless constructor. + */ + public List getConstructorParameters(Constructor constructor, TestInfo testInfo) { + return testMethodProcessors.stream() + .map(processor -> processor.maybeGetConstructorParameters(constructor, testInfo)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .orElseThrow( + () -> + new IllegalStateException( + String.format( + "Could not generate parameter values for %s. Did you forget an annotation?", + constructor))); + } + + /** + * Returns the parameters with which {@code testInfo.getMethod()} should be invoked. + * + *

          This method is never called for a parameterless {@code testInfo.getMethod()}. + */ + public List getTestMethodParameters(TestInfo testInfo) { + return testMethodProcessors.stream() + .map(processor -> processor.maybeGetTestMethodParameters(testInfo)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .orElseThrow( + () -> + new IllegalStateException( + String.format( + "Could not generate parameter values for %s. Did you forget an annotation?", + testInfo.getMethod()))); + } + + /** + * Optionally process the test instance right after construction to ready it for the given test. + */ + public void postProcessTestInstance(Object testInstance, TestInfo testInfo) { + for (TestMethodProcessor testMethodProcessor : testMethodProcessors) { + testMethodProcessor.postProcessTestInstance(testInstance, testInfo); + } + } + + /** Optionally validates the given constructor. */ + public ExecutableValidationResult validateConstructor(Constructor constructor) { + return testMethodProcessors.stream() + .map(processor -> processor.validateConstructor(constructor)) + .filter(ExecutableValidationResult::wasValidated) + .findFirst() + .orElse(ExecutableValidationResult.notValidated()); + } + + /** Optionally validates the given method. */ + public ExecutableValidationResult validateTestMethod(Method testMethod) { + return testMethodProcessors.stream() + .map(processor -> processor.validateTestMethod(testMethod)) + .filter(ExecutableValidationResult::wasValidated) + .findFirst() + .orElse(ExecutableValidationResult.notValidated()); + } +} diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessors.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessors.java deleted file mode 100644 index b6dc4c2..0000000 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessors.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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. - * - *

          Note that this includes support for {@link org.junit.runners.Parameterized}. - */ - public static ImmutableList - 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: - * - *

            - *
          • No support for {@link org.junit.runners.Parameterized} - *
          • No support for class and method-level parameters, except for @TestParameters - *
          - */ - public static ImmutableList 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/TestParameterAnnotationMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 6a23746..3c51e0d 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -716,7 +716,7 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { * corresponding to the cartesian product of both annotations. */ @Override - public List processTest(Class testClass, TestInfo originalTest) { + public List calculateTestInfos(TestInfo originalTest) { List> parameterValuesForMethod = getParameterValuesForMethod(originalTest.getMethod()); @@ -742,7 +742,8 @@ class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { .withExtraAnnotation( TestIndexHolderFactory.create( /* methodIndex= */ strictIndexOf( - getMethodsIncludingParents(testClass), originalTest.getMethod()), + getMethodsIncludingParents(testClass.getJavaClass()), + originalTest.getMethod()), parametersIndex, testClass.getName()))); } diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java index dd6c63f..6b9c237 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java @@ -14,7 +14,6 @@ package com.google.testing.junit.testparameterinjector; -import java.util.List; import org.junit.runners.model.InitializationError; /** @@ -30,7 +29,8 @@ public final class TestParameterInjector extends PluggableTestRunner { } @Override - protected List createTestMethodProcessorList() { - return TestMethodProcessors.createNewParameterizedProcessorsWithLegacyFeatures(getTestClass()); + protected TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.createNewParameterizedProcessorsWithLegacyFeatures( + getTestClass()); } } diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java index 3e5e4c8..3b41ff2 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java @@ -99,7 +99,7 @@ class TestParametersMethodProcessor implements TestMethodProcessor { } @Override - public List processTest(Class clazz, TestInfo originalTest) { + public List calculateTestInfos(TestInfo originalTest) { boolean constructorIsParameterized = hasRelevantAnnotation(testClass.getOnlyConstructor()); boolean methodIsParameterized = hasRelevantAnnotation(originalTest.getMethod()); diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java index 7662bd9..a44df08 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java @@ -73,8 +73,8 @@ public class PluggableTestRunnerTest { PluggableTestRunner.run( new PluggableTestRunner(TestAndMethodRuleTestClass.class) { @Override - protected List createTestMethodProcessorList() { - return ImmutableList.of(); + protected TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.empty(); } }); @@ -101,8 +101,8 @@ public class PluggableTestRunnerTest { PluggableTestRunner.run( new PluggableTestRunner(CustomTestAnnotationTestClass.class) { @Override - protected List createTestMethodProcessorList() { - return ImmutableList.of(); + protected TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.empty(); } @Override @@ -140,8 +140,8 @@ public class PluggableTestRunnerTest { PluggableTestRunner.run( new PluggableTestRunner(SortedPluggableTestRunnerTestClass.class) { @Override - protected List createTestMethodProcessorList() { - return ImmutableList.of(); + protected TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.empty(); } @Override diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java index 62299fc..fb16f8d 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java @@ -927,26 +927,26 @@ public class TestParameterAnnotationMethodProcessorTest { assertNoFailures( PluggableTestRunner.run( newTestRunnerWithParameterizedSupport( - TestParameterAnnotationMethodProcessor::forAllAnnotationPlacements))); + TestMethodProcessorList::createNewParameterizedProcessorsWithLegacyFeatures))); assertNoFailures( PluggableTestRunner.run( newTestRunnerWithParameterizedSupport( - TestParameterAnnotationMethodProcessor::onlyForFieldsAndParameters))); + TestMethodProcessorList::createNewParameterizedProcessors))); break; case SUCCESS_FOR_ALL_PLACEMENTS_ONLY: assertNoFailures( PluggableTestRunner.run( newTestRunnerWithParameterizedSupport( - TestParameterAnnotationMethodProcessor::forAllAnnotationPlacements))); + TestMethodProcessorList::createNewParameterizedProcessorsWithLegacyFeatures))); assertThrows( IllegalStateException.class, () -> PluggableTestRunner.run( newTestRunnerWithParameterizedSupport( - TestParameterAnnotationMethodProcessor::onlyForFieldsAndParameters))); + TestMethodProcessorList::createNewParameterizedProcessors))); break; case FAILURE: @@ -955,23 +955,24 @@ public class TestParameterAnnotationMethodProcessorTest { () -> PluggableTestRunner.run( newTestRunnerWithParameterizedSupport( - TestParameterAnnotationMethodProcessor::forAllAnnotationPlacements))); + TestMethodProcessorList + ::createNewParameterizedProcessorsWithLegacyFeatures))); assertThrows( IllegalStateException.class, () -> PluggableTestRunner.run( newTestRunnerWithParameterizedSupport( - TestParameterAnnotationMethodProcessor::onlyForFieldsAndParameters))); + TestMethodProcessorList::createNewParameterizedProcessors))); break; } } private PluggableTestRunner newTestRunnerWithParameterizedSupport( - Function processor) throws Exception { + Function processorListGenerator) throws Exception { return new PluggableTestRunner(testClass) { @Override - protected List createTestMethodProcessorList() { - return ImmutableList.of(processor.apply(getTestClass())); + protected TestMethodProcessorList createTestMethodProcessorList() { + return processorListGenerator.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 index efc5c4b..0221deb 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java @@ -203,8 +203,8 @@ public class TestParameterTest { PluggableTestRunner.run( new PluggableTestRunner(testClass) { @Override - protected List createTestMethodProcessorList() { - return TestMethodProcessors.createNewParameterizedProcessorsWithLegacyFeatures( + protected TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.createNewParameterizedProcessorsWithLegacyFeatures( getTestClass()); } }); diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java index 2db6bad..9af1409 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java @@ -595,8 +595,8 @@ public class TestParametersMethodProcessorTest { private PluggableTestRunner newTestRunner() throws Exception { return new PluggableTestRunner(testClass) { @Override - protected List createTestMethodProcessorList() { - return TestMethodProcessors.createNewParameterizedProcessorsWithLegacyFeatures( + protected TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.createNewParameterizedProcessorsWithLegacyFeatures( getTestClass()); } }; -- cgit v1.2.3 From 2257e915e4e0d4949c32908e5b1aae70f3e13bc1 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 21 Dec 2021 18:37:21 +0000 Subject: Mark TestMethodProcessor implementations as final --- .../junit/testparameterinjector/ParameterizedTestMethodProcessor.java | 2 +- .../testparameterinjector/TestParameterAnnotationMethodProcessor.java | 2 +- .../junit/testparameterinjector/TestParametersMethodProcessor.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java index 531224e..96e3723 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java @@ -48,7 +48,7 @@ import org.junit.runners.model.TestClass; * instead requires a single class constructor with one argument for each parameter returned by the * {@link Parameters} method. */ -class ParameterizedTestMethodProcessor implements TestMethodProcessor { +final class ParameterizedTestMethodProcessor implements TestMethodProcessor { /** * The parameters as returned by the {@link Parameters} annotated method, or {@link diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 3c51e0d..53563be 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -66,7 +66,7 @@ import org.junit.runners.model.TestClass; * * @see TestParameterAnnotation */ -class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { +final class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { /** * Class to hold an annotation type and origin and one of the values as returned by the {@code diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java index 3b41ff2..c5f9bf8 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java @@ -54,7 +54,7 @@ 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 { +final class TestParametersMethodProcessor implements TestMethodProcessor { private final TestClass testClass; -- cgit v1.2.3 From f3941779de0d2b307d02f63f8a99a6cc39479b56 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 22 Dec 2021 14:04:43 +0000 Subject: Remove TestParameterInjector support for org.junit.runners.Parameterized, which was undocumented. --- CHANGELOG.md | 10 ++++++++++ .../junit/testparameterinjector/TestParameterInjector.java | 7 ++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50bc3a5..9557846 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 1.7 + +- Remove `TestParameterInjector` support for `org.junit.runners.Parameterized`, + which was undocumented and thus unlikely to be used. + +## 1.6 + +- Bugfixes +- Better documentation + ## 1.5 - `@TestParameters` can now also be used as a repeated annotation: diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java index 6b9c237..db24479 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java @@ -18,9 +18,7 @@ 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). + * be parameterized with its own unique set of test parameters. */ public final class TestParameterInjector extends PluggableTestRunner { @@ -30,7 +28,6 @@ public final class TestParameterInjector extends PluggableTestRunner { @Override protected TestMethodProcessorList createTestMethodProcessorList() { - return TestMethodProcessorList.createNewParameterizedProcessorsWithLegacyFeatures( - getTestClass()); + return TestMethodProcessorList.createNewParameterizedProcessors(getTestClass()); } } -- cgit v1.2.3 From 24835bcd849ec6c2a72ad19ad27959704494fd72 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Thu, 23 Dec 2021 12:36:48 +0000 Subject: Bump version to v1.7 in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c08aab8..f1507bb 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ And add the following dependency to your `.pom` file: com.google.testparameterinjector test-parameter-injector - 1.6 + 1.7 ``` -- cgit v1.2.3 From 6ef0a4b69a03ef5a0324c13a98dc6c989cb1d483 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 5 Jan 2022 20:31:41 +0000 Subject: Rename Make the TestMethodProcessor implementations stateless classes that don't depend on JUnit4. This is a prerequisite for https://github.com/google/TestParameterInjector/issues/11. --- .../ParameterizedTestMethodProcessor.java | 211 ----------------- .../testparameterinjector/PluggableTestRunner.java | 7 +- .../junit/testparameterinjector/TestInfo.java | 32 ++- .../testparameterinjector/TestMethodProcessor.java | 9 +- .../TestMethodProcessorList.java | 26 +-- .../TestParameterAnnotationMethodProcessor.java | 258 +++++++++++---------- .../TestParametersMethodProcessor.java | 40 ++-- .../junit/testparameterinjector/TestInfoTest.java | 5 +- ...TestParameterAnnotationMethodProcessorTest.java | 17 -- .../testparameterinjector/TestParameterTest.java | 3 +- .../TestParametersMethodProcessorTest.java | 3 +- 11 files changed, 201 insertions(+), 410 deletions(-) delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java deleted file mode 100644 index 96e3723..0000000 --- a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterizedTestMethodProcessor.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * 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.lang.reflect.Method; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import org.junit.runners.Parameterized.Parameters; -import org.junit.runners.model.FrameworkMethod; -import org.junit.runners.model.TestClass; - -/** - * {@code TestMethodProcessor} implementation for supporting {@link org.junit.runners.Parameterized} - * tests. - * - *

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

          The {@link Parameters} annotated method can return either a {@code Collection} or a - * {@code Collection}. - * - *

          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. - */ -final 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> 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 testNamePattern; - - ParameterizedTestMethodProcessor(TestClass testClass) { - Optional 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.>of((Iterable) parameters); - } else if (parameters instanceof Object[]) { - parametersForAllTests = - Optional.>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 ExecutableValidationResult validateConstructor(Constructor constructor) { - if (parametersForAllTests.isPresent()) { - Class[] parameterTypes = constructor.getParameterTypes(); - List testParameters = getTestParameters(0); - - if (parameterTypes.length != testParameters.size()) { - return ExecutableValidationResult.validated( - new IllegalStateException( - "Mismatch constructor parameter count with values" - + " returned by the @Parameters method")); - } - List errors = new ArrayList<>(); - for (int i = 0; i < testParameters.size(); i++) { - if (!parameterTypes[i].isAssignableFrom(testParameters.get(i).getClass())) { - errors.add( - new IllegalStateException( - String.format( - "Mismatch constructor parameter type %s with value" - + " returned by the @Parameters method: %s", - parameterTypes[i], testParameters.get(i)))); - } - } - return ExecutableValidationResult.validated(errors); - } else { - return ExecutableValidationResult.notValidated(); - } - } - - @Override - public ExecutableValidationResult validateTestMethod(Method testMethod) { - return ExecutableValidationResult.notValidated(); - } - - @Override - public List calculateTestInfos(TestInfo originalTest) { - if (parametersForAllTests.isPresent()) { - ImmutableList.Builder 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 Optional> maybeGetConstructorParameters( - Constructor constructor, TestInfo testInfo) { - if (parametersForAllTests.isPresent()) { - return Optional.of( - getTestParameters(testInfo.getAnnotation(TestIndexHolder.class).testIndex())); - } else { - return Optional.absent(); - } - } - - @Override - public Optional> maybeGetTestMethodParameters(TestInfo testInfo) { - return Optional.absent(); - } - - @Override - public void postProcessTestInstance(Object testInstance, TestInfo testInfo) {} - - /** - * 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 List getTestParameters(int testIndex) { - Object parameters = Iterables.get(parametersForAllTests.get(), testIndex); - if (parameters instanceof Object[]) { - return Arrays.asList((Object[]) parameters); - } else { - return Arrays.asList(parameters); - } - } - - private Optional getParametersMethod(TestClass testClass) { - List 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 index d48c803..6c676f3 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java @@ -225,7 +225,9 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { } private ImmutableList processMethod(FrameworkMethod initialMethod) { - return getTestMethodProcessors().calculateTestInfos(initialMethod.getMethod()).stream() + return getTestMethodProcessors() + .calculateTestInfos(initialMethod.getMethod(), getTestClass().getJavaClass()) + .stream() .map(testInfo -> new OverriddenFrameworkMethod(testInfo.getMethod(), testInfo)) .collect(toImmutableList()); } @@ -347,7 +349,8 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { .collect(toImmutableList()); for (FrameworkMethod testMethod : testMethods) { ExecutableValidationResult validationResult = - getTestMethodProcessors().validateTestMethod(testMethod.getMethod()); + getTestMethodProcessors() + .validateTestMethod(testMethod.getMethod(), getTestClass().getJavaClass()); if (validationResult.wasValidated()) { errorsReturned.addAll(validationResult.validationErrors()); diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java index 5074ffc..69777d2 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java @@ -49,7 +49,15 @@ abstract class TestInfo { public abstract Method getMethod(); - public String getName() { + /** + * The test class that is being run. + * + *

          Note that this is not always the same as the class that declares {@link #getMethod()} + * because test methods can be inherited. + */ + public abstract Class getTestClass(); + + public final String getName() { if (getParameters().isEmpty()) { return getMethod().getName(); } else { @@ -65,7 +73,7 @@ abstract class TestInfo { public abstract ImmutableList getAnnotations(); @Nullable - public T getAnnotation(Class annotationClass) { + public final T getAnnotation(Class annotationClass) { for (Annotation annotation : getAnnotations()) { if (annotationClass.isInstance(annotation)) { return annotationClass.cast(annotation); @@ -74,9 +82,10 @@ abstract class TestInfo { return null; } - TestInfo withExtraParameters(List parameters) { + final TestInfo withExtraParameters(List parameters) { return new AutoValue_TestInfo( getMethod(), + getTestClass(), ImmutableList.builder() .addAll(this.getParameters()) .addAll(parameters) @@ -84,10 +93,10 @@ abstract class TestInfo { getAnnotations()); } - TestInfo withExtraAnnotation(Annotation annotation) { + final TestInfo withExtraAnnotation(Annotation annotation) { ImmutableList newAnnotations = ImmutableList.builder().addAll(this.getAnnotations()).add(annotation).build(); - return new AutoValue_TestInfo(getMethod(), getParameters(), newAnnotations); + return new AutoValue_TestInfo(getMethod(), getTestClass(), getParameters(), newAnnotations); } /** @@ -100,6 +109,7 @@ abstract class TestInfo { BiFunction parameterWithIndexToNewName) { return new AutoValue_TestInfo( getMethod(), + getTestClass(), IntStream.range(0, getParameters().size()) .mapToObj( parameterIndex -> { @@ -111,14 +121,16 @@ abstract class TestInfo { getAnnotations()); } - public static TestInfo legacyCreate(Method method, String name, List annotations) { + public static TestInfo legacyCreate( + Method method, Class testClass, String name, List annotations) { return new AutoValue_TestInfo( - method, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations)); + method, testClass, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations)); } - static TestInfo createWithoutParameters(Method method, List annotations) { + static TestInfo createWithoutParameters( + Method method, Class testClass, List annotations) { return new AutoValue_TestInfo( - method, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations)); + method, testClass, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations)); } static ImmutableList shortenNamesIfNecessary(List testInfos) { @@ -284,7 +296,7 @@ abstract class TestInfo { */ abstract int getIndexInValueSource(); - TestInfoParameter withName(String newName) { + final TestInfoParameter withName(String newName) { return create(newName, getValue(), getIndexInValueSource()); } diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java index 2558686..60a01bc 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java @@ -56,6 +56,11 @@ interface TestMethodProcessor { /** Optionally validates the given constructor. */ ExecutableValidationResult validateConstructor(Constructor constructor); - /** Optionally validates the given method. */ - ExecutableValidationResult validateTestMethod(Method testMethod); + /** + * Optionally validates the given method. + * + *

          Note that the given method is not necessarily declared in the given class because test + * methods can be inherited. + */ + ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass); } diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java index 4aa72d8..ef6e503 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java @@ -34,20 +34,6 @@ final class TestMethodProcessorList { this.testMethodProcessors = testMethodProcessors; } - /** - * Returns a TestMethodProcessorList that supports all features that this package supports. - * - *

          Note that this includes support for {@link org.junit.runners.Parameterized}. - */ - public static TestMethodProcessorList createNewParameterizedProcessorsWithLegacyFeatures( - TestClass testClass) { - return new TestMethodProcessorList( - ImmutableList.of( - new ParameterizedTestMethodProcessor(testClass), - new TestParametersMethodProcessor(testClass), - TestParameterAnnotationMethodProcessor.forAllAnnotationPlacements(testClass))); - } - /** * Returns a TestMethodProcessorList that supports all features that this package supports, except * the following legacy features: @@ -60,8 +46,8 @@ final class TestMethodProcessorList { public static TestMethodProcessorList createNewParameterizedProcessors(TestClass testClass) { return new TestMethodProcessorList( ImmutableList.of( - new TestParametersMethodProcessor(testClass), - TestParameterAnnotationMethodProcessor.onlyForFieldsAndParameters(testClass))); + new TestParametersMethodProcessor(), + TestParameterAnnotationMethodProcessor.onlyForFieldsAndParameters())); } static TestMethodProcessorList empty() { @@ -75,11 +61,11 @@ final class TestMethodProcessorList { *

          The returned list always contains at least one element. If there is no parameterization, * this would be the TestInfo for running the test method without parameters. */ - public List calculateTestInfos(Method testMethod) { + public List calculateTestInfos(Method testMethod, Class testClass) { List testInfos = ImmutableList.of( TestInfo.createWithoutParameters( - testMethod, ImmutableList.copyOf(testMethod.getAnnotations()))); + testMethod, testClass, ImmutableList.copyOf(testMethod.getAnnotations()))); for (final TestMethodProcessor testMethodProcessor : testMethodProcessors) { testInfos = @@ -151,9 +137,9 @@ final class TestMethodProcessorList { } /** Optionally validates the given method. */ - public ExecutableValidationResult validateTestMethod(Method testMethod) { + public ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass) { return testMethodProcessors.stream() - .map(processor -> processor.validateTestMethod(testMethod)) + .map(processor -> processor.validateTestMethod(testMethod, testClass)) .filter(ExecutableValidationResult::wasValidated) .findFirst() .orElse(ExecutableValidationResult.notValidated()); diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 53563be..114fb16 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -28,6 +28,8 @@ import com.google.common.base.Optional; import com.google.common.base.Throwables; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; @@ -58,7 +60,6 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; import javax.annotation.Nullable; -import org.junit.runners.model.TestClass; /** * {@code TestMethodProcessor} implementation for supporting parameterized tests annotated with @@ -167,10 +168,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } else { return annotationType -> Optional.fromNullable( - new TestParameterAnnotationMethodProcessor( - new TestClass(testInfo.getMethod().getDeclaringClass()), - /* onlyForFieldsAndParameters= */ false) - .getParameterValuesForTest(testIndexHolder).stream() + new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ false) + .getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()).stream() .filter(matches(annotationType)) .map(TestParameterValue::value) .findFirst() @@ -284,15 +283,16 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } } - private final TestClass testClass; private final boolean onlyForFieldsAndParameters; - private volatile ImmutableList cachedAnnotationTypeOrigins; + private final LoadingCache, ImmutableList> + annotationTypeOriginsCache = + CacheBuilder.newBuilder() + .maximumSize(1000) + .build(CacheLoader.from(this::calculateAnnotationTypeOrigins)); private final Cache>> parameterValuesCache = CacheBuilder.newBuilder().maximumSize(1000).build(); - private TestParameterAnnotationMethodProcessor( - TestClass testClass, boolean onlyForFieldsAndParameters) { - this.testClass = testClass; + private TestParameterAnnotationMethodProcessor(boolean onlyForFieldsAndParameters) { this.onlyForFieldsAndParameters = onlyForFieldsAndParameters; } @@ -307,9 +307,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso *

        • At the test class * */ - static TestMethodProcessor forAllAnnotationPlacements(TestClass testClass) { - return new TestParameterAnnotationMethodProcessor( - testClass, /* onlyForFieldsAndParameters= */ false); + static TestMethodProcessor forAllAnnotationPlacements() { + return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ false); } /** @@ -319,102 +318,103 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso *

          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); + static TestMethodProcessor onlyForFieldsAndParameters() { + return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ true); } - private ImmutableList getAnnotationTypeOrigins( - Origin firstOrigin, Origin... otherOrigins) { - if (cachedAnnotationTypeOrigins == null) { - // Collect all annotations used in declared fields and methods that have themselves a - // @TestParameterAnnotation annotation. - List fieldAnnotations = - extractTestParameterAnnotations( - streamWithParents(testClass.getJavaClass()) - .flatMap(c -> stream(c.getDeclaredFields())) - .flatMap(field -> stream(field.getAnnotations())), - Origin.FIELD); - List methodAnnotations = - extractTestParameterAnnotations( - stream(testClass.getJavaClass().getMethods()) - .flatMap(method -> stream(method.getAnnotations())), - Origin.METHOD); - List parameterAnnotations = - extractTestParameterAnnotations( - stream(testClass.getJavaClass().getMethods()) - .flatMap(method -> stream(method.getParameterAnnotations()).flatMap(Stream::of)), - Origin.METHOD_PARAMETER); - List classAnnotations = - extractTestParameterAnnotations( - stream(testClass.getJavaClass().getAnnotations()), Origin.CLASS); - List constructorAnnotations = - extractTestParameterAnnotations( - stream(testClass.getJavaClass().getConstructors()) - .flatMap(constructor -> stream(constructor.getAnnotations())), - Origin.CONSTRUCTOR); - List constructorParameterAnnotations = - extractTestParameterAnnotations( - stream(testClass.getJavaClass().getConstructors()) - .flatMap( - constructor -> - stream(constructor.getParameterAnnotations()).flatMap(Stream::of)), - Origin.CONSTRUCTOR_PARAMETER); - - checkDuplicatedClassAndFieldAnnotations( - constructorAnnotations, classAnnotations, fieldAnnotations); - - checkDuplicatedFieldsAnnotations(methodAnnotations, fieldAnnotations); + private ImmutableList calculateAnnotationTypeOrigins(Class testClass) { + // Collect all annotations used in declared fields and methods that have themselves a + // @TestParameterAnnotation annotation. + List fieldAnnotations = + extractTestParameterAnnotations( + streamWithParents(testClass) + .flatMap(c -> stream(c.getDeclaredFields())) + .flatMap(field -> stream(field.getAnnotations())), + Origin.FIELD); + List methodAnnotations = + extractTestParameterAnnotations( + stream(testClass.getMethods()).flatMap(method -> stream(method.getAnnotations())), + Origin.METHOD); + List parameterAnnotations = + extractTestParameterAnnotations( + stream(testClass.getMethods()) + .flatMap(method -> stream(method.getParameterAnnotations()).flatMap(Stream::of)), + Origin.METHOD_PARAMETER); + List classAnnotations = + extractTestParameterAnnotations(stream(testClass.getAnnotations()), Origin.CLASS); + List constructorAnnotations = + extractTestParameterAnnotations( + stream(testClass.getConstructors()) + .flatMap(constructor -> stream(constructor.getAnnotations())), + Origin.CONSTRUCTOR); + List constructorParameterAnnotations = + extractTestParameterAnnotations( + stream(testClass.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( + 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); - } + checkState( + classAnnotations.stream().distinct().count() == classAnnotations.size(), + "Annotations should not be duplicated on the class."); - 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()); + 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); } + return 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()); + } + + private ImmutableList getAnnotationTypeOrigins( + Class testClass, Origin firstOrigin, Origin... otherOrigins) { Set originsToFilterBy = ImmutableSet.builder().add(firstOrigin).add(otherOrigins).build(); - return cachedAnnotationTypeOrigins.stream() - .filter(annotationTypeOrigin -> originsToFilterBy.contains(annotationTypeOrigin.origin())) - .collect(toImmutableList()); + try { + return annotationTypeOriginsCache.getUnchecked(testClass).stream() + .filter(annotationTypeOrigin -> originsToFilterBy.contains(annotationTypeOrigin.origin())) + .collect(toImmutableList()); + } catch (UncheckedExecutionException e) { + Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); + throw e; + } } private void checkDuplicatedFieldsAnnotations( @@ -484,12 +484,13 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso // The constructor has parameters, they must be injected by a TestParameterAnnotation // annotation. Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); + Class testClass = constructor.getDeclaringClass(); return ExecutableValidationResult.validated( validateMethodOrConstructorParameters( removeOverrides( getAnnotationTypeOrigins( - Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER), - testClass.getJavaClass()), + testClass, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER), + testClass), testClass, constructor, parameterTypes, @@ -497,7 +498,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } @Override - public ExecutableValidationResult validateTestMethod(Method testMethod) { + public ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass) { Class[] methodParameterTypes = testMethod.getParameterTypes(); if (methodParameterTypes.length == 0) { return ExecutableValidationResult.notValidated(); @@ -520,7 +521,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso Annotation[][] parametersAnnotations = testMethod.getParameterAnnotations(); errors.addAll( validateMethodOrConstructorParameters( - getAnnotationTypeOrigins(Origin.CLASS, Origin.METHOD, Origin.METHOD_PARAMETER), + getAnnotationTypeOrigins( + testClass, Origin.CLASS, Origin.METHOD, Origin.METHOD_PARAMETER), testClass, testMethod, methodParameterTypes, @@ -532,7 +534,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso private List validateMethodOrConstructorParameters( List annotationTypeOrigins, - TestClass testClass, + Class testClass, AnnotatedElement methodOrConstructor, Class[] parameterTypes, Annotation[][] parametersAnnotations) { @@ -579,7 +581,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso // been evaluated. filterAnnotationTypeOriginsByOrigin( annotationTypeOrigins, Origin.CLASS, Origin.CONSTRUCTOR, Origin.METHOD), - testClass.getJavaClass(), + testClass, methodOrConstructor); // If no annotation is present, simply compare the type. for (Class testParameterAnnotationType : @@ -628,7 +630,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return Optional.absent(); } else { TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); - List testParameterValues = getParameterValuesForTest(testIndexHolder); + List testParameterValues = + getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()); Class[] parameterTypes = constructor.getParameterTypes(); Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); @@ -670,7 +673,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso checkState(testIndexHolder != null); List testParameterValues = filterByOrigin( - getParameterValuesForTest(testIndexHolder), + getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()), Origin.CLASS, Origin.METHOD, Origin.METHOD_PARAMETER); @@ -718,7 +721,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso @Override public List calculateTestInfos(TestInfo originalTest) { List> parameterValuesForMethod = - getParameterValuesForMethod(originalTest.getMethod()); + getParameterValuesForMethod(originalTest.getMethod(), originalTest.getTestClass()); if (parameterValuesForMethod.equals(ImmutableList.of(ImmutableList.of()))) { // This test is not parameterized @@ -742,22 +745,23 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso .withExtraAnnotation( TestIndexHolderFactory.create( /* methodIndex= */ strictIndexOf( - getMethodsIncludingParents(testClass.getJavaClass()), + getMethodsIncludingParents(originalTest.getTestClass()), originalTest.getMethod()), parametersIndex, - testClass.getName()))); + originalTest.getTestClass().getName()))); } return testInfos.build(); } - private List> getParameterValuesForMethod(Method method) { + private List> getParameterValuesForMethod( + Method method, Class testClass) { try { return parameterValuesCache.get( method, () -> { List> testParameterValuesList = - getAnnotationValuesForUsedAnnotationTypes(testClass.getJavaClass(), method); + getAnnotationValuesForUsedAnnotationTypes(method, testClass); return Lists.cartesianProduct(testParameterValuesList).stream() .filter( @@ -778,16 +782,17 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } } - private List getParameterValuesForTest(TestIndexHolder testIndexHolder) { + private List getParameterValuesForTest( + TestIndexHolder testIndexHolder, Class testClass) { verify( testIndexHolder.testClassName().equals(testClass.getName()), "The class for which the given annotation was created (%s) is not the same as the test" + " class that this runner is handling (%s)", testIndexHolder.testClassName(), testClass.getName()); - Method testMethod = - getMethodsIncludingParents(testClass.getJavaClass()).get(testIndexHolder.methodIndex()); - return getParameterValuesForMethod(testMethod).get(testIndexHolder.parametersIndex()); + Method testMethod = getMethodsIncludingParents(testClass).get(testIndexHolder.methodIndex()); + return getParameterValuesForMethod(testMethod, testClass) + .get(testIndexHolder.parametersIndex()); } /** @@ -795,15 +800,15 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso * class. */ private ImmutableList> getAnnotationValuesForUsedAnnotationTypes( - Class testClass, Method method) { + Method method, Class testClass) { ImmutableList 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() + getAnnotationTypeOrigins(testClass, Origin.CLASS).stream(), + getAnnotationTypeOrigins(testClass, Origin.FIELD).stream(), + getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR).stream(), + getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR_PARAMETER).stream(), + getAnnotationTypeOrigins(testClass, Origin.METHOD).stream(), + getAnnotationTypeOrigins(testClass, Origin.METHOD_PARAMETER).stream() .sorted(annotationComparator(method.getParameterAnnotations()))) .flatMap(x -> x) .collect(toImmutableList()); @@ -1038,7 +1043,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); try { if (testIndexHolder != null) { - List testParameterValues = getParameterValuesForTest(testIndexHolder); + List testParameterValues = + getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()); // Do not include {@link Origin#METHOD_PARAMETER} nor {@link Origin#CONSTRUCTOR_PARAMETER} // annotations. @@ -1152,7 +1158,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso /** * The index of the set of parameters to run the test method with in the list produced by {@link - * #getParameterValuesForMethod(Method)}. + * #getParameterValuesForMethod}. */ int parametersIndex(); diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java index c5f9bf8..0d9f383 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java @@ -16,6 +16,7 @@ package com.google.testing.junit.testparameterinjector; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Verify.verify; +import static com.google.common.collect.Iterables.getOnlyElement; import static java.util.Arrays.stream; import static java.util.stream.Collectors.toList; @@ -50,30 +51,23 @@ import java.util.Objects; import java.util.stream.Collector; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.junit.runners.model.TestClass; /** {@code TestMethodProcessor} implementation for supporting {@link TestParameters}. */ @SuppressWarnings("AndroidJdkLibsChecker") // Parameter is not available on old Android SDKs. final class TestParametersMethodProcessor implements TestMethodProcessor { - private final TestClass testClass; - private final LoadingCache> parameterValuesByConstructorOrMethodCache = CacheBuilder.newBuilder() .maximumSize(1000) .build(CacheLoader.from(TestParametersMethodProcessor::toParameterValuesList)); - public TestParametersMethodProcessor(TestClass testClass) { - this.testClass = testClass; - } - @Override public ExecutableValidationResult validateConstructor(Constructor constructor) { if (hasRelevantAnnotation(constructor)) { try { // This method throws an exception if there is a validation error - getConstructorParameters(); + getConstructorParameters(constructor); } catch (Throwable t) { return ExecutableValidationResult.validated(t); } @@ -84,7 +78,7 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { } @Override - public ExecutableValidationResult validateTestMethod(Method testMethod) { + public ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass) { if (hasRelevantAnnotation(testMethod)) { try { // This method throws an exception if there is a validation error @@ -100,7 +94,8 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { @Override public List calculateTestInfos(TestInfo originalTest) { - boolean constructorIsParameterized = hasRelevantAnnotation(testClass.getOnlyConstructor()); + boolean constructorIsParameterized = + hasRelevantAnnotation(getOnlyConstructor(originalTest.getTestClass())); boolean methodIsParameterized = hasRelevantAnnotation(originalTest.getMethod()); if (!constructorIsParameterized && !methodIsParameterized) { @@ -110,7 +105,7 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { ImmutableList.Builder testInfos = ImmutableList.builder(); ImmutableList> constructorParametersList = - getConstructorParametersOrSingleAbsentElement(); + getConstructorParametersOrSingleAbsentElement(originalTest.getTestClass()); ImmutableList> methodParametersList = getMethodParametersOrSingleAbsentElement(originalTest.getMethod()); for (int constructorParametersIndex = 0; @@ -160,9 +155,12 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { } private ImmutableList> - getConstructorParametersOrSingleAbsentElement() { - return hasRelevantAnnotation(testClass.getOnlyConstructor()) - ? getConstructorParameters().stream().map(Optional::of).collect(toImmutableList()) + getConstructorParametersOrSingleAbsentElement(Class testClass) { + Constructor constructor = getOnlyConstructor(testClass); + return hasRelevantAnnotation(constructor) + ? getConstructorParameters(constructor).stream() + .map(Optional::of) + .collect(toImmutableList()) : ImmutableList.of(Optional.absent()); } @@ -177,7 +175,8 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { public Optional> maybeGetConstructorParameters( Constructor constructor, TestInfo testInfo) { if (hasRelevantAnnotation(constructor)) { - ImmutableList parameterValuesList = getConstructorParameters(); + ImmutableList parameterValuesList = + getConstructorParameters(constructor); TestParametersValues parametersValues = parameterValuesList.get( testInfo.getAnnotation(TestIndexHolder.class).constructorParametersIndex()); @@ -206,9 +205,9 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { @Override public void postProcessTestInstance(Object testInstance, TestInfo testInfo) {} - private ImmutableList getConstructorParameters() { + private ImmutableList getConstructorParameters(Constructor constructor) { try { - return parameterValuesByConstructorOrMethodCache.getUnchecked(testClass.getOnlyConstructor()); + return parameterValuesByConstructorOrMethodCache.getUnchecked(constructor); } catch (UncheckedExecutionException e) { // Rethrow IllegalStateException because they can be caused by user mistakes and the user // doesn't need to know that the caching layer is in between. @@ -448,6 +447,13 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { .collect(toList()); } + private static Constructor getOnlyConstructor(Class testClass) { + ImmutableList> constructors = ImmutableList.copyOf(testClass.getConstructors()); + checkState( + constructors.size() == 1, "Expected exactly one constructor, but got %s", constructors); + return getOnlyElement(constructors); + } + // Immutable collectors are re-implemented here because they are missing from the Android // collection library. private static Collector> toImmutableList() { diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java index e04a52a..f6fd512 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java @@ -56,6 +56,7 @@ public class TestInfoTest { + "000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000"), + getClass(), /* annotations= */ ImmutableList.of())); ImmutableList result = TestInfo.shortenNamesIfNecessary(givenTestInfos); @@ -237,7 +238,9 @@ public class TestInfoTest { private static TestInfo fakeTestInfo(TestInfoParameter... parameters) throws NoSuchMethodException { return TestInfo.createWithoutParameters( - String.class.getMethod("toLowerCase"), /* annotations= */ ImmutableList.of()) + String.class.getMethod("toLowerCase"), + String.class, + /* annotations= */ ImmutableList.of()) .withExtraParameters(ImmutableList.copyOf(parameters)); } diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java index fb16f8d..39f7b69 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java @@ -924,11 +924,6 @@ public class TestParameterAnnotationMethodProcessorTest { public void test() throws Exception { switch (result) { case SUCCESS_ALWAYS: - assertNoFailures( - PluggableTestRunner.run( - newTestRunnerWithParameterizedSupport( - TestMethodProcessorList::createNewParameterizedProcessorsWithLegacyFeatures))); - assertNoFailures( PluggableTestRunner.run( newTestRunnerWithParameterizedSupport( @@ -936,11 +931,6 @@ public class TestParameterAnnotationMethodProcessorTest { break; case SUCCESS_FOR_ALL_PLACEMENTS_ONLY: - assertNoFailures( - PluggableTestRunner.run( - newTestRunnerWithParameterizedSupport( - TestMethodProcessorList::createNewParameterizedProcessorsWithLegacyFeatures))); - assertThrows( IllegalStateException.class, () -> @@ -950,13 +940,6 @@ public class TestParameterAnnotationMethodProcessorTest { break; case FAILURE: - assertThrows( - IllegalStateException.class, - () -> - PluggableTestRunner.run( - newTestRunnerWithParameterizedSupport( - TestMethodProcessorList - ::createNewParameterizedProcessorsWithLegacyFeatures))); assertThrows( IllegalStateException.class, () -> diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java index 0221deb..fb99ae0 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java @@ -204,8 +204,7 @@ public class TestParameterTest { new PluggableTestRunner(testClass) { @Override protected TestMethodProcessorList createTestMethodProcessorList() { - return TestMethodProcessorList.createNewParameterizedProcessorsWithLegacyFeatures( - getTestClass()); + return TestMethodProcessorList.createNewParameterizedProcessors(getTestClass()); } }); diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java index 9af1409..d8df449 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java @@ -596,8 +596,7 @@ public class TestParametersMethodProcessorTest { return new PluggableTestRunner(testClass) { @Override protected TestMethodProcessorList createTestMethodProcessorList() { - return TestMethodProcessorList.createNewParameterizedProcessorsWithLegacyFeatures( - getTestClass()); + return TestMethodProcessorList.createNewParameterizedProcessors(getTestClass()); } }; } -- cgit v1.2.3 From 3aae89dc13435c65067ce1d000245840b03e2122 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 7 Jan 2022 20:18:40 +0000 Subject: Remove the unused TestClass parameter from TestMethodProcessorList.createNewParameterizedProcessors(). --- .../junit/testparameterinjector/TestMethodProcessorList.java | 3 +-- .../testing/junit/testparameterinjector/TestParameterInjector.java | 2 +- .../TestParameterAnnotationMethodProcessorTest.java | 6 +++--- .../testing/junit/testparameterinjector/TestParameterTest.java | 2 +- .../testparameterinjector/TestParametersMethodProcessorTest.java | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java index ef6e503..867d994 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java @@ -21,7 +21,6 @@ import com.google.common.collect.ImmutableList; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.List; -import org.junit.runners.model.TestClass; /** * Combined version of all {@link TestMethodProcessor} implementations that this package supports. @@ -43,7 +42,7 @@ final class TestMethodProcessorList { *

        • No support for class and method-level parameters, except for @TestParameters * */ - public static TestMethodProcessorList createNewParameterizedProcessors(TestClass testClass) { + public static TestMethodProcessorList createNewParameterizedProcessors() { return new TestMethodProcessorList( ImmutableList.of( new TestParametersMethodProcessor(), diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java index db24479..537969a 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java @@ -28,6 +28,6 @@ public final class TestParameterInjector extends PluggableTestRunner { @Override protected TestMethodProcessorList createTestMethodProcessorList() { - return TestMethodProcessorList.createNewParameterizedProcessors(getTestClass()); + return TestMethodProcessorList.createNewParameterizedProcessors(); } } diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java index 39f7b69..af3808f 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java @@ -927,7 +927,7 @@ public class TestParameterAnnotationMethodProcessorTest { assertNoFailures( PluggableTestRunner.run( newTestRunnerWithParameterizedSupport( - TestMethodProcessorList::createNewParameterizedProcessors))); + testClass -> TestMethodProcessorList.createNewParameterizedProcessors()))); break; case SUCCESS_FOR_ALL_PLACEMENTS_ONLY: @@ -936,7 +936,7 @@ public class TestParameterAnnotationMethodProcessorTest { () -> PluggableTestRunner.run( newTestRunnerWithParameterizedSupport( - TestMethodProcessorList::createNewParameterizedProcessors))); + testClass -> TestMethodProcessorList.createNewParameterizedProcessors()))); break; case FAILURE: @@ -945,7 +945,7 @@ public class TestParameterAnnotationMethodProcessorTest { () -> PluggableTestRunner.run( newTestRunnerWithParameterizedSupport( - TestMethodProcessorList::createNewParameterizedProcessors))); + testClass -> TestMethodProcessorList.createNewParameterizedProcessors()))); break; } } diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java index fb99ae0..e8c449d 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java @@ -204,7 +204,7 @@ public class TestParameterTest { new PluggableTestRunner(testClass) { @Override protected TestMethodProcessorList createTestMethodProcessorList() { - return TestMethodProcessorList.createNewParameterizedProcessors(getTestClass()); + return TestMethodProcessorList.createNewParameterizedProcessors(); } }); diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java index d8df449..3e15277 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java @@ -596,7 +596,7 @@ public class TestParametersMethodProcessorTest { return new PluggableTestRunner(testClass) { @Override protected TestMethodProcessorList createTestMethodProcessorList() { - return TestMethodProcessorList.createNewParameterizedProcessors(getTestClass()); + return TestMethodProcessorList.createNewParameterizedProcessors(); } }; } -- cgit v1.2.3 From c72defd80abea48060369ae51142c353a2e35347 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 7 Jan 2022 20:32:17 +0000 Subject: Bugfix: Don't filter parameters that are duplicated between field and constructor parameter. The extended test case in this test used to fail. Now it succeeds. --- .../TestParameterAnnotationMethodProcessor.java | 18 +++------------- .../testparameterinjector/TestParameterTest.java | 24 ++++++++++++++++------ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 114fb16..7efe653 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -870,13 +870,9 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso case FIELD: // Fall through. case CLASS: return getAnnotationListWithType( - getOnlyConstructor(testClass).getAnnotations(), - annotationTypeOrigin.annotationType()) - .isEmpty() - && getAnnotationListWithType( - getOnlyConstructor(testClass).getParameterAnnotations(), - annotationTypeOrigin.annotationType()) - .isEmpty(); + getOnlyConstructor(testClass).getAnnotations(), + annotationTypeOrigin.annotationType()) + .isEmpty(); default: return true; } @@ -1014,14 +1010,6 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return resultBuilder.build(); } - private ImmutableList getAnnotationListWithType( - Annotation[][] parameterAnnotations, Class annotationType) { - return stream(parameterAnnotations) - .flatMap(Stream::of) - .filter(annotation -> annotation.annotationType().equals(annotationType)) - .collect(toImmutableList()); - } - private ImmutableList getAnnotationListWithType( Annotation[] annotations, Class annotationType) { return stream(annotations) diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java index e8c449d..85cb686 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java @@ -76,14 +76,16 @@ public class TestParameterTest { @RunAsTest public static class AnnotatedConstructorParameter { - private static List testedParameters; + private static List testedParameters; - private final TestEnum enumParameter; + private final TestEnum constructorEnum; - public AnnotatedConstructorParameter(@TestParameter TestEnum enumParameter) { - this.enumParameter = enumParameter; + public AnnotatedConstructorParameter(@TestParameter TestEnum constructorEnum) { + this.constructorEnum = constructorEnum; } + @TestParameter TestEnum fieldEnum; + @BeforeClass public static void initializeStaticFields() { assertWithMessage("Expecting this class to be run only once").that(testedParameters).isNull(); @@ -92,12 +94,22 @@ public class TestParameterTest { @Test public void test() { - testedParameters.add(enumParameter); + testedParameters.add(String.format("%s:%s", fieldEnum, constructorEnum)); } @AfterClass public static void completedAllParameterizedTests() { - assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO, TestEnum.THREE); + assertThat(testedParameters) + .containsExactly( + "ONE:ONE", + "ONE:TWO", + "ONE:THREE", + "TWO:ONE", + "TWO:TWO", + "TWO:THREE", + "THREE:ONE", + "THREE:TWO", + "THREE:THREE"); } } -- cgit v1.2.3 From bbd3b29f325d154a71a74ffcaa309d7f87bd9287 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 7 Jan 2022 20:34:08 +0000 Subject: TestParameterInjector: Move the test method checks into PluggableTestRunner. Reason: These checks are not (all) applicable to JUnit5. --- .../testparameterinjector/PluggableTestRunner.java | 10 ++++++++++ .../TestParameterAnnotationMethodProcessor.java | 22 ++-------------------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java index 6c676f3..1905ab1 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java @@ -25,6 +25,7 @@ import com.google.common.collect.Lists; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.List; import java.util.stream.Collector; import java.util.stream.Collectors; @@ -352,6 +353,15 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { getTestMethodProcessors() .validateTestMethod(testMethod.getMethod(), getTestClass().getJavaClass()); + if (Modifier.isStatic(testMethod.getMethod().getModifiers())) { + errorsReturned.add( + new Exception(String.format("Method %s() should not be static", testMethod.getName()))); + } + if (!Modifier.isPublic(testMethod.getMethod().getModifiers())) { + errorsReturned.add( + new Exception(String.format("Method %s() should be public", testMethod.getName()))); + } + if (validationResult.wasValidated()) { errorsReturned.addAll(validationResult.validationErrors()); } else { diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 7efe653..0f3665d 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -43,7 +43,6 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; import java.text.MessageFormat; import java.util.ArrayList; @@ -504,31 +503,14 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return ExecutableValidationResult.notValidated(); } else { // The method has parameters, they must be injected by a TestParameterAnnotation annotation. - - List errors = new ArrayList<>(); - if (Modifier.isStatic(testMethod.getModifiers())) { - errors.add( - new Exception(String.format("Method %s() should not be static", testMethod.getName()))); - } - if (!Modifier.isPublic(testMethod.getModifiers())) { - errors.add( - new Exception(String.format("Method %s() should be public", testMethod.getName()))); - } - if (testMethod.getReturnType() != Void.TYPE) { - errors.add( - new Exception(String.format("Method %s() should return void", testMethod.getName()))); - } - Annotation[][] parametersAnnotations = testMethod.getParameterAnnotations(); - errors.addAll( + return ExecutableValidationResult.validated( validateMethodOrConstructorParameters( getAnnotationTypeOrigins( testClass, Origin.CLASS, Origin.METHOD, Origin.METHOD_PARAMETER), testClass, testMethod, methodParameterTypes, - parametersAnnotations)); - - return ExecutableValidationResult.validated(errors); + testMethod.getParameterAnnotations())); } } -- cgit v1.2.3 From 243baa20ddecf557adb5ca90ba70f6af787f5542 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 7 Jan 2022 20:39:26 +0000 Subject: TestParameterInjector: Replace reflection calls from public-only access to include private and package private access. This is necessary because JUnit5 tests can be package private. --- .../TestParameterAnnotationMethodProcessor.java | 11 ++++--- .../TestParametersMethodProcessor.java | 3 +- ...TestParameterAnnotationMethodProcessorTest.java | 36 ++++++++++++++++++++-- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 0f3665d..b2be9b6 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -336,19 +336,20 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso Origin.METHOD); List parameterAnnotations = extractTestParameterAnnotations( - stream(testClass.getMethods()) + streamWithParents(testClass) + .flatMap(c -> stream(c.getDeclaredMethods())) .flatMap(method -> stream(method.getParameterAnnotations()).flatMap(Stream::of)), Origin.METHOD_PARAMETER); List classAnnotations = extractTestParameterAnnotations(stream(testClass.getAnnotations()), Origin.CLASS); List constructorAnnotations = extractTestParameterAnnotations( - stream(testClass.getConstructors()) + stream(testClass.getDeclaredConstructors()) .flatMap(constructor -> stream(constructor.getAnnotations())), Origin.CONSTRUCTOR); List constructorParameterAnnotations = extractTestParameterAnnotations( - stream(testClass.getConstructors()) + stream(testClass.getDeclaredConstructors()) .flatMap( constructor -> stream(constructor.getParameterAnnotations()).flatMap(Stream::of)), @@ -1000,7 +1001,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } private static Constructor getOnlyConstructor(Class testClass) { - Constructor[] constructors = testClass.getConstructors(); + Constructor[] constructors = testClass.getDeclaredConstructors(); checkState( constructors.length == 1, "a single public constructor is required for class %s", @@ -1259,7 +1260,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso private ImmutableList getMethodsIncludingParents(Class clazz) { ImmutableList.Builder resultBuilder = ImmutableList.builder(); while (clazz != null) { - resultBuilder.add(clazz.getMethods()); + resultBuilder.add(clazz.getDeclaredMethods()); clazz = clazz.getSuperclass(); } return resultBuilder.build(); diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java index 0d9f383..bffc3b4 100644 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java +++ b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java @@ -448,7 +448,8 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { } private static Constructor getOnlyConstructor(Class testClass) { - ImmutableList> constructors = ImmutableList.copyOf(testClass.getConstructors()); + ImmutableList> constructors = + ImmutableList.copyOf(testClass.getDeclaredConstructors()); checkState( constructors.size() == 1, "Expected exactly one constructor, but got %s", constructors); return getOnlyElement(constructors); diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java index af3808f..9af29ae 100644 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java +++ b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java @@ -26,6 +26,7 @@ import static org.junit.Assert.fail; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider; +import com.google.testing.junit.testparameterinjector.TestParameterAnnotationMethodProcessorTest.ErrorNonStaticProviderClass.NonStaticProvider; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.util.ArrayList; @@ -502,6 +503,30 @@ public class TestParameterAnnotationMethodProcessorTest { } } + public abstract static class BaseClassWithSingleTest { + @Rule public TestName testName = new TestName(); + + static List allTestNames; + + @BeforeClass + public static void resetStaticState() { + allTestNames = new ArrayList<>(); + } + + @Test + public void testInBase(@TestParameter boolean b) { + allTestNames.add(testName.getMethodName()); + } + + @AfterClass + public static void completedAllParameterizedTests() { + assertThat(allTestNames).containsExactly("testInBase[b=true]", "testInBase[b=false]"); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class SimpleTestInheritedFromBaseClass extends BaseClassWithSingleTest {} + public abstract static class BaseClassWithAnnotations { @Rule public TestName testName = new TestName(); @@ -778,6 +803,13 @@ public class TestParameterAnnotationMethodProcessorTest { } } + @ClassTestResult(Result.FAILURE) + public static class ErrorNonPublicTestMethod { + + @Test + void test(@TestParameter boolean b) {} + } + public enum EnumA { A1, A2 @@ -932,7 +964,7 @@ public class TestParameterAnnotationMethodProcessorTest { case SUCCESS_FOR_ALL_PLACEMENTS_ONLY: assertThrows( - IllegalStateException.class, + RuntimeException.class, () -> PluggableTestRunner.run( newTestRunnerWithParameterizedSupport( @@ -941,7 +973,7 @@ public class TestParameterAnnotationMethodProcessorTest { case FAILURE: assertThrows( - IllegalStateException.class, + RuntimeException.class, () -> PluggableTestRunner.run( newTestRunnerWithParameterizedSupport( -- cgit v1.2.3 From 0dba80e52f671ca02e23e997e8c58b2ddb16fd7d Mon Sep 17 00:00:00 2001 From: Alex Jurkowski Date: Tue, 11 Jan 2022 11:53:37 -0500 Subject: Add maven caching to the `build` GitHub action. This should speed up the action by avoiding downloading of all dependencies for every build. --- .github/workflows/build.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5759fb4..717250f 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -33,8 +33,9 @@ jobs: with: distribution: 'zulu' java-version: 11 + cache: maven - - run: mvn verify javadoc:javadoc + - run: mvn --update-snapshots -B verify javadoc:javadoc - name: Deploy docs to website if: ${{ github.ref == 'refs/heads/main' }} -- cgit v1.2.3 From dd5ffa98e2edb5b18c03bbd9ca6dc52e2fc56cb7 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 14 Jan 2022 09:52:57 +0000 Subject: Add basic support for JUnit5 in new Maven target. See https://github.com/google/TestParameterInjector/issues/11 Notable things that are missing in this first version: - @Nested support - Support for injecting other types, such as JUnit5's TestInfo --- junit4/pom.xml | 41 + .../BaseTestParameterValidator.java | 83 ++ .../ExecutableValidationResult.java | 72 ++ .../ParameterValueParsing.java | 249 ++++ .../testparameterinjector/PluggableTestRunner.java | 432 +++++++ .../testparameterinjector/ProtoValueParsing.java | 25 + .../junit/testparameterinjector/TestInfo.java | 309 +++++ .../testparameterinjector/TestMethodProcessor.java | 66 + .../TestMethodProcessorList.java | 146 +++ .../junit/testparameterinjector/TestParameter.java | 224 ++++ .../TestParameterAnnotation.java | 251 ++++ .../TestParameterAnnotationMethodProcessor.java | 1290 ++++++++++++++++++++ .../TestParameterInjector.java | 33 + .../TestParameterValidator.java | 68 ++ .../TestParameterValueProvider.java | 52 + .../testparameterinjector/TestParameterValues.java | 27 + .../testparameterinjector/TestParameters.java | 259 ++++ .../TestParametersMethodProcessor.java | 485 ++++++++ .../ParameterValueParsingTest.java | 147 +++ .../PluggableTestRunnerTest.java | 154 +++ .../junit/testparameterinjector/TestInfoTest.java | 253 ++++ ...TestParameterAnnotationMethodProcessorTest.java | 1012 +++++++++++++++ .../testparameterinjector/TestParameterTest.java | 243 ++++ .../TestParametersMethodProcessorTest.java | 621 ++++++++++ junit5/pom.xml | 60 + .../BaseTestParameterValidator.java | 83 ++ .../ExecutableValidationResult.java | 72 ++ .../ParameterValueParsing.java | 249 ++++ .../testparameterinjector/ProtoValueParsing.java | 25 + .../junit/testparameterinjector/TestInfo.java | 309 +++++ .../testparameterinjector/TestMethodProcessor.java | 66 + .../TestMethodProcessorList.java | 146 +++ .../junit/testparameterinjector/TestParameter.java | 224 ++++ .../TestParameterAnnotation.java | 251 ++++ .../TestParameterAnnotationMethodProcessor.java | 1290 ++++++++++++++++++++ .../TestParameterInjectorExtension.java | 140 +++ .../TestParameterInjectorTest.java | 50 + .../TestParameterValidator.java | 68 ++ .../TestParameterValueProvider.java | 52 + .../testparameterinjector/TestParameterValues.java | 27 + .../testparameterinjector/TestParameters.java | 259 ++++ .../TestParametersMethodProcessor.java | 485 ++++++++ .../TestParameterInjectorJUnit5Test.java | 607 +++++++++ pom.xml | 16 +- .../BaseTestParameterValidator.java | 83 -- .../ExecutableValidationResult.java | 61 - .../ParameterValueParsing.java | 249 ---- .../testparameterinjector/PluggableTestRunner.java | 432 ------- .../testparameterinjector/ProtoValueParsing.java | 25 - .../junit/testparameterinjector/TestInfo.java | 309 ----- .../testparameterinjector/TestMethodProcessor.java | 66 - .../TestMethodProcessorList.java | 146 --- .../junit/testparameterinjector/TestParameter.java | 224 ---- .../TestParameterAnnotation.java | 251 ---- .../TestParameterAnnotationMethodProcessor.java | 1290 -------------------- .../TestParameterInjector.java | 33 - .../TestParameterValidator.java | 68 -- .../TestParameterValueProvider.java | 52 - .../testparameterinjector/TestParameterValues.java | 27 - .../testparameterinjector/TestParameters.java | 259 ---- .../TestParametersMethodProcessor.java | 485 -------- .../ParameterValueParsingTest.java | 147 --- .../PluggableTestRunnerTest.java | 154 --- .../junit/testparameterinjector/TestInfoTest.java | 253 ---- ...TestParameterAnnotationMethodProcessorTest.java | 1012 --------------- .../testparameterinjector/TestParameterTest.java | 243 ---- .../TestParametersMethodProcessorTest.java | 621 ---------- 67 files changed, 11014 insertions(+), 6497 deletions(-) create mode 100644 junit4/pom.xml create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java create mode 100644 junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java create mode 100644 junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java create mode 100644 junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java create mode 100644 junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java create mode 100644 junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java create mode 100644 junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java create mode 100644 junit5/pom.xml create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorExtension.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorTest.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java create mode 100644 junit5/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorJUnit5Test.java delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java delete mode 100644 src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java delete mode 100644 src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java delete mode 100644 src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java delete mode 100644 src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java delete mode 100644 src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java delete mode 100644 src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java delete mode 100644 src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java diff --git a/junit4/pom.xml b/junit4/pom.xml new file mode 100644 index 0000000..789f648 --- /dev/null +++ b/junit4/pom.xml @@ -0,0 +1,41 @@ + + + + + 4.0.0 + + + com.google.testparameterinjector + test-parameter-injector-parent + HEAD-SNAPSHOT + + + test-parameter-injector + + TestParameterInjector for JUnit4 + + + + + junit + junit + 4.13.2 + + + diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java new file mode 100644 index 0000000..ab5003e --- /dev/null +++ b/junit4/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> 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 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 parameter : parameters) { + List 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 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>> getIndependentParameters( + Context context); +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java new file mode 100644 index 0000000..47b445b --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java @@ -0,0 +1,72 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.Iterables.getOnlyElement; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import java.util.Collection; + +/** + * Value class that captures the result of a validating a single constructor or test method. + * + *

          If the validation is not validated by any processor, it will be validated using the default + * validator. If a processor validates a constructor/test method, the remaining processors will + * *not* be called. + */ +@AutoValue +abstract class ExecutableValidationResult { + + /** Returns true if the properties of the given constructor/test method were validated. */ + public abstract boolean wasValidated(); + + /** Returns the validation errors, if any. */ + public abstract ImmutableList validationErrors(); + + static ExecutableValidationResult notValidated() { + return of(/* wasValidated= */ false, /* validationErrors= */ ImmutableList.of()); + } + + static ExecutableValidationResult validated(Collection errors) { + return of(/* wasValidated= */ true, /* validationErrors= */ errors); + } + + static ExecutableValidationResult validated(Throwable error) { + return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of(error)); + } + + static ExecutableValidationResult valid() { + return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of()); + } + + private static ExecutableValidationResult of( + boolean wasValidated, Collection validationErrors) { + checkArgument(wasValidated || validationErrors.isEmpty()); + return new AutoValue_ExecutableValidationResult( + wasValidated, ImmutableList.copyOf(validationErrors)); + } + + void assertValid() { + if (wasValidated() && !validationErrors().isEmpty()) { + if (validationErrors().size() == 1) { + throw new AssertionError(getOnlyElement(validationErrors())); + } else { + throw new AssertionError(String.format("Found validation errors: %s", validationErrors())); + } + } + } +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java new file mode 100644 index 0000000..f7c7cd6 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.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.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 static java.util.stream.Collectors.toMap; + +import com.google.common.collect.Lists; +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 > Enum parseEnum(String str, Class enumType) { + return Enum.valueOf((Class) 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) + .supportParsedType(String.class, Float::valueOf); + + yamlValueTransformer + .ifJavaType(Double.class) + .supportParsedType(Double.class, identity()) + .supportParsedType(Integer.class, Integer::doubleValue) + .supportParsedType(Long.class, Long::doubleValue) + .supportParsedType(String.class, Double::valueOf); + + yamlValueTransformer + .ifJavaType(Enum.class) + .supportParsedType( + String.class, str -> ParameterValueParsing.parseEnum(str, javaType.getRawType())); + + yamlValueTransformer + .ifJavaType(MessageLite.class) + .supportParsedType(String.class, str -> parseTextprotoMessage(str, javaType.getRawType())) + .supportParsedType( + Map.class, + map -> + getProtoValueParser() + .parseProtobufMessage((Map) 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 -> parseYamlMapToJavaMap(map, javaType)); + + return yamlValueTransformer.transformedJavaValue(); + } + + private static Map parseYamlMapToJavaMap(Map map, TypeToken javaType) { + return map.entrySet().stream() + .collect( + toMap( + entry -> + parseYamlObjectToJavaType( + entry.getKey(), getGenericParameterType(javaType, /* parameterIndex= */ 0)), + entry -> + parseYamlObjectToJavaType( + entry.getValue(), + getGenericParameterType(javaType, /* parameterIndex= */ 1)))); + } + + 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; + } + + SupportedJavaType ifJavaType(Class 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 { + + private final Class supportedJavaType; + + private SupportedJavaType(Class supportedJavaType) { + this.supportedJavaType = supportedJavaType; + } + + @SuppressWarnings("unchecked") + SupportedJavaType supportParsedType( + Class parsedYamlType, Function transformation) { + if (Primitives.wrap(supportedJavaType).isAssignableFrom(Primitives.wrap(javaType))) { + if (Primitives.wrap(parsedYamlType).isInstance(parsedYaml)) { + checkState( + transformedJavaValue == null, + "This case is already handled. This is a bug in" + + " testparameterinjector.TestParametersMethodProcessor."); + try { + transformedJavaValue = checkNotNull(transformation.apply((ParsedYamlT) parsedYaml)); + } catch (Exception e) { + throw new IllegalArgumentException( + String.format( + "Could not map YAML value %s (class = %s) to java class %s", + parsedYaml, parsedYaml.getClass(), javaType), + e); + } + } + } + + return this; + } + } + } + + static 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/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java new file mode 100644 index 0000000..1905ab1 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java @@ -0,0 +1,432 @@ +/* + * 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.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.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. + * + *

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

          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 currentTestInfo = new ThreadLocal<>(); + + private TestMethodProcessorList testMethodProcessors; + + protected PluggableTestRunner(Class klass) throws InitializationError { + super(klass); + } + + /** Returns the TestMethodProcessorList to use. This is meant to be overridden by subclasses. */ + protected abstract TestMethodProcessorList createTestMethodProcessorList(); + + /** + * This method is run to perform optional additional operations on the test instance, right after + * it was created. + */ + protected void finalizeCreatedTestInstance(Object testInstance) { + // Do nothing by default + } + + /** + * If true, all test methods (across different TestMethodProcessors) will be sorted in a + * deterministic way. + * + *

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

          This should be deterministic. The order should not change, even when tests are added/removed + * or between releases. + */ + protected Stream sortTestMethods(Stream 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. + * + *

          Defaults to {@link Test}. + */ + protected ImmutableList> 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 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 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 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 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 run(PluggableTestRunner testRunner) throws Exception { + final ImmutableList.Builder 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 computeTestMethods() { + Stream 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 annotations = testInfo.getAnnotations(); + return annotations.toArray(new Annotation[0]); + } + + @Override + public T getAnnotation(final Class 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 processMethod(FrameworkMethod initialMethod) { + return getTestMethodProcessors() + .calculateTestInfos(initialMethod.getMethod(), getTestClass().getJavaClass()) + .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) { + TestInfo testInfo = ((OverriddenFrameworkMethod) frameworkMethod).getTestInfo(); + + if (testInfo.getMethod().getParameterTypes().length == 0) { + return super.methodInvoker(frameworkMethod, testObject); + } else { + List parameters = getTestMethodProcessors().getTestMethodParameters(testInfo); + return new Statement() { + @Override + public void evaluate() throws Throwable { + frameworkMethod.invokeExplosively(testObject, parameters.toArray()); + } + }; + } + } + + /** Modifies the statement with each {@link MethodRule} and {@link TestRule} */ + private Statement withRules(FrameworkMethod method, Object target, Statement statement) { + ImmutableList testRules = + Stream.of( + getInnerTestRules().stream(), + getTestRules(target).stream(), + getOuterTestRules().stream()) + .flatMap(x -> x) + .collect(toImmutableList()); + + Iterable 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 { + TestInfo testInfo = ((OverriddenFrameworkMethod) method).getTestInfo(); + Constructor constructor = getTestClass().getOnlyConstructor(); + + // Construct a test instance + Object testInstance; + if (constructor.getParameterTypes().length == 0) { + testInstance = createTest(); + } else { + List constructorParameters = + getTestMethodProcessors().getConstructorParameters(constructor, testInfo); + try { + testInstance = constructor.newInstance(constructorParameters.toArray()); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + // Run all post processors on the newly created instance + getTestMethodProcessors().postProcessTestInstance(testInstance, testInfo); + + finalizeCreatedTestInstance(testInstance); + + return testInstance; + } + + @Override + protected final void validateZeroArgConstructor(List errorsReturned) { + ExecutableValidationResult validationResult = + getTestMethodProcessors().validateConstructor(getTestClass().getOnlyConstructor()); + + if (validationResult.wasValidated()) { + errorsReturned.addAll(validationResult.validationErrors()); + } else { + super.validateZeroArgConstructor(errorsReturned); + } + } + + @Override + protected final void validateTestMethods(List errorsReturned) { + List testMethods = + getSupportedTestAnnotations().stream() + .flatMap(annotation -> getTestClass().getAnnotatedMethods(annotation).stream()) + .collect(toImmutableList()); + for (FrameworkMethod testMethod : testMethods) { + ExecutableValidationResult validationResult = + getTestMethodProcessors() + .validateTestMethod(testMethod.getMethod(), getTestClass().getJavaClass()); + + if (Modifier.isStatic(testMethod.getMethod().getModifiers())) { + errorsReturned.add( + new Exception(String.format("Method %s() should not be static", testMethod.getName()))); + } + if (!Modifier.isPublic(testMethod.getMethod().getModifiers())) { + errorsReturned.add( + new Exception(String.format("Method %s() should be public", testMethod.getName()))); + } + + if (validationResult.wasValidated()) { + errorsReturned.addAll(validationResult.validationErrors()); + } else { + testMethod.validatePublicVoidNoArg(/* isStatic= */ false, errorsReturned); + } + } + } + + // Fix for ParentRunner bug: + // Overriding this method because a superclass (ParentRunner) is calling this in its constructor + // and then throwing an InitializationError that doesn't have any of the causes in the exception + // message. + @Override + protected final void collectInitializationErrors(List 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 annotation, boolean isStatic, List errors) { + super.validatePublicVoidNoArgMethods(annotation, isStatic, errors); + } + + private synchronized TestMethodProcessorList getTestMethodProcessors() { + if (testMethodProcessors == null) { + testMethodProcessors = createTestMethodProcessorList(); + } + return testMethodProcessors; + } + + /** {@link MethodRule} that sets up the Context for each test. */ + private static class ContextMethodRule implements MethodRule { + @Override + public Statement apply(Statement statement, FrameworkMethod method, Object o) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + currentTestInfo.set(((OverriddenFrameworkMethod) method).getTestInfo()); + try { + statement.evaluate(); + } finally { + currentTestInfo.set(null); + } + } + }; + } + } + + private static Collector> toImmutableList() { + return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf); + } +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java new file mode 100644 index 0000000..61cf13b --- /dev/null +++ b/junit4/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 map, Class javaType); +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java new file mode 100644 index 0000000..69777d2 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java @@ -0,0 +1,309 @@ +/* + * 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.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. + * + *

          See b/168325767 for the reason behind this. tl;dr the name is put into a Unix file with max + * 255 characters. The surrounding constant characters take up 31 characters. The max is reduced + * by an additional 24 characters to account for future changes. + */ + static final int MAX_TEST_NAME_LENGTH = 200; + + public abstract Method getMethod(); + + /** + * The test class that is being run. + * + *

          Note that this is not always the same as the class that declares {@link #getMethod()} + * because test methods can be inherited. + */ + public abstract Class getTestClass(); + + public final String getName() { + if (getParameters().isEmpty()) { + return getMethod().getName(); + } else { + return String.format( + "%s[%s]", + getMethod().getName(), + getParameters().stream().map(TestInfoParameter::getName).collect(joining(","))); + } + } + + abstract ImmutableList getParameters(); + + public abstract ImmutableList getAnnotations(); + + @Nullable + public final T getAnnotation(Class annotationClass) { + for (Annotation annotation : getAnnotations()) { + if (annotationClass.isInstance(annotation)) { + return annotationClass.cast(annotation); + } + } + return null; + } + + final TestInfo withExtraParameters(List parameters) { + return new AutoValue_TestInfo( + getMethod(), + getTestClass(), + ImmutableList.builder() + .addAll(this.getParameters()) + .addAll(parameters) + .build(), + getAnnotations()); + } + + final TestInfo withExtraAnnotation(Annotation annotation) { + ImmutableList newAnnotations = + ImmutableList.builder().addAll(this.getAnnotations()).add(annotation).build(); + return new AutoValue_TestInfo(getMethod(), getTestClass(), getParameters(), newAnnotations); + } + + /** + * Returns a new TestInfo instance with updated parameter names. + * + * @param parameterWithIndexToNewName A function of the parameter and its index in the {@link + * #getParameters()} list to the new name. + */ + private TestInfo withUpdatedParameterNames( + BiFunction parameterWithIndexToNewName) { + return new AutoValue_TestInfo( + getMethod(), + getTestClass(), + 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, Class testClass, String name, List annotations) { + return new AutoValue_TestInfo( + method, testClass, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations)); + } + + static TestInfo createWithoutParameters( + Method method, Class testClass, List annotations) { + return new AutoValue_TestInfo( + method, testClass, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations)); + } + + static ImmutableList shortenNamesIfNecessary(List testInfos) { + if (testInfos.stream().anyMatch(info -> info.getName().length() > MAX_TEST_NAME_LENGTH)) { + int numberOfParameters = testInfos.get(0).getParameters().size(); + + if (numberOfParameters == 0) { + return ImmutableList.copyOf(testInfos); + } else { + Set 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; + + // Subtract 4 characters to leave place for joining commas and the parameter index. + return maxLengthOfAllParameters / numberOfParameters - 4; + } + + static ImmutableList deduplicateTestNames(List 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 maybeAddTypesIfDuplicate(List testInfos) { + Multimap testNameToInfo = + MultimapBuilder.linkedHashKeys().arrayListValues().build(); + for (TestInfo testInfo : testInfos) { + testNameToInfo.put(testInfo.getName(), testInfo); + } + + return testNameToInfo.keySet().stream() + .flatMap( + testName -> { + Collection 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 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 deduplicateWithNumberPrefixes( + ImmutableList 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 Collector> 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(); + + final 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/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java new file mode 100644 index 0000000..60a01bc --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java @@ -0,0 +1,66 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import com.google.common.base.Optional; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; + +/** + * Interface to change the list of methods used in a test. + * + *

          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 calculateTestInfos(TestInfo originalTest); + + /** + * If this processor can handle the given constructor, returns the parameters with which it should + * be invoked. + * + *

          This method is never called for a parameterless constructor. + */ + Optional> maybeGetConstructorParameters( + Constructor constructor, TestInfo testInfo); + + /** + * If this processor can handle the given test, returns the parameters with which {@code + * testInfo.getMethod()} should be invoked. + * + *

          This method is never called for a parameterless {@code testInfo.getMethod()}. + */ + Optional> maybeGetTestMethodParameters(TestInfo testInfo); + + /** + * Optionally process the test instance right after construction to ready it for the given test + * instance. + */ + void postProcessTestInstance(Object testInstance, TestInfo testInfo); + + /** Optionally validates the given constructor. */ + ExecutableValidationResult validateConstructor(Constructor constructor); + + /** + * Optionally validates the given method. + * + *

          Note that the given method is not necessarily declared in the given class because test + * methods can be inherited. + */ + ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass); +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java new file mode 100644 index 0000000..867d994 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java @@ -0,0 +1,146 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static java.util.stream.Collectors.toList; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; + +/** + * Combined version of all {@link TestMethodProcessor} implementations that this package supports. + */ +final class TestMethodProcessorList { + + private final ImmutableList testMethodProcessors; + + private TestMethodProcessorList(ImmutableList testMethodProcessors) { + this.testMethodProcessors = testMethodProcessors; + } + + /** + * Returns a TestMethodProcessorList that supports all features that this package supports, except + * the following legacy features: + * + *

            + *
          • No support for {@link org.junit.runners.Parameterized} + *
          • No support for class and method-level parameters, except for @TestParameters + *
          + */ + public static TestMethodProcessorList createNewParameterizedProcessors() { + return new TestMethodProcessorList( + ImmutableList.of( + new TestParametersMethodProcessor(), + TestParameterAnnotationMethodProcessor.onlyForFieldsAndParameters())); + } + + static TestMethodProcessorList empty() { + return new TestMethodProcessorList(ImmutableList.of()); + } + + /** + * Calculates the TestInfo instances for the given test method. Each TestInfo corresponds to a + * single test. + * + *

          The returned list always contains at least one element. If there is no parameterization, + * this would be the TestInfo for running the test method without parameters. + */ + public List calculateTestInfos(Method testMethod, Class testClass) { + List testInfos = + ImmutableList.of( + TestInfo.createWithoutParameters( + testMethod, testClass, ImmutableList.copyOf(testMethod.getAnnotations()))); + + for (final TestMethodProcessor testMethodProcessor : testMethodProcessors) { + testInfos = + testInfos.stream() + .flatMap( + lastTestInfo -> testMethodProcessor.calculateTestInfos(lastTestInfo).stream()) + .collect(toList()); + } + + testInfos = TestInfo.deduplicateTestNames(TestInfo.shortenNamesIfNecessary(testInfos)); + + return testInfos; + } + + /** + * Returns the parameters with which it should be invoked. + * + *

          This method is never called for a parameterless constructor. + */ + public List getConstructorParameters(Constructor constructor, TestInfo testInfo) { + return testMethodProcessors.stream() + .map(processor -> processor.maybeGetConstructorParameters(constructor, testInfo)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .orElseThrow( + () -> + new IllegalStateException( + String.format( + "Could not generate parameter values for %s. Did you forget an annotation?", + constructor))); + } + + /** + * Returns the parameters with which {@code testInfo.getMethod()} should be invoked. + * + *

          This method is never called for a parameterless {@code testInfo.getMethod()}. + */ + public List getTestMethodParameters(TestInfo testInfo) { + return testMethodProcessors.stream() + .map(processor -> processor.maybeGetTestMethodParameters(testInfo)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .orElseThrow( + () -> + new IllegalStateException( + String.format( + "Could not generate parameter values for %s. Did you forget an annotation?", + testInfo.getMethod()))); + } + + /** + * Optionally process the test instance right after construction to ready it for the given test. + */ + public void postProcessTestInstance(Object testInstance, TestInfo testInfo) { + for (TestMethodProcessor testMethodProcessor : testMethodProcessors) { + testMethodProcessor.postProcessTestInstance(testInstance, testInfo); + } + } + + /** Optionally validates the given constructor. */ + public ExecutableValidationResult validateConstructor(Constructor constructor) { + return testMethodProcessors.stream() + .map(processor -> processor.validateConstructor(constructor)) + .filter(ExecutableValidationResult::wasValidated) + .findFirst() + .orElse(ExecutableValidationResult.notValidated()); + } + + /** Optionally validates the given method. */ + public ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass) { + return testMethodProcessors.stream() + .map(processor -> processor.validateTestMethod(testMethod, testClass)) + .filter(ExecutableValidationResult::wasValidated) + .findFirst() + .orElse(ExecutableValidationResult.notValidated()); + } +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java new file mode 100644 index 0000000..6725d16 --- /dev/null +++ b/junit4/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. + * + *

          For enums and booleans, the values can be automatically derived as all possible values: + * + *

          + * {@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 }
          + * 
          + * + *

          The values can be explicitly defined as a parsed string: + * + *

          + * 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)]
          + * }
          + * 
          + * + *

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

          Types that are supported: + * + *

            + *
          • String: No parsing happens + *
          • boolean: Specified as YAML boolean + *
          • long and int: Specified as YAML integer + *
          • float and double: Specified as YAML floating point or integer + *
          • Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()} + *
          • Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML bytes + * (example: "!!binary 'ZGF0YQ=='") + *
          + * + *

          For dynamic sets of parameters or parameter types that are not supported here, use {@link + * #valuesProvider()} and leave this field empty. + * + *

          For examples, see {@link TestParameter}. + */ + String[] value() default {}; + + /** + * Sets a provider that will return a list of parameter values. + * + *

          If this field is set, {@link #value()} must be empty and vice versa. + * + *

          Example + * + *

          +   * {@literal @}Test
          +   * public void matchesAllOf_throwsOnNull(
          +   *     {@literal @}TestParameter(valuesProvider = CharMatcherProvider.class)
          +   *         CharMatcher charMatcher) {
          +   *   assertThrows(NullPointerException.class, () -> charMatcher.matchesAllOf(null));
          +   * }
          +   *
          +   * private static final class CharMatcherProvider implements TestParameterValuesProvider {
          +   *   {@literal @}Override
          +   *   public {@literal List} provideValues() {
          +   *     return ImmutableList.of(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace());
          +   *   }
          +   * }
          +   * 
          + */ + Class 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 provideValues() { + return ImmutableList.of(); + } + } + + /** Implementation of this parameter annotation. */ + final class InternalImplementationOfThisParameter implements TestParameterValueProvider { + @Override + public List provideValues( + Annotation uncastAnnotation, Optional> 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 annotationType, Optional> 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 getValuesFromProvider( + Class valuesProvider) { + try { + Constructor 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/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java new file mode 100644 index 0000000..8c04bc0 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java @@ -0,0 +1,251 @@ +/* + * 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. + * + *

          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: + * + *

          {@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();
          + *     }
          + * }
          + * }
          + * + *

          An alternative is to use a method parameter for injection: + * + *

          {@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();
          + *     }
          + * }
          + * }
          + * + *

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

          {@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();
          + *     }
          + * }
          + * }
          + * + *

          Class constructors can also be annotated with @TestParameterAnnotation annotations, as shown + * below: + * + *

          {@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() {...}
          + * }
          + * }
          + * + *

          Each field that needs to be injected from a parameter requires its dedicated distinct + * annotation. + * + *

          If the same annotation is defined both on the class and method, the method parameter values + * take precedence. + * + *

          If the same annotation is defined both on the class and constructor, the constructor parameter + * values take precedence. + * + *

          Annotations cannot be duplicated between the constructor or constructor parameters and a + * method or method parameter. + * + *

          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 validator() default DefaultValidator.class; + + /** Specifies a value provider for the parameter to provide the values to test. */ + Class valueProvider() default DefaultValueProvider.class; + + /** Default {@link TestParameterValidator} implementation which skips no test. */ + class DefaultValidator implements TestParameterValidator { + + @Override + public boolean shouldSkip(Context context) { + return false; + } + } + + /** + * Default {@link TestParameterValueProvider} implementation that gets its values from the + * annotation's `value` method. + */ + class DefaultValueProvider implements TestParameterValueProvider { + + @Override + public List provideValues(Annotation annotation, Optional> 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 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 annotationType, Optional> 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 annotationType) { + Method valueMethod; + try { + valueMethod = annotationType.getMethod("value"); + } catch (NoSuchMethodException e) { + throw new RuntimeException( + "The @TestParameterAnnotation annotation should have a single value() method.", e); + } + Object parameters; + try { + parameters = valueMethod.invoke(annotation); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof IllegalAccessError) { + // There seems to be a bug or at least something weird with the JVM that causes + // IllegalAccessError to be thrown because the return value is not visible when it is a + // non-public nested type. See + // http://mail.openjdk.java.net/pipermail/core-libs-dev/2014-January/024180.html for more + // info. + throw new RuntimeException( + String.format( + "Could not access %s.value(). This is probably because %s is not visible to the" + + " annotation proxy. To fix this, make %s public.", + annotationType.getSimpleName(), + valueMethod.getReturnType().getSimpleName(), + valueMethod.getReturnType().getSimpleName())); + // Note: Not chaining the exception to reduce the clutter for the reader + } else { + throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e); + } + } catch (Exception e) { + throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e); + } + return parameters; + } + } +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java new file mode 100644 index 0000000..b2be9b6 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -0,0 +1,1290 @@ +/* + * 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.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +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; + +/** + * {@code TestMethodProcessor} implementation for supporting parameterized tests annotated with + * {@link TestParameterAnnotation}. + * + * @see TestParameterAnnotation + */ +final class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { + + /** + * Class to hold an annotation type and origin and one of the values as returned by the {@code + * value()} method. + */ + @AutoValue + abstract static class 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 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> 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 paramName(); + + /** + * Returns a String that represents this value and is fit for use in a test name (between + * brackets). + */ + String toTestNameString() { + Class 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 create( + AnnotationWithMetadata annotationWithMetadata, Origin origin) { + List 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(/* onlyForFieldsAndParameters= */ false) + .getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()).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 getTestParameterValue( + TestInfo testInfo, Class annotationType) { + return getTestParameterValues(testInfo).getValue(annotationType); + } + + private static List getParametersAnnotationValues( + AnnotationWithMetadata annotationWithMetadata) { + Annotation annotation = annotationWithMetadata.annotation(); + TestParameterAnnotation testParameter = + annotation.annotationType().getAnnotation(TestParameterAnnotation.class); + Class 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 matches(Class 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 annotationType(); + + /** Where the annotation was declared. */ + abstract Origin origin(); + + public static AnnotationTypeOrigin create( + Class 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> 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 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 boolean onlyForFieldsAndParameters; + private final LoadingCache, ImmutableList> + annotationTypeOriginsCache = + CacheBuilder.newBuilder() + .maximumSize(1000) + .build(CacheLoader.from(this::calculateAnnotationTypeOrigins)); + private final Cache>> parameterValuesCache = + CacheBuilder.newBuilder().maximumSize(1000).build(); + + private TestParameterAnnotationMethodProcessor(boolean onlyForFieldsAndParameters) { + this.onlyForFieldsAndParameters = onlyForFieldsAndParameters; + } + + /** + * Constructs a new {@link TestMethodProcessor} that handles {@link + * TestParameterAnnotation}-annotated annotations that are placed anywhere: + * + *
            + *
          • At a method / constructor parameter + *
          • At a field + *
          • At a method / constructor on the class + *
          • At the test class + *
          + */ + static TestMethodProcessor forAllAnnotationPlacements() { + return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ false); + } + + /** + * Constructs a new {@link TestMethodProcessor} that handles {@link + * TestParameterAnnotation}-annotated annotations that are placed at fields or parameters. + * + *

          Note that this excludes class and method-level annotations, as is the default (using the + * constructor). + */ + static TestMethodProcessor onlyForFieldsAndParameters() { + return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ true); + } + + private ImmutableList calculateAnnotationTypeOrigins(Class testClass) { + // Collect all annotations used in declared fields and methods that have themselves a + // @TestParameterAnnotation annotation. + List fieldAnnotations = + extractTestParameterAnnotations( + streamWithParents(testClass) + .flatMap(c -> stream(c.getDeclaredFields())) + .flatMap(field -> stream(field.getAnnotations())), + Origin.FIELD); + List methodAnnotations = + extractTestParameterAnnotations( + stream(testClass.getMethods()).flatMap(method -> stream(method.getAnnotations())), + Origin.METHOD); + List parameterAnnotations = + extractTestParameterAnnotations( + streamWithParents(testClass) + .flatMap(c -> stream(c.getDeclaredMethods())) + .flatMap(method -> stream(method.getParameterAnnotations()).flatMap(Stream::of)), + Origin.METHOD_PARAMETER); + List classAnnotations = + extractTestParameterAnnotations(stream(testClass.getAnnotations()), Origin.CLASS); + List constructorAnnotations = + extractTestParameterAnnotations( + stream(testClass.getDeclaredConstructors()) + .flatMap(constructor -> stream(constructor.getAnnotations())), + Origin.CONSTRUCTOR); + List constructorParameterAnnotations = + extractTestParameterAnnotations( + stream(testClass.getDeclaredConstructors()) + .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); + } + + return 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()); + } + + private ImmutableList getAnnotationTypeOrigins( + Class testClass, Origin firstOrigin, Origin... otherOrigins) { + Set originsToFilterBy = + ImmutableSet.builder().add(firstOrigin).add(otherOrigins).build(); + try { + return annotationTypeOriginsCache.getUnchecked(testClass).stream() + .filter(annotationTypeOrigin -> originsToFilterBy.contains(annotationTypeOrigin.origin())) + .collect(toImmutableList()); + } catch (UncheckedExecutionException e) { + Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); + throw e; + } + } + + private void checkDuplicatedFieldsAnnotations( + List methodAnnotations, List 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> 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 constructorAnnotations, + List classAnnotations, + List fieldAnnotations) { + ImmutableSet> classAnnotationTypes = + classAnnotations.stream() + .map(AnnotationTypeOrigin::annotationType) + .collect(toImmutableSet()); + + ImmutableSet> uniqueFieldAnnotations = + fieldAnnotations.stream() + .map(AnnotationTypeOrigin::annotationType) + .collect(toImmutableSet()); + ImmutableSet> 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 extractTestParameterAnnotations( + Stream 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 ExecutableValidationResult validateConstructor(Constructor constructor) { + Class[] parameterTypes = constructor.getParameterTypes(); + if (parameterTypes.length == 0) { + return ExecutableValidationResult.notValidated(); + } + // The constructor has parameters, they must be injected by a TestParameterAnnotation + // annotation. + Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); + Class testClass = constructor.getDeclaringClass(); + return ExecutableValidationResult.validated( + validateMethodOrConstructorParameters( + removeOverrides( + getAnnotationTypeOrigins( + testClass, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER), + testClass), + testClass, + constructor, + parameterTypes, + parameterAnnotations)); + } + + @Override + public ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass) { + Class[] methodParameterTypes = testMethod.getParameterTypes(); + if (methodParameterTypes.length == 0) { + return ExecutableValidationResult.notValidated(); + } else { + // The method has parameters, they must be injected by a TestParameterAnnotation annotation. + return ExecutableValidationResult.validated( + validateMethodOrConstructorParameters( + getAnnotationTypeOrigins( + testClass, Origin.CLASS, Origin.METHOD, Origin.METHOD_PARAMETER), + testClass, + testMethod, + methodParameterTypes, + testMethod.getParameterAnnotations())); + } + } + + private List validateMethodOrConstructorParameters( + List annotationTypeOrigins, + Class testClass, + AnnotatedElement methodOrConstructor, + Class[] parameterTypes, + Annotation[][] parametersAnnotations) { + List errors = new ArrayList<>(); + + for (int parameterIndex = 0; parameterIndex < parameterTypes.length; parameterIndex++) { + Class parameterType = parameterTypes[parameterIndex]; + Annotation[] parameterAnnotations = parametersAnnotations[parameterIndex]; + boolean matchingTestParameterAnnotationFound = false; + // First, handle the case where the method parameter specifies the test parameter explicitly, + // e.g. {@code public void test(@ColorParameter({...}) Color c)}. + for (AnnotationTypeOrigin testParameterAnnotationType : annotationTypeOrigins) { + for (Annotation parameterAnnotation : parameterAnnotations) { + if (parameterAnnotation + .annotationType() + .equals(testParameterAnnotationType.annotationType())) { + // Verify that the type is assignable with the return type of the 'value' method. + Class valueMethodReturnType = + getValueMethodReturnType( + testParameterAnnotationType.annotationType(), + /* paramClass = */ Optional.of(parameterType)); + if (!parameterType.isAssignableFrom(valueMethodReturnType)) { + errors.add( + new IllegalStateException( + String.format( + "Parameter of type %s annotated with %s does not match" + + " expected type %s in method/constructor %s", + parameterType.getName(), + testParameterAnnotationType.annotationType().getName(), + valueMethodReturnType.getName(), + methodOrConstructor))); + } else { + matchingTestParameterAnnotationFound = true; + } + } + } + } + // Second, handle the case where the method parameter does not specify the test parameter, + // and instead relies on the type matching, e.g. {@code public void test(Color c)}. + if (!matchingTestParameterAnnotationFound) { + List> testParameterAnnotationTypes = + getTestParameterAnnotations( + // Do not include METHOD_PARAMETER or CONSTRUCTOR_PARAMETER since they have already + // been evaluated. + filterAnnotationTypeOriginsByOrigin( + annotationTypeOrigins, Origin.CLASS, Origin.CONSTRUCTOR, Origin.METHOD), + testClass, + methodOrConstructor); + // If no annotation is present, simply compare the type. + for (Class testParameterAnnotationType : + testParameterAnnotationTypes) { + if (parameterType.isAssignableFrom( + getValueMethodReturnType( + testParameterAnnotationType, /* paramClass = */ Optional.absent()))) { + if (matchingTestParameterAnnotationFound) { + errors.add( + new IllegalStateException( + String.format( + "Ambiguous method/constructor parameter type, matching multiple" + + " annotations for parameter of type %s in method %s", + parameterType.getName(), methodOrConstructor))); + } + matchingTestParameterAnnotationFound = true; + } + } + } + if (!matchingTestParameterAnnotationFound) { + errors.add( + new IllegalStateException( + String.format( + "No matching test parameter annotation found" + + " for parameter of type %s in method/constructor %s", + parameterType.getName(), methodOrConstructor))); + } + } + return errors; + } + + @Override + public Optional> maybeGetConstructorParameters( + Constructor constructor, TestInfo testInfo) { + if (testInfo.getAnnotation(TestIndexHolder.class) == null + // Explicitly skip @TestParameters annotated methods to ensure compatibility. + // + // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported) + // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;). + // Without the @TestParameters check below, this class would try to find parameters for + // these methods. When there are no method parameters, this is a no-op, but when the method + // is annotated with @TestParameters, this throws an exception (because there are method + // parameters that this processor has no values for - they are provided by the + // @TestParameters processor). + || constructor.isAnnotationPresent(TestParameters.class)) { + return Optional.absent(); + } else { + TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); + List testParameterValues = + getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()); + + Class[] parameterTypes = constructor.getParameterTypes(); + Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); + List parameterValues = new ArrayList<>(/* initialCapacity= */ parameterTypes.length); + List> processedAnnotationTypes = new ArrayList<>(); + List parameterValuesForConstructor = + filterByOrigin( + testParameterValues, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER); + for (int i = 0; i < parameterTypes.length; i++) { + // Initialize each parameter value from the corresponding TestParameterAnnotation value. + parameterValues.add( + getParameterValue( + parameterValuesForConstructor, + parameterTypes[i], + parameterAnnotations[i], + processedAnnotationTypes)); + } + return Optional.of(parameterValues); + } + } + + @Override + public Optional> maybeGetTestMethodParameters(TestInfo testInfo) { + Method testMethod = testInfo.getMethod(); + if (testInfo.getAnnotation(TestIndexHolder.class) == null + // Explicitly skip @TestParameters annotated methods to ensure compatibility. + // + // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported) + // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;). + // Without the @TestParameters check below, this class would try to find parameters for + // these methods. When there are no method parameters, this is a no-op, but when the method + // is annotated with @TestParameters, this throws an exception (because there are method + // parameters that this processor has no values for - they are provided by the + // @TestParameters processor). + || testMethod.isAnnotationPresent(TestParameters.class)) { + return Optional.absent(); + } else { + TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); + checkState(testIndexHolder != null); + List testParameterValues = + filterByOrigin( + getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()), + Origin.CLASS, + Origin.METHOD, + Origin.METHOD_PARAMETER); + + Class[] parameterTypes = testMethod.getParameterTypes(); + Annotation[][] parametersAnnotations = testMethod.getParameterAnnotations(); + ArrayList parameterValues = + new ArrayList<>(/* initialCapacity= */ parameterTypes.length); + + List> processedAnnotationTypes = new ArrayList<>(); + for (int i = 0; i < parameterTypes.length; i++) { + parameterValues.add( + getParameterValue( + testParameterValues, + parameterTypes[i], + parametersAnnotations[i], + processedAnnotationTypes)); + } + + return Optional.of(parameterValues); + } + } + + /** + * Returns the {@link TestInfo}, one for each result of the cartesian product of each test + * parameter values. + * + *

          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)}). + * + *

          For multiple annotations (say, {@code @TestParameter("foo", "bar")} and + * {@code @ColorParameter({BLUE, WHITE})}), it will generate the following result: + * + *

            + *
          • ("foo", BLUE) + *
          • ("foo", WHITE) + *
          • ("bar", BLUE) + *
          • ("bar", WHITE) + *
          • + *
          + * + * corresponding to the cartesian product of both annotations. + */ + @Override + public List calculateTestInfos(TestInfo originalTest) { + List> parameterValuesForMethod = + getParameterValuesForMethod(originalTest.getMethod(), originalTest.getTestClass()); + + if (parameterValuesForMethod.equals(ImmutableList.of(ImmutableList.of()))) { + // This test is not parameterized + return ImmutableList.of(originalTest); + } + + ImmutableList.Builder testInfos = ImmutableList.builder(); + for (int parametersIndex = 0; + parametersIndex < parameterValuesForMethod.size(); + ++parametersIndex) { + List 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(originalTest.getTestClass()), + originalTest.getMethod()), + parametersIndex, + originalTest.getTestClass().getName()))); + } + + return testInfos.build(); + } + + private List> getParameterValuesForMethod( + Method method, Class testClass) { + try { + return parameterValuesCache.get( + method, + () -> { + List> testParameterValuesList = + getAnnotationValuesForUsedAnnotationTypes(method, testClass); + + 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 getParameterValuesForTest( + TestIndexHolder testIndexHolder, Class testClass) { + verify( + testIndexHolder.testClassName().equals(testClass.getName()), + "The class for which the given annotation was created (%s) is not the same as the test" + + " class that this runner is handling (%s)", + testIndexHolder.testClassName(), + testClass.getName()); + Method testMethod = getMethodsIncludingParents(testClass).get(testIndexHolder.methodIndex()); + return getParameterValuesForMethod(testMethod, testClass) + .get(testIndexHolder.parametersIndex()); + } + + /** + * Returns the list of annotation index for all annotations defined in a given test method and its + * class. + */ + private ImmutableList> getAnnotationValuesForUsedAnnotationTypes( + Method method, Class testClass) { + ImmutableList annotationTypes = + Stream.of( + getAnnotationTypeOrigins(testClass, Origin.CLASS).stream(), + getAnnotationTypeOrigins(testClass, Origin.FIELD).stream(), + getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR).stream(), + getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR_PARAMETER).stream(), + getAnnotationTypeOrigins(testClass, Origin.METHOD).stream(), + getAnnotationTypeOrigins(testClass, 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 annotationComparator( + Annotation[][] parameterAnnotations) { + ImmutableList 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}. + * + *

          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 removeOverrides( + List 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 removeOverrides( + List 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(); + default: + return true; + } + }) + .collect(toCollection(ArrayList::new)); + } + + /** + * Returns the given annotations defined either on the method parameters, method or the test + * class. + * + *

          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> getAnnotationFromParametersOrTestOrClass( + AnnotationTypeOrigin annotationTypeOrigin, Method method, Class testClass) { + Origin origin = annotationTypeOrigin.origin(); + Class annotationType = annotationTypeOrigin.annotationType(); + if (origin == Origin.CONSTRUCTOR_PARAMETER) { + Constructor constructor = getOnlyConstructor(testClass); + List 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 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 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> toTestParameterValueList( + List annotationWithMetadatas, Origin origin) { + return annotationWithMetadatas.stream() + .map(annotationWithMetadata -> TestParameterValue.create(annotationWithMetadata, origin)) + .collect(toImmutableList()); + } + + private static ImmutableList getAnnotationWithMetadataListWithType( + Method callable, Class annotationType) { + try { + return getAnnotationWithMetadataListWithType(callable.getParameters(), annotationType); + } catch (NoSuchMethodError ignored) { + return getAnnotationWithMetadataListWithType( + callable.getParameterTypes(), callable.getParameterAnnotations(), annotationType); + } + } + + private static ImmutableList getAnnotationWithMetadataListWithType( + Constructor callable, Class 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 getAnnotationWithMetadataListWithType( + Parameter[] parameters, Class 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 getAnnotationWithMetadataListWithType( + Class[] parameterTypes, + Annotation[][] annotations, + Class annotationType) { + checkArgument(parameterTypes.length == annotations.length); + + ImmutableList.Builder 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 getAnnotationListWithType( + Annotation[] annotations, Class annotationType) { + return stream(annotations) + .filter(annotation -> annotation.annotationType().equals(annotationType)) + .collect(toImmutableList()); + } + + private static Constructor getOnlyConstructor(Class testClass) { + Constructor[] constructors = testClass.getDeclaredConstructors(); + checkState( + constructors.length == 1, + "a single public constructor is required for class %s", + testClass); + return constructors[0]; + } + + @Override + public void postProcessTestInstance(Object testInstance, TestInfo testInfo) { + TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); + try { + if (testIndexHolder != null) { + List testParameterValues = + getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()); + + // Do not include {@link Origin#METHOD_PARAMETER} nor {@link Origin#CONSTRUCTOR_PARAMETER} + // annotations. + List testParameterValuesForFieldInjection = + filterByOrigin(testParameterValues, Origin.CLASS, Origin.FIELD, Origin.METHOD); + // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class + // in the example above. + List remainingTestParameterValuesForFieldInjection = + new ArrayList<>(testParameterValuesForFieldInjection); + for (Field declaredField : + streamWithParents(testInstance.getClass()) + .flatMap(c -> stream(c.getDeclaredFields())) + .collect(toImmutableList())) { + for (TestParameterValue testParameterValue : + remainingTestParameterValuesForFieldInjection) { + if (declaredField.isAnnotationPresent( + testParameterValue.annotationTypeOrigin().annotationType())) { + declaredField.setAccessible(true); + declaredField.set(testInstance, testParameterValue.value()); + remainingTestParameterValuesForFieldInjection.remove(testParameterValue); + break; + } + } + } + } + } catch (IllegalAccessException 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 filterByOrigin( + List testParameterValues, Origin... origins) { + Set 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 filterAnnotationTypeOriginsByOrigin( + List annotationTypeOrigins, Origin... origins) { + List originList = Arrays.asList(origins); + return annotationTypeOrigins.stream() + .filter(annotationTypeOrigin -> originList.contains(annotationTypeOrigin.origin())) + .collect(toImmutableList()); + } + + /** Returns a {@link TestParameterAnnotation}'s value for a method or constructor parameter. */ + private Object getParameterValue( + List testParameterValues, + Class methodParameterType, + Annotation[] parameterAnnotations, + List> processedAnnotationTypes) { + List> 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 annotationType = + testParameterValue.annotationTypeOrigin().annotationType(); + if (parameterAnnotation.annotationType().equals(annotationType)) { + // If multiple annotations exist, ensure that the proper one is selected. + // For instance, for: + // + // test(@FooParameter(1,2) Foo foo, @FooParameter(3,4) Foo bar) {} + // + // 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}. + */ + int parametersIndex(); + + /** + * The full name of the test class. Only used for verifying that assumptions about the above + * indices are valid. + */ + String testClassName(); + } + + /** Factory for {@link TestIndexHolder}. */ + static class TestIndexHolderFactory { + @AutoAnnotation + static TestIndexHolder create(int methodIndex, int parametersIndex, String testClassName) { + return new AutoAnnotation_TestParameterAnnotationMethodProcessor_TestIndexHolderFactory_create( + methodIndex, parametersIndex, testClassName); + } + + private TestIndexHolderFactory() {} + } + + /** + * Returns whether the test should be skipped according to the {@code annotationType}'s {@link + * TestParameterValidator} and the current list of {@link TestParameterValue}. + */ + private static boolean callShouldSkip( + Class annotationType, List testParameterValues) { + TestParameterAnnotation annotation = + annotationType.getAnnotation(TestParameterAnnotation.class); + Class 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 testParameterValues; + private final Set valueList; + + public ValidatorContext(List testParameterValues) { + this.testParameterValues = testParameterValues; + this.valueList = testParameterValues.stream().map(TestParameterValue::value).collect(toSet()); + } + + @Override + public boolean has(Class testParameter, Object value) { + return getValue(testParameter).transform(value::equals).or(false); + } + + @Override + public , U extends Enum> boolean has(T value1, U value2) { + return valueList.contains(value1) && valueList.contains(value2); + } + + @Override + public Optional getValue(Class testParameter) { + return getParameter(testParameter).transform(TestParameterValue::value); + } + + @Override + public List getSpecifiedValues(Class testParameter) { + return getParameter(testParameter) + .transform(TestParameterValue::specifiedValues) + .or(ImmutableList.of()); + } + + private Optional getParameter(Class 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 annotationType, Optional> paramClass) { + TestParameterAnnotation testParameter = + annotationType.getAnnotation(TestParameterAnnotation.class); + Class 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> getTestParameterAnnotations( + List annotationTypeOrigins, + final Class testClass, + AnnotatedElement methodOrConstructor) { + return annotationTypeOrigins.stream() + .map(AnnotationTypeOrigin::annotationType) + .filter( + annotationType -> + testClass.isAnnotationPresent(annotationType) + || methodOrConstructor.isAnnotationPresent(annotationType)) + .collect(toImmutableList()); + } + + private int strictIndexOf(List haystack, T needle) { + int index = haystack.indexOf(needle); + checkArgument(index >= 0, "Could not find '%s' in %s", needle, haystack); + return index; + } + + private ImmutableList getMethodsIncludingParents(Class clazz) { + ImmutableList.Builder resultBuilder = ImmutableList.builder(); + while (clazz != null) { + resultBuilder.add(clazz.getDeclaredMethods()); + clazz = clazz.getSuperclass(); + } + return resultBuilder.build(); + } + + private static Stream> streamWithParents(Class clazz) { + Stream.Builder> 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 Collector> toImmutableList() { + return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf); + } + + private static Collector> toImmutableSet() { + return Collectors.collectingAndThen(Collectors.toList(), ImmutableSet::copyOf); + } +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java new file mode 100644 index 0000000..537969a --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java @@ -0,0 +1,33 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import org.junit.runners.model.InitializationError; + +/** + * A 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. + */ +public final class TestParameterInjector extends PluggableTestRunner { + + public TestParameterInjector(Class testClass) throws InitializationError { + super(testClass); + } + + @Override + protected TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.createNewParameterizedProcessors(); + } +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java new file mode 100644 index 0000000..3733833 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java @@ -0,0 +1,68 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import com.google.common.base.Optional; +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Validator interface which allows {@link TestParameterAnnotation} annotations to validate the set + * of annotation values for a given test instance, and to selectively skip the test. + */ +interface TestParameterValidator { + + /** + * This interface allows to access information on the current testwhen implementing {@link + * TestParameterValidator}. + */ + interface Context { + + /** Returns whether the current test has the {@link TestParameterAnnotation} value(s). */ + boolean has(Class 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. + */ + , U extends Enum> boolean has(T value1, U value2); + + /** + * Returns all the current test value for a given {@link TestParameterAnnotation} annotated + * annotation. + */ + Optional getValue(Class testParameter); + + /** + * Returns all the values specified for a given {@link TestParameterAnnotation} annotated + * annotation in the test. + * + *

          For example, if the test annotates '@Foo(a,b,c)', getSpecifiedValues(Foo.class) will + * return [a,b,c]. + */ + List getSpecifiedValues(Class testParameter); + } + + /** + * Returns whether the test should be skipped based on the annotations' values. + * + *

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

          This method is not invoked in the context of a running test statement. + */ + boolean shouldSkip(Context context); +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java new file mode 100644 index 0000000..6c398aa --- /dev/null +++ b/junit4/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 provideValues(Annotation annotation, Optional> 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 annotationType, Optional> parameterClass); +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java new file mode 100644 index 0000000..5207ec6 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import com.google.common.base.Optional; +import java.lang.annotation.Annotation; + +/** Interface to retrieve the {@link TestParameterAnnotation} values for a test. */ +interface TestParameterValues { + /** + * Returns a {@link TestParameterAnnotation} value for the current test as specified by {@code + * testInfo}, or {@link Optional#absent()} if the {@code annotationType} is not found. + */ + Optional getValue(Class annotationType); +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java new file mode 100644 index 0000000..9b1c5c9 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java @@ -0,0 +1,259 @@ +/* + * 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.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * Annotation that can be placed (repeatedly) on @Test-methods or a test constructor to indicate the + * sets of parameters that it should be invoked with. + * + *

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

          Note: If this annotation is used in a test class, the other methods in that class can still + * use other types of parameterization, such as {@linkplain TestParameter @TestParameter}. + * + *

          See {@link #value()} for simple examples. + * + *

          Warning: This annotation can only be used if the compiled java code contains the parameter + * names. This is typically done by passing the {@code -parameters} option to the Java compiler, + * which requires using Java 8 or higher and may not be available on Android. + */ +@Retention(RUNTIME) +@Target({CONSTRUCTOR, METHOD}) +@Repeatable(TestParameters.RepeatedTestParameters.class) +public @interface TestParameters { + + /** + * Specifies one or more stringified sets of parameters in YAML format. Each set corresponds to a + * single invocation of a test method. + * + *

          Each element in this array is a full parameter set, formatted as a YAML mapping. The mapping + * keys must match the parameter names and the mapping values will be converted to the parameter + * type if possible. See yaml.org for the YAML syntax and the section below on the supported + * parameter types. + * + *

          There are two distinct ways of using this annotation: repeated vs single: + * + *

          Recommended usage: Separate annotation per parameter set + * + *

          This approach uses multiple @TestParameters annotations, one for each set of parameters, for + * example: + * + *

          +   * {@literal @}Test
          +   * {@literal @}TestParameters("{age: 17, expectIsAdult: false}")
          +   * {@literal @}TestParameters("{age: 22, expectIsAdult: true}")
          +   * public void personIsAdult(int age, boolean expectIsAdult) { ... }
          +   *
          +   * {@literal @}Test
          +   * {@literal @}TestParameters("{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}")
          +   * {@literal @}TestParameters("{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}")
          +   * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... }
          +   * 
          + * + *

          Old discouraged usage: Single annotation with all parameter sets + * + *

          This approach uses a single @TestParameter annotation for all parameter sets, for example: + * + *

          +   * {@literal @}Test
          +   * {@literal @}TestParameters({
          +   *   "{age: 17, expectIsAdult: false}",
          +   *   "{age: 22, expectIsAdult: true}",
          +   * })
          +   * public void personIsAdult(int age, boolean expectIsAdult) { ... }
          +   *
          +   * {@literal @}Test
          +   * {@literal @}TestParameters({
          +   *   "{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}",
          +   *   "{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}",
          +   * })
          +   * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... }
          +   * 
          + * + *

          Supported parameter types + * + *

            + *
          • YAML primitives: + *
              + *
            • String: Specified as YAML string + *
            • boolean: Specified as YAML boolean + *
            • long and int: Specified as YAML integer + *
            • float and double: Specified as YAML floating point or integer + *
            + *
          • + *
          • Parsed types: + *
              + *
            • Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()} + *
            • Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML + * bytes (example: "!!binary 'ZGF0YQ=='") + *
            + *
          • + *
          + * + *

          For dynamic sets of parameters or parameter types that are not supported here, use {@link + * #valuesProvider()} and leave this field empty. + */ + String[] value() default {}; + + /** + * Overrides the name of the parameter set that is used in the test name. + * + *

          This can only be set if {@link #value()} has exactly one element. If not set, the YAML + * string in {@link #value()} is used in the test name. + * + *

          For example: If this name is set to "young adult", then the test name might be + * "personIsAdult[young adult]" where the default might have been "personIsAdult[{age: 17, + * expectIsAdult: false}]". + */ + String customName() default ""; + + /** + * Sets a provider that will return a list of parameter sets. Each element in the returned list + * corresponds to a single invocation of a test method. + * + *

          If this field is set, {@link #value()} must be empty and vice versa. + * + *

          Example + * + *

          +   * {@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} 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()
          +   *     );
          +   *   }
          +   * }
          +   * 
          + */ + Class valuesProvider() default + DefaultTestParametersValuesProvider.class; + + /** Interface for custom providers of test parameter values. */ + interface TestParametersValuesProvider { + List 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. + * + *

          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 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 parametersMap = new LinkedHashMap<>(); + + /** + * Sets a name for this set of parameters that will be used for describing this test. + * + *

          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 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 provideValues() { + return ImmutableList.of(); + } + } + + /** + * Holder annotation for multiple @TestParameters annotations. This should never be used directly. + */ + @Retention(RUNTIME) + @Target({CONSTRUCTOR, METHOD}) + @interface RepeatedTestParameters { + TestParameters[] value(); + } +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java new file mode 100644 index 0000000..bffc3b4 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java @@ -0,0 +1,485 @@ +/* + * 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 com.google.common.collect.Iterables.getOnlyElement; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toList; + +import com.google.auto.value.AutoAnnotation; +import com.google.common.base.Optional; +import com.google.common.base.Throwables; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.common.primitives.Primitives; +import com.google.common.reflect.TypeToken; +import com.google.common.util.concurrent.UncheckedExecutionException; +import com.google.testing.junit.testparameterinjector.TestInfo.TestInfoParameter; +import com.google.testing.junit.testparameterinjector.TestParameters.DefaultTestParametersValuesProvider; +import com.google.testing.junit.testparameterinjector.TestParameters.RepeatedTestParameters; +import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValues; +import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValuesProvider; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** {@code TestMethodProcessor} implementation for supporting {@link TestParameters}. */ +@SuppressWarnings("AndroidJdkLibsChecker") // Parameter is not available on old Android SDKs. +final class TestParametersMethodProcessor implements TestMethodProcessor { + + private final LoadingCache> + parameterValuesByConstructorOrMethodCache = + CacheBuilder.newBuilder() + .maximumSize(1000) + .build(CacheLoader.from(TestParametersMethodProcessor::toParameterValuesList)); + + @Override + public ExecutableValidationResult validateConstructor(Constructor constructor) { + if (hasRelevantAnnotation(constructor)) { + try { + // This method throws an exception if there is a validation error + getConstructorParameters(constructor); + } catch (Throwable t) { + return ExecutableValidationResult.validated(t); + } + return ExecutableValidationResult.valid(); + } else { + return ExecutableValidationResult.notValidated(); + } + } + + @Override + public ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass) { + if (hasRelevantAnnotation(testMethod)) { + try { + // This method throws an exception if there is a validation error + getMethodParameters(testMethod); + } catch (Throwable t) { + return ExecutableValidationResult.validated(t); + } + return ExecutableValidationResult.valid(); + } else { + return ExecutableValidationResult.notValidated(); + } + } + + @Override + public List calculateTestInfos(TestInfo originalTest) { + boolean constructorIsParameterized = + hasRelevantAnnotation(getOnlyConstructor(originalTest.getTestClass())); + boolean methodIsParameterized = hasRelevantAnnotation(originalTest.getMethod()); + + if (!constructorIsParameterized && !methodIsParameterized) { + return ImmutableList.of(originalTest); + } + + ImmutableList.Builder testInfos = ImmutableList.builder(); + + ImmutableList> constructorParametersList = + getConstructorParametersOrSingleAbsentElement(originalTest.getTestClass()); + ImmutableList> methodParametersList = + getMethodParametersOrSingleAbsentElement(originalTest.getMethod()); + for (int constructorParametersIndex = 0; + constructorParametersIndex < constructorParametersList.size(); + ++constructorParametersIndex) { + Optional constructorParameters = + constructorParametersList.get(constructorParametersIndex); + + for (int methodParametersIndex = 0; + methodParametersIndex < methodParametersList.size(); + ++methodParametersIndex) { + Optional 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> + getConstructorParametersOrSingleAbsentElement(Class testClass) { + Constructor constructor = getOnlyConstructor(testClass); + return hasRelevantAnnotation(constructor) + ? getConstructorParameters(constructor).stream() + .map(Optional::of) + .collect(toImmutableList()) + : ImmutableList.of(Optional.absent()); + } + + private ImmutableList> getMethodParametersOrSingleAbsentElement( + Method method) { + return hasRelevantAnnotation(method) + ? getMethodParameters(method).stream().map(Optional::of).collect(toImmutableList()) + : ImmutableList.of(Optional.absent()); + } + + @Override + public Optional> maybeGetConstructorParameters( + Constructor constructor, TestInfo testInfo) { + if (hasRelevantAnnotation(constructor)) { + ImmutableList parameterValuesList = + getConstructorParameters(constructor); + TestParametersValues parametersValues = + parameterValuesList.get( + testInfo.getAnnotation(TestIndexHolder.class).constructorParametersIndex()); + + return Optional.of(toParameterList(parametersValues, constructor.getParameters())); + } else { + return Optional.absent(); + } + } + + @Override + public Optional> maybeGetTestMethodParameters(TestInfo testInfo) { + Method testMethod = testInfo.getMethod(); + if (hasRelevantAnnotation(testMethod)) { + ImmutableList parameterValuesList = getMethodParameters(testMethod); + TestParametersValues parametersValues = + parameterValuesList.get( + testInfo.getAnnotation(TestIndexHolder.class).methodParametersIndex()); + + return Optional.of(toParameterList(parametersValues, testMethod.getParameters())); + } else { + return Optional.absent(); + } + } + + @Override + public void postProcessTestInstance(Object testInstance, TestInfo testInfo) {} + + private ImmutableList getConstructorParameters(Constructor constructor) { + try { + return parameterValuesByConstructorOrMethodCache.getUnchecked(constructor); + } catch (UncheckedExecutionException e) { + // Rethrow IllegalStateException because they can be caused by user mistakes and the user + // doesn't need to know that the caching layer is in between. + Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); + throw e; + } + } + + private ImmutableList getMethodParameters(Method method) { + try { + return parameterValuesByConstructorOrMethodCache.getUnchecked(method); + } catch (UncheckedExecutionException e) { + // Rethrow IllegalStateException because they can be caused by user mistakes and the user + // doesn't need to know that the caching layer is in between. + Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); + throw e; + } + } + + private static ImmutableList toParameterValuesList(Executable executable) { + checkParameterNamesArePresent(executable); + ImmutableList parametersList = ImmutableList.copyOf(executable.getParameters()); + + if (executable.isAnnotationPresent(TestParameters.class)) { + checkState( + !executable.isAnnotationPresent(RepeatedTestParameters.class), + "Unexpected situation: Both @TestParameters and @RepeatedTestParameters annotating the" + + " same method"); + TestParameters annotation = executable.getAnnotation(TestParameters.class); + boolean valueIsSet = annotation.value().length > 0; + boolean valuesProviderIsSet = + !annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class); + + checkState( + !(valueIsSet && valuesProviderIsSet), + "It is not allowed to specify both value and valuesProvider in @TestParameters(value=%s," + + " valuesProvider=%s) on %s()", + Arrays.toString(annotation.value()), + annotation.valuesProvider().getSimpleName(), + executable.getName()); + checkState( + valueIsSet || valuesProviderIsSet, + "Either a value or a valuesProvider must be set in @TestParameters on %s()", + executable.getName()); + if (!annotation.customName().isEmpty()) { + checkState( + annotation.value().length == 1, + "Setting @TestParameters.customName is only allowed if there is exactly one YAML string" + + " in @TestParameters.value (on %s())", + executable.getName()); + } + + if (valueIsSet) { + return stream(annotation.value()) + .map(yamlMap -> toParameterValues(yamlMap, parametersList, annotation.customName())) + .collect(toImmutableList()); + } else { + return toParameterValuesList(annotation.valuesProvider(), parametersList); + } + } else { // Not annotated with @TestParameters + verify( + executable.isAnnotationPresent(RepeatedTestParameters.class), + "This method should only be called for executables with at least one relevant" + + " annotation"); + + return stream(executable.getAnnotation(RepeatedTestParameters.class).value()) + .map( + annotation -> + toParameterValues( + validateAndGetSingleValueFromRepeatedAnnotation(annotation, executable), + parametersList, + annotation.customName())) + .collect(toImmutableList()); + } + } + + private static ImmutableList toParameterValuesList( + Class valuesProvider, List parameters) { + try { + Constructor 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 checkParameterNamesArePresent(Executable executable) { + checkState( + stream(executable.getParameters()).allMatch(Parameter::isNamePresent), + "" + + "No parameter name could be found for %s, which likely means that parameter names" + + " aren't available at runtime. Please ensure that the this test was built with the" + + " -parameters compiler option.\n" + + "\n" + + "In Maven, you do this by adding true to the" + + " maven-compiler-plugin's configuration. For example:\n" + + "\n" + + "\n" + + " \n" + + " \n" + + " org.apache.maven.plugins\n" + + " maven-compiler-plugin\n" + + " 3.8.1\n" + + " \n" + + " \n" + + " -parameters\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + "\n" + + "Don't forget to run `mvn clean` after making this change.", + executable.getName()); + } + + private static String validateAndGetSingleValueFromRepeatedAnnotation( + TestParameters annotation, Executable executable) { + checkState( + annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class), + "Setting a valuesProvider is not supported for methods/constructors with" + + " multiple @TestParameters annotations on %s()", + executable.getName()); + checkState( + annotation.value().length > 0, + "Either a value or a valuesProvider must be set in @TestParameters on %s()", + executable.getName()); + checkState( + annotation.value().length == 1, + "When specifying more than one @TestParameter for a method/constructor, each annotation" + + " must have exactly one value. Instead, got %s values on %s(): %s", + annotation.value().length, + executable.getName(), + Arrays.toString(annotation.value())); + + return annotation.value()[0]; + } + + private static void validateThatValuesMatchParameters( + TestParametersValues testParametersValues, List parameters) { + ImmutableMap 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 parameters, String maybeCustomName) { + Object yamlMapObject = ParameterValueParsing.parseYamlStringToObject(yamlString); + checkState( + yamlMapObject instanceof Map, + "Cannot map YAML string '%s' to parameters because it is not a mapping", + yamlString); + Map yamlMap = (Map) yamlMapObject; + + ImmutableMap parametersByName = + Maps.uniqueIndex(parameters, Parameter::getName); + checkState( + yamlMap.keySet().equals(parametersByName.keySet()), + "Cannot map YAML string '%s' to parameters %s", + yamlString, + parametersByName.keySet()); + + @SuppressWarnings("unchecked") + Map checkedYamlMap = (Map) yamlMap; + + return TestParametersValues.builder() + .name(maybeCustomName.isEmpty() ? yamlString : maybeCustomName) + .addParameters( + Maps.transformEntries( + checkedYamlMap, + (parameterName, parsedYaml) -> + ParameterValueParsing.parseYamlObjectToJavaType( + parsedYaml, + TypeToken.of(parametersByName.get(parameterName).getParameterizedType())))) + .build(); + } + + // Note: We're not using the Executable interface here because it isn't supported by Java 7 and + // this code is called even if only @TestParameter is used. In other places, Executable is usable + // because @TestParameters only works for Java 8 anyway. + private static boolean hasRelevantAnnotation(Constructor executable) { + return executable.isAnnotationPresent(TestParameters.class) + || executable.isAnnotationPresent(RepeatedTestParameters.class); + } + + private static boolean hasRelevantAnnotation(Method executable) { + return executable.isAnnotationPresent(TestParameters.class) + || executable.isAnnotationPresent(RepeatedTestParameters.class); + } + + private static List toParameterList( + TestParametersValues parametersValues, Parameter[] parameters) { + return stream(parameters) + .map(parameter -> parametersValues.parametersMap().get(parameter.getName())) + .collect(toList()); + } + + private static Constructor getOnlyConstructor(Class testClass) { + ImmutableList> constructors = + ImmutableList.copyOf(testClass.getDeclaredConstructors()); + checkState( + constructors.size() == 1, "Expected exactly one constructor, but got %s", constructors); + return getOnlyElement(constructors); + } + + // Immutable collectors are re-implemented here because they are missing from the Android + // collection library. + private static Collector> 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/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java new file mode 100644 index 0000000..7b98707 --- /dev/null +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java @@ -0,0 +1,147 @@ +/* + * 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.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class ParameterValueParsingTest { + + @Test + public void parseEnum_success() throws Exception { + Enum result = ParameterValueParsing.parseEnum("BBB", TestEnum.class); + + assertThat(result).isEqualTo(TestEnum.BBB); + } + + @Test + @TestParameters({ + "{yamlString: '{a: b, c: 15}', valid: true}", + "{yamlString: '{a: b c: 15', valid: false}", + "{yamlString: 'a: b c: 15', valid: false}", + }) + public void isValidYamlString_success(String yamlString, boolean valid) throws Exception { + boolean result = ParameterValueParsing.isValidYamlString(yamlString); + + assertThat(result).isEqualTo(valid); + } + + enum ParseYamlValueToJavaTypeCases { + STRING_TO_STRING( + /* yamlString= */ "abc", /* javaClass= */ String.class, /* expectedResult= */ "abc"), + BOOLEAN_TO_STRING( + /* yamlString= */ "true", /* javaClass= */ String.class, /* expectedResult= */ "true"), + INT_TO_STRING( + /* yamlString= */ "123", /* javaClass= */ String.class, /* expectedResult= */ "123"), + LONG_TO_STRING( + /* yamlString= */ "442147483648", + /* javaClass= */ String.class, + /* expectedResult= */ "442147483648"), + DOUBLE_TO_STRING( + /* yamlString= */ "1.23", /* javaClass= */ String.class, /* expectedResult= */ "1.23"), + + BOOLEAN_TO_BOOLEAN( + /* yamlString= */ "true", /* javaClass= */ Boolean.class, /* expectedResult= */ true), + + INT_TO_INT(/* yamlString= */ "123", /* javaClass= */ int.class, /* expectedResult= */ 123), + + LONG_TO_LONG( + /* yamlString= */ "442147483648", + /* javaClass= */ long.class, + /* expectedResult= */ 442147483648L), + INT_TO_LONG(/* yamlString= */ "123", /* javaClass= */ Long.class, /* expectedResult= */ 123L), + + DOUBLE_TO_DOUBLE( + /* yamlString= */ "1.23", /* javaClass= */ Double.class, /* expectedResult= */ 1.23), + INT_TO_DOUBLE( + /* yamlString= */ "123", /* javaClass= */ Double.class, /* expectedResult= */ 123.0), + LONG_TO_DOUBLE( + /* yamlString= */ "442147483648", + /* javaClass= */ Double.class, + /* expectedResult= */ 442147483648.0), + NAN_TO_DOUBLE( + /* yamlString= */ "NaN", /* javaClass= */ Double.class, /* expectedResult= */ Double.NaN), + INFINITY_TO_DOUBLE( + /* yamlString= */ "Infinity", + /* javaClass= */ Double.class, + /* expectedResult= */ Double.POSITIVE_INFINITY), + POSITIVE_INFINITY_TO_DOUBLE( + /* yamlString= */ "+Infinity", + /* javaClass= */ Double.class, + /* expectedResult= */ Double.POSITIVE_INFINITY), + NEGATIVE_INFINITY_TO_DOUBLE( + /* yamlString= */ "-Infinity", + /* javaClass= */ Double.class, + /* expectedResult= */ Double.NEGATIVE_INFINITY), + + DOUBLE_TO_FLOAT( + /* yamlString= */ "1.23", /* javaClass= */ Float.class, /* expectedResult= */ 1.23F), + INT_TO_FLOAT(/* yamlString= */ "123", /* javaClass= */ Float.class, /* expectedResult= */ 123F), + + STRING_TO_ENUM( + /* yamlString= */ "AAA", + /* javaClass= */ TestEnum.class, + /* expectedResult= */ TestEnum.AAA), + + STRING_TO_BYTES( + /* yamlString= */ "data", + /* javaClass= */ byte[].class, + /* expectedResult= */ "data".getBytes()), + + BYTES_TO_BYTES( + /* yamlString= */ "!!binary 'ZGF0YQ=='", + /* javaClass= */ byte[].class, + /* expectedResult= */ "data".getBytes()), + + STRING_TO_BYTESTRING( + /* yamlString= */ "'data'", + /* javaClass= */ ByteString.class, + /* expectedResult= */ ByteString.copyFromUtf8("data")), + + BINARY_TO_BYTESTRING( + /* yamlString= */ "!!binary 'ZGF0YQ=='", + /* javaClass= */ ByteString.class, + /* expectedResult= */ ByteString.copyFromUtf8("data")); + + final String yamlString; + final Class javaClass; + final Object expectedResult; + + ParseYamlValueToJavaTypeCases(String yamlString, Class javaClass, Object expectedResult) { + this.yamlString = yamlString; + this.javaClass = javaClass; + this.expectedResult = expectedResult; + } + } + + @Test + public void parseYamlStringToJavaType_success( + @TestParameter ParseYamlValueToJavaTypeCases parseYamlValueToJavaTypeCases) throws Exception { + Object result = + ParameterValueParsing.parseYamlStringToJavaType( + parseYamlValueToJavaTypeCases.yamlString, parseYamlValueToJavaTypeCases.javaClass); + + assertThat(result).isEqualTo(parseYamlValueToJavaTypeCases.expectedResult); + } + + private enum TestEnum { + AAA, + BBB; + } +} diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java new file mode 100644 index 0000000..a44df08 --- /dev/null +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java @@ -0,0 +1,154 @@ +/* + * 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 TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.empty(); + } + }); + + 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 TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.empty(); + } + + @Override + protected ImmutableList> getSupportedTestAnnotations() { + return ImmutableList.of(Test.class, CustomTest.class); + } + }); + + assertThat(testMethodInvocationCount).isEqualTo(2); + } + + private static final List 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 TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.empty(); + } + + @Override + protected Stream sortTestMethods(Stream methods) { + return methods.sorted(comparing(FrameworkMethod::getName).reversed()); + } + }); + assertThat(testOrder).containsExactly("c", "b", "a"); + } +} diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java new file mode 100644 index 0000000..f6fd512 --- /dev/null +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java @@ -0,0 +1,253 @@ +/* + * 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 result = TestInfo.shortenNamesIfNecessary(ImmutableList.of()); + + assertThat(result).isEmpty(); + } + + @Test + public void shortenNamesIfNecessary_noParameters() throws Exception { + ImmutableList givenTestInfos = ImmutableList.of(fakeTestInfo()); + + ImmutableList result = TestInfo.shortenNamesIfNecessary(givenTestInfos); + + assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder(); + } + + @Test + public void shortenNamesIfNecessary_veryLongTestMethodName_noParameters() throws Exception { + ImmutableList givenTestInfos = + ImmutableList.of( + TestInfo.createWithoutParameters( + getClass() + .getMethod( + "unusedMethodThatHasAVeryLongNameForTest000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000"), + getClass(), + /* annotations= */ ImmutableList.of())); + + ImmutableList result = TestInfo.shortenNamesIfNecessary(givenTestInfos); + + assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder(); + } + + @Test + public void shortenNamesIfNecessary_noShorteningNeeded() throws Exception { + ImmutableList 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 result = TestInfo.shortenNamesIfNecessary(givenTestInfos); + + assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder(); + } + + @Test + public void shortenNamesIfNecessary_singleParameterTooLong_twoParameters() throws Exception { + ImmutableList 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 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" + + " 000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "00000000000000000000000000000000000000000000000000000000000000000000...]") + .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 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 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 result = TestInfo.deduplicateTestNames(givenTestInfos); + + assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder(); + } + + @Test + public void deduplicateTestNames_duplicateParameterNamesWithDifferentTypes() throws Exception { + ImmutableList 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 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"), + String.class, + /* annotations= */ ImmutableList.of()) + .withExtraParameters(ImmutableList.copyOf(parameters)); + } + + private static IterableSubject assertThatTestNamesOf(List result) { + return assertThat(result.stream().map(TestInfo::getName).collect(toList())); + } + + public void + unusedMethodThatHasAVeryLongNameForTest000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000() {} +} diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java new file mode 100644 index 0000000..9af29ae --- /dev/null +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java @@ -0,0 +1,1012 @@ +/* + * 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.Iterables.getOnlyElement; +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 java.util.stream.Collectors.joining; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; + +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider; +import com.google.testing.junit.testparameterinjector.TestParameterAnnotationMethodProcessorTest.ErrorNonStaticProviderClass.NonStaticProvider; +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.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 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 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 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 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 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> 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 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 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 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 filename limit of 255 characters." + + " =========================================================...]"); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class DuplicateTestNames { + + @Rule public TestName testName = new TestName(); + + private static List allTestNames; + private static List 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 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 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 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 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 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 BaseClassWithSingleTest { + @Rule public TestName testName = new TestName(); + + static List allTestNames; + + @BeforeClass + public static void resetStaticState() { + allTestNames = new ArrayList<>(); + } + + @Test + public void testInBase(@TestParameter boolean b) { + allTestNames.add(testName.getMethodName()); + } + + @AfterClass + public static void completedAllParameterizedTests() { + assertThat(allTestNames).containsExactly("testInBase[b=true]", "testInBase[b=false]"); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class SimpleTestInheritedFromBaseClass extends BaseClassWithSingleTest {} + + public abstract static class BaseClassWithAnnotations { + @Rule public TestName testName = new TestName(); + + static List 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) + public @interface EnumEvaluatorParameter { + TestEnum[] value() default {}; + } + + public static class TestEnumValidator implements TestParameterValidator { + + @Override + public boolean shouldSkip(Context context) { + return context.has(EnumEvaluatorParameter.class, TestEnum.THREE); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class MethodEvaluatorClass { + + private static List 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<>(); + } + + @AfterClass + public static void completedAllParameterizedTests() { + assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class FieldEvaluatorClass { + + private static List 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); + } + } + + @AfterClass + public static void completedAllParameterizedTests() { + assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class ConstructorClass { + + private static List 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 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) + public static class ErrorDuplicatedConstructorMethodAnnotation { + + private static List 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 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(); + } + } + } + + @ClassTestResult(Result.FAILURE) + public static class ErrorNonPublicTestMethod { + + @Test + void test(@TestParameter boolean b) {} + } + + 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>> 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> 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 > void assertAllEnumsAreIncluded(Enum[] values) { + Set> enumSet = new HashSet<>(Arrays.asList(values)); + for (List 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 parameters() { + return Arrays.stream(TestParameterAnnotationMethodProcessorTest.class.getClasses()) + .filter(cls -> cls.isAnnotationPresent(ClassTestResult.class)) + .map( + cls -> + new Object[] { + cls.getSimpleName(), cls, cls.getAnnotation(ClassTestResult.class).value() + }) + .collect(toImmutableList()); + } + + private final Class testClass; + private final Result result; + + public TestParameterAnnotationMethodProcessorTest( + String name, Class testClass, Result result) { + this.testClass = testClass; + this.result = result; + } + + @Test + public void test() throws Exception { + switch (result) { + case SUCCESS_ALWAYS: + assertNoFailures( + PluggableTestRunner.run( + newTestRunnerWithParameterizedSupport( + testClass -> TestMethodProcessorList.createNewParameterizedProcessors()))); + break; + + case SUCCESS_FOR_ALL_PLACEMENTS_ONLY: + assertThrows( + RuntimeException.class, + () -> + PluggableTestRunner.run( + newTestRunnerWithParameterizedSupport( + testClass -> TestMethodProcessorList.createNewParameterizedProcessors()))); + break; + + case FAILURE: + assertThrows( + RuntimeException.class, + () -> + PluggableTestRunner.run( + newTestRunnerWithParameterizedSupport( + testClass -> TestMethodProcessorList.createNewParameterizedProcessors()))); + break; + } + } + + private PluggableTestRunner newTestRunnerWithParameterizedSupport( + Function processorListGenerator) throws Exception { + return new PluggableTestRunner(testClass) { + @Override + protected TestMethodProcessorList createTestMethodProcessorList() { + return processorListGenerator.apply(getTestClass()); + } + }; + } + + private static void assertNoFailures(List failures) { + if (failures.size() == 1) { + throw new AssertionError(getOnlyElement(failures).getException()); + } else if (failures.size() > 1) { + throw new AssertionError( + String.format( + "Test failed unexpectedly:\n\n%s", + failures.stream() + .map( + f -> + String.format( + "<<%s>> %s", + f.getDescription(), + Throwables.getStackTraceAsString(f.getException()))) + .collect(joining("\n------------------------------------\n")))); + } + } +} diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java new file mode 100644 index 0000000..85cb686 --- /dev/null +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java @@ -0,0 +1,243 @@ +/* + * 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.Iterables.getOnlyElement; +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.util.stream.Collectors.joining; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Throwables; +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 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 testedParameters; + + private final TestEnum constructorEnum; + + public AnnotatedConstructorParameter(@TestParameter TestEnum constructorEnum) { + this.constructorEnum = constructorEnum; + } + + @TestParameter TestEnum fieldEnum; + + @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(String.format("%s:%s", fieldEnum, constructorEnum)); + } + + @AfterClass + public static void completedAllParameterizedTests() { + assertThat(testedParameters) + .containsExactly( + "ONE:ONE", + "ONE:TWO", + "ONE:THREE", + "TWO:ONE", + "TWO:TWO", + "TWO:THREE", + "THREE:ONE", + "THREE:TWO", + "THREE:THREE"); + } + } + + @RunAsTest + public static class MultipleAnnotatedParameters { + private static List 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 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 provideValues() { + return newArrayList("A", "B", null); + } + } + + private static final class CharMatcherProvider implements TestParameterValuesProvider { + @Override + public List provideValues() { + return newArrayList(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace()); + } + } + } + + @Parameters(name = "{0}") + public static Collection 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 failures = + PluggableTestRunner.run( + new PluggableTestRunner(testClass) { + @Override + protected TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.createNewParameterizedProcessors(); + } + }); + + assertNoFailures(failures); + } + + private static void assertNoFailures(List failures) { + if (failures.size() == 1) { + throw new AssertionError(getOnlyElement(failures).getException()); + } else if (failures.size() > 1) { + throw new AssertionError( + String.format( + "Test failed unexpectedly:\n\n%s", + failures.stream() + .map( + f -> + String.format( + "<<%s>> %s", + f.getDescription(), + Throwables.getStackTraceAsString(f.getException()))) + .collect(joining("\n------------------------------------\n")))); + } + } +} diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java new file mode 100644 index 0000000..3e15277 --- /dev/null +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java @@ -0,0 +1,621 @@ +/* + * 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.Iterables.getOnlyElement; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.util.stream.Collectors.joining; +import static org.junit.Assert.assertThrows; + +import com.google.common.base.Throwables; +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 java.util.Optional; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +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) +public class TestParametersMethodProcessorTest { + + @Retention(RUNTIME) + @interface RunAsTest { + String failsWithMessage() default ""; + } + + public enum TestEnum { + ONE, + TWO, + THREE; + } + + private static final class TestEnumValuesProvider implements TestParametersValuesProvider { + @Override + public List 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()); + } + } + + @RunAsTest + public static class SimpleMethodAnnotation { + @Rule public TestName testName = new TestName(); + + private static Map testNameToStringifiedParametersMap; + + @BeforeClass + public static void resetStaticState() { + testNameToStringifiedParametersMap = new LinkedHashMap<>(); + } + + @Test + @TestParameters("{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}") + @TestParameters("{testEnum: TWO,\ntestLong: 22,\ntestBoolean: true,\r\n\r\n testString: 'DEF'}") + @TestParameters("{testEnum: null, testLong: 33, testBoolean: false, testString: null}") + public void test(TestEnum testEnum, long testLong, boolean testBoolean, String testString) { + testNameToStringifiedParametersMap.put( + testName.getMethodName(), + String.format("%s,%s,%s,%s", testEnum, testLong, testBoolean, testString)); + } + + @Test + @TestParameters({ + "{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}", + "{testEnum: TWO,\ntestLong: 22,\ntestBoolean: true,\r\n\r\n testString: 'DEF'}", + "{testEnum: null, testLong: 33, testBoolean: false, testString: null}", + }) + public void test_singleAnnotation( + TestEnum testEnum, long testLong, boolean testBoolean, String testString) { + testNameToStringifiedParametersMap.put( + testName.getMethodName(), + String.format("%s,%s,%s,%s", testEnum, testLong, testBoolean, testString)); + } + + @Test + @TestParameters("{testString: ABC}") + @TestParameters( + "{testString: 'This is a very long string (240 characters) that would normally cause" + + " Sponge+Tin to exceed the filename limit of 255 characters." + + " =================================================================================" + + "=============='}") + public void test2_withLongNames(String testString) { + testNameToStringifiedParametersMap.put(testName.getMethodName(), testString); + } + + @Test + @TestParameters( + "{testEnums: [ONE, TWO, THREE], testLongs: [11, 4], testBooleans: [false, true]," + + " testStrings: [ABC, '123']}") + @TestParameters( + "{testEnums: [TWO],\ntestLongs: [22],\ntestBooleans: [true],\r\n\r\n testStrings: ['DEF']}") + @TestParameters("{testEnums: [], testLongs: [], testBooleans: [], testStrings: []}") + public void test3_withRepeatedParams( + List testEnums, + List testLongs, + List testBooleans, + List testStrings) { + testNameToStringifiedParametersMap.put( + testName.getMethodName(), + String.format("%s,%s,%s,%s", testEnums, testLongs, testBooleans, testStrings)); + } + + @Test + @TestParameters(customName = "custom1", value = "{testEnum: ONE}") + @TestParameters("{testEnum: TWO}") + @TestParameters(customName = "custom3", value = "{testEnum: THREE}") + public void test4_withCustomName(TestEnum testEnum) { + testNameToStringifiedParametersMap.put(testName.getMethodName(), String.valueOf(testEnum)); + } + + @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", + "test_singleAnnotation[{testEnum: ONE, testLong: 11, testBoolean: false, testString:" + + " ABC}]", + "ONE,11,false,ABC", + "test_singleAnnotation[{testEnum: TWO, testLong: 22, testBoolean: true, testString:" + + " 'DEF'}]", + "TWO,22,true,DEF", + "test_singleAnnotation[{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 to exceed the filename limit of 255" + + " characters. =============================...]", + "This is a very long string (240 characters) that would normally cause Sponge+Tin to" + + " exceed the filename limit of 255 characters." + + " =================================================================================" + + "==============", + "test3_withRepeatedParams[{testEnums: [ONE, TWO, THREE], testLongs: [11, 4]," + + " testBooleans: [false, true], testStrings: [ABC, '123']}]", + "[ONE, TWO, THREE],[11, 4],[false, true],[ABC, 123]", + "test3_withRepeatedParams[{testEnums: [TWO], testLongs: [22], testBooleans: [true]," + + " testStrings: ['DEF']}]", + "[TWO],[22],[true],[DEF]", + "test3_withRepeatedParams[{testEnums: [], testLongs: [], testBooleans: []," + + " testStrings: []}]", + "[],[],[],[]", + "test4_withCustomName[custom1]", + "ONE", + "test4_withCustomName[{testEnum: TWO}]", + "TWO", + "test4_withCustomName[custom3]", + "THREE"); + } + } + + @RunAsTest + public static class SimpleConstructorAnnotation { + + @Rule public TestName testName = new TestName(); + + private static Map 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 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); + } + } + + public abstract static class BaseClassWithMethodAnnotation { + @Rule public TestName testName = new TestName(); + + static List 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}") + @TestParameters("{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 allTestNames; + private static List testNamesThatInvokedBefore; + private static List testNamesThatInvokedAfter; + + @TestParameters("{testEnum: ONE}") + @TestParameters("{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}") + @TestParameters("{testString: DEF}") + public void test2(String testString) { + allTestNames.add(testName.getMethodName()); + } + + @Test + @TestParameters("{testString: ABC}") + @TestParameters( + "{testString: 'This is a very long string (240 characters) that would normally cause" + + " Sponge+Tin to exceed the filename limit of 255 characters." + + " =================================================================================" + + "=============='}") + public void test3_withLongNames(String testString) { + 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 allTestNames; + + @TestParameter TestEnum testEnumA; + + @TestParameters("{testEnumB: ONE}") + @TestParameters("{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}"}) + 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]"); + } + } + + @RunAsTest( + failsWithMessage = + "Either a value or a valuesProvider must be set in @TestParameters on test1()") + public static class InvalidTestBecauseEmptyAnnotation { + @Test + @TestParameters + public void test1() {} + } + + @RunAsTest( + failsWithMessage = + "Either a value or a valuesProvider must be set in @TestParameters on" + + " com.google.testing.junit.testparameterinjector.TestParametersMethodProcessorTest" + + "$InvalidTestBecauseEmptyAnnotationOnConstructor()") + public static class InvalidTestBecauseEmptyAnnotationOnConstructor { + @TestParameters + public InvalidTestBecauseEmptyAnnotationOnConstructor() {} + + @Test + public void test1() {} + } + + @RunAsTest( + failsWithMessage = + "It is not allowed to specify both value and valuesProvider in" + + " @TestParameters(value=[{testEnum: ONE}], valuesProvider=TestEnumValuesProvider)" + + " on test1()") + public static class InvalidTestBecauseCombiningValueWithProvider { + + @Test + @TestParameters(value = "{testEnum: ONE}", valuesProvider = TestEnumValuesProvider.class) + public void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "Either a value or a valuesProvider must be set in @TestParameters on test1()") + public static class InvalidTestBecauseRepeatedAnnotationIsEmpty { + @Test + @TestParameters(value = "{testEnum: ONE}") + @TestParameters + public void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "When specifying more than one @TestParameter for a method/constructor, each annotation" + + " must have exactly one value. Instead, got 2 values on test1(): [{testEnum: TWO}," + + " {testEnum: THREE}]") + public static class InvalidTestBecauseRepeatedAnnotationHasMultipleValues { + @Test + @TestParameters(value = "{testEnum: ONE}") + @TestParameters(value = {"{testEnum: TWO}", "{testEnum: THREE}"}) + public void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "Setting a valuesProvider is not supported for methods/constructors with" + + " multiple @TestParameters annotations on test1()") + public static class InvalidTestBecauseRepeatedAnnotationHasProvider { + @Test + @TestParameters(valuesProvider = TestEnumValuesProvider.class) + @TestParameters(valuesProvider = TestEnumValuesProvider.class) + public void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "Setting @TestParameters.customName is only allowed if there is exactly one YAML string" + + " in @TestParameters.value (on test1())") + public static class InvalidTestBecauseNamedAnnotationHasMultipleValues { + @Test + @TestParameters( + customName = "custom", + value = {"{testEnum: TWO}", "{testEnum: THREE}"}) + public void test1(TestEnum testEnum) {} + } + + @Parameters(name = "{0}") + public static Collection parameters() { + return Arrays.stream(TestParametersMethodProcessorTest.class.getClasses()) + .filter(cls -> cls.isAnnotationPresent(RunAsTest.class)) + .map( + cls -> + new Object[] { + cls.getSimpleName(), cls, cls.getAnnotation(RunAsTest.class).failsWithMessage() + }) + .collect(toImmutableList()); + } + + private final Class testClass; + private final Optional maybeFailureMessage; + + public TestParametersMethodProcessorTest( + String name, Class testClass, String failsWithMessage) { + this.testClass = testClass; + this.maybeFailureMessage = + failsWithMessage.isEmpty() ? Optional.empty() : Optional.of(failsWithMessage); + } + + @Test + public void test_success() throws Exception { + assume().that(maybeFailureMessage.isPresent()).isFalse(); + + List failures = PluggableTestRunner.run(newTestRunner()); + + assertNoFailures(failures); + } + + @Test + public void test_failure() throws Exception { + assume().that(maybeFailureMessage.isPresent()).isTrue(); + + IllegalStateException exception = + assertThrows(IllegalStateException.class, () -> PluggableTestRunner.run(newTestRunner())); + + assertThat(exception).hasMessageThat().isEqualTo(maybeFailureMessage.get()); + } + + private PluggableTestRunner newTestRunner() throws Exception { + return new PluggableTestRunner(testClass) { + @Override + protected TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.createNewParameterizedProcessors(); + } + }; + } + + private static void assertNoFailures(List failures) { + if (failures.size() == 1) { + throw new AssertionError(getOnlyElement(failures).getException()); + } else if (failures.size() > 1) { + throw new AssertionError( + String.format( + "Test failed unexpectedly:\n\n%s", + failures.stream() + .map( + f -> + String.format( + "<<%s>> %s", + f.getDescription(), + Throwables.getStackTraceAsString(f.getException()))) + .collect(joining("\n------------------------------------\n")))); + } + } +} diff --git a/junit5/pom.xml b/junit5/pom.xml new file mode 100644 index 0000000..345604b --- /dev/null +++ b/junit5/pom.xml @@ -0,0 +1,60 @@ + + + + + 4.0.0 + + + com.google.testparameterinjector + test-parameter-injector-parent + HEAD-SNAPSHOT + + + test-parameter-injector-junit5 + + TestParameterInjector for JUnit5 + + + + + org.junit.jupiter + junit-jupiter + 5.8.1 + + + org.junit.jupiter + junit-jupiter-engine + 5.8.1 + + + + + org.junit.jupiter + junit-jupiter-params + 5.8.1 + test + + + org.junit.platform + junit-platform-launcher + 1.8.1 + test + + + diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java new file mode 100644 index 0000000..692a713 --- /dev/null +++ b/junit5/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.junit5; + +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> 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 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 parameter : parameters) { + List 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 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>> getIndependentParameters( + Context context); +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java new file mode 100644 index 0000000..4c0c40d --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java @@ -0,0 +1,72 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.Iterables.getOnlyElement; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import java.util.Collection; + +/** + * Value class that captures the result of a validating a single constructor or test method. + * + *

          If the validation is not validated by any processor, it will be validated using the default + * validator. If a processor validates a constructor/test method, the remaining processors will + * *not* be called. + */ +@AutoValue +abstract class ExecutableValidationResult { + + /** Returns true if the properties of the given constructor/test method were validated. */ + public abstract boolean wasValidated(); + + /** Returns the validation errors, if any. */ + public abstract ImmutableList validationErrors(); + + static ExecutableValidationResult notValidated() { + return of(/* wasValidated= */ false, /* validationErrors= */ ImmutableList.of()); + } + + static ExecutableValidationResult validated(Collection errors) { + return of(/* wasValidated= */ true, /* validationErrors= */ errors); + } + + static ExecutableValidationResult validated(Throwable error) { + return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of(error)); + } + + static ExecutableValidationResult valid() { + return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of()); + } + + private static ExecutableValidationResult of( + boolean wasValidated, Collection validationErrors) { + checkArgument(wasValidated || validationErrors.isEmpty()); + return new AutoValue_ExecutableValidationResult( + wasValidated, ImmutableList.copyOf(validationErrors)); + } + + void assertValid() { + if (wasValidated() && !validationErrors().isEmpty()) { + if (validationErrors().size() == 1) { + throw new AssertionError(getOnlyElement(validationErrors())); + } else { + throw new AssertionError(String.format("Found validation errors: %s", validationErrors())); + } + } + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java new file mode 100644 index 0000000..59d2351 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.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.junit5; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; + +import com.google.common.collect.Lists; +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 > Enum parseEnum(String str, Class enumType) { + return Enum.valueOf((Class) 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) + .supportParsedType(String.class, Float::valueOf); + + yamlValueTransformer + .ifJavaType(Double.class) + .supportParsedType(Double.class, identity()) + .supportParsedType(Integer.class, Integer::doubleValue) + .supportParsedType(Long.class, Long::doubleValue) + .supportParsedType(String.class, Double::valueOf); + + yamlValueTransformer + .ifJavaType(Enum.class) + .supportParsedType( + String.class, str -> ParameterValueParsing.parseEnum(str, javaType.getRawType())); + + yamlValueTransformer + .ifJavaType(MessageLite.class) + .supportParsedType(String.class, str -> parseTextprotoMessage(str, javaType.getRawType())) + .supportParsedType( + Map.class, + map -> + getProtoValueParser() + .parseProtobufMessage((Map) 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 -> parseYamlMapToJavaMap(map, javaType)); + + return yamlValueTransformer.transformedJavaValue(); + } + + private static Map parseYamlMapToJavaMap(Map map, TypeToken javaType) { + return map.entrySet().stream() + .collect( + toMap( + entry -> + parseYamlObjectToJavaType( + entry.getKey(), getGenericParameterType(javaType, /* parameterIndex= */ 0)), + entry -> + parseYamlObjectToJavaType( + entry.getValue(), + getGenericParameterType(javaType, /* parameterIndex= */ 1)))); + } + + 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; + } + + SupportedJavaType ifJavaType(Class 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 { + + private final Class supportedJavaType; + + private SupportedJavaType(Class supportedJavaType) { + this.supportedJavaType = supportedJavaType; + } + + @SuppressWarnings("unchecked") + SupportedJavaType supportParsedType( + Class parsedYamlType, Function transformation) { + if (Primitives.wrap(supportedJavaType).isAssignableFrom(Primitives.wrap(javaType))) { + if (Primitives.wrap(parsedYamlType).isInstance(parsedYaml)) { + checkState( + transformedJavaValue == null, + "This case is already handled. This is a bug in" + + " testparameterinjector.TestParametersMethodProcessor."); + try { + transformedJavaValue = checkNotNull(transformation.apply((ParsedYamlT) parsedYaml)); + } catch (Exception e) { + throw new IllegalArgumentException( + String.format( + "Could not map YAML value %s (class = %s) to java class %s", + parsedYaml, parsedYaml.getClass(), javaType), + e); + } + } + } + + return this; + } + } + } + + static 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.junit5.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/junit5/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java new file mode 100644 index 0000000..d270295 --- /dev/null +++ b/junit5/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.junit5; + +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 map, Class javaType); +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java new file mode 100644 index 0000000..c6b1cc3 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java @@ -0,0 +1,309 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static 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. + * + *

          See b/168325767 for the reason behind this. tl;dr the name is put into a Unix file with max + * 255 characters. The surrounding constant characters take up 31 characters. The max is reduced + * by an additional 24 characters to account for future changes. + */ + static final int MAX_TEST_NAME_LENGTH = 200; + + public abstract Method getMethod(); + + /** + * The test class that is being run. + * + *

          Note that this is not always the same as the class that declares {@link #getMethod()} + * because test methods can be inherited. + */ + public abstract Class getTestClass(); + + public final String getName() { + if (getParameters().isEmpty()) { + return getMethod().getName(); + } else { + return String.format( + "%s[%s]", + getMethod().getName(), + getParameters().stream().map(TestInfoParameter::getName).collect(joining(","))); + } + } + + abstract ImmutableList getParameters(); + + public abstract ImmutableList getAnnotations(); + + @Nullable + public final T getAnnotation(Class annotationClass) { + for (Annotation annotation : getAnnotations()) { + if (annotationClass.isInstance(annotation)) { + return annotationClass.cast(annotation); + } + } + return null; + } + + final TestInfo withExtraParameters(List parameters) { + return new AutoValue_TestInfo( + getMethod(), + getTestClass(), + ImmutableList.builder() + .addAll(this.getParameters()) + .addAll(parameters) + .build(), + getAnnotations()); + } + + final TestInfo withExtraAnnotation(Annotation annotation) { + ImmutableList newAnnotations = + ImmutableList.builder().addAll(this.getAnnotations()).add(annotation).build(); + return new AutoValue_TestInfo(getMethod(), getTestClass(), getParameters(), newAnnotations); + } + + /** + * Returns a new TestInfo instance with updated parameter names. + * + * @param parameterWithIndexToNewName A function of the parameter and its index in the {@link + * #getParameters()} list to the new name. + */ + private TestInfo withUpdatedParameterNames( + BiFunction parameterWithIndexToNewName) { + return new AutoValue_TestInfo( + getMethod(), + getTestClass(), + 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, Class testClass, String name, List annotations) { + return new AutoValue_TestInfo( + method, testClass, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations)); + } + + static TestInfo createWithoutParameters( + Method method, Class testClass, List annotations) { + return new AutoValue_TestInfo( + method, testClass, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations)); + } + + static ImmutableList shortenNamesIfNecessary(List testInfos) { + if (testInfos.stream().anyMatch(info -> info.getName().length() > MAX_TEST_NAME_LENGTH)) { + int numberOfParameters = testInfos.get(0).getParameters().size(); + + if (numberOfParameters == 0) { + return ImmutableList.copyOf(testInfos); + } else { + Set 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; + + // Subtract 4 characters to leave place for joining commas and the parameter index. + return maxLengthOfAllParameters / numberOfParameters - 4; + } + + static ImmutableList deduplicateTestNames(List 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 maybeAddTypesIfDuplicate(List testInfos) { + Multimap testNameToInfo = + MultimapBuilder.linkedHashKeys().arrayListValues().build(); + for (TestInfo testInfo : testInfos) { + testNameToInfo.put(testInfo.getName(), testInfo); + } + + return testNameToInfo.keySet().stream() + .flatMap( + testName -> { + Collection 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 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 deduplicateWithNumberPrefixes( + ImmutableList 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 Collector> 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(); + + final 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/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java new file mode 100644 index 0000000..48e9a5e --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java @@ -0,0 +1,66 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import com.google.common.base.Optional; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; + +/** + * Interface to change the list of methods used in a test. + * + *

          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 calculateTestInfos(TestInfo originalTest); + + /** + * If this processor can handle the given constructor, returns the parameters with which it should + * be invoked. + * + *

          This method is never called for a parameterless constructor. + */ + Optional> maybeGetConstructorParameters( + Constructor constructor, TestInfo testInfo); + + /** + * If this processor can handle the given test, returns the parameters with which {@code + * testInfo.getMethod()} should be invoked. + * + *

          This method is never called for a parameterless {@code testInfo.getMethod()}. + */ + Optional> maybeGetTestMethodParameters(TestInfo testInfo); + + /** + * Optionally process the test instance right after construction to ready it for the given test + * instance. + */ + void postProcessTestInstance(Object testInstance, TestInfo testInfo); + + /** Optionally validates the given constructor. */ + ExecutableValidationResult validateConstructor(Constructor constructor); + + /** + * Optionally validates the given method. + * + *

          Note that the given method is not necessarily declared in the given class because test + * methods can be inherited. + */ + ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass); +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java new file mode 100644 index 0000000..aa2355d --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java @@ -0,0 +1,146 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static java.util.stream.Collectors.toList; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; + +/** + * Combined version of all {@link TestMethodProcessor} implementations that this package supports. + */ +final class TestMethodProcessorList { + + private final ImmutableList testMethodProcessors; + + private TestMethodProcessorList(ImmutableList testMethodProcessors) { + this.testMethodProcessors = testMethodProcessors; + } + + /** + * Returns a TestMethodProcessorList that supports all features that this package supports, except + * the following legacy features: + * + *

            + *
          • No support for {@link org.junit.runners.Parameterized} + *
          • No support for class and method-level parameters, except for @TestParameters + *
          + */ + public static TestMethodProcessorList createNewParameterizedProcessors() { + return new TestMethodProcessorList( + ImmutableList.of( + new TestParametersMethodProcessor(), + TestParameterAnnotationMethodProcessor.onlyForFieldsAndParameters())); + } + + static TestMethodProcessorList empty() { + return new TestMethodProcessorList(ImmutableList.of()); + } + + /** + * Calculates the TestInfo instances for the given test method. Each TestInfo corresponds to a + * single test. + * + *

          The returned list always contains at least one element. If there is no parameterization, + * this would be the TestInfo for running the test method without parameters. + */ + public List calculateTestInfos(Method testMethod, Class testClass) { + List testInfos = + ImmutableList.of( + TestInfo.createWithoutParameters( + testMethod, testClass, ImmutableList.copyOf(testMethod.getAnnotations()))); + + for (final TestMethodProcessor testMethodProcessor : testMethodProcessors) { + testInfos = + testInfos.stream() + .flatMap( + lastTestInfo -> testMethodProcessor.calculateTestInfos(lastTestInfo).stream()) + .collect(toList()); + } + + testInfos = TestInfo.deduplicateTestNames(TestInfo.shortenNamesIfNecessary(testInfos)); + + return testInfos; + } + + /** + * Returns the parameters with which it should be invoked. + * + *

          This method is never called for a parameterless constructor. + */ + public List getConstructorParameters(Constructor constructor, TestInfo testInfo) { + return testMethodProcessors.stream() + .map(processor -> processor.maybeGetConstructorParameters(constructor, testInfo)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .orElseThrow( + () -> + new IllegalStateException( + String.format( + "Could not generate parameter values for %s. Did you forget an annotation?", + constructor))); + } + + /** + * Returns the parameters with which {@code testInfo.getMethod()} should be invoked. + * + *

          This method is never called for a parameterless {@code testInfo.getMethod()}. + */ + public List getTestMethodParameters(TestInfo testInfo) { + return testMethodProcessors.stream() + .map(processor -> processor.maybeGetTestMethodParameters(testInfo)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .orElseThrow( + () -> + new IllegalStateException( + String.format( + "Could not generate parameter values for %s. Did you forget an annotation?", + testInfo.getMethod()))); + } + + /** + * Optionally process the test instance right after construction to ready it for the given test. + */ + public void postProcessTestInstance(Object testInstance, TestInfo testInfo) { + for (TestMethodProcessor testMethodProcessor : testMethodProcessors) { + testMethodProcessor.postProcessTestInstance(testInstance, testInfo); + } + } + + /** Optionally validates the given constructor. */ + public ExecutableValidationResult validateConstructor(Constructor constructor) { + return testMethodProcessors.stream() + .map(processor -> processor.validateConstructor(constructor)) + .filter(ExecutableValidationResult::wasValidated) + .findFirst() + .orElse(ExecutableValidationResult.notValidated()); + } + + /** Optionally validates the given method. */ + public ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass) { + return testMethodProcessors.stream() + .map(processor -> processor.validateTestMethod(testMethod, testClass)) + .filter(ExecutableValidationResult::wasValidated) + .findFirst() + .orElse(ExecutableValidationResult.notValidated()); + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java new file mode 100644 index 0000000..f31854d --- /dev/null +++ b/junit5/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.junit5; + +import static com.google.common.base.Preconditions.checkState; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import 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.junit5.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. + * + *

          For enums and booleans, the values can be automatically derived as all possible values: + * + *

          + * {@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 }
          + * 
          + * + *

          The values can be explicitly defined as a parsed string: + * + *

          + * 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)]
          + * }
          + * 
          + * + *

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

          Types that are supported: + * + *

            + *
          • String: No parsing happens + *
          • boolean: Specified as YAML boolean + *
          • long and int: Specified as YAML integer + *
          • float and double: Specified as YAML floating point or integer + *
          • Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()} + *
          • Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML bytes + * (example: "!!binary 'ZGF0YQ=='") + *
          + * + *

          For dynamic sets of parameters or parameter types that are not supported here, use {@link + * #valuesProvider()} and leave this field empty. + * + *

          For examples, see {@link TestParameter}. + */ + String[] value() default {}; + + /** + * Sets a provider that will return a list of parameter values. + * + *

          If this field is set, {@link #value()} must be empty and vice versa. + * + *

          Example + * + *

          +   * {@literal @}Test
          +   * public void matchesAllOf_throwsOnNull(
          +   *     {@literal @}TestParameter(valuesProvider = CharMatcherProvider.class)
          +   *         CharMatcher charMatcher) {
          +   *   assertThrows(NullPointerException.class, () -> charMatcher.matchesAllOf(null));
          +   * }
          +   *
          +   * private static final class CharMatcherProvider implements TestParameterValuesProvider {
          +   *   {@literal @}Override
          +   *   public {@literal List} provideValues() {
          +   *     return ImmutableList.of(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace());
          +   *   }
          +   * }
          +   * 
          + */ + Class 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 provideValues() { + return ImmutableList.of(); + } + } + + /** Implementation of this parameter annotation. */ + final class InternalImplementationOfThisParameter implements TestParameterValueProvider { + @Override + public List provideValues( + Annotation uncastAnnotation, Optional> 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 annotationType, Optional> 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 getValuesFromProvider( + Class valuesProvider) { + try { + Constructor 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/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java new file mode 100644 index 0000000..e169eb3 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java @@ -0,0 +1,251 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Verify.verify; +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.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. + * + *

          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: + * + *

          {@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();
          + *     }
          + * }
          + * }
          + * + *

          An alternative is to use a method parameter for injection: + * + *

          {@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();
          + *     }
          + * }
          + * }
          + * + *

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

          {@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();
          + *     }
          + * }
          + * }
          + * + *

          Class constructors can also be annotated with @TestParameterAnnotation annotations, as shown + * below: + * + *

          {@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() {...}
          + * }
          + * }
          + * + *

          Each field that needs to be injected from a parameter requires its dedicated distinct + * annotation. + * + *

          If the same annotation is defined both on the class and method, the method parameter values + * take precedence. + * + *

          If the same annotation is defined both on the class and constructor, the constructor parameter + * values take precedence. + * + *

          Annotations cannot be duplicated between the constructor or constructor parameters and a + * method or method parameter. + * + *

          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 validator() default DefaultValidator.class; + + /** Specifies a value provider for the parameter to provide the values to test. */ + Class valueProvider() default DefaultValueProvider.class; + + /** Default {@link TestParameterValidator} implementation which skips no test. */ + class DefaultValidator implements TestParameterValidator { + + @Override + public boolean shouldSkip(Context context) { + return false; + } + } + + /** + * Default {@link TestParameterValueProvider} implementation that gets its values from the + * annotation's `value` method. + */ + class DefaultValueProvider implements TestParameterValueProvider { + + @Override + public List provideValues(Annotation annotation, Optional> 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 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 annotationType, Optional> 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 annotationType) { + Method valueMethod; + try { + valueMethod = annotationType.getMethod("value"); + } catch (NoSuchMethodException e) { + throw new RuntimeException( + "The @TestParameterAnnotation annotation should have a single value() method.", e); + } + Object parameters; + try { + parameters = valueMethod.invoke(annotation); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof IllegalAccessError) { + // There seems to be a bug or at least something weird with the JVM that causes + // IllegalAccessError to be thrown because the return value is not visible when it is a + // non-public nested type. See + // http://mail.openjdk.java.net/pipermail/core-libs-dev/2014-January/024180.html for more + // info. + throw new RuntimeException( + String.format( + "Could not access %s.value(). This is probably because %s is not visible to the" + + " annotation proxy. To fix this, make %s public.", + annotationType.getSimpleName(), + valueMethod.getReturnType().getSimpleName(), + valueMethod.getReturnType().getSimpleName())); + // Note: Not chaining the exception to reduce the clutter for the reader + } else { + throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e); + } + } catch (Exception e) { + throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e); + } + return parameters; + } + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java new file mode 100644 index 0000000..c06cb3a --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -0,0 +1,1290 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Verify.verify; +import static 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.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +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.junit5.TestInfo.TestInfoParameter; +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.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; + +/** + * {@code TestMethodProcessor} implementation for supporting parameterized tests annotated with + * {@link TestParameterAnnotation}. + * + * @see TestParameterAnnotation + */ +final class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { + + /** + * Class to hold an annotation type and origin and one of the values as returned by the {@code + * value()} method. + */ + @AutoValue + abstract static class 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 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> 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 paramName(); + + /** + * Returns a String that represents this value and is fit for use in a test name (between + * brackets). + */ + String toTestNameString() { + Class 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 create( + AnnotationWithMetadata annotationWithMetadata, Origin origin) { + List 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(/* onlyForFieldsAndParameters= */ false) + .getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()).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 getTestParameterValue( + TestInfo testInfo, Class annotationType) { + return getTestParameterValues(testInfo).getValue(annotationType); + } + + private static List getParametersAnnotationValues( + AnnotationWithMetadata annotationWithMetadata) { + Annotation annotation = annotationWithMetadata.annotation(); + TestParameterAnnotation testParameter = + annotation.annotationType().getAnnotation(TestParameterAnnotation.class); + Class 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 matches(Class 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 annotationType(); + + /** Where the annotation was declared. */ + abstract Origin origin(); + + public static AnnotationTypeOrigin create( + Class 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> 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 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 boolean onlyForFieldsAndParameters; + private final LoadingCache, ImmutableList> + annotationTypeOriginsCache = + CacheBuilder.newBuilder() + .maximumSize(1000) + .build(CacheLoader.from(this::calculateAnnotationTypeOrigins)); + private final Cache>> parameterValuesCache = + CacheBuilder.newBuilder().maximumSize(1000).build(); + + private TestParameterAnnotationMethodProcessor(boolean onlyForFieldsAndParameters) { + this.onlyForFieldsAndParameters = onlyForFieldsAndParameters; + } + + /** + * Constructs a new {@link TestMethodProcessor} that handles {@link + * TestParameterAnnotation}-annotated annotations that are placed anywhere: + * + *
            + *
          • At a method / constructor parameter + *
          • At a field + *
          • At a method / constructor on the class + *
          • At the test class + *
          + */ + static TestMethodProcessor forAllAnnotationPlacements() { + return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ false); + } + + /** + * Constructs a new {@link TestMethodProcessor} that handles {@link + * TestParameterAnnotation}-annotated annotations that are placed at fields or parameters. + * + *

          Note that this excludes class and method-level annotations, as is the default (using the + * constructor). + */ + static TestMethodProcessor onlyForFieldsAndParameters() { + return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ true); + } + + private ImmutableList calculateAnnotationTypeOrigins(Class testClass) { + // Collect all annotations used in declared fields and methods that have themselves a + // @TestParameterAnnotation annotation. + List fieldAnnotations = + extractTestParameterAnnotations( + streamWithParents(testClass) + .flatMap(c -> stream(c.getDeclaredFields())) + .flatMap(field -> stream(field.getAnnotations())), + Origin.FIELD); + List methodAnnotations = + extractTestParameterAnnotations( + stream(testClass.getMethods()).flatMap(method -> stream(method.getAnnotations())), + Origin.METHOD); + List parameterAnnotations = + extractTestParameterAnnotations( + streamWithParents(testClass) + .flatMap(c -> stream(c.getDeclaredMethods())) + .flatMap(method -> stream(method.getParameterAnnotations()).flatMap(Stream::of)), + Origin.METHOD_PARAMETER); + List classAnnotations = + extractTestParameterAnnotations(stream(testClass.getAnnotations()), Origin.CLASS); + List constructorAnnotations = + extractTestParameterAnnotations( + stream(testClass.getDeclaredConstructors()) + .flatMap(constructor -> stream(constructor.getAnnotations())), + Origin.CONSTRUCTOR); + List constructorParameterAnnotations = + extractTestParameterAnnotations( + stream(testClass.getDeclaredConstructors()) + .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); + } + + return 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()); + } + + private ImmutableList getAnnotationTypeOrigins( + Class testClass, Origin firstOrigin, Origin... otherOrigins) { + Set originsToFilterBy = + ImmutableSet.builder().add(firstOrigin).add(otherOrigins).build(); + try { + return annotationTypeOriginsCache.getUnchecked(testClass).stream() + .filter(annotationTypeOrigin -> originsToFilterBy.contains(annotationTypeOrigin.origin())) + .collect(toImmutableList()); + } catch (UncheckedExecutionException e) { + Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); + throw e; + } + } + + private void checkDuplicatedFieldsAnnotations( + List methodAnnotations, List 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> 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 constructorAnnotations, + List classAnnotations, + List fieldAnnotations) { + ImmutableSet> classAnnotationTypes = + classAnnotations.stream() + .map(AnnotationTypeOrigin::annotationType) + .collect(toImmutableSet()); + + ImmutableSet> uniqueFieldAnnotations = + fieldAnnotations.stream() + .map(AnnotationTypeOrigin::annotationType) + .collect(toImmutableSet()); + ImmutableSet> 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 extractTestParameterAnnotations( + Stream 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 ExecutableValidationResult validateConstructor(Constructor constructor) { + Class[] parameterTypes = constructor.getParameterTypes(); + if (parameterTypes.length == 0) { + return ExecutableValidationResult.notValidated(); + } + // The constructor has parameters, they must be injected by a TestParameterAnnotation + // annotation. + Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); + Class testClass = constructor.getDeclaringClass(); + return ExecutableValidationResult.validated( + validateMethodOrConstructorParameters( + removeOverrides( + getAnnotationTypeOrigins( + testClass, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER), + testClass), + testClass, + constructor, + parameterTypes, + parameterAnnotations)); + } + + @Override + public ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass) { + Class[] methodParameterTypes = testMethod.getParameterTypes(); + if (methodParameterTypes.length == 0) { + return ExecutableValidationResult.notValidated(); + } else { + // The method has parameters, they must be injected by a TestParameterAnnotation annotation. + return ExecutableValidationResult.validated( + validateMethodOrConstructorParameters( + getAnnotationTypeOrigins( + testClass, Origin.CLASS, Origin.METHOD, Origin.METHOD_PARAMETER), + testClass, + testMethod, + methodParameterTypes, + testMethod.getParameterAnnotations())); + } + } + + private List validateMethodOrConstructorParameters( + List annotationTypeOrigins, + Class testClass, + AnnotatedElement methodOrConstructor, + Class[] parameterTypes, + Annotation[][] parametersAnnotations) { + List errors = new ArrayList<>(); + + for (int parameterIndex = 0; parameterIndex < parameterTypes.length; parameterIndex++) { + Class parameterType = parameterTypes[parameterIndex]; + Annotation[] parameterAnnotations = parametersAnnotations[parameterIndex]; + boolean matchingTestParameterAnnotationFound = false; + // First, handle the case where the method parameter specifies the test parameter explicitly, + // e.g. {@code public void test(@ColorParameter({...}) Color c)}. + for (AnnotationTypeOrigin testParameterAnnotationType : annotationTypeOrigins) { + for (Annotation parameterAnnotation : parameterAnnotations) { + if (parameterAnnotation + .annotationType() + .equals(testParameterAnnotationType.annotationType())) { + // Verify that the type is assignable with the return type of the 'value' method. + Class valueMethodReturnType = + getValueMethodReturnType( + testParameterAnnotationType.annotationType(), + /* paramClass = */ Optional.of(parameterType)); + if (!parameterType.isAssignableFrom(valueMethodReturnType)) { + errors.add( + new IllegalStateException( + String.format( + "Parameter of type %s annotated with %s does not match" + + " expected type %s in method/constructor %s", + parameterType.getName(), + testParameterAnnotationType.annotationType().getName(), + valueMethodReturnType.getName(), + methodOrConstructor))); + } else { + matchingTestParameterAnnotationFound = true; + } + } + } + } + // Second, handle the case where the method parameter does not specify the test parameter, + // and instead relies on the type matching, e.g. {@code public void test(Color c)}. + if (!matchingTestParameterAnnotationFound) { + List> testParameterAnnotationTypes = + getTestParameterAnnotations( + // Do not include METHOD_PARAMETER or CONSTRUCTOR_PARAMETER since they have already + // been evaluated. + filterAnnotationTypeOriginsByOrigin( + annotationTypeOrigins, Origin.CLASS, Origin.CONSTRUCTOR, Origin.METHOD), + testClass, + methodOrConstructor); + // If no annotation is present, simply compare the type. + for (Class testParameterAnnotationType : + testParameterAnnotationTypes) { + if (parameterType.isAssignableFrom( + getValueMethodReturnType( + testParameterAnnotationType, /* paramClass = */ Optional.absent()))) { + if (matchingTestParameterAnnotationFound) { + errors.add( + new IllegalStateException( + String.format( + "Ambiguous method/constructor parameter type, matching multiple" + + " annotations for parameter of type %s in method %s", + parameterType.getName(), methodOrConstructor))); + } + matchingTestParameterAnnotationFound = true; + } + } + } + if (!matchingTestParameterAnnotationFound) { + errors.add( + new IllegalStateException( + String.format( + "No matching test parameter annotation found" + + " for parameter of type %s in method/constructor %s", + parameterType.getName(), methodOrConstructor))); + } + } + return errors; + } + + @Override + public Optional> maybeGetConstructorParameters( + Constructor constructor, TestInfo testInfo) { + if (testInfo.getAnnotation(TestIndexHolder.class) == null + // Explicitly skip @TestParameters annotated methods to ensure compatibility. + // + // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported) + // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;). + // Without the @TestParameters check below, this class would try to find parameters for + // these methods. When there are no method parameters, this is a no-op, but when the method + // is annotated with @TestParameters, this throws an exception (because there are method + // parameters that this processor has no values for - they are provided by the + // @TestParameters processor). + || constructor.isAnnotationPresent(TestParameters.class)) { + return Optional.absent(); + } else { + TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); + List testParameterValues = + getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()); + + Class[] parameterTypes = constructor.getParameterTypes(); + Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); + List parameterValues = new ArrayList<>(/* initialCapacity= */ parameterTypes.length); + List> processedAnnotationTypes = new ArrayList<>(); + List parameterValuesForConstructor = + filterByOrigin( + testParameterValues, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER); + for (int i = 0; i < parameterTypes.length; i++) { + // Initialize each parameter value from the corresponding TestParameterAnnotation value. + parameterValues.add( + getParameterValue( + parameterValuesForConstructor, + parameterTypes[i], + parameterAnnotations[i], + processedAnnotationTypes)); + } + return Optional.of(parameterValues); + } + } + + @Override + public Optional> maybeGetTestMethodParameters(TestInfo testInfo) { + Method testMethod = testInfo.getMethod(); + if (testInfo.getAnnotation(TestIndexHolder.class) == null + // Explicitly skip @TestParameters annotated methods to ensure compatibility. + // + // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported) + // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;). + // Without the @TestParameters check below, this class would try to find parameters for + // these methods. When there are no method parameters, this is a no-op, but when the method + // is annotated with @TestParameters, this throws an exception (because there are method + // parameters that this processor has no values for - they are provided by the + // @TestParameters processor). + || testMethod.isAnnotationPresent(TestParameters.class)) { + return Optional.absent(); + } else { + TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); + checkState(testIndexHolder != null); + List testParameterValues = + filterByOrigin( + getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()), + Origin.CLASS, + Origin.METHOD, + Origin.METHOD_PARAMETER); + + Class[] parameterTypes = testMethod.getParameterTypes(); + Annotation[][] parametersAnnotations = testMethod.getParameterAnnotations(); + ArrayList parameterValues = + new ArrayList<>(/* initialCapacity= */ parameterTypes.length); + + List> processedAnnotationTypes = new ArrayList<>(); + for (int i = 0; i < parameterTypes.length; i++) { + parameterValues.add( + getParameterValue( + testParameterValues, + parameterTypes[i], + parametersAnnotations[i], + processedAnnotationTypes)); + } + + return Optional.of(parameterValues); + } + } + + /** + * Returns the {@link TestInfo}, one for each result of the cartesian product of each test + * parameter values. + * + *

          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)}). + * + *

          For multiple annotations (say, {@code @TestParameter("foo", "bar")} and + * {@code @ColorParameter({BLUE, WHITE})}), it will generate the following result: + * + *

            + *
          • ("foo", BLUE) + *
          • ("foo", WHITE) + *
          • ("bar", BLUE) + *
          • ("bar", WHITE) + *
          • + *
          + * + * corresponding to the cartesian product of both annotations. + */ + @Override + public List calculateTestInfos(TestInfo originalTest) { + List> parameterValuesForMethod = + getParameterValuesForMethod(originalTest.getMethod(), originalTest.getTestClass()); + + if (parameterValuesForMethod.equals(ImmutableList.of(ImmutableList.of()))) { + // This test is not parameterized + return ImmutableList.of(originalTest); + } + + ImmutableList.Builder testInfos = ImmutableList.builder(); + for (int parametersIndex = 0; + parametersIndex < parameterValuesForMethod.size(); + ++parametersIndex) { + List 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(originalTest.getTestClass()), + originalTest.getMethod()), + parametersIndex, + originalTest.getTestClass().getName()))); + } + + return testInfos.build(); + } + + private List> getParameterValuesForMethod( + Method method, Class testClass) { + try { + return parameterValuesCache.get( + method, + () -> { + List> testParameterValuesList = + getAnnotationValuesForUsedAnnotationTypes(method, testClass); + + 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 getParameterValuesForTest( + TestIndexHolder testIndexHolder, Class testClass) { + verify( + testIndexHolder.testClassName().equals(testClass.getName()), + "The class for which the given annotation was created (%s) is not the same as the test" + + " class that this runner is handling (%s)", + testIndexHolder.testClassName(), + testClass.getName()); + Method testMethod = getMethodsIncludingParents(testClass).get(testIndexHolder.methodIndex()); + return getParameterValuesForMethod(testMethod, testClass) + .get(testIndexHolder.parametersIndex()); + } + + /** + * Returns the list of annotation index for all annotations defined in a given test method and its + * class. + */ + private ImmutableList> getAnnotationValuesForUsedAnnotationTypes( + Method method, Class testClass) { + ImmutableList annotationTypes = + Stream.of( + getAnnotationTypeOrigins(testClass, Origin.CLASS).stream(), + getAnnotationTypeOrigins(testClass, Origin.FIELD).stream(), + getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR).stream(), + getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR_PARAMETER).stream(), + getAnnotationTypeOrigins(testClass, Origin.METHOD).stream(), + getAnnotationTypeOrigins(testClass, 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 annotationComparator( + Annotation[][] parameterAnnotations) { + ImmutableList 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}. + * + *

          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 removeOverrides( + List 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 removeOverrides( + List 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(); + default: + return true; + } + }) + .collect(toCollection(ArrayList::new)); + } + + /** + * Returns the given annotations defined either on the method parameters, method or the test + * class. + * + *

          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> getAnnotationFromParametersOrTestOrClass( + AnnotationTypeOrigin annotationTypeOrigin, Method method, Class testClass) { + Origin origin = annotationTypeOrigin.origin(); + Class annotationType = annotationTypeOrigin.annotationType(); + if (origin == Origin.CONSTRUCTOR_PARAMETER) { + Constructor constructor = getOnlyConstructor(testClass); + List 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 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 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> toTestParameterValueList( + List annotationWithMetadatas, Origin origin) { + return annotationWithMetadatas.stream() + .map(annotationWithMetadata -> TestParameterValue.create(annotationWithMetadata, origin)) + .collect(toImmutableList()); + } + + private static ImmutableList getAnnotationWithMetadataListWithType( + Method callable, Class annotationType) { + try { + return getAnnotationWithMetadataListWithType(callable.getParameters(), annotationType); + } catch (NoSuchMethodError ignored) { + return getAnnotationWithMetadataListWithType( + callable.getParameterTypes(), callable.getParameterAnnotations(), annotationType); + } + } + + private static ImmutableList getAnnotationWithMetadataListWithType( + Constructor callable, Class 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 getAnnotationWithMetadataListWithType( + Parameter[] parameters, Class 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 getAnnotationWithMetadataListWithType( + Class[] parameterTypes, + Annotation[][] annotations, + Class annotationType) { + checkArgument(parameterTypes.length == annotations.length); + + ImmutableList.Builder 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 getAnnotationListWithType( + Annotation[] annotations, Class annotationType) { + return stream(annotations) + .filter(annotation -> annotation.annotationType().equals(annotationType)) + .collect(toImmutableList()); + } + + private static Constructor getOnlyConstructor(Class testClass) { + Constructor[] constructors = testClass.getDeclaredConstructors(); + checkState( + constructors.length == 1, + "a single public constructor is required for class %s", + testClass); + return constructors[0]; + } + + @Override + public void postProcessTestInstance(Object testInstance, TestInfo testInfo) { + TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); + try { + if (testIndexHolder != null) { + List testParameterValues = + getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()); + + // Do not include {@link Origin#METHOD_PARAMETER} nor {@link Origin#CONSTRUCTOR_PARAMETER} + // annotations. + List testParameterValuesForFieldInjection = + filterByOrigin(testParameterValues, Origin.CLASS, Origin.FIELD, Origin.METHOD); + // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class + // in the example above. + List remainingTestParameterValuesForFieldInjection = + new ArrayList<>(testParameterValuesForFieldInjection); + for (Field declaredField : + streamWithParents(testInstance.getClass()) + .flatMap(c -> stream(c.getDeclaredFields())) + .collect(toImmutableList())) { + for (TestParameterValue testParameterValue : + remainingTestParameterValuesForFieldInjection) { + if (declaredField.isAnnotationPresent( + testParameterValue.annotationTypeOrigin().annotationType())) { + declaredField.setAccessible(true); + declaredField.set(testInstance, testParameterValue.value()); + remainingTestParameterValuesForFieldInjection.remove(testParameterValue); + break; + } + } + } + } + } catch (IllegalAccessException 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 filterByOrigin( + List testParameterValues, Origin... origins) { + Set 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 filterAnnotationTypeOriginsByOrigin( + List annotationTypeOrigins, Origin... origins) { + List originList = Arrays.asList(origins); + return annotationTypeOrigins.stream() + .filter(annotationTypeOrigin -> originList.contains(annotationTypeOrigin.origin())) + .collect(toImmutableList()); + } + + /** Returns a {@link TestParameterAnnotation}'s value for a method or constructor parameter. */ + private Object getParameterValue( + List testParameterValues, + Class methodParameterType, + Annotation[] parameterAnnotations, + List> processedAnnotationTypes) { + List> 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 annotationType = + testParameterValue.annotationTypeOrigin().annotationType(); + if (parameterAnnotation.annotationType().equals(annotationType)) { + // If multiple annotations exist, ensure that the proper one is selected. + // For instance, for: + // + // test(@FooParameter(1,2) Foo foo, @FooParameter(3,4) Foo bar) {} + // + // 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}. + */ + int parametersIndex(); + + /** + * The full name of the test class. Only used for verifying that assumptions about the above + * indices are valid. + */ + String testClassName(); + } + + /** Factory for {@link TestIndexHolder}. */ + static class TestIndexHolderFactory { + @AutoAnnotation + static TestIndexHolder create(int methodIndex, int parametersIndex, String testClassName) { + return new AutoAnnotation_TestParameterAnnotationMethodProcessor_TestIndexHolderFactory_create( + methodIndex, parametersIndex, testClassName); + } + + private TestIndexHolderFactory() {} + } + + /** + * Returns whether the test should be skipped according to the {@code annotationType}'s {@link + * TestParameterValidator} and the current list of {@link TestParameterValue}. + */ + private static boolean callShouldSkip( + Class annotationType, List testParameterValues) { + TestParameterAnnotation annotation = + annotationType.getAnnotation(TestParameterAnnotation.class); + Class 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 testParameterValues; + private final Set valueList; + + public ValidatorContext(List testParameterValues) { + this.testParameterValues = testParameterValues; + this.valueList = testParameterValues.stream().map(TestParameterValue::value).collect(toSet()); + } + + @Override + public boolean has(Class testParameter, Object value) { + return getValue(testParameter).transform(value::equals).or(false); + } + + @Override + public , U extends Enum> boolean has(T value1, U value2) { + return valueList.contains(value1) && valueList.contains(value2); + } + + @Override + public Optional getValue(Class testParameter) { + return getParameter(testParameter).transform(TestParameterValue::value); + } + + @Override + public List getSpecifiedValues(Class testParameter) { + return getParameter(testParameter) + .transform(TestParameterValue::specifiedValues) + .or(ImmutableList.of()); + } + + private Optional getParameter(Class 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 annotationType, Optional> paramClass) { + TestParameterAnnotation testParameter = + annotationType.getAnnotation(TestParameterAnnotation.class); + Class 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> getTestParameterAnnotations( + List annotationTypeOrigins, + final Class testClass, + AnnotatedElement methodOrConstructor) { + return annotationTypeOrigins.stream() + .map(AnnotationTypeOrigin::annotationType) + .filter( + annotationType -> + testClass.isAnnotationPresent(annotationType) + || methodOrConstructor.isAnnotationPresent(annotationType)) + .collect(toImmutableList()); + } + + private int strictIndexOf(List haystack, T needle) { + int index = haystack.indexOf(needle); + checkArgument(index >= 0, "Could not find '%s' in %s", needle, haystack); + return index; + } + + private ImmutableList getMethodsIncludingParents(Class clazz) { + ImmutableList.Builder resultBuilder = ImmutableList.builder(); + while (clazz != null) { + resultBuilder.add(clazz.getDeclaredMethods()); + clazz = clazz.getSuperclass(); + } + return resultBuilder.build(); + } + + private static Stream> streamWithParents(Class clazz) { + Stream.Builder> 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 Collector> toImmutableList() { + return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf); + } + + private static Collector> toImmutableSet() { + return Collectors.collectingAndThen(Collectors.toList(), ImmutableSet::copyOf); + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorExtension.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorExtension.java new file mode 100644 index 0000000..6d6aa51 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorExtension.java @@ -0,0 +1,140 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Iterables.getOnlyElement; + +import com.google.auto.value.AutoValue; +import com.google.auto.value.extension.memoized.Memoized; +import com.google.common.collect.ImmutableList; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.TestInstancePostProcessor; +import org.junit.jupiter.api.extension.TestTemplateInvocationContext; +import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; + +/** Implements the TestParameterInjector logic for JUnit5 (Jupiter). */ +class TestParameterInjectorExtension implements TestTemplateInvocationContextProvider { + + private static final TestMethodProcessorList testMethodProcessors = + TestMethodProcessorList.createNewParameterizedProcessors(); + + @Override + public boolean supportsTestTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideTestTemplateInvocationContexts( + ExtensionContext extensionContext) { + validateTestMethodAndConstructor( + extensionContext.getRequiredTestMethod(), extensionContext.getRequiredTestClass()); + List testInfos = + testMethodProcessors.calculateTestInfos( + extensionContext.getRequiredTestMethod(), extensionContext.getRequiredTestClass()); + + return testInfos.stream().map(CustomInvocationContext::of); + } + + private void validateTestMethodAndConstructor(Method testMethod, Class testClass) { + checkState( + testClass.getDeclaredConstructors().length == 1, + "Only a single constructor is allowed, but found %s in %s", + testClass.getDeclaredConstructors().length, + testClass.getSimpleName()); + Constructor constructor = + getOnlyElement(ImmutableList.copyOf(testClass.getDeclaredConstructors())); + + testMethodProcessors.validateConstructor(constructor).assertValid(); + + testMethodProcessors.validateTestMethod(testMethod, testClass).assertValid(); + + checkState( + testMethod.getAnnotation(TestParameterInjectorTest.class) != null, + "Each test method handled by this extension should be annotated with" + + " @TestParameterInjectorTest"); + } + + @AutoValue + abstract static class CustomInvocationContext implements TestTemplateInvocationContext { + + abstract TestInfo testInfo(); + + @Memoized + List getConstructorParameters() { + Constructor constructor = + getOnlyElement(ImmutableList.copyOf(testInfo().getTestClass().getDeclaredConstructors())); + + return testMethodProcessors.getConstructorParameters(constructor, testInfo()); + } + + @Memoized + List getTestMethodParameters() { + return testMethodProcessors.getTestMethodParameters(testInfo()); + } + + static CustomInvocationContext of(TestInfo testInfo) { + return new AutoValue_TestParameterInjectorExtension_CustomInvocationContext(testInfo); + } + + @Override + public String getDisplayName(int invocationIndex) { + return testInfo().getName(); + } + + @Override + public List getAdditionalExtensions() { + return ImmutableList.of(new CustomAdditionalExtension()); + } + + class CustomAdditionalExtension implements ParameterResolver, TestInstancePostProcessor { + + @Override + public boolean supportsParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) { + if (parameterContext.getDeclaringExecutable() instanceof Constructor) { + return true; + } else { + return parameterContext + .getDeclaringExecutable() + .isAnnotationPresent(TestParameterInjectorTest.class); + } + } + + @Override + public Object resolveParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) { + if (parameterContext.getDeclaringExecutable() instanceof Constructor) { + return getConstructorParameters().get(parameterContext.getIndex()); + } else { + return getTestMethodParameters().get(parameterContext.getIndex()); + } + } + + @Override + public void postProcessTestInstance(Object testInstance, ExtensionContext extensionContext) + throws Exception { + testMethodProcessors.postProcessTestInstance(testInstance, testInfo()); + } + } + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorTest.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorTest.java new file mode 100644 index 0000000..e17179a --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorTest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Replacement for JUnit5's @Test for test methods that want to use @TestParameter[s]. + * + *

          Example: + * + *

          + * class MyTest {
          + *   {@literal @}TestParameterInjectorTest
          + *   void withParameter_success({@literal @}TestParameter boolean bool) {
          + *     // ...
          + *   }
          + *
          + *   {@literal @}TestParameterInjectorTest
          + *   {@literal @}TestParameters("{name: 1, number: 3.3}")
          + *   {@literal @}TestParameters("{name: abc, number: 5}")
          + *   void withParameters_success(String name, double number) {
          + *     // ...
          + *   }
          + * }
          + * 
          + */ +@TestTemplate +@ExtendWith(TestParameterInjectorExtension.class) +@Retention(RUNTIME) +@Target({METHOD}) +public @interface TestParameterInjectorTest {} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java new file mode 100644 index 0000000..70db746 --- /dev/null +++ b/junit5/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.junit5; + +import com.google.common.base.Optional; +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Validator interface which allows {@link TestParameterAnnotation} annotations to validate the set + * of annotation values for a given test instance, and to selectively skip the test. + */ +interface TestParameterValidator { + + /** + * This interface allows to access information on the current testwhen implementing {@link + * TestParameterValidator}. + */ + interface Context { + + /** Returns whether the current test has the {@link TestParameterAnnotation} value(s). */ + boolean has(Class 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. + */ + , U extends Enum> boolean has(T value1, U value2); + + /** + * Returns all the current test value for a given {@link TestParameterAnnotation} annotated + * annotation. + */ + Optional getValue(Class testParameter); + + /** + * Returns all the values specified for a given {@link TestParameterAnnotation} annotated + * annotation in the test. + * + *

          For example, if the test annotates '@Foo(a,b,c)', getSpecifiedValues(Foo.class) will + * return [a,b,c]. + */ + List getSpecifiedValues(Class testParameter); + } + + /** + * Returns whether the test should be skipped based on the annotations' values. + * + *

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

          This method is not invoked in the context of a running test statement. + */ + boolean shouldSkip(Context context); +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java new file mode 100644 index 0000000..47ed601 --- /dev/null +++ b/junit5/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.junit5; + +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 provideValues(Annotation annotation, Optional> 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 annotationType, Optional> parameterClass); +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java new file mode 100644 index 0000000..b2c88a6 --- /dev/null +++ b/junit5/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.junit5; + +import com.google.common.base.Optional; +import java.lang.annotation.Annotation; + +/** Interface to retrieve the {@link TestParameterAnnotation} values for a test. */ +interface TestParameterValues { + /** + * Returns a {@link TestParameterAnnotation} value for the current test as specified by {@code + * testInfo}, or {@link Optional#absent()} if the {@code annotationType} is not found. + */ + Optional getValue(Class annotationType); +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java new file mode 100644 index 0000000..3a3c40c --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java @@ -0,0 +1,259 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkState; +import static java.lang.annotation.ElementType.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.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * Annotation that can be placed (repeatedly) on @Test-methods or a test constructor to indicate the + * sets of parameters that it should be invoked with. + * + *

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

          Note: If this annotation is used in a test class, the other methods in that class can still + * use other types of parameterization, such as {@linkplain TestParameter @TestParameter}. + * + *

          See {@link #value()} for simple examples. + * + *

          Warning: This annotation can only be used if the compiled java code contains the parameter + * names. This is typically done by passing the {@code -parameters} option to the Java compiler, + * which requires using Java 8 or higher and may not be available on Android. + */ +@Retention(RUNTIME) +@Target({CONSTRUCTOR, METHOD}) +@Repeatable(TestParameters.RepeatedTestParameters.class) +public @interface TestParameters { + + /** + * Specifies one or more stringified sets of parameters in YAML format. Each set corresponds to a + * single invocation of a test method. + * + *

          Each element in this array is a full parameter set, formatted as a YAML mapping. The mapping + * keys must match the parameter names and the mapping values will be converted to the parameter + * type if possible. See yaml.org for the YAML syntax and the section below on the supported + * parameter types. + * + *

          There are two distinct ways of using this annotation: repeated vs single: + * + *

          Recommended usage: Separate annotation per parameter set + * + *

          This approach uses multiple @TestParameters annotations, one for each set of parameters, for + * example: + * + *

          +   * {@literal @}Test
          +   * {@literal @}TestParameters("{age: 17, expectIsAdult: false}")
          +   * {@literal @}TestParameters("{age: 22, expectIsAdult: true}")
          +   * public void personIsAdult(int age, boolean expectIsAdult) { ... }
          +   *
          +   * {@literal @}Test
          +   * {@literal @}TestParameters("{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}")
          +   * {@literal @}TestParameters("{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}")
          +   * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... }
          +   * 
          + * + *

          Old discouraged usage: Single annotation with all parameter sets + * + *

          This approach uses a single @TestParameter annotation for all parameter sets, for example: + * + *

          +   * {@literal @}Test
          +   * {@literal @}TestParameters({
          +   *   "{age: 17, expectIsAdult: false}",
          +   *   "{age: 22, expectIsAdult: true}",
          +   * })
          +   * public void personIsAdult(int age, boolean expectIsAdult) { ... }
          +   *
          +   * {@literal @}Test
          +   * {@literal @}TestParameters({
          +   *   "{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}",
          +   *   "{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}",
          +   * })
          +   * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... }
          +   * 
          + * + *

          Supported parameter types + * + *

            + *
          • YAML primitives: + *
              + *
            • String: Specified as YAML string + *
            • boolean: Specified as YAML boolean + *
            • long and int: Specified as YAML integer + *
            • float and double: Specified as YAML floating point or integer + *
            + *
          • + *
          • Parsed types: + *
              + *
            • Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()} + *
            • Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML + * bytes (example: "!!binary 'ZGF0YQ=='") + *
            + *
          • + *
          + * + *

          For dynamic sets of parameters or parameter types that are not supported here, use {@link + * #valuesProvider()} and leave this field empty. + */ + String[] value() default {}; + + /** + * Overrides the name of the parameter set that is used in the test name. + * + *

          This can only be set if {@link #value()} has exactly one element. If not set, the YAML + * string in {@link #value()} is used in the test name. + * + *

          For example: If this name is set to "young adult", then the test name might be + * "personIsAdult[young adult]" where the default might have been "personIsAdult[{age: 17, + * expectIsAdult: false}]". + */ + String customName() default ""; + + /** + * Sets a provider that will return a list of parameter sets. Each element in the returned list + * corresponds to a single invocation of a test method. + * + *

          If this field is set, {@link #value()} must be empty and vice versa. + * + *

          Example + * + *

          +   * {@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} 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()
          +   *     );
          +   *   }
          +   * }
          +   * 
          + */ + Class valuesProvider() default + DefaultTestParametersValuesProvider.class; + + /** Interface for custom providers of test parameter values. */ + interface TestParametersValuesProvider { + List 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. + * + *

          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 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 parametersMap = new LinkedHashMap<>(); + + /** + * Sets a name for this set of parameters that will be used for describing this test. + * + *

          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 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 provideValues() { + return ImmutableList.of(); + } + } + + /** + * Holder annotation for multiple @TestParameters annotations. This should never be used directly. + */ + @Retention(RUNTIME) + @Target({CONSTRUCTOR, METHOD}) + @interface RepeatedTestParameters { + TestParameters[] value(); + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java new file mode 100644 index 0000000..4879ca7 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java @@ -0,0 +1,485 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Verify.verify; +import static com.google.common.collect.Iterables.getOnlyElement; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toList; + +import com.google.auto.value.AutoAnnotation; +import com.google.common.base.Optional; +import com.google.common.base.Throwables; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.common.primitives.Primitives; +import com.google.common.reflect.TypeToken; +import com.google.common.util.concurrent.UncheckedExecutionException; +import com.google.testing.junit.testparameterinjector.junit5.TestInfo.TestInfoParameter; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters.DefaultTestParametersValuesProvider; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters.RepeatedTestParameters; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValues; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValuesProvider; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** {@code TestMethodProcessor} implementation for supporting {@link TestParameters}. */ +@SuppressWarnings("AndroidJdkLibsChecker") // Parameter is not available on old Android SDKs. +final class TestParametersMethodProcessor implements TestMethodProcessor { + + private final LoadingCache> + parameterValuesByConstructorOrMethodCache = + CacheBuilder.newBuilder() + .maximumSize(1000) + .build(CacheLoader.from(TestParametersMethodProcessor::toParameterValuesList)); + + @Override + public ExecutableValidationResult validateConstructor(Constructor constructor) { + if (hasRelevantAnnotation(constructor)) { + try { + // This method throws an exception if there is a validation error + getConstructorParameters(constructor); + } catch (Throwable t) { + return ExecutableValidationResult.validated(t); + } + return ExecutableValidationResult.valid(); + } else { + return ExecutableValidationResult.notValidated(); + } + } + + @Override + public ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass) { + if (hasRelevantAnnotation(testMethod)) { + try { + // This method throws an exception if there is a validation error + getMethodParameters(testMethod); + } catch (Throwable t) { + return ExecutableValidationResult.validated(t); + } + return ExecutableValidationResult.valid(); + } else { + return ExecutableValidationResult.notValidated(); + } + } + + @Override + public List calculateTestInfos(TestInfo originalTest) { + boolean constructorIsParameterized = + hasRelevantAnnotation(getOnlyConstructor(originalTest.getTestClass())); + boolean methodIsParameterized = hasRelevantAnnotation(originalTest.getMethod()); + + if (!constructorIsParameterized && !methodIsParameterized) { + return ImmutableList.of(originalTest); + } + + ImmutableList.Builder testInfos = ImmutableList.builder(); + + ImmutableList> constructorParametersList = + getConstructorParametersOrSingleAbsentElement(originalTest.getTestClass()); + ImmutableList> methodParametersList = + getMethodParametersOrSingleAbsentElement(originalTest.getMethod()); + for (int constructorParametersIndex = 0; + constructorParametersIndex < constructorParametersList.size(); + ++constructorParametersIndex) { + Optional constructorParameters = + constructorParametersList.get(constructorParametersIndex); + + for (int methodParametersIndex = 0; + methodParametersIndex < methodParametersList.size(); + ++methodParametersIndex) { + Optional 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> + getConstructorParametersOrSingleAbsentElement(Class testClass) { + Constructor constructor = getOnlyConstructor(testClass); + return hasRelevantAnnotation(constructor) + ? getConstructorParameters(constructor).stream() + .map(Optional::of) + .collect(toImmutableList()) + : ImmutableList.of(Optional.absent()); + } + + private ImmutableList> getMethodParametersOrSingleAbsentElement( + Method method) { + return hasRelevantAnnotation(method) + ? getMethodParameters(method).stream().map(Optional::of).collect(toImmutableList()) + : ImmutableList.of(Optional.absent()); + } + + @Override + public Optional> maybeGetConstructorParameters( + Constructor constructor, TestInfo testInfo) { + if (hasRelevantAnnotation(constructor)) { + ImmutableList parameterValuesList = + getConstructorParameters(constructor); + TestParametersValues parametersValues = + parameterValuesList.get( + testInfo.getAnnotation(TestIndexHolder.class).constructorParametersIndex()); + + return Optional.of(toParameterList(parametersValues, constructor.getParameters())); + } else { + return Optional.absent(); + } + } + + @Override + public Optional> maybeGetTestMethodParameters(TestInfo testInfo) { + Method testMethod = testInfo.getMethod(); + if (hasRelevantAnnotation(testMethod)) { + ImmutableList parameterValuesList = getMethodParameters(testMethod); + TestParametersValues parametersValues = + parameterValuesList.get( + testInfo.getAnnotation(TestIndexHolder.class).methodParametersIndex()); + + return Optional.of(toParameterList(parametersValues, testMethod.getParameters())); + } else { + return Optional.absent(); + } + } + + @Override + public void postProcessTestInstance(Object testInstance, TestInfo testInfo) {} + + private ImmutableList getConstructorParameters(Constructor constructor) { + try { + return parameterValuesByConstructorOrMethodCache.getUnchecked(constructor); + } catch (UncheckedExecutionException e) { + // Rethrow IllegalStateException because they can be caused by user mistakes and the user + // doesn't need to know that the caching layer is in between. + Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); + throw e; + } + } + + private ImmutableList getMethodParameters(Method method) { + try { + return parameterValuesByConstructorOrMethodCache.getUnchecked(method); + } catch (UncheckedExecutionException e) { + // Rethrow IllegalStateException because they can be caused by user mistakes and the user + // doesn't need to know that the caching layer is in between. + Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); + throw e; + } + } + + private static ImmutableList toParameterValuesList(Executable executable) { + checkParameterNamesArePresent(executable); + ImmutableList parametersList = ImmutableList.copyOf(executable.getParameters()); + + if (executable.isAnnotationPresent(TestParameters.class)) { + checkState( + !executable.isAnnotationPresent(RepeatedTestParameters.class), + "Unexpected situation: Both @TestParameters and @RepeatedTestParameters annotating the" + + " same method"); + TestParameters annotation = executable.getAnnotation(TestParameters.class); + boolean valueIsSet = annotation.value().length > 0; + boolean valuesProviderIsSet = + !annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class); + + checkState( + !(valueIsSet && valuesProviderIsSet), + "It is not allowed to specify both value and valuesProvider in @TestParameters(value=%s," + + " valuesProvider=%s) on %s()", + Arrays.toString(annotation.value()), + annotation.valuesProvider().getSimpleName(), + executable.getName()); + checkState( + valueIsSet || valuesProviderIsSet, + "Either a value or a valuesProvider must be set in @TestParameters on %s()", + executable.getName()); + if (!annotation.customName().isEmpty()) { + checkState( + annotation.value().length == 1, + "Setting @TestParameters.customName is only allowed if there is exactly one YAML string" + + " in @TestParameters.value (on %s())", + executable.getName()); + } + + if (valueIsSet) { + return stream(annotation.value()) + .map(yamlMap -> toParameterValues(yamlMap, parametersList, annotation.customName())) + .collect(toImmutableList()); + } else { + return toParameterValuesList(annotation.valuesProvider(), parametersList); + } + } else { // Not annotated with @TestParameters + verify( + executable.isAnnotationPresent(RepeatedTestParameters.class), + "This method should only be called for executables with at least one relevant" + + " annotation"); + + return stream(executable.getAnnotation(RepeatedTestParameters.class).value()) + .map( + annotation -> + toParameterValues( + validateAndGetSingleValueFromRepeatedAnnotation(annotation, executable), + parametersList, + annotation.customName())) + .collect(toImmutableList()); + } + } + + private static ImmutableList toParameterValuesList( + Class valuesProvider, List parameters) { + try { + Constructor 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 checkParameterNamesArePresent(Executable executable) { + checkState( + stream(executable.getParameters()).allMatch(Parameter::isNamePresent), + "" + + "No parameter name could be found for %s, which likely means that parameter names" + + " aren't available at runtime. Please ensure that the this test was built with the" + + " -parameters compiler option.\n" + + "\n" + + "In Maven, you do this by adding true to the" + + " maven-compiler-plugin's configuration. For example:\n" + + "\n" + + "\n" + + " \n" + + " \n" + + " org.apache.maven.plugins\n" + + " maven-compiler-plugin\n" + + " 3.8.1\n" + + " \n" + + " \n" + + " -parameters\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + "\n" + + "Don't forget to run `mvn clean` after making this change.", + executable.getName()); + } + + private static String validateAndGetSingleValueFromRepeatedAnnotation( + TestParameters annotation, Executable executable) { + checkState( + annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class), + "Setting a valuesProvider is not supported for methods/constructors with" + + " multiple @TestParameters annotations on %s()", + executable.getName()); + checkState( + annotation.value().length > 0, + "Either a value or a valuesProvider must be set in @TestParameters on %s()", + executable.getName()); + checkState( + annotation.value().length == 1, + "When specifying more than one @TestParameter for a method/constructor, each annotation" + + " must have exactly one value. Instead, got %s values on %s(): %s", + annotation.value().length, + executable.getName(), + Arrays.toString(annotation.value())); + + return annotation.value()[0]; + } + + private static void validateThatValuesMatchParameters( + TestParametersValues testParametersValues, List parameters) { + ImmutableMap 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 parameters, String maybeCustomName) { + Object yamlMapObject = ParameterValueParsing.parseYamlStringToObject(yamlString); + checkState( + yamlMapObject instanceof Map, + "Cannot map YAML string '%s' to parameters because it is not a mapping", + yamlString); + Map yamlMap = (Map) yamlMapObject; + + ImmutableMap parametersByName = + Maps.uniqueIndex(parameters, Parameter::getName); + checkState( + yamlMap.keySet().equals(parametersByName.keySet()), + "Cannot map YAML string '%s' to parameters %s", + yamlString, + parametersByName.keySet()); + + @SuppressWarnings("unchecked") + Map checkedYamlMap = (Map) yamlMap; + + return TestParametersValues.builder() + .name(maybeCustomName.isEmpty() ? yamlString : maybeCustomName) + .addParameters( + Maps.transformEntries( + checkedYamlMap, + (parameterName, parsedYaml) -> + ParameterValueParsing.parseYamlObjectToJavaType( + parsedYaml, + TypeToken.of(parametersByName.get(parameterName).getParameterizedType())))) + .build(); + } + + // Note: We're not using the Executable interface here because it isn't supported by Java 7 and + // this code is called even if only @TestParameter is used. In other places, Executable is usable + // because @TestParameters only works for Java 8 anyway. + private static boolean hasRelevantAnnotation(Constructor executable) { + return executable.isAnnotationPresent(TestParameters.class) + || executable.isAnnotationPresent(RepeatedTestParameters.class); + } + + private static boolean hasRelevantAnnotation(Method executable) { + return executable.isAnnotationPresent(TestParameters.class) + || executable.isAnnotationPresent(RepeatedTestParameters.class); + } + + private static List toParameterList( + TestParametersValues parametersValues, Parameter[] parameters) { + return stream(parameters) + .map(parameter -> parametersValues.parametersMap().get(parameter.getName())) + .collect(toList()); + } + + private static Constructor getOnlyConstructor(Class testClass) { + ImmutableList> constructors = + ImmutableList.copyOf(testClass.getDeclaredConstructors()); + checkState( + constructors.size() == 1, "Expected exactly one constructor, but got %s", constructors); + return getOnlyElement(constructors); + } + + // Immutable collectors are re-implemented here because they are missing from the Android + // collection library. + private static Collector> 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/junit5/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorJUnit5Test.java b/junit5/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorJUnit5Test.java new file mode 100644 index 0000000..59ffb18 --- /dev/null +++ b/junit5/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorJUnit5Test.java @@ -0,0 +1,607 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5_otherpackage; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.testing.junit.testparameterinjector.junit5.TestParameter; +import com.google.testing.junit.testparameterinjector.junit5.TestParameter.TestParameterValuesProvider; +import com.google.testing.junit.testparameterinjector.junit5.TestParameterInjectorTest; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValues; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValuesProvider; +import java.lang.annotation.Retention; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.TestExecutionResult.Status; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestIdentifier; +import org.junit.platform.launcher.TestPlan; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.launcher.core.LauncherFactory; + +/** Tests the full feature set of TestParameterInjector with JUnit5 (Jupiter). */ +class TestParameterInjectorJUnit5Test { + + abstract static class SuccessfulTestCaseBase { + private static Map testNameToStringifiedParameters; + private static ImmutableMap expectedTestNameToStringifiedParameters; + private String testName; + + @BeforeAll + private static void checkStaticFieldAreNull() { + checkState(testNameToStringifiedParameters == null); + checkState(expectedTestNameToStringifiedParameters == null); + } + + @BeforeEach + private void storeTestName(org.junit.jupiter.api.TestInfo testInfo) { + testName = testInfo.getDisplayName(); + } + + final void storeTestParametersForThisTest(Object... params) { + if (testNameToStringifiedParameters == null) { + testNameToStringifiedParameters = new LinkedHashMap<>(); + // Copying this into a static field because @AfterAll methods have to be static + expectedTestNameToStringifiedParameters = expectedTestNameToStringifiedParameters(); + } + checkState( + !testNameToStringifiedParameters.containsKey(testName), + "Parameters for the test with name '%s' are already stored. This might mean that there" + + " are duplicate test names", + testName); + testNameToStringifiedParameters.put( + testName, stream(params).map(String::valueOf).collect(joining(":"))); + } + + abstract ImmutableMap expectedTestNameToStringifiedParameters(); + + @AfterAll + private static void completedAllTests() { + try { + assertWithMessage(toCopyPastableJavaString(testNameToStringifiedParameters)) + .that(testNameToStringifiedParameters) + .isEqualTo(expectedTestNameToStringifiedParameters); + } finally { + testNameToStringifiedParameters = null; + expectedTestNameToStringifiedParameters = null; + } + } + } + + @RunAsTest + static class SimpleCases_WithoutExplicitConstructor extends SuccessfulTestCaseBase { + @Test + void withoutCustomAnnotation() { + storeTestParametersForThisTest(); + } + + @TestParameterInjectorTest + void withoutParameters() { + storeTestParametersForThisTest(); + } + + @TestParameterInjectorTest + @TestParameters("{name: 1, number: 3.3}") + @TestParameters("{name: abc, number: 5}") + void withParameters_success(String name, double number) { + storeTestParametersForThisTest(name, number); + } + + @TestParameterInjectorTest + void withParameter_success( + @TestParameter({"2", "xyz"}) String name, @TestParameter boolean bool) { + storeTestParametersForThisTest(name, bool); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("withoutCustomAnnotation()", "") + .put("withoutParameters", "") + .put("withParameters_success[{name: 1, number: 3.3}]", "1:3.3") + .put("withParameters_success[{name: abc, number: 5}]", "abc:5.0") + .put("withParameter_success[2,bool=false]", "2:false") + .put("withParameter_success[2,bool=true]", "2:true") + .put("withParameter_success[xyz,bool=false]", "xyz:false") + .put("withParameter_success[xyz,bool=true]", "xyz:true") + .build(); + } + } + + @RunAsTest + static class SimpleCases_WithParameterizedConstructor_TestParameter + extends SuccessfulTestCaseBase { + private final boolean constr; + + @TestParameter({"AAA", "BBB"}) + private String field; + + SimpleCases_WithParameterizedConstructor_TestParameter(@TestParameter boolean constr) { + this.constr = constr; + } + + @TestParameterInjectorTest + void withoutParameters() { + storeTestParametersForThisTest(constr, field); + } + + @TestParameterInjectorTest + @TestParameters("{name: 1}") + @TestParameters("{name: abc}") + void withParameters_success(String name) { + storeTestParametersForThisTest(constr, field, name); + } + + @TestParameterInjectorTest + void withParameter_success(@TestParameter({"2", "xyz"}) String name) { + storeTestParametersForThisTest(constr, field, name); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("withParameters_success[{name: 1},AAA,constr=false]", "false:AAA:1") + .put("withParameters_success[{name: 1},AAA,constr=true]", "true:AAA:1") + .put("withParameters_success[{name: 1},BBB,constr=false]", "false:BBB:1") + .put("withParameters_success[{name: 1},BBB,constr=true]", "true:BBB:1") + .put("withParameters_success[{name: abc},AAA,constr=false]", "false:AAA:abc") + .put("withParameters_success[{name: abc},AAA,constr=true]", "true:AAA:abc") + .put("withParameters_success[{name: abc},BBB,constr=false]", "false:BBB:abc") + .put("withParameters_success[{name: abc},BBB,constr=true]", "true:BBB:abc") + .put("withParameter_success[AAA,constr=false,2]", "false:AAA:2") + .put("withParameter_success[AAA,constr=false,xyz]", "false:AAA:xyz") + .put("withParameter_success[AAA,constr=true,2]", "true:AAA:2") + .put("withParameter_success[AAA,constr=true,xyz]", "true:AAA:xyz") + .put("withParameter_success[BBB,constr=false,2]", "false:BBB:2") + .put("withParameter_success[BBB,constr=false,xyz]", "false:BBB:xyz") + .put("withParameter_success[BBB,constr=true,2]", "true:BBB:2") + .put("withParameter_success[BBB,constr=true,xyz]", "true:BBB:xyz") + .put("withoutParameters[AAA,constr=false]", "false:AAA") + .put("withoutParameters[AAA,constr=true]", "true:AAA") + .put("withoutParameters[BBB,constr=false]", "false:BBB") + .put("withoutParameters[BBB,constr=true]", "true:BBB") + .build(); + } + } + + @RunAsTest + static class SimpleCases_WithParameterizedConstructor_TestParameters + extends SuccessfulTestCaseBase { + private final boolean constr; + + @TestParameter({"AAA", "BBB"}) + private String field; + + @TestParameters("{constr: true}") + @TestParameters("{constr: false}") + SimpleCases_WithParameterizedConstructor_TestParameters(boolean constr) { + this.constr = constr; + } + + @TestParameterInjectorTest + void withoutParameters() { + storeTestParametersForThisTest(constr, field); + } + + @TestParameterInjectorTest + @TestParameters("{name: 1}") + @TestParameters("{name: abc}") + void withParameters_success(String name) { + storeTestParametersForThisTest(constr, field, name); + } + + @TestParameterInjectorTest + void withParameter_success(@TestParameter({"2", "xyz"}) String name) { + storeTestParametersForThisTest(constr, field, name); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("withParameters_success[{constr: true},{name: 1},AAA]", "true:AAA:1") + .put("withParameters_success[{constr: true},{name: 1},BBB]", "true:BBB:1") + .put("withParameters_success[{constr: true},{name: abc},AAA]", "true:AAA:abc") + .put("withParameters_success[{constr: true},{name: abc},BBB]", "true:BBB:abc") + .put("withParameters_success[{constr: false},{name: 1},AAA]", "false:AAA:1") + .put("withParameters_success[{constr: false},{name: 1},BBB]", "false:BBB:1") + .put("withParameters_success[{constr: false},{name: abc},AAA]", "false:AAA:abc") + .put("withParameters_success[{constr: false},{name: abc},BBB]", "false:BBB:abc") + .put("withParameter_success[{constr: true},AAA,2]", "true:AAA:2") + .put("withParameter_success[{constr: true},AAA,xyz]", "true:AAA:xyz") + .put("withParameter_success[{constr: true},BBB,2]", "true:BBB:2") + .put("withParameter_success[{constr: true},BBB,xyz]", "true:BBB:xyz") + .put("withParameter_success[{constr: false},AAA,2]", "false:AAA:2") + .put("withParameter_success[{constr: false},AAA,xyz]", "false:AAA:xyz") + .put("withParameter_success[{constr: false},BBB,2]", "false:BBB:2") + .put("withParameter_success[{constr: false},BBB,xyz]", "false:BBB:xyz") + .put("withoutParameters[{constr: true},AAA]", "true:AAA") + .put("withoutParameters[{constr: true},BBB]", "true:BBB") + .put("withoutParameters[{constr: false},AAA]", "false:AAA") + .put("withoutParameters[{constr: false},BBB]", "false:BBB") + .build(); + } + } + + @RunAsTest + public static class AdvancedCases_WithValuesProvider extends SuccessfulTestCaseBase { + private final TestEnum testEnum; + + @TestParameters(valuesProvider = TestEnumValuesProvider.class) + public AdvancedCases_WithValuesProvider(TestEnum testEnum) { + this.testEnum = testEnum; + } + + @TestParameterInjectorTest + void test1() { + storeTestParametersForThisTest(testEnum); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test1[one]", "ONE") + .put("test1[two]", "TWO") + .put("test1[null-case]", "null") + .build(); + } + } + + @RunAsTest + public static class AdvancedCases_WithValueProvider extends SuccessfulTestCaseBase { + @TestParameterInjectorTest + void stringTest(@TestParameter(valuesProvider = TestStringProvider.class) String string) { + storeTestParametersForThisTest(string); + } + + @TestParameterInjectorTest + void charMatcherTest( + @TestParameter(valuesProvider = CharMatcherProvider.class) CharMatcher charMatcher) { + storeTestParametersForThisTest(charMatcher); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("stringTest[A]", "A") + .put("stringTest[B]", "B") + .put("stringTest[null]", "null") + .put("charMatcherTest[CharMatcher.any()]", "CharMatcher.any()") + .put("charMatcherTest[CharMatcher.ascii()]", "CharMatcher.ascii()") + .put("charMatcherTest[CharMatcher.whitespace()]", "CharMatcher.whitespace()") + .build(); + } + + private static final class TestStringProvider implements TestParameterValuesProvider { + @Override + public List provideValues() { + return newArrayList("A", "B", null); + } + } + + private static final class CharMatcherProvider implements TestParameterValuesProvider { + @Override + public List provideValues() { + return newArrayList(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace()); + } + } + } + + public abstract static class BaseClassWithTestParametersMethod extends SuccessfulTestCaseBase { + @TestParameterInjectorTest + @TestParameters("{testEnum: ONE}") + @TestParameters("{testEnum: TWO}") + void testInBase(TestEnum testEnum) { + storeTestParametersForThisTest(testEnum); + } + } + + @RunAsTest + public static class AdvancedCases_WithBaseClass_TestParametersMethodInBase + extends BaseClassWithTestParametersMethod { + @TestParameterInjectorTest + @TestParameters({"{testEnum: TWO}", "{testEnum: THREE}"}) + void testInChild(TestEnum testEnum) { + storeTestParametersForThisTest(testEnum); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("testInBase[{testEnum: ONE}]", "ONE") + .put("testInBase[{testEnum: TWO}]", "TWO") + .put("testInChild[{testEnum: TWO}]", "TWO") + .put("testInChild[{testEnum: THREE}]", "THREE") + .build(); + } + } + + public abstract static class BaseClassWithTestParameterMethod extends SuccessfulTestCaseBase { + @TestParameterInjectorTest + void testInBase(@TestParameter({"ONE", "TWO"}) TestEnum testEnum) { + storeTestParametersForThisTest(testEnum); + } + } + + @RunAsTest + public static class AdvancedCases_WithBaseClass_TestParameterMethodInBase + extends BaseClassWithTestParameterMethod { + @TestParameterInjectorTest + @TestParameters({"{testEnum: TWO}", "{testEnum: THREE}"}) + void testInChild(TestEnum testEnum) { + storeTestParametersForThisTest(testEnum); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("testInBase[ONE]", "ONE") + .put("testInBase[TWO]", "TWO") + .put("testInChild[{testEnum: TWO}]", "TWO") + .put("testInChild[{testEnum: THREE}]", "THREE") + .build(); + } + } + + public abstract static class BaseClassWithTestParameterField extends SuccessfulTestCaseBase { + @TestParameter TestEnum fieldInBase; + } + + @RunAsTest + public static class AdvancedCases_WithBaseClass_TestParameterFieldInBase + extends BaseClassWithTestParameterField { + @TestParameterInjectorTest + void testInChild() { + storeTestParametersForThisTest(fieldInBase); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("testInChild[ONE]", "ONE") + .put("testInChild[TWO]", "TWO") + .put("testInChild[THREE]", "THREE") + .build(); + } + } + + @RunAsTest( + failsWithMessage = + "Either a value or a valuesProvider must be set in @TestParameters on test1()") + public static class InvalidTest_TestParameters_EmptyAnnotation { + @TestParameterInjectorTest + @TestParameters + void test1() {} + } + + @RunAsTest( + failsWithMessage = "Either a value or a valuesProvider must be set in @TestParameters on ") + public static class InvalidTest_TestParameters_EmptyAnnotationOnConstructor { + @TestParameters + public InvalidTest_TestParameters_EmptyAnnotationOnConstructor() {} + + @TestParameterInjectorTest + void test1() {} + } + + @RunAsTest( + failsWithMessage = + "It is not allowed to specify both value and valuesProvider in" + + " @TestParameters(value=[{testEnum: ONE}], valuesProvider=TestEnumValuesProvider)" + + " on test1()") + public static class InvalidTest_TestParameters_CombiningValueWithProvider { + @TestParameterInjectorTest + @TestParameters(value = "{testEnum: ONE}", valuesProvider = TestEnumValuesProvider.class) + void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "Either a value or a valuesProvider must be set in @TestParameters on test1()") + public static class InvalidTest_TestParameters_RepeatedAnnotationIsEmpty { + @TestParameterInjectorTest + @TestParameters(value = "{testEnum: ONE}") + @TestParameters + void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "When specifying more than one @TestParameter for a method/constructor, each annotation" + + " must have exactly one value. Instead, got 2 values on test1(): [{testEnum: TWO}," + + " {testEnum: THREE}]") + public static class InvalidTest_TestParameters_RepeatedAnnotationHasMultipleValues { + @TestParameterInjectorTest + @TestParameters(value = "{testEnum: ONE}") + @TestParameters(value = {"{testEnum: TWO}", "{testEnum: THREE}"}) + void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "Setting a valuesProvider is not supported for methods/constructors with" + + " multiple @TestParameters annotations on test1()") + public static class InvalidTest_TestParameters_RepeatedAnnotationHasProvider { + @TestParameterInjectorTest + @TestParameters(valuesProvider = TestEnumValuesProvider.class) + @TestParameters(valuesProvider = TestEnumValuesProvider.class) + void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "Setting @TestParameters.customName is only allowed if there is exactly one YAML string" + + " in @TestParameters.value (on test1())") + public static class InvalidTest_TestParameters_NamedAnnotationHasMultipleValues { + @TestParameterInjectorTest + @TestParameters( + customName = "custom", + value = {"{testEnum: TWO}", "{testEnum: THREE}"}) + void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "Could not find a no-arg constructor for NonStaticProvider, probably because it is a" + + " not-static inner class. You can fix this by making NonStaticProvider static.") + public static class InvalidTest_TestParameter_NonStaticProviderClass { + @TestParameterInjectorTest + void test(@TestParameter(valuesProvider = NonStaticProvider.class) int i) {} + + @SuppressWarnings("ClassCanBeStatic") + class NonStaticProvider implements TestParameterValuesProvider { + @Override + public List provideValues() { + return ImmutableList.of(); + } + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("provideTestClassesThatExpectSuccess") + void runTest_success(Class testClass) { + FailureListener listener = new FailureListener(); + LauncherDiscoveryRequest request = + LauncherDiscoveryRequestBuilder.request() + .selectors(DiscoverySelectors.selectClass(testClass)) + .build(); + Launcher launcher = LauncherFactory.create(); + launcher.registerTestExecutionListeners(listener); + launcher.execute(request); + + assertNoFailures(listener.failures); + } + + @ParameterizedTest(name = "{0} fails with '{1}'") + @MethodSource("provideTestClassesThatExpectFailure") + void runTest_failure(Class testClass, String failureMessage) { + FailureListener listener = new FailureListener(); + LauncherDiscoveryRequest request = + LauncherDiscoveryRequestBuilder.request() + .selectors(DiscoverySelectors.selectClass(testClass)) + .build(); + Launcher launcher = LauncherFactory.create(); + TestPlan testPlan = launcher.discover(request); + launcher.registerTestExecutionListeners(listener); + launcher.execute(request); + + assertThat(listener.failures).hasSize(1); + assertThat(getOnlyElement(listener.failures)).contains(failureMessage); + } + + private static Stream> provideTestClassesThatExpectSuccess() { + return stream(TestParameterInjectorJUnit5Test.class.getDeclaredClasses()) + .filter( + cls -> + cls.isAnnotationPresent(RunAsTest.class) + && cls.getAnnotation(RunAsTest.class).failsWithMessage().isEmpty()); + } + + private static Stream provideTestClassesThatExpectFailure() { + return stream(TestParameterInjectorJUnit5Test.class.getDeclaredClasses()) + .filter( + cls -> + cls.isAnnotationPresent(RunAsTest.class) + && !cls.getAnnotation(RunAsTest.class).failsWithMessage().isEmpty()) + .map(cls -> Arguments.of(cls, cls.getAnnotation(RunAsTest.class).failsWithMessage())); + } + + private static void assertNoFailures(List failures) { + if (failures.size() == 1) { + throw new AssertionError(getOnlyElement(failures)); + } else if (failures.size() > 1) { + throw new AssertionError( + String.format( + "Test failed unexpectedly:\n\n%s", + failures.stream().collect(joining("\n------------------------------------\n")))); + } + } + + private static String toCopyPastableJavaString(Map map) { + StringBuilder resultBuilder = new StringBuilder(); + resultBuilder.append("\n----------------------\n"); + resultBuilder.append("ImmutableMap.builder()\n"); + map.forEach( + (key, value) -> + resultBuilder.append(String.format(" .put(\"%s\", \"%s\")\n", key, value))); + resultBuilder.append(" .build()\n"); + resultBuilder.append("----------------------\n"); + return resultBuilder.toString(); + } + + class FailureListener implements TestExecutionListener { + final List failures = new ArrayList<>(); + + @Override + public void executionFinished( + TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { + if (testExecutionResult.getStatus() != Status.SUCCESSFUL) { + failures.add( + String.format( + "%s --> %s", + testIdentifier.getDisplayName(), + testExecutionResult.getThrowable().isPresent() + ? Throwables.getStackTraceAsString(testExecutionResult.getThrowable().get()) + : testExecutionResult)); + } + } + } + + @Retention(RUNTIME) + @interface RunAsTest { + String failsWithMessage() default ""; + } + + public enum TestEnum { + ONE, + TWO, + THREE; + } + + private static final class TestEnumValuesProvider implements TestParametersValuesProvider { + @Override + public List provideValues() { + return ImmutableList.of( + TestParametersValues.builder().name("one").addParameter("testEnum", TestEnum.ONE).build(), + TestParametersValues.builder().name("two").addParameter("testEnum", TestEnum.TWO).build(), + TestParametersValues.builder().name("null-case").addParameter("testEnum", null).build()); + } + } +} diff --git a/pom.xml b/pom.xml index b753189..c739fe2 100644 --- a/pom.xml +++ b/pom.xml @@ -21,10 +21,17 @@ 4.0.0 com.google.testparameterinjector - test-parameter-injector + test-parameter-injector-parent HEAD-SNAPSHOT - TestParameterInjector + pom + + + junit4 + junit5 + + + TestParameterInjector parent project for internal use A simple yet powerful parameterized test runner. @@ -137,11 +144,6 @@ protobuf-lite 3.0.1 - - junit - junit - 4.13.2 - org.yaml snakeyaml diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java b/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java deleted file mode 100644 index ab5003e..0000000 --- a/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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> 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 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 parameter : parameters) { - List 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 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>> getIndependentParameters( - Context context); -} diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java b/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java deleted file mode 100644 index 5dc6344..0000000 --- a/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 com.google.auto.value.AutoValue; -import com.google.common.collect.ImmutableList; -import java.util.Collection; - -/** - * Value class that captures the result of a validating a single constructor or test method. - * - *

          If the validation is not validated by any processor, it will be validated using the default - * validator. If a processor validates a constructor/test method, the remaining processors will - * *not* be called. - */ -@AutoValue -abstract class ExecutableValidationResult { - - /** Returns true if the properties of the given constructor/test method were validated. */ - public abstract boolean wasValidated(); - - /** Returns the validation errors, if any. */ - public abstract ImmutableList validationErrors(); - - static ExecutableValidationResult notValidated() { - return of(/* wasValidated= */ false, /* validationErrors= */ ImmutableList.of()); - } - - static ExecutableValidationResult validated(Collection errors) { - return of(/* wasValidated= */ true, /* validationErrors= */ errors); - } - - static ExecutableValidationResult validated(Throwable error) { - return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of(error)); - } - - static ExecutableValidationResult valid() { - return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of()); - } - - private static ExecutableValidationResult of( - boolean wasValidated, Collection validationErrors) { - checkArgument(wasValidated || validationErrors.isEmpty()); - return new AutoValue_ExecutableValidationResult( - wasValidated, ImmutableList.copyOf(validationErrors)); - } -} diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java b/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java deleted file mode 100644 index f7c7cd6..0000000 --- a/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * 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 static java.util.stream.Collectors.toMap; - -import com.google.common.collect.Lists; -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 > Enum parseEnum(String str, Class enumType) { - return Enum.valueOf((Class) 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) - .supportParsedType(String.class, Float::valueOf); - - yamlValueTransformer - .ifJavaType(Double.class) - .supportParsedType(Double.class, identity()) - .supportParsedType(Integer.class, Integer::doubleValue) - .supportParsedType(Long.class, Long::doubleValue) - .supportParsedType(String.class, Double::valueOf); - - yamlValueTransformer - .ifJavaType(Enum.class) - .supportParsedType( - String.class, str -> ParameterValueParsing.parseEnum(str, javaType.getRawType())); - - yamlValueTransformer - .ifJavaType(MessageLite.class) - .supportParsedType(String.class, str -> parseTextprotoMessage(str, javaType.getRawType())) - .supportParsedType( - Map.class, - map -> - getProtoValueParser() - .parseProtobufMessage((Map) 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 -> parseYamlMapToJavaMap(map, javaType)); - - return yamlValueTransformer.transformedJavaValue(); - } - - private static Map parseYamlMapToJavaMap(Map map, TypeToken javaType) { - return map.entrySet().stream() - .collect( - toMap( - entry -> - parseYamlObjectToJavaType( - entry.getKey(), getGenericParameterType(javaType, /* parameterIndex= */ 0)), - entry -> - parseYamlObjectToJavaType( - entry.getValue(), - getGenericParameterType(javaType, /* parameterIndex= */ 1)))); - } - - 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; - } - - SupportedJavaType ifJavaType(Class 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 { - - private final Class supportedJavaType; - - private SupportedJavaType(Class supportedJavaType) { - this.supportedJavaType = supportedJavaType; - } - - @SuppressWarnings("unchecked") - SupportedJavaType supportParsedType( - Class parsedYamlType, Function transformation) { - if (Primitives.wrap(supportedJavaType).isAssignableFrom(Primitives.wrap(javaType))) { - if (Primitives.wrap(parsedYamlType).isInstance(parsedYaml)) { - checkState( - transformedJavaValue == null, - "This case is already handled. This is a bug in" - + " testparameterinjector.TestParametersMethodProcessor."); - try { - transformedJavaValue = checkNotNull(transformation.apply((ParsedYamlT) parsedYaml)); - } catch (Exception e) { - throw new IllegalArgumentException( - String.format( - "Could not map YAML value %s (class = %s) to java class %s", - parsedYaml, parsedYaml.getClass(), javaType), - e); - } - } - } - - return this; - } - } - } - - static 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/PluggableTestRunner.java b/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java deleted file mode 100644 index 1905ab1..0000000 --- a/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java +++ /dev/null @@ -1,432 +0,0 @@ -/* - * 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.Throwables; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; -import java.lang.annotation.Annotation; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.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. - * - *

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

          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 currentTestInfo = new ThreadLocal<>(); - - private TestMethodProcessorList testMethodProcessors; - - protected PluggableTestRunner(Class klass) throws InitializationError { - super(klass); - } - - /** Returns the TestMethodProcessorList to use. This is meant to be overridden by subclasses. */ - protected abstract TestMethodProcessorList createTestMethodProcessorList(); - - /** - * This method is run to perform optional additional operations on the test instance, right after - * it was created. - */ - protected void finalizeCreatedTestInstance(Object testInstance) { - // Do nothing by default - } - - /** - * If true, all test methods (across different TestMethodProcessors) will be sorted in a - * deterministic way. - * - *

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

          This should be deterministic. The order should not change, even when tests are added/removed - * or between releases. - */ - protected Stream sortTestMethods(Stream 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. - * - *

          Defaults to {@link Test}. - */ - protected ImmutableList> 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 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 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 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 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 run(PluggableTestRunner testRunner) throws Exception { - final ImmutableList.Builder 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 computeTestMethods() { - Stream 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 annotations = testInfo.getAnnotations(); - return annotations.toArray(new Annotation[0]); - } - - @Override - public T getAnnotation(final Class 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 processMethod(FrameworkMethod initialMethod) { - return getTestMethodProcessors() - .calculateTestInfos(initialMethod.getMethod(), getTestClass().getJavaClass()) - .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) { - TestInfo testInfo = ((OverriddenFrameworkMethod) frameworkMethod).getTestInfo(); - - if (testInfo.getMethod().getParameterTypes().length == 0) { - return super.methodInvoker(frameworkMethod, testObject); - } else { - List parameters = getTestMethodProcessors().getTestMethodParameters(testInfo); - return new Statement() { - @Override - public void evaluate() throws Throwable { - frameworkMethod.invokeExplosively(testObject, parameters.toArray()); - } - }; - } - } - - /** Modifies the statement with each {@link MethodRule} and {@link TestRule} */ - private Statement withRules(FrameworkMethod method, Object target, Statement statement) { - ImmutableList testRules = - Stream.of( - getInnerTestRules().stream(), - getTestRules(target).stream(), - getOuterTestRules().stream()) - .flatMap(x -> x) - .collect(toImmutableList()); - - Iterable 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 { - TestInfo testInfo = ((OverriddenFrameworkMethod) method).getTestInfo(); - Constructor constructor = getTestClass().getOnlyConstructor(); - - // Construct a test instance - Object testInstance; - if (constructor.getParameterTypes().length == 0) { - testInstance = createTest(); - } else { - List constructorParameters = - getTestMethodProcessors().getConstructorParameters(constructor, testInfo); - try { - testInstance = constructor.newInstance(constructorParameters.toArray()); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - - // Run all post processors on the newly created instance - getTestMethodProcessors().postProcessTestInstance(testInstance, testInfo); - - finalizeCreatedTestInstance(testInstance); - - return testInstance; - } - - @Override - protected final void validateZeroArgConstructor(List errorsReturned) { - ExecutableValidationResult validationResult = - getTestMethodProcessors().validateConstructor(getTestClass().getOnlyConstructor()); - - if (validationResult.wasValidated()) { - errorsReturned.addAll(validationResult.validationErrors()); - } else { - super.validateZeroArgConstructor(errorsReturned); - } - } - - @Override - protected final void validateTestMethods(List errorsReturned) { - List testMethods = - getSupportedTestAnnotations().stream() - .flatMap(annotation -> getTestClass().getAnnotatedMethods(annotation).stream()) - .collect(toImmutableList()); - for (FrameworkMethod testMethod : testMethods) { - ExecutableValidationResult validationResult = - getTestMethodProcessors() - .validateTestMethod(testMethod.getMethod(), getTestClass().getJavaClass()); - - if (Modifier.isStatic(testMethod.getMethod().getModifiers())) { - errorsReturned.add( - new Exception(String.format("Method %s() should not be static", testMethod.getName()))); - } - if (!Modifier.isPublic(testMethod.getMethod().getModifiers())) { - errorsReturned.add( - new Exception(String.format("Method %s() should be public", testMethod.getName()))); - } - - if (validationResult.wasValidated()) { - errorsReturned.addAll(validationResult.validationErrors()); - } else { - testMethod.validatePublicVoidNoArg(/* isStatic= */ false, errorsReturned); - } - } - } - - // Fix for ParentRunner bug: - // Overriding this method because a superclass (ParentRunner) is calling this in its constructor - // and then throwing an InitializationError that doesn't have any of the causes in the exception - // message. - @Override - protected final void collectInitializationErrors(List 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 annotation, boolean isStatic, List errors) { - super.validatePublicVoidNoArgMethods(annotation, isStatic, errors); - } - - private synchronized TestMethodProcessorList getTestMethodProcessors() { - if (testMethodProcessors == null) { - testMethodProcessors = createTestMethodProcessorList(); - } - return testMethodProcessors; - } - - /** {@link MethodRule} that sets up the Context for each test. */ - private static class ContextMethodRule implements MethodRule { - @Override - public Statement apply(Statement statement, FrameworkMethod method, Object o) { - return new Statement() { - @Override - public void evaluate() throws Throwable { - currentTestInfo.set(((OverriddenFrameworkMethod) method).getTestInfo()); - try { - statement.evaluate(); - } finally { - currentTestInfo.set(null); - } - } - }; - } - } - - private static Collector> 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 deleted file mode 100644 index 61cf13b..0000000 --- a/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 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 deleted file mode 100644 index 69777d2..0000000 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java +++ /dev/null @@ -1,309 +0,0 @@ -/* - * 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.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. - * - *

          See b/168325767 for the reason behind this. tl;dr the name is put into a Unix file with max - * 255 characters. The surrounding constant characters take up 31 characters. The max is reduced - * by an additional 24 characters to account for future changes. - */ - static final int MAX_TEST_NAME_LENGTH = 200; - - public abstract Method getMethod(); - - /** - * The test class that is being run. - * - *

          Note that this is not always the same as the class that declares {@link #getMethod()} - * because test methods can be inherited. - */ - public abstract Class getTestClass(); - - public final String getName() { - if (getParameters().isEmpty()) { - return getMethod().getName(); - } else { - return String.format( - "%s[%s]", - getMethod().getName(), - getParameters().stream().map(TestInfoParameter::getName).collect(joining(","))); - } - } - - abstract ImmutableList getParameters(); - - public abstract ImmutableList getAnnotations(); - - @Nullable - public final T getAnnotation(Class annotationClass) { - for (Annotation annotation : getAnnotations()) { - if (annotationClass.isInstance(annotation)) { - return annotationClass.cast(annotation); - } - } - return null; - } - - final TestInfo withExtraParameters(List parameters) { - return new AutoValue_TestInfo( - getMethod(), - getTestClass(), - ImmutableList.builder() - .addAll(this.getParameters()) - .addAll(parameters) - .build(), - getAnnotations()); - } - - final TestInfo withExtraAnnotation(Annotation annotation) { - ImmutableList newAnnotations = - ImmutableList.builder().addAll(this.getAnnotations()).add(annotation).build(); - return new AutoValue_TestInfo(getMethod(), getTestClass(), getParameters(), newAnnotations); - } - - /** - * Returns a new TestInfo instance with updated parameter names. - * - * @param parameterWithIndexToNewName A function of the parameter and its index in the {@link - * #getParameters()} list to the new name. - */ - private TestInfo withUpdatedParameterNames( - BiFunction parameterWithIndexToNewName) { - return new AutoValue_TestInfo( - getMethod(), - getTestClass(), - 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, Class testClass, String name, List annotations) { - return new AutoValue_TestInfo( - method, testClass, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations)); - } - - static TestInfo createWithoutParameters( - Method method, Class testClass, List annotations) { - return new AutoValue_TestInfo( - method, testClass, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations)); - } - - static ImmutableList shortenNamesIfNecessary(List testInfos) { - if (testInfos.stream().anyMatch(info -> info.getName().length() > MAX_TEST_NAME_LENGTH)) { - int numberOfParameters = testInfos.get(0).getParameters().size(); - - if (numberOfParameters == 0) { - return ImmutableList.copyOf(testInfos); - } else { - Set 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; - - // Subtract 4 characters to leave place for joining commas and the parameter index. - return maxLengthOfAllParameters / numberOfParameters - 4; - } - - static ImmutableList deduplicateTestNames(List 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 maybeAddTypesIfDuplicate(List testInfos) { - Multimap testNameToInfo = - MultimapBuilder.linkedHashKeys().arrayListValues().build(); - for (TestInfo testInfo : testInfos) { - testNameToInfo.put(testInfo.getName(), testInfo); - } - - return testNameToInfo.keySet().stream() - .flatMap( - testName -> { - Collection 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 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 deduplicateWithNumberPrefixes( - ImmutableList 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 Collector> 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(); - - final 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 deleted file mode 100644 index 60a01bc..0000000 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector; - -import com.google.common.base.Optional; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.util.List; - -/** - * Interface to change the list of methods used in a test. - * - *

          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 calculateTestInfos(TestInfo originalTest); - - /** - * If this processor can handle the given constructor, returns the parameters with which it should - * be invoked. - * - *

          This method is never called for a parameterless constructor. - */ - Optional> maybeGetConstructorParameters( - Constructor constructor, TestInfo testInfo); - - /** - * If this processor can handle the given test, returns the parameters with which {@code - * testInfo.getMethod()} should be invoked. - * - *

          This method is never called for a parameterless {@code testInfo.getMethod()}. - */ - Optional> maybeGetTestMethodParameters(TestInfo testInfo); - - /** - * Optionally process the test instance right after construction to ready it for the given test - * instance. - */ - void postProcessTestInstance(Object testInstance, TestInfo testInfo); - - /** Optionally validates the given constructor. */ - ExecutableValidationResult validateConstructor(Constructor constructor); - - /** - * Optionally validates the given method. - * - *

          Note that the given method is not necessarily declared in the given class because test - * methods can be inherited. - */ - ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass); -} diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java deleted file mode 100644 index 867d994..0000000 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * 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.stream.Collectors.toList; - -import com.google.common.base.Optional; -import com.google.common.collect.ImmutableList; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.util.List; - -/** - * Combined version of all {@link TestMethodProcessor} implementations that this package supports. - */ -final class TestMethodProcessorList { - - private final ImmutableList testMethodProcessors; - - private TestMethodProcessorList(ImmutableList testMethodProcessors) { - this.testMethodProcessors = testMethodProcessors; - } - - /** - * Returns a TestMethodProcessorList that supports all features that this package supports, except - * the following legacy features: - * - *

            - *
          • No support for {@link org.junit.runners.Parameterized} - *
          • No support for class and method-level parameters, except for @TestParameters - *
          - */ - public static TestMethodProcessorList createNewParameterizedProcessors() { - return new TestMethodProcessorList( - ImmutableList.of( - new TestParametersMethodProcessor(), - TestParameterAnnotationMethodProcessor.onlyForFieldsAndParameters())); - } - - static TestMethodProcessorList empty() { - return new TestMethodProcessorList(ImmutableList.of()); - } - - /** - * Calculates the TestInfo instances for the given test method. Each TestInfo corresponds to a - * single test. - * - *

          The returned list always contains at least one element. If there is no parameterization, - * this would be the TestInfo for running the test method without parameters. - */ - public List calculateTestInfos(Method testMethod, Class testClass) { - List testInfos = - ImmutableList.of( - TestInfo.createWithoutParameters( - testMethod, testClass, ImmutableList.copyOf(testMethod.getAnnotations()))); - - for (final TestMethodProcessor testMethodProcessor : testMethodProcessors) { - testInfos = - testInfos.stream() - .flatMap( - lastTestInfo -> testMethodProcessor.calculateTestInfos(lastTestInfo).stream()) - .collect(toList()); - } - - testInfos = TestInfo.deduplicateTestNames(TestInfo.shortenNamesIfNecessary(testInfos)); - - return testInfos; - } - - /** - * Returns the parameters with which it should be invoked. - * - *

          This method is never called for a parameterless constructor. - */ - public List getConstructorParameters(Constructor constructor, TestInfo testInfo) { - return testMethodProcessors.stream() - .map(processor -> processor.maybeGetConstructorParameters(constructor, testInfo)) - .filter(Optional::isPresent) - .map(Optional::get) - .findFirst() - .orElseThrow( - () -> - new IllegalStateException( - String.format( - "Could not generate parameter values for %s. Did you forget an annotation?", - constructor))); - } - - /** - * Returns the parameters with which {@code testInfo.getMethod()} should be invoked. - * - *

          This method is never called for a parameterless {@code testInfo.getMethod()}. - */ - public List getTestMethodParameters(TestInfo testInfo) { - return testMethodProcessors.stream() - .map(processor -> processor.maybeGetTestMethodParameters(testInfo)) - .filter(Optional::isPresent) - .map(Optional::get) - .findFirst() - .orElseThrow( - () -> - new IllegalStateException( - String.format( - "Could not generate parameter values for %s. Did you forget an annotation?", - testInfo.getMethod()))); - } - - /** - * Optionally process the test instance right after construction to ready it for the given test. - */ - public void postProcessTestInstance(Object testInstance, TestInfo testInfo) { - for (TestMethodProcessor testMethodProcessor : testMethodProcessors) { - testMethodProcessor.postProcessTestInstance(testInstance, testInfo); - } - } - - /** Optionally validates the given constructor. */ - public ExecutableValidationResult validateConstructor(Constructor constructor) { - return testMethodProcessors.stream() - .map(processor -> processor.validateConstructor(constructor)) - .filter(ExecutableValidationResult::wasValidated) - .findFirst() - .orElse(ExecutableValidationResult.notValidated()); - } - - /** Optionally validates the given method. */ - public ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass) { - return testMethodProcessors.stream() - .map(processor -> processor.validateTestMethod(testMethod, testClass)) - .filter(ExecutableValidationResult::wasValidated) - .findFirst() - .orElse(ExecutableValidationResult.notValidated()); - } -} diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java deleted file mode 100644 index 6725d16..0000000 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * 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. - * - *

          For enums and booleans, the values can be automatically derived as all possible values: - * - *

          - * {@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 }
          - * 
          - * - *

          The values can be explicitly defined as a parsed string: - * - *

          - * 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)]
          - * }
          - * 
          - * - *

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

          Types that are supported: - * - *

            - *
          • String: No parsing happens - *
          • boolean: Specified as YAML boolean - *
          • long and int: Specified as YAML integer - *
          • float and double: Specified as YAML floating point or integer - *
          • Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()} - *
          • Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML bytes - * (example: "!!binary 'ZGF0YQ=='") - *
          - * - *

          For dynamic sets of parameters or parameter types that are not supported here, use {@link - * #valuesProvider()} and leave this field empty. - * - *

          For examples, see {@link TestParameter}. - */ - String[] value() default {}; - - /** - * Sets a provider that will return a list of parameter values. - * - *

          If this field is set, {@link #value()} must be empty and vice versa. - * - *

          Example - * - *

          -   * {@literal @}Test
          -   * public void matchesAllOf_throwsOnNull(
          -   *     {@literal @}TestParameter(valuesProvider = CharMatcherProvider.class)
          -   *         CharMatcher charMatcher) {
          -   *   assertThrows(NullPointerException.class, () -> charMatcher.matchesAllOf(null));
          -   * }
          -   *
          -   * private static final class CharMatcherProvider implements TestParameterValuesProvider {
          -   *   {@literal @}Override
          -   *   public {@literal List} provideValues() {
          -   *     return ImmutableList.of(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace());
          -   *   }
          -   * }
          -   * 
          - */ - Class 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 provideValues() { - return ImmutableList.of(); - } - } - - /** Implementation of this parameter annotation. */ - final class InternalImplementationOfThisParameter implements TestParameterValueProvider { - @Override - public List provideValues( - Annotation uncastAnnotation, Optional> 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 annotationType, Optional> 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 getValuesFromProvider( - Class valuesProvider) { - try { - Constructor 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 deleted file mode 100644 index 8c04bc0..0000000 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * 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. - * - *

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

          {@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();
          - *     }
          - * }
          - * }
          - * - *

          An alternative is to use a method parameter for injection: - * - *

          {@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();
          - *     }
          - * }
          - * }
          - * - *

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

          {@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();
          - *     }
          - * }
          - * }
          - * - *

          Class constructors can also be annotated with @TestParameterAnnotation annotations, as shown - * below: - * - *

          {@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() {...}
          - * }
          - * }
          - * - *

          Each field that needs to be injected from a parameter requires its dedicated distinct - * annotation. - * - *

          If the same annotation is defined both on the class and method, the method parameter values - * take precedence. - * - *

          If the same annotation is defined both on the class and constructor, the constructor parameter - * values take precedence. - * - *

          Annotations cannot be duplicated between the constructor or constructor parameters and a - * method or method parameter. - * - *

          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 validator() default DefaultValidator.class; - - /** Specifies a value provider for the parameter to provide the values to test. */ - Class valueProvider() default DefaultValueProvider.class; - - /** Default {@link TestParameterValidator} implementation which skips no test. */ - class DefaultValidator implements TestParameterValidator { - - @Override - public boolean shouldSkip(Context context) { - return false; - } - } - - /** - * Default {@link TestParameterValueProvider} implementation that gets its values from the - * annotation's `value` method. - */ - class DefaultValueProvider implements TestParameterValueProvider { - - @Override - public List provideValues(Annotation annotation, Optional> 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 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 annotationType, Optional> 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 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 deleted file mode 100644 index b2be9b6..0000000 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ /dev/null @@ -1,1290 +0,0 @@ -/* - * 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.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -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; - -/** - * {@code TestMethodProcessor} implementation for supporting parameterized tests annotated with - * {@link TestParameterAnnotation}. - * - * @see TestParameterAnnotation - */ -final class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { - - /** - * Class to hold an annotation type and origin and one of the values as returned by the {@code - * value()} method. - */ - @AutoValue - abstract static class 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 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> 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 paramName(); - - /** - * Returns a String that represents this value and is fit for use in a test name (between - * brackets). - */ - String toTestNameString() { - Class 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 create( - AnnotationWithMetadata annotationWithMetadata, Origin origin) { - List 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(/* onlyForFieldsAndParameters= */ false) - .getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()).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 getTestParameterValue( - TestInfo testInfo, Class annotationType) { - return getTestParameterValues(testInfo).getValue(annotationType); - } - - private static List getParametersAnnotationValues( - AnnotationWithMetadata annotationWithMetadata) { - Annotation annotation = annotationWithMetadata.annotation(); - TestParameterAnnotation testParameter = - annotation.annotationType().getAnnotation(TestParameterAnnotation.class); - Class 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 matches(Class 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 annotationType(); - - /** Where the annotation was declared. */ - abstract Origin origin(); - - public static AnnotationTypeOrigin create( - Class 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> 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 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 boolean onlyForFieldsAndParameters; - private final LoadingCache, ImmutableList> - annotationTypeOriginsCache = - CacheBuilder.newBuilder() - .maximumSize(1000) - .build(CacheLoader.from(this::calculateAnnotationTypeOrigins)); - private final Cache>> parameterValuesCache = - CacheBuilder.newBuilder().maximumSize(1000).build(); - - private TestParameterAnnotationMethodProcessor(boolean onlyForFieldsAndParameters) { - this.onlyForFieldsAndParameters = onlyForFieldsAndParameters; - } - - /** - * Constructs a new {@link TestMethodProcessor} that handles {@link - * TestParameterAnnotation}-annotated annotations that are placed anywhere: - * - *
            - *
          • At a method / constructor parameter - *
          • At a field - *
          • At a method / constructor on the class - *
          • At the test class - *
          - */ - static TestMethodProcessor forAllAnnotationPlacements() { - return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ false); - } - - /** - * Constructs a new {@link TestMethodProcessor} that handles {@link - * TestParameterAnnotation}-annotated annotations that are placed at fields or parameters. - * - *

          Note that this excludes class and method-level annotations, as is the default (using the - * constructor). - */ - static TestMethodProcessor onlyForFieldsAndParameters() { - return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ true); - } - - private ImmutableList calculateAnnotationTypeOrigins(Class testClass) { - // Collect all annotations used in declared fields and methods that have themselves a - // @TestParameterAnnotation annotation. - List fieldAnnotations = - extractTestParameterAnnotations( - streamWithParents(testClass) - .flatMap(c -> stream(c.getDeclaredFields())) - .flatMap(field -> stream(field.getAnnotations())), - Origin.FIELD); - List methodAnnotations = - extractTestParameterAnnotations( - stream(testClass.getMethods()).flatMap(method -> stream(method.getAnnotations())), - Origin.METHOD); - List parameterAnnotations = - extractTestParameterAnnotations( - streamWithParents(testClass) - .flatMap(c -> stream(c.getDeclaredMethods())) - .flatMap(method -> stream(method.getParameterAnnotations()).flatMap(Stream::of)), - Origin.METHOD_PARAMETER); - List classAnnotations = - extractTestParameterAnnotations(stream(testClass.getAnnotations()), Origin.CLASS); - List constructorAnnotations = - extractTestParameterAnnotations( - stream(testClass.getDeclaredConstructors()) - .flatMap(constructor -> stream(constructor.getAnnotations())), - Origin.CONSTRUCTOR); - List constructorParameterAnnotations = - extractTestParameterAnnotations( - stream(testClass.getDeclaredConstructors()) - .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); - } - - return 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()); - } - - private ImmutableList getAnnotationTypeOrigins( - Class testClass, Origin firstOrigin, Origin... otherOrigins) { - Set originsToFilterBy = - ImmutableSet.builder().add(firstOrigin).add(otherOrigins).build(); - try { - return annotationTypeOriginsCache.getUnchecked(testClass).stream() - .filter(annotationTypeOrigin -> originsToFilterBy.contains(annotationTypeOrigin.origin())) - .collect(toImmutableList()); - } catch (UncheckedExecutionException e) { - Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); - throw e; - } - } - - private void checkDuplicatedFieldsAnnotations( - List methodAnnotations, List 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> 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 constructorAnnotations, - List classAnnotations, - List fieldAnnotations) { - ImmutableSet> classAnnotationTypes = - classAnnotations.stream() - .map(AnnotationTypeOrigin::annotationType) - .collect(toImmutableSet()); - - ImmutableSet> uniqueFieldAnnotations = - fieldAnnotations.stream() - .map(AnnotationTypeOrigin::annotationType) - .collect(toImmutableSet()); - ImmutableSet> 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 extractTestParameterAnnotations( - Stream 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 ExecutableValidationResult validateConstructor(Constructor constructor) { - Class[] parameterTypes = constructor.getParameterTypes(); - if (parameterTypes.length == 0) { - return ExecutableValidationResult.notValidated(); - } - // The constructor has parameters, they must be injected by a TestParameterAnnotation - // annotation. - Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); - Class testClass = constructor.getDeclaringClass(); - return ExecutableValidationResult.validated( - validateMethodOrConstructorParameters( - removeOverrides( - getAnnotationTypeOrigins( - testClass, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER), - testClass), - testClass, - constructor, - parameterTypes, - parameterAnnotations)); - } - - @Override - public ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass) { - Class[] methodParameterTypes = testMethod.getParameterTypes(); - if (methodParameterTypes.length == 0) { - return ExecutableValidationResult.notValidated(); - } else { - // The method has parameters, they must be injected by a TestParameterAnnotation annotation. - return ExecutableValidationResult.validated( - validateMethodOrConstructorParameters( - getAnnotationTypeOrigins( - testClass, Origin.CLASS, Origin.METHOD, Origin.METHOD_PARAMETER), - testClass, - testMethod, - methodParameterTypes, - testMethod.getParameterAnnotations())); - } - } - - private List validateMethodOrConstructorParameters( - List annotationTypeOrigins, - Class testClass, - AnnotatedElement methodOrConstructor, - Class[] parameterTypes, - Annotation[][] parametersAnnotations) { - List errors = new ArrayList<>(); - - for (int parameterIndex = 0; parameterIndex < parameterTypes.length; parameterIndex++) { - Class parameterType = parameterTypes[parameterIndex]; - Annotation[] parameterAnnotations = parametersAnnotations[parameterIndex]; - boolean matchingTestParameterAnnotationFound = false; - // First, handle the case where the method parameter specifies the test parameter explicitly, - // e.g. {@code public void test(@ColorParameter({...}) Color c)}. - for (AnnotationTypeOrigin testParameterAnnotationType : annotationTypeOrigins) { - for (Annotation parameterAnnotation : parameterAnnotations) { - if (parameterAnnotation - .annotationType() - .equals(testParameterAnnotationType.annotationType())) { - // Verify that the type is assignable with the return type of the 'value' method. - Class valueMethodReturnType = - getValueMethodReturnType( - testParameterAnnotationType.annotationType(), - /* paramClass = */ Optional.of(parameterType)); - if (!parameterType.isAssignableFrom(valueMethodReturnType)) { - errors.add( - new IllegalStateException( - String.format( - "Parameter of type %s annotated with %s does not match" - + " expected type %s in method/constructor %s", - parameterType.getName(), - testParameterAnnotationType.annotationType().getName(), - valueMethodReturnType.getName(), - methodOrConstructor))); - } else { - matchingTestParameterAnnotationFound = true; - } - } - } - } - // Second, handle the case where the method parameter does not specify the test parameter, - // and instead relies on the type matching, e.g. {@code public void test(Color c)}. - if (!matchingTestParameterAnnotationFound) { - List> testParameterAnnotationTypes = - getTestParameterAnnotations( - // Do not include METHOD_PARAMETER or CONSTRUCTOR_PARAMETER since they have already - // been evaluated. - filterAnnotationTypeOriginsByOrigin( - annotationTypeOrigins, Origin.CLASS, Origin.CONSTRUCTOR, Origin.METHOD), - testClass, - methodOrConstructor); - // If no annotation is present, simply compare the type. - for (Class testParameterAnnotationType : - testParameterAnnotationTypes) { - if (parameterType.isAssignableFrom( - getValueMethodReturnType( - testParameterAnnotationType, /* paramClass = */ Optional.absent()))) { - if (matchingTestParameterAnnotationFound) { - errors.add( - new IllegalStateException( - String.format( - "Ambiguous method/constructor parameter type, matching multiple" - + " annotations for parameter of type %s in method %s", - parameterType.getName(), methodOrConstructor))); - } - matchingTestParameterAnnotationFound = true; - } - } - } - if (!matchingTestParameterAnnotationFound) { - errors.add( - new IllegalStateException( - String.format( - "No matching test parameter annotation found" - + " for parameter of type %s in method/constructor %s", - parameterType.getName(), methodOrConstructor))); - } - } - return errors; - } - - @Override - public Optional> maybeGetConstructorParameters( - Constructor constructor, TestInfo testInfo) { - if (testInfo.getAnnotation(TestIndexHolder.class) == null - // Explicitly skip @TestParameters annotated methods to ensure compatibility. - // - // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported) - // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;). - // Without the @TestParameters check below, this class would try to find parameters for - // these methods. When there are no method parameters, this is a no-op, but when the method - // is annotated with @TestParameters, this throws an exception (because there are method - // parameters that this processor has no values for - they are provided by the - // @TestParameters processor). - || constructor.isAnnotationPresent(TestParameters.class)) { - return Optional.absent(); - } else { - TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); - List testParameterValues = - getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()); - - Class[] parameterTypes = constructor.getParameterTypes(); - Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); - List parameterValues = new ArrayList<>(/* initialCapacity= */ parameterTypes.length); - List> processedAnnotationTypes = new ArrayList<>(); - List parameterValuesForConstructor = - filterByOrigin( - testParameterValues, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER); - for (int i = 0; i < parameterTypes.length; i++) { - // Initialize each parameter value from the corresponding TestParameterAnnotation value. - parameterValues.add( - getParameterValue( - parameterValuesForConstructor, - parameterTypes[i], - parameterAnnotations[i], - processedAnnotationTypes)); - } - return Optional.of(parameterValues); - } - } - - @Override - public Optional> maybeGetTestMethodParameters(TestInfo testInfo) { - Method testMethod = testInfo.getMethod(); - if (testInfo.getAnnotation(TestIndexHolder.class) == null - // Explicitly skip @TestParameters annotated methods to ensure compatibility. - // - // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported) - // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;). - // Without the @TestParameters check below, this class would try to find parameters for - // these methods. When there are no method parameters, this is a no-op, but when the method - // is annotated with @TestParameters, this throws an exception (because there are method - // parameters that this processor has no values for - they are provided by the - // @TestParameters processor). - || testMethod.isAnnotationPresent(TestParameters.class)) { - return Optional.absent(); - } else { - TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); - checkState(testIndexHolder != null); - List testParameterValues = - filterByOrigin( - getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()), - Origin.CLASS, - Origin.METHOD, - Origin.METHOD_PARAMETER); - - Class[] parameterTypes = testMethod.getParameterTypes(); - Annotation[][] parametersAnnotations = testMethod.getParameterAnnotations(); - ArrayList parameterValues = - new ArrayList<>(/* initialCapacity= */ parameterTypes.length); - - List> processedAnnotationTypes = new ArrayList<>(); - for (int i = 0; i < parameterTypes.length; i++) { - parameterValues.add( - getParameterValue( - testParameterValues, - parameterTypes[i], - parametersAnnotations[i], - processedAnnotationTypes)); - } - - return Optional.of(parameterValues); - } - } - - /** - * Returns the {@link TestInfo}, one for each result of the cartesian product of each test - * parameter values. - * - *

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

          For multiple annotations (say, {@code @TestParameter("foo", "bar")} and - * {@code @ColorParameter({BLUE, WHITE})}), it will generate the following result: - * - *

            - *
          • ("foo", BLUE) - *
          • ("foo", WHITE) - *
          • ("bar", BLUE) - *
          • ("bar", WHITE) - *
          • - *
          - * - * corresponding to the cartesian product of both annotations. - */ - @Override - public List calculateTestInfos(TestInfo originalTest) { - List> parameterValuesForMethod = - getParameterValuesForMethod(originalTest.getMethod(), originalTest.getTestClass()); - - if (parameterValuesForMethod.equals(ImmutableList.of(ImmutableList.of()))) { - // This test is not parameterized - return ImmutableList.of(originalTest); - } - - ImmutableList.Builder testInfos = ImmutableList.builder(); - for (int parametersIndex = 0; - parametersIndex < parameterValuesForMethod.size(); - ++parametersIndex) { - List 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(originalTest.getTestClass()), - originalTest.getMethod()), - parametersIndex, - originalTest.getTestClass().getName()))); - } - - return testInfos.build(); - } - - private List> getParameterValuesForMethod( - Method method, Class testClass) { - try { - return parameterValuesCache.get( - method, - () -> { - List> testParameterValuesList = - getAnnotationValuesForUsedAnnotationTypes(method, testClass); - - 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 getParameterValuesForTest( - TestIndexHolder testIndexHolder, Class testClass) { - verify( - testIndexHolder.testClassName().equals(testClass.getName()), - "The class for which the given annotation was created (%s) is not the same as the test" - + " class that this runner is handling (%s)", - testIndexHolder.testClassName(), - testClass.getName()); - Method testMethod = getMethodsIncludingParents(testClass).get(testIndexHolder.methodIndex()); - return getParameterValuesForMethod(testMethod, testClass) - .get(testIndexHolder.parametersIndex()); - } - - /** - * Returns the list of annotation index for all annotations defined in a given test method and its - * class. - */ - private ImmutableList> getAnnotationValuesForUsedAnnotationTypes( - Method method, Class testClass) { - ImmutableList annotationTypes = - Stream.of( - getAnnotationTypeOrigins(testClass, Origin.CLASS).stream(), - getAnnotationTypeOrigins(testClass, Origin.FIELD).stream(), - getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR).stream(), - getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR_PARAMETER).stream(), - getAnnotationTypeOrigins(testClass, Origin.METHOD).stream(), - getAnnotationTypeOrigins(testClass, 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 annotationComparator( - Annotation[][] parameterAnnotations) { - ImmutableList 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}. - * - *

          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 removeOverrides( - List 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 removeOverrides( - List 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(); - default: - return true; - } - }) - .collect(toCollection(ArrayList::new)); - } - - /** - * Returns the given annotations defined either on the method parameters, method or the test - * class. - * - *

          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> getAnnotationFromParametersOrTestOrClass( - AnnotationTypeOrigin annotationTypeOrigin, Method method, Class testClass) { - Origin origin = annotationTypeOrigin.origin(); - Class annotationType = annotationTypeOrigin.annotationType(); - if (origin == Origin.CONSTRUCTOR_PARAMETER) { - Constructor constructor = getOnlyConstructor(testClass); - List 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 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 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> toTestParameterValueList( - List annotationWithMetadatas, Origin origin) { - return annotationWithMetadatas.stream() - .map(annotationWithMetadata -> TestParameterValue.create(annotationWithMetadata, origin)) - .collect(toImmutableList()); - } - - private static ImmutableList getAnnotationWithMetadataListWithType( - Method callable, Class annotationType) { - try { - return getAnnotationWithMetadataListWithType(callable.getParameters(), annotationType); - } catch (NoSuchMethodError ignored) { - return getAnnotationWithMetadataListWithType( - callable.getParameterTypes(), callable.getParameterAnnotations(), annotationType); - } - } - - private static ImmutableList getAnnotationWithMetadataListWithType( - Constructor callable, Class 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 getAnnotationWithMetadataListWithType( - Parameter[] parameters, Class 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 getAnnotationWithMetadataListWithType( - Class[] parameterTypes, - Annotation[][] annotations, - Class annotationType) { - checkArgument(parameterTypes.length == annotations.length); - - ImmutableList.Builder 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 getAnnotationListWithType( - Annotation[] annotations, Class annotationType) { - return stream(annotations) - .filter(annotation -> annotation.annotationType().equals(annotationType)) - .collect(toImmutableList()); - } - - private static Constructor getOnlyConstructor(Class testClass) { - Constructor[] constructors = testClass.getDeclaredConstructors(); - checkState( - constructors.length == 1, - "a single public constructor is required for class %s", - testClass); - return constructors[0]; - } - - @Override - public void postProcessTestInstance(Object testInstance, TestInfo testInfo) { - TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); - try { - if (testIndexHolder != null) { - List testParameterValues = - getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()); - - // Do not include {@link Origin#METHOD_PARAMETER} nor {@link Origin#CONSTRUCTOR_PARAMETER} - // annotations. - List testParameterValuesForFieldInjection = - filterByOrigin(testParameterValues, Origin.CLASS, Origin.FIELD, Origin.METHOD); - // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class - // in the example above. - List remainingTestParameterValuesForFieldInjection = - new ArrayList<>(testParameterValuesForFieldInjection); - for (Field declaredField : - streamWithParents(testInstance.getClass()) - .flatMap(c -> stream(c.getDeclaredFields())) - .collect(toImmutableList())) { - for (TestParameterValue testParameterValue : - remainingTestParameterValuesForFieldInjection) { - if (declaredField.isAnnotationPresent( - testParameterValue.annotationTypeOrigin().annotationType())) { - declaredField.setAccessible(true); - declaredField.set(testInstance, testParameterValue.value()); - remainingTestParameterValuesForFieldInjection.remove(testParameterValue); - break; - } - } - } - } - } catch (IllegalAccessException 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 filterByOrigin( - List testParameterValues, Origin... origins) { - Set 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 filterAnnotationTypeOriginsByOrigin( - List annotationTypeOrigins, Origin... origins) { - List originList = Arrays.asList(origins); - return annotationTypeOrigins.stream() - .filter(annotationTypeOrigin -> originList.contains(annotationTypeOrigin.origin())) - .collect(toImmutableList()); - } - - /** Returns a {@link TestParameterAnnotation}'s value for a method or constructor parameter. */ - private Object getParameterValue( - List testParameterValues, - Class methodParameterType, - Annotation[] parameterAnnotations, - List> processedAnnotationTypes) { - List> 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 annotationType = - testParameterValue.annotationTypeOrigin().annotationType(); - if (parameterAnnotation.annotationType().equals(annotationType)) { - // If multiple annotations exist, ensure that the proper one is selected. - // For instance, for: - // - // test(@FooParameter(1,2) Foo foo, @FooParameter(3,4) Foo bar) {} - // - // 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}. - */ - int parametersIndex(); - - /** - * The full name of the test class. Only used for verifying that assumptions about the above - * indices are valid. - */ - String testClassName(); - } - - /** Factory for {@link TestIndexHolder}. */ - static class TestIndexHolderFactory { - @AutoAnnotation - static TestIndexHolder create(int methodIndex, int parametersIndex, String testClassName) { - return new AutoAnnotation_TestParameterAnnotationMethodProcessor_TestIndexHolderFactory_create( - methodIndex, parametersIndex, testClassName); - } - - private TestIndexHolderFactory() {} - } - - /** - * Returns whether the test should be skipped according to the {@code annotationType}'s {@link - * TestParameterValidator} and the current list of {@link TestParameterValue}. - */ - private static boolean callShouldSkip( - Class annotationType, List testParameterValues) { - TestParameterAnnotation annotation = - annotationType.getAnnotation(TestParameterAnnotation.class); - Class 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 testParameterValues; - private final Set valueList; - - public ValidatorContext(List testParameterValues) { - this.testParameterValues = testParameterValues; - this.valueList = testParameterValues.stream().map(TestParameterValue::value).collect(toSet()); - } - - @Override - public boolean has(Class testParameter, Object value) { - return getValue(testParameter).transform(value::equals).or(false); - } - - @Override - public , U extends Enum> boolean has(T value1, U value2) { - return valueList.contains(value1) && valueList.contains(value2); - } - - @Override - public Optional getValue(Class testParameter) { - return getParameter(testParameter).transform(TestParameterValue::value); - } - - @Override - public List getSpecifiedValues(Class testParameter) { - return getParameter(testParameter) - .transform(TestParameterValue::specifiedValues) - .or(ImmutableList.of()); - } - - private Optional getParameter(Class 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 annotationType, Optional> paramClass) { - TestParameterAnnotation testParameter = - annotationType.getAnnotation(TestParameterAnnotation.class); - Class 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> getTestParameterAnnotations( - List annotationTypeOrigins, - final Class testClass, - AnnotatedElement methodOrConstructor) { - return annotationTypeOrigins.stream() - .map(AnnotationTypeOrigin::annotationType) - .filter( - annotationType -> - testClass.isAnnotationPresent(annotationType) - || methodOrConstructor.isAnnotationPresent(annotationType)) - .collect(toImmutableList()); - } - - private int strictIndexOf(List haystack, T needle) { - int index = haystack.indexOf(needle); - checkArgument(index >= 0, "Could not find '%s' in %s", needle, haystack); - return index; - } - - private ImmutableList getMethodsIncludingParents(Class clazz) { - ImmutableList.Builder resultBuilder = ImmutableList.builder(); - while (clazz != null) { - resultBuilder.add(clazz.getDeclaredMethods()); - clazz = clazz.getSuperclass(); - } - return resultBuilder.build(); - } - - private static Stream> streamWithParents(Class clazz) { - Stream.Builder> 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 Collector> toImmutableList() { - return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf); - } - - private static Collector> 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 deleted file mode 100644 index 537969a..0000000 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector; - -import org.junit.runners.model.InitializationError; - -/** - * A 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. - */ -public final class TestParameterInjector extends PluggableTestRunner { - - public TestParameterInjector(Class testClass) throws InitializationError { - super(testClass); - } - - @Override - protected TestMethodProcessorList createTestMethodProcessorList() { - return TestMethodProcessorList.createNewParameterizedProcessors(); - } -} diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java deleted file mode 100644 index 3733833..0000000 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 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. - */ - , U extends Enum> boolean has(T value1, U value2); - - /** - * Returns all the current test value for a given {@link TestParameterAnnotation} annotated - * annotation. - */ - Optional getValue(Class testParameter); - - /** - * Returns all the values specified for a given {@link TestParameterAnnotation} annotated - * annotation in the test. - * - *

          For example, if the test annotates '@Foo(a,b,c)', getSpecifiedValues(Foo.class) will - * return [a,b,c]. - */ - List getSpecifiedValues(Class testParameter); - } - - /** - * Returns whether the test should be skipped based on the annotations' values. - * - *

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

          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 deleted file mode 100644 index 6c398aa..0000000 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 provideValues(Annotation annotation, Optional> 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 annotationType, Optional> 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 deleted file mode 100644 index 5207ec6..0000000 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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 getValue(Class 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 deleted file mode 100644 index 9b1c5c9..0000000 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * 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.Repeatable; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import javax.annotation.Nullable; - -/** - * Annotation that can be placed (repeatedly) on @Test-methods or a test constructor to indicate the - * sets of parameters that it should be invoked with. - * - *

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

          Note: If this annotation is used in a test class, the other methods in that class can still - * use other types of parameterization, such as {@linkplain TestParameter @TestParameter}. - * - *

          See {@link #value()} for simple examples. - * - *

          Warning: This annotation can only be used if the compiled java code contains the parameter - * names. This is typically done by passing the {@code -parameters} option to the Java compiler, - * which requires using Java 8 or higher and may not be available on Android. - */ -@Retention(RUNTIME) -@Target({CONSTRUCTOR, METHOD}) -@Repeatable(TestParameters.RepeatedTestParameters.class) -public @interface TestParameters { - - /** - * Specifies one or more stringified sets of parameters in YAML format. Each set corresponds to a - * single invocation of a test method. - * - *

          Each element in this array is a full parameter set, formatted as a YAML mapping. The mapping - * keys must match the parameter names and the mapping values will be converted to the parameter - * type if possible. See yaml.org for the YAML syntax and the section below on the supported - * parameter types. - * - *

          There are two distinct ways of using this annotation: repeated vs single: - * - *

          Recommended usage: Separate annotation per parameter set - * - *

          This approach uses multiple @TestParameters annotations, one for each set of parameters, for - * example: - * - *

          -   * {@literal @}Test
          -   * {@literal @}TestParameters("{age: 17, expectIsAdult: false}")
          -   * {@literal @}TestParameters("{age: 22, expectIsAdult: true}")
          -   * public void personIsAdult(int age, boolean expectIsAdult) { ... }
          -   *
          -   * {@literal @}Test
          -   * {@literal @}TestParameters("{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}")
          -   * {@literal @}TestParameters("{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}")
          -   * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... }
          -   * 
          - * - *

          Old discouraged usage: Single annotation with all parameter sets - * - *

          This approach uses a single @TestParameter annotation for all parameter sets, for example: - * - *

          -   * {@literal @}Test
          -   * {@literal @}TestParameters({
          -   *   "{age: 17, expectIsAdult: false}",
          -   *   "{age: 22, expectIsAdult: true}",
          -   * })
          -   * public void personIsAdult(int age, boolean expectIsAdult) { ... }
          -   *
          -   * {@literal @}Test
          -   * {@literal @}TestParameters({
          -   *   "{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}",
          -   *   "{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}",
          -   * })
          -   * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... }
          -   * 
          - * - *

          Supported parameter types - * - *

            - *
          • YAML primitives: - *
              - *
            • String: Specified as YAML string - *
            • boolean: Specified as YAML boolean - *
            • long and int: Specified as YAML integer - *
            • float and double: Specified as YAML floating point or integer - *
            - *
          • - *
          • Parsed types: - *
              - *
            • Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()} - *
            • Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML - * bytes (example: "!!binary 'ZGF0YQ=='") - *
            - *
          • - *
          - * - *

          For dynamic sets of parameters or parameter types that are not supported here, use {@link - * #valuesProvider()} and leave this field empty. - */ - String[] value() default {}; - - /** - * Overrides the name of the parameter set that is used in the test name. - * - *

          This can only be set if {@link #value()} has exactly one element. If not set, the YAML - * string in {@link #value()} is used in the test name. - * - *

          For example: If this name is set to "young adult", then the test name might be - * "personIsAdult[young adult]" where the default might have been "personIsAdult[{age: 17, - * expectIsAdult: false}]". - */ - String customName() default ""; - - /** - * Sets a provider that will return a list of parameter sets. Each element in the returned list - * corresponds to a single invocation of a test method. - * - *

          If this field is set, {@link #value()} must be empty and vice versa. - * - *

          Example - * - *

          -   * {@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} 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()
          -   *     );
          -   *   }
          -   * }
          -   * 
          - */ - Class valuesProvider() default - DefaultTestParametersValuesProvider.class; - - /** Interface for custom providers of test parameter values. */ - interface TestParametersValuesProvider { - List 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. - * - *

          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 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 parametersMap = new LinkedHashMap<>(); - - /** - * Sets a name for this set of parameters that will be used for describing this test. - * - *

          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 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 provideValues() { - return ImmutableList.of(); - } - } - - /** - * Holder annotation for multiple @TestParameters annotations. This should never be used directly. - */ - @Retention(RUNTIME) - @Target({CONSTRUCTOR, METHOD}) - @interface RepeatedTestParameters { - TestParameters[] value(); - } -} diff --git a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java deleted file mode 100644 index bffc3b4..0000000 --- a/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java +++ /dev/null @@ -1,485 +0,0 @@ -/* - * 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 com.google.common.collect.Iterables.getOnlyElement; -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toList; - -import com.google.auto.value.AutoAnnotation; -import com.google.common.base.Optional; -import com.google.common.base.Throwables; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Maps; -import com.google.common.primitives.Primitives; -import com.google.common.reflect.TypeToken; -import com.google.common.util.concurrent.UncheckedExecutionException; -import com.google.testing.junit.testparameterinjector.TestInfo.TestInfoParameter; -import com.google.testing.junit.testparameterinjector.TestParameters.DefaultTestParametersValuesProvider; -import com.google.testing.junit.testparameterinjector.TestParameters.RepeatedTestParameters; -import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValues; -import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValuesProvider; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.reflect.Constructor; -import java.lang.reflect.Executable; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.Parameter; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collector; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** {@code TestMethodProcessor} implementation for supporting {@link TestParameters}. */ -@SuppressWarnings("AndroidJdkLibsChecker") // Parameter is not available on old Android SDKs. -final class TestParametersMethodProcessor implements TestMethodProcessor { - - private final LoadingCache> - parameterValuesByConstructorOrMethodCache = - CacheBuilder.newBuilder() - .maximumSize(1000) - .build(CacheLoader.from(TestParametersMethodProcessor::toParameterValuesList)); - - @Override - public ExecutableValidationResult validateConstructor(Constructor constructor) { - if (hasRelevantAnnotation(constructor)) { - try { - // This method throws an exception if there is a validation error - getConstructorParameters(constructor); - } catch (Throwable t) { - return ExecutableValidationResult.validated(t); - } - return ExecutableValidationResult.valid(); - } else { - return ExecutableValidationResult.notValidated(); - } - } - - @Override - public ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass) { - if (hasRelevantAnnotation(testMethod)) { - try { - // This method throws an exception if there is a validation error - getMethodParameters(testMethod); - } catch (Throwable t) { - return ExecutableValidationResult.validated(t); - } - return ExecutableValidationResult.valid(); - } else { - return ExecutableValidationResult.notValidated(); - } - } - - @Override - public List calculateTestInfos(TestInfo originalTest) { - boolean constructorIsParameterized = - hasRelevantAnnotation(getOnlyConstructor(originalTest.getTestClass())); - boolean methodIsParameterized = hasRelevantAnnotation(originalTest.getMethod()); - - if (!constructorIsParameterized && !methodIsParameterized) { - return ImmutableList.of(originalTest); - } - - ImmutableList.Builder testInfos = ImmutableList.builder(); - - ImmutableList> constructorParametersList = - getConstructorParametersOrSingleAbsentElement(originalTest.getTestClass()); - ImmutableList> methodParametersList = - getMethodParametersOrSingleAbsentElement(originalTest.getMethod()); - for (int constructorParametersIndex = 0; - constructorParametersIndex < constructorParametersList.size(); - ++constructorParametersIndex) { - Optional constructorParameters = - constructorParametersList.get(constructorParametersIndex); - - for (int methodParametersIndex = 0; - methodParametersIndex < methodParametersList.size(); - ++methodParametersIndex) { - Optional 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> - getConstructorParametersOrSingleAbsentElement(Class testClass) { - Constructor constructor = getOnlyConstructor(testClass); - return hasRelevantAnnotation(constructor) - ? getConstructorParameters(constructor).stream() - .map(Optional::of) - .collect(toImmutableList()) - : ImmutableList.of(Optional.absent()); - } - - private ImmutableList> getMethodParametersOrSingleAbsentElement( - Method method) { - return hasRelevantAnnotation(method) - ? getMethodParameters(method).stream().map(Optional::of).collect(toImmutableList()) - : ImmutableList.of(Optional.absent()); - } - - @Override - public Optional> maybeGetConstructorParameters( - Constructor constructor, TestInfo testInfo) { - if (hasRelevantAnnotation(constructor)) { - ImmutableList parameterValuesList = - getConstructorParameters(constructor); - TestParametersValues parametersValues = - parameterValuesList.get( - testInfo.getAnnotation(TestIndexHolder.class).constructorParametersIndex()); - - return Optional.of(toParameterList(parametersValues, constructor.getParameters())); - } else { - return Optional.absent(); - } - } - - @Override - public Optional> maybeGetTestMethodParameters(TestInfo testInfo) { - Method testMethod = testInfo.getMethod(); - if (hasRelevantAnnotation(testMethod)) { - ImmutableList parameterValuesList = getMethodParameters(testMethod); - TestParametersValues parametersValues = - parameterValuesList.get( - testInfo.getAnnotation(TestIndexHolder.class).methodParametersIndex()); - - return Optional.of(toParameterList(parametersValues, testMethod.getParameters())); - } else { - return Optional.absent(); - } - } - - @Override - public void postProcessTestInstance(Object testInstance, TestInfo testInfo) {} - - private ImmutableList getConstructorParameters(Constructor constructor) { - try { - return parameterValuesByConstructorOrMethodCache.getUnchecked(constructor); - } catch (UncheckedExecutionException e) { - // Rethrow IllegalStateException because they can be caused by user mistakes and the user - // doesn't need to know that the caching layer is in between. - Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); - throw e; - } - } - - private ImmutableList getMethodParameters(Method method) { - try { - return parameterValuesByConstructorOrMethodCache.getUnchecked(method); - } catch (UncheckedExecutionException e) { - // Rethrow IllegalStateException because they can be caused by user mistakes and the user - // doesn't need to know that the caching layer is in between. - Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); - throw e; - } - } - - private static ImmutableList toParameterValuesList(Executable executable) { - checkParameterNamesArePresent(executable); - ImmutableList parametersList = ImmutableList.copyOf(executable.getParameters()); - - if (executable.isAnnotationPresent(TestParameters.class)) { - checkState( - !executable.isAnnotationPresent(RepeatedTestParameters.class), - "Unexpected situation: Both @TestParameters and @RepeatedTestParameters annotating the" - + " same method"); - TestParameters annotation = executable.getAnnotation(TestParameters.class); - boolean valueIsSet = annotation.value().length > 0; - boolean valuesProviderIsSet = - !annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class); - - checkState( - !(valueIsSet && valuesProviderIsSet), - "It is not allowed to specify both value and valuesProvider in @TestParameters(value=%s," - + " valuesProvider=%s) on %s()", - Arrays.toString(annotation.value()), - annotation.valuesProvider().getSimpleName(), - executable.getName()); - checkState( - valueIsSet || valuesProviderIsSet, - "Either a value or a valuesProvider must be set in @TestParameters on %s()", - executable.getName()); - if (!annotation.customName().isEmpty()) { - checkState( - annotation.value().length == 1, - "Setting @TestParameters.customName is only allowed if there is exactly one YAML string" - + " in @TestParameters.value (on %s())", - executable.getName()); - } - - if (valueIsSet) { - return stream(annotation.value()) - .map(yamlMap -> toParameterValues(yamlMap, parametersList, annotation.customName())) - .collect(toImmutableList()); - } else { - return toParameterValuesList(annotation.valuesProvider(), parametersList); - } - } else { // Not annotated with @TestParameters - verify( - executable.isAnnotationPresent(RepeatedTestParameters.class), - "This method should only be called for executables with at least one relevant" - + " annotation"); - - return stream(executable.getAnnotation(RepeatedTestParameters.class).value()) - .map( - annotation -> - toParameterValues( - validateAndGetSingleValueFromRepeatedAnnotation(annotation, executable), - parametersList, - annotation.customName())) - .collect(toImmutableList()); - } - } - - private static ImmutableList toParameterValuesList( - Class valuesProvider, List parameters) { - try { - Constructor 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 checkParameterNamesArePresent(Executable executable) { - checkState( - stream(executable.getParameters()).allMatch(Parameter::isNamePresent), - "" - + "No parameter name could be found for %s, which likely means that parameter names" - + " aren't available at runtime. Please ensure that the this test was built with the" - + " -parameters compiler option.\n" - + "\n" - + "In Maven, you do this by adding true to the" - + " maven-compiler-plugin's configuration. For example:\n" - + "\n" - + "\n" - + " \n" - + " \n" - + " org.apache.maven.plugins\n" - + " maven-compiler-plugin\n" - + " 3.8.1\n" - + " \n" - + " \n" - + " -parameters\n" - + " \n" - + " \n" - + " \n" - + " \n" - + "\n" - + "\n" - + "Don't forget to run `mvn clean` after making this change.", - executable.getName()); - } - - private static String validateAndGetSingleValueFromRepeatedAnnotation( - TestParameters annotation, Executable executable) { - checkState( - annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class), - "Setting a valuesProvider is not supported for methods/constructors with" - + " multiple @TestParameters annotations on %s()", - executable.getName()); - checkState( - annotation.value().length > 0, - "Either a value or a valuesProvider must be set in @TestParameters on %s()", - executable.getName()); - checkState( - annotation.value().length == 1, - "When specifying more than one @TestParameter for a method/constructor, each annotation" - + " must have exactly one value. Instead, got %s values on %s(): %s", - annotation.value().length, - executable.getName(), - Arrays.toString(annotation.value())); - - return annotation.value()[0]; - } - - private static void validateThatValuesMatchParameters( - TestParametersValues testParametersValues, List parameters) { - ImmutableMap 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 parameters, String maybeCustomName) { - Object yamlMapObject = ParameterValueParsing.parseYamlStringToObject(yamlString); - checkState( - yamlMapObject instanceof Map, - "Cannot map YAML string '%s' to parameters because it is not a mapping", - yamlString); - Map yamlMap = (Map) yamlMapObject; - - ImmutableMap parametersByName = - Maps.uniqueIndex(parameters, Parameter::getName); - checkState( - yamlMap.keySet().equals(parametersByName.keySet()), - "Cannot map YAML string '%s' to parameters %s", - yamlString, - parametersByName.keySet()); - - @SuppressWarnings("unchecked") - Map checkedYamlMap = (Map) yamlMap; - - return TestParametersValues.builder() - .name(maybeCustomName.isEmpty() ? yamlString : maybeCustomName) - .addParameters( - Maps.transformEntries( - checkedYamlMap, - (parameterName, parsedYaml) -> - ParameterValueParsing.parseYamlObjectToJavaType( - parsedYaml, - TypeToken.of(parametersByName.get(parameterName).getParameterizedType())))) - .build(); - } - - // Note: We're not using the Executable interface here because it isn't supported by Java 7 and - // this code is called even if only @TestParameter is used. In other places, Executable is usable - // because @TestParameters only works for Java 8 anyway. - private static boolean hasRelevantAnnotation(Constructor executable) { - return executable.isAnnotationPresent(TestParameters.class) - || executable.isAnnotationPresent(RepeatedTestParameters.class); - } - - private static boolean hasRelevantAnnotation(Method executable) { - return executable.isAnnotationPresent(TestParameters.class) - || executable.isAnnotationPresent(RepeatedTestParameters.class); - } - - private static List toParameterList( - TestParametersValues parametersValues, Parameter[] parameters) { - return stream(parameters) - .map(parameter -> parametersValues.parametersMap().get(parameter.getName())) - .collect(toList()); - } - - private static Constructor getOnlyConstructor(Class testClass) { - ImmutableList> constructors = - ImmutableList.copyOf(testClass.getDeclaredConstructors()); - checkState( - constructors.size() == 1, "Expected exactly one constructor, but got %s", constructors); - return getOnlyElement(constructors); - } - - // Immutable collectors are re-implemented here because they are missing from the Android - // collection library. - private static Collector> 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 deleted file mode 100644 index 7b98707..0000000 --- a/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * 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.Test; -import org.junit.runner.RunWith; - -@RunWith(TestParameterInjector.class) -public class ParameterValueParsingTest { - - @Test - public void parseEnum_success() throws Exception { - Enum result = ParameterValueParsing.parseEnum("BBB", TestEnum.class); - - assertThat(result).isEqualTo(TestEnum.BBB); - } - - @Test - @TestParameters({ - "{yamlString: '{a: b, c: 15}', valid: true}", - "{yamlString: '{a: b c: 15', valid: false}", - "{yamlString: 'a: b c: 15', valid: false}", - }) - public void isValidYamlString_success(String yamlString, boolean valid) throws Exception { - boolean result = ParameterValueParsing.isValidYamlString(yamlString); - - assertThat(result).isEqualTo(valid); - } - - enum ParseYamlValueToJavaTypeCases { - STRING_TO_STRING( - /* yamlString= */ "abc", /* javaClass= */ String.class, /* expectedResult= */ "abc"), - BOOLEAN_TO_STRING( - /* yamlString= */ "true", /* javaClass= */ String.class, /* expectedResult= */ "true"), - INT_TO_STRING( - /* yamlString= */ "123", /* javaClass= */ String.class, /* expectedResult= */ "123"), - LONG_TO_STRING( - /* yamlString= */ "442147483648", - /* javaClass= */ String.class, - /* expectedResult= */ "442147483648"), - DOUBLE_TO_STRING( - /* yamlString= */ "1.23", /* javaClass= */ String.class, /* expectedResult= */ "1.23"), - - BOOLEAN_TO_BOOLEAN( - /* yamlString= */ "true", /* javaClass= */ Boolean.class, /* expectedResult= */ true), - - INT_TO_INT(/* yamlString= */ "123", /* javaClass= */ int.class, /* expectedResult= */ 123), - - LONG_TO_LONG( - /* yamlString= */ "442147483648", - /* javaClass= */ long.class, - /* expectedResult= */ 442147483648L), - INT_TO_LONG(/* yamlString= */ "123", /* javaClass= */ Long.class, /* expectedResult= */ 123L), - - DOUBLE_TO_DOUBLE( - /* yamlString= */ "1.23", /* javaClass= */ Double.class, /* expectedResult= */ 1.23), - INT_TO_DOUBLE( - /* yamlString= */ "123", /* javaClass= */ Double.class, /* expectedResult= */ 123.0), - LONG_TO_DOUBLE( - /* yamlString= */ "442147483648", - /* javaClass= */ Double.class, - /* expectedResult= */ 442147483648.0), - NAN_TO_DOUBLE( - /* yamlString= */ "NaN", /* javaClass= */ Double.class, /* expectedResult= */ Double.NaN), - INFINITY_TO_DOUBLE( - /* yamlString= */ "Infinity", - /* javaClass= */ Double.class, - /* expectedResult= */ Double.POSITIVE_INFINITY), - POSITIVE_INFINITY_TO_DOUBLE( - /* yamlString= */ "+Infinity", - /* javaClass= */ Double.class, - /* expectedResult= */ Double.POSITIVE_INFINITY), - NEGATIVE_INFINITY_TO_DOUBLE( - /* yamlString= */ "-Infinity", - /* javaClass= */ Double.class, - /* expectedResult= */ Double.NEGATIVE_INFINITY), - - DOUBLE_TO_FLOAT( - /* yamlString= */ "1.23", /* javaClass= */ Float.class, /* expectedResult= */ 1.23F), - INT_TO_FLOAT(/* yamlString= */ "123", /* javaClass= */ Float.class, /* expectedResult= */ 123F), - - STRING_TO_ENUM( - /* yamlString= */ "AAA", - /* javaClass= */ TestEnum.class, - /* expectedResult= */ TestEnum.AAA), - - STRING_TO_BYTES( - /* yamlString= */ "data", - /* javaClass= */ byte[].class, - /* expectedResult= */ "data".getBytes()), - - BYTES_TO_BYTES( - /* yamlString= */ "!!binary 'ZGF0YQ=='", - /* javaClass= */ byte[].class, - /* expectedResult= */ "data".getBytes()), - - STRING_TO_BYTESTRING( - /* yamlString= */ "'data'", - /* javaClass= */ ByteString.class, - /* expectedResult= */ ByteString.copyFromUtf8("data")), - - BINARY_TO_BYTESTRING( - /* yamlString= */ "!!binary 'ZGF0YQ=='", - /* javaClass= */ ByteString.class, - /* expectedResult= */ ByteString.copyFromUtf8("data")); - - final String yamlString; - final Class javaClass; - final Object expectedResult; - - ParseYamlValueToJavaTypeCases(String yamlString, Class javaClass, Object expectedResult) { - this.yamlString = yamlString; - this.javaClass = javaClass; - this.expectedResult = expectedResult; - } - } - - @Test - public void parseYamlStringToJavaType_success( - @TestParameter ParseYamlValueToJavaTypeCases parseYamlValueToJavaTypeCases) throws Exception { - Object result = - ParameterValueParsing.parseYamlStringToJavaType( - parseYamlValueToJavaTypeCases.yamlString, parseYamlValueToJavaTypeCases.javaClass); - - assertThat(result).isEqualTo(parseYamlValueToJavaTypeCases.expectedResult); - } - - 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 deleted file mode 100644 index a44df08..0000000 --- a/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * 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 TestMethodProcessorList createTestMethodProcessorList() { - return TestMethodProcessorList.empty(); - } - }); - - 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 TestMethodProcessorList createTestMethodProcessorList() { - return TestMethodProcessorList.empty(); - } - - @Override - protected ImmutableList> getSupportedTestAnnotations() { - return ImmutableList.of(Test.class, CustomTest.class); - } - }); - - assertThat(testMethodInvocationCount).isEqualTo(2); - } - - private static final List 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 TestMethodProcessorList createTestMethodProcessorList() { - return TestMethodProcessorList.empty(); - } - - @Override - protected Stream sortTestMethods(Stream 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 deleted file mode 100644 index f6fd512..0000000 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java +++ /dev/null @@ -1,253 +0,0 @@ -/* - * 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 result = TestInfo.shortenNamesIfNecessary(ImmutableList.of()); - - assertThat(result).isEmpty(); - } - - @Test - public void shortenNamesIfNecessary_noParameters() throws Exception { - ImmutableList givenTestInfos = ImmutableList.of(fakeTestInfo()); - - ImmutableList result = TestInfo.shortenNamesIfNecessary(givenTestInfos); - - assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder(); - } - - @Test - public void shortenNamesIfNecessary_veryLongTestMethodName_noParameters() throws Exception { - ImmutableList givenTestInfos = - ImmutableList.of( - TestInfo.createWithoutParameters( - getClass() - .getMethod( - "unusedMethodThatHasAVeryLongNameForTest000000000000000000000000000000000" - + "000000000000000000000000000000000000000000000000000000000000000000" - + "000000000000000000000000000000000000000000000000000000000000000000" - + "000000000000000000000000"), - getClass(), - /* annotations= */ ImmutableList.of())); - - ImmutableList result = TestInfo.shortenNamesIfNecessary(givenTestInfos); - - assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder(); - } - - @Test - public void shortenNamesIfNecessary_noShorteningNeeded() throws Exception { - ImmutableList 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 result = TestInfo.shortenNamesIfNecessary(givenTestInfos); - - assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder(); - } - - @Test - public void shortenNamesIfNecessary_singleParameterTooLong_twoParameters() throws Exception { - ImmutableList 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 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" - + " 000000000000000000000000000000000000000000000000000000000000000000000000000000" - + "00000000000000000000000000000000000000000000000000000000000000000000...]") - .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 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 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 result = TestInfo.deduplicateTestNames(givenTestInfos); - - assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder(); - } - - @Test - public void deduplicateTestNames_duplicateParameterNamesWithDifferentTypes() throws Exception { - ImmutableList 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 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"), - String.class, - /* annotations= */ ImmutableList.of()) - .withExtraParameters(ImmutableList.copyOf(parameters)); - } - - private static IterableSubject assertThatTestNamesOf(List 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 deleted file mode 100644 index 9af29ae..0000000 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java +++ /dev/null @@ -1,1012 +0,0 @@ -/* - * 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.Iterables.getOnlyElement; -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 java.util.stream.Collectors.joining; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.fail; - -import com.google.common.base.Throwables; -import com.google.common.collect.ImmutableList; -import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider; -import com.google.testing.junit.testparameterinjector.TestParameterAnnotationMethodProcessorTest.ErrorNonStaticProviderClass.NonStaticProvider; -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.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 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 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 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 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 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> 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 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 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 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 filename limit of 255 characters." - + " =========================================================...]"); - } - } - - @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class DuplicateTestNames { - - @Rule public TestName testName = new TestName(); - - private static List allTestNames; - private static List 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 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 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 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 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 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 BaseClassWithSingleTest { - @Rule public TestName testName = new TestName(); - - static List allTestNames; - - @BeforeClass - public static void resetStaticState() { - allTestNames = new ArrayList<>(); - } - - @Test - public void testInBase(@TestParameter boolean b) { - allTestNames.add(testName.getMethodName()); - } - - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(allTestNames).containsExactly("testInBase[b=true]", "testInBase[b=false]"); - } - } - - @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class SimpleTestInheritedFromBaseClass extends BaseClassWithSingleTest {} - - public abstract static class BaseClassWithAnnotations { - @Rule public TestName testName = new TestName(); - - static List 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) - public @interface EnumEvaluatorParameter { - TestEnum[] value() default {}; - } - - public static class TestEnumValidator implements TestParameterValidator { - - @Override - public boolean shouldSkip(Context context) { - return context.has(EnumEvaluatorParameter.class, TestEnum.THREE); - } - } - - @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class MethodEvaluatorClass { - - private static List 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<>(); - } - - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO); - } - } - - @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class FieldEvaluatorClass { - - private static List 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); - } - } - - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO); - } - } - - @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class ConstructorClass { - - private static List 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 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) - public static class ErrorDuplicatedConstructorMethodAnnotation { - - private static List 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 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(); - } - } - } - - @ClassTestResult(Result.FAILURE) - public static class ErrorNonPublicTestMethod { - - @Test - void test(@TestParameter boolean b) {} - } - - 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>> 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> 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 > void assertAllEnumsAreIncluded(Enum[] values) { - Set> enumSet = new HashSet<>(Arrays.asList(values)); - for (List 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 parameters() { - return Arrays.stream(TestParameterAnnotationMethodProcessorTest.class.getClasses()) - .filter(cls -> cls.isAnnotationPresent(ClassTestResult.class)) - .map( - cls -> - new Object[] { - cls.getSimpleName(), cls, cls.getAnnotation(ClassTestResult.class).value() - }) - .collect(toImmutableList()); - } - - private final Class testClass; - private final Result result; - - public TestParameterAnnotationMethodProcessorTest( - String name, Class testClass, Result result) { - this.testClass = testClass; - this.result = result; - } - - @Test - public void test() throws Exception { - switch (result) { - case SUCCESS_ALWAYS: - assertNoFailures( - PluggableTestRunner.run( - newTestRunnerWithParameterizedSupport( - testClass -> TestMethodProcessorList.createNewParameterizedProcessors()))); - break; - - case SUCCESS_FOR_ALL_PLACEMENTS_ONLY: - assertThrows( - RuntimeException.class, - () -> - PluggableTestRunner.run( - newTestRunnerWithParameterizedSupport( - testClass -> TestMethodProcessorList.createNewParameterizedProcessors()))); - break; - - case FAILURE: - assertThrows( - RuntimeException.class, - () -> - PluggableTestRunner.run( - newTestRunnerWithParameterizedSupport( - testClass -> TestMethodProcessorList.createNewParameterizedProcessors()))); - break; - } - } - - private PluggableTestRunner newTestRunnerWithParameterizedSupport( - Function processorListGenerator) throws Exception { - return new PluggableTestRunner(testClass) { - @Override - protected TestMethodProcessorList createTestMethodProcessorList() { - return processorListGenerator.apply(getTestClass()); - } - }; - } - - private static void assertNoFailures(List failures) { - if (failures.size() == 1) { - throw new AssertionError(getOnlyElement(failures).getException()); - } else if (failures.size() > 1) { - throw new AssertionError( - String.format( - "Test failed unexpectedly:\n\n%s", - failures.stream() - .map( - f -> - String.format( - "<<%s>> %s", - f.getDescription(), - Throwables.getStackTraceAsString(f.getException()))) - .collect(joining("\n------------------------------------\n")))); - } - } -} diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java deleted file mode 100644 index 85cb686..0000000 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * 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.Iterables.getOnlyElement; -import static com.google.common.collect.Lists.newArrayList; -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; -import static java.lang.annotation.RetentionPolicy.RUNTIME; -import static java.util.stream.Collectors.joining; - -import com.google.common.base.CharMatcher; -import com.google.common.base.Throwables; -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 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 testedParameters; - - private final TestEnum constructorEnum; - - public AnnotatedConstructorParameter(@TestParameter TestEnum constructorEnum) { - this.constructorEnum = constructorEnum; - } - - @TestParameter TestEnum fieldEnum; - - @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(String.format("%s:%s", fieldEnum, constructorEnum)); - } - - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testedParameters) - .containsExactly( - "ONE:ONE", - "ONE:TWO", - "ONE:THREE", - "TWO:ONE", - "TWO:TWO", - "TWO:THREE", - "THREE:ONE", - "THREE:TWO", - "THREE:THREE"); - } - } - - @RunAsTest - public static class MultipleAnnotatedParameters { - private static List 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 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 provideValues() { - return newArrayList("A", "B", null); - } - } - - private static final class CharMatcherProvider implements TestParameterValuesProvider { - @Override - public List provideValues() { - return newArrayList(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace()); - } - } - } - - @Parameters(name = "{0}") - public static Collection 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 failures = - PluggableTestRunner.run( - new PluggableTestRunner(testClass) { - @Override - protected TestMethodProcessorList createTestMethodProcessorList() { - return TestMethodProcessorList.createNewParameterizedProcessors(); - } - }); - - assertNoFailures(failures); - } - - private static void assertNoFailures(List failures) { - if (failures.size() == 1) { - throw new AssertionError(getOnlyElement(failures).getException()); - } else if (failures.size() > 1) { - throw new AssertionError( - String.format( - "Test failed unexpectedly:\n\n%s", - failures.stream() - .map( - f -> - String.format( - "<<%s>> %s", - f.getDescription(), - Throwables.getStackTraceAsString(f.getException()))) - .collect(joining("\n------------------------------------\n")))); - } - } -} diff --git a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java b/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java deleted file mode 100644 index 3e15277..0000000 --- a/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java +++ /dev/null @@ -1,621 +0,0 @@ -/* - * 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.Iterables.getOnlyElement; -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.TruthJUnit.assume; -import static java.lang.annotation.RetentionPolicy.RUNTIME; -import static java.util.stream.Collectors.joining; -import static org.junit.Assert.assertThrows; - -import com.google.common.base.Throwables; -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 java.util.Optional; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -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) -public class TestParametersMethodProcessorTest { - - @Retention(RUNTIME) - @interface RunAsTest { - String failsWithMessage() default ""; - } - - public enum TestEnum { - ONE, - TWO, - THREE; - } - - private static final class TestEnumValuesProvider implements TestParametersValuesProvider { - @Override - public List 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()); - } - } - - @RunAsTest - public static class SimpleMethodAnnotation { - @Rule public TestName testName = new TestName(); - - private static Map testNameToStringifiedParametersMap; - - @BeforeClass - public static void resetStaticState() { - testNameToStringifiedParametersMap = new LinkedHashMap<>(); - } - - @Test - @TestParameters("{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}") - @TestParameters("{testEnum: TWO,\ntestLong: 22,\ntestBoolean: true,\r\n\r\n testString: 'DEF'}") - @TestParameters("{testEnum: null, testLong: 33, testBoolean: false, testString: null}") - public void test(TestEnum testEnum, long testLong, boolean testBoolean, String testString) { - testNameToStringifiedParametersMap.put( - testName.getMethodName(), - String.format("%s,%s,%s,%s", testEnum, testLong, testBoolean, testString)); - } - - @Test - @TestParameters({ - "{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}", - "{testEnum: TWO,\ntestLong: 22,\ntestBoolean: true,\r\n\r\n testString: 'DEF'}", - "{testEnum: null, testLong: 33, testBoolean: false, testString: null}", - }) - public void test_singleAnnotation( - TestEnum testEnum, long testLong, boolean testBoolean, String testString) { - testNameToStringifiedParametersMap.put( - testName.getMethodName(), - String.format("%s,%s,%s,%s", testEnum, testLong, testBoolean, testString)); - } - - @Test - @TestParameters("{testString: ABC}") - @TestParameters( - "{testString: 'This is a very long string (240 characters) that would normally cause" - + " Sponge+Tin to exceed the filename limit of 255 characters." - + " =================================================================================" - + "=============='}") - public void test2_withLongNames(String testString) { - testNameToStringifiedParametersMap.put(testName.getMethodName(), testString); - } - - @Test - @TestParameters( - "{testEnums: [ONE, TWO, THREE], testLongs: [11, 4], testBooleans: [false, true]," - + " testStrings: [ABC, '123']}") - @TestParameters( - "{testEnums: [TWO],\ntestLongs: [22],\ntestBooleans: [true],\r\n\r\n testStrings: ['DEF']}") - @TestParameters("{testEnums: [], testLongs: [], testBooleans: [], testStrings: []}") - public void test3_withRepeatedParams( - List testEnums, - List testLongs, - List testBooleans, - List testStrings) { - testNameToStringifiedParametersMap.put( - testName.getMethodName(), - String.format("%s,%s,%s,%s", testEnums, testLongs, testBooleans, testStrings)); - } - - @Test - @TestParameters(customName = "custom1", value = "{testEnum: ONE}") - @TestParameters("{testEnum: TWO}") - @TestParameters(customName = "custom3", value = "{testEnum: THREE}") - public void test4_withCustomName(TestEnum testEnum) { - testNameToStringifiedParametersMap.put(testName.getMethodName(), String.valueOf(testEnum)); - } - - @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", - "test_singleAnnotation[{testEnum: ONE, testLong: 11, testBoolean: false, testString:" - + " ABC}]", - "ONE,11,false,ABC", - "test_singleAnnotation[{testEnum: TWO, testLong: 22, testBoolean: true, testString:" - + " 'DEF'}]", - "TWO,22,true,DEF", - "test_singleAnnotation[{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 to exceed the filename limit of 255" - + " characters. =============================...]", - "This is a very long string (240 characters) that would normally cause Sponge+Tin to" - + " exceed the filename limit of 255 characters." - + " =================================================================================" - + "==============", - "test3_withRepeatedParams[{testEnums: [ONE, TWO, THREE], testLongs: [11, 4]," - + " testBooleans: [false, true], testStrings: [ABC, '123']}]", - "[ONE, TWO, THREE],[11, 4],[false, true],[ABC, 123]", - "test3_withRepeatedParams[{testEnums: [TWO], testLongs: [22], testBooleans: [true]," - + " testStrings: ['DEF']}]", - "[TWO],[22],[true],[DEF]", - "test3_withRepeatedParams[{testEnums: [], testLongs: [], testBooleans: []," - + " testStrings: []}]", - "[],[],[],[]", - "test4_withCustomName[custom1]", - "ONE", - "test4_withCustomName[{testEnum: TWO}]", - "TWO", - "test4_withCustomName[custom3]", - "THREE"); - } - } - - @RunAsTest - public static class SimpleConstructorAnnotation { - - @Rule public TestName testName = new TestName(); - - private static Map 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 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); - } - } - - public abstract static class BaseClassWithMethodAnnotation { - @Rule public TestName testName = new TestName(); - - static List 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}") - @TestParameters("{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 allTestNames; - private static List testNamesThatInvokedBefore; - private static List testNamesThatInvokedAfter; - - @TestParameters("{testEnum: ONE}") - @TestParameters("{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}") - @TestParameters("{testString: DEF}") - public void test2(String testString) { - allTestNames.add(testName.getMethodName()); - } - - @Test - @TestParameters("{testString: ABC}") - @TestParameters( - "{testString: 'This is a very long string (240 characters) that would normally cause" - + " Sponge+Tin to exceed the filename limit of 255 characters." - + " =================================================================================" - + "=============='}") - public void test3_withLongNames(String testString) { - 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 allTestNames; - - @TestParameter TestEnum testEnumA; - - @TestParameters("{testEnumB: ONE}") - @TestParameters("{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}"}) - 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]"); - } - } - - @RunAsTest( - failsWithMessage = - "Either a value or a valuesProvider must be set in @TestParameters on test1()") - public static class InvalidTestBecauseEmptyAnnotation { - @Test - @TestParameters - public void test1() {} - } - - @RunAsTest( - failsWithMessage = - "Either a value or a valuesProvider must be set in @TestParameters on" - + " com.google.testing.junit.testparameterinjector.TestParametersMethodProcessorTest" - + "$InvalidTestBecauseEmptyAnnotationOnConstructor()") - public static class InvalidTestBecauseEmptyAnnotationOnConstructor { - @TestParameters - public InvalidTestBecauseEmptyAnnotationOnConstructor() {} - - @Test - public void test1() {} - } - - @RunAsTest( - failsWithMessage = - "It is not allowed to specify both value and valuesProvider in" - + " @TestParameters(value=[{testEnum: ONE}], valuesProvider=TestEnumValuesProvider)" - + " on test1()") - public static class InvalidTestBecauseCombiningValueWithProvider { - - @Test - @TestParameters(value = "{testEnum: ONE}", valuesProvider = TestEnumValuesProvider.class) - public void test1(TestEnum testEnum) {} - } - - @RunAsTest( - failsWithMessage = - "Either a value or a valuesProvider must be set in @TestParameters on test1()") - public static class InvalidTestBecauseRepeatedAnnotationIsEmpty { - @Test - @TestParameters(value = "{testEnum: ONE}") - @TestParameters - public void test1(TestEnum testEnum) {} - } - - @RunAsTest( - failsWithMessage = - "When specifying more than one @TestParameter for a method/constructor, each annotation" - + " must have exactly one value. Instead, got 2 values on test1(): [{testEnum: TWO}," - + " {testEnum: THREE}]") - public static class InvalidTestBecauseRepeatedAnnotationHasMultipleValues { - @Test - @TestParameters(value = "{testEnum: ONE}") - @TestParameters(value = {"{testEnum: TWO}", "{testEnum: THREE}"}) - public void test1(TestEnum testEnum) {} - } - - @RunAsTest( - failsWithMessage = - "Setting a valuesProvider is not supported for methods/constructors with" - + " multiple @TestParameters annotations on test1()") - public static class InvalidTestBecauseRepeatedAnnotationHasProvider { - @Test - @TestParameters(valuesProvider = TestEnumValuesProvider.class) - @TestParameters(valuesProvider = TestEnumValuesProvider.class) - public void test1(TestEnum testEnum) {} - } - - @RunAsTest( - failsWithMessage = - "Setting @TestParameters.customName is only allowed if there is exactly one YAML string" - + " in @TestParameters.value (on test1())") - public static class InvalidTestBecauseNamedAnnotationHasMultipleValues { - @Test - @TestParameters( - customName = "custom", - value = {"{testEnum: TWO}", "{testEnum: THREE}"}) - public void test1(TestEnum testEnum) {} - } - - @Parameters(name = "{0}") - public static Collection parameters() { - return Arrays.stream(TestParametersMethodProcessorTest.class.getClasses()) - .filter(cls -> cls.isAnnotationPresent(RunAsTest.class)) - .map( - cls -> - new Object[] { - cls.getSimpleName(), cls, cls.getAnnotation(RunAsTest.class).failsWithMessage() - }) - .collect(toImmutableList()); - } - - private final Class testClass; - private final Optional maybeFailureMessage; - - public TestParametersMethodProcessorTest( - String name, Class testClass, String failsWithMessage) { - this.testClass = testClass; - this.maybeFailureMessage = - failsWithMessage.isEmpty() ? Optional.empty() : Optional.of(failsWithMessage); - } - - @Test - public void test_success() throws Exception { - assume().that(maybeFailureMessage.isPresent()).isFalse(); - - List failures = PluggableTestRunner.run(newTestRunner()); - - assertNoFailures(failures); - } - - @Test - public void test_failure() throws Exception { - assume().that(maybeFailureMessage.isPresent()).isTrue(); - - IllegalStateException exception = - assertThrows(IllegalStateException.class, () -> PluggableTestRunner.run(newTestRunner())); - - assertThat(exception).hasMessageThat().isEqualTo(maybeFailureMessage.get()); - } - - private PluggableTestRunner newTestRunner() throws Exception { - return new PluggableTestRunner(testClass) { - @Override - protected TestMethodProcessorList createTestMethodProcessorList() { - return TestMethodProcessorList.createNewParameterizedProcessors(); - } - }; - } - - private static void assertNoFailures(List failures) { - if (failures.size() == 1) { - throw new AssertionError(getOnlyElement(failures).getException()); - } else if (failures.size() > 1) { - throw new AssertionError( - String.format( - "Test failed unexpectedly:\n\n%s", - failures.stream() - .map( - f -> - String.format( - "<<%s>> %s", - f.getDescription(), - Throwables.getStackTraceAsString(f.getException()))) - .collect(joining("\n------------------------------------\n")))); - } - } -} -- cgit v1.2.3 From a28310d4494b4ccdfe8fb39eb271ad42fde275b9 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Mon, 17 Jan 2022 13:27:31 +0000 Subject: Fix a broken reference to the API docs --- .github/workflows/build.yaml | 2 +- .github/workflows/release.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 717250f..8eefb23 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -43,6 +43,6 @@ jobs: with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BRANCH: site - FOLDER: target/site/apidocs + FOLDER: junit4/target/site/apidocs TARGET_FOLDER: docs/latest/ CLEAN: true diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 79ab019..b81dfb7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -38,6 +38,6 @@ jobs: with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BRANCH: site - FOLDER: target/site/apidocs + FOLDER: junit4/target/site/apidocs TARGET_FOLDER: docs/1.x/ CLEAN: true -- cgit v1.2.3 From 4381601dceadb3b1d28adeb82cc259eb22a15398 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Mon, 17 Jan 2022 14:37:32 +0000 Subject: Specify that TestParameterInjector should be used only with JUnit4. --- .../testing/junit/testparameterinjector/TestParameterInjector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java index 537969a..8b23e53 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java @@ -17,7 +17,7 @@ package com.google.testing.junit.testparameterinjector; import org.junit.runners.model.InitializationError; /** - * A JUnit test runner which knows how to instantiate and run test classes where each test case may + * A JUnit4 test runner which knows how to instantiate and run test classes where each test case may * be parameterized with its own unique set of test parameters. */ public final class TestParameterInjector extends PluggableTestRunner { -- cgit v1.2.3 From 03ce7c6d02b5dca757e11adb374c35ac08cd74d9 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Mon, 17 Jan 2022 14:59:48 +0000 Subject: Move the files around so the directory tree reflects the new package name --- .../BaseTestParameterValidator.java | 83 -- .../ExecutableValidationResult.java | 72 -- .../ParameterValueParsing.java | 249 ---- .../testparameterinjector/ProtoValueParsing.java | 25 - .../junit/testparameterinjector/TestInfo.java | 309 ----- .../testparameterinjector/TestMethodProcessor.java | 66 - .../TestMethodProcessorList.java | 146 --- .../junit/testparameterinjector/TestParameter.java | 224 ---- .../TestParameterAnnotation.java | 251 ---- .../TestParameterAnnotationMethodProcessor.java | 1290 -------------------- .../TestParameterInjectorExtension.java | 140 --- .../TestParameterInjectorTest.java | 50 - .../TestParameterValidator.java | 68 -- .../TestParameterValueProvider.java | 52 - .../testparameterinjector/TestParameterValues.java | 27 - .../testparameterinjector/TestParameters.java | 259 ---- .../TestParametersMethodProcessor.java | 485 -------- .../junit5/BaseTestParameterValidator.java | 83 ++ .../junit5/ExecutableValidationResult.java | 72 ++ .../junit5/ParameterValueParsing.java | 249 ++++ .../junit5/ProtoValueParsing.java | 25 + .../testparameterinjector/junit5/TestInfo.java | 309 +++++ .../junit5/TestMethodProcessor.java | 66 + .../junit5/TestMethodProcessorList.java | 146 +++ .../junit5/TestParameter.java | 224 ++++ .../junit5/TestParameterAnnotation.java | 251 ++++ .../TestParameterAnnotationMethodProcessor.java | 1290 ++++++++++++++++++++ .../junit5/TestParameterInjectorExtension.java | 140 +++ .../junit5/TestParameterInjectorTest.java | 50 + .../junit5/TestParameterValidator.java | 68 ++ .../junit5/TestParameterValueProvider.java | 52 + .../junit5/TestParameterValues.java | 27 + .../junit5/TestParameters.java | 259 ++++ .../junit5/TestParametersMethodProcessor.java | 485 ++++++++ .../TestParameterInjectorJUnit5Test.java | 607 --------- .../junit5/TestParameterInjectorJUnit5Test.java | 607 +++++++++ 36 files changed, 4403 insertions(+), 4403 deletions(-) delete mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java delete mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java delete mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java delete mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java delete mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java delete mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java delete mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java delete mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java delete mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java delete mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java delete mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorExtension.java delete mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorTest.java delete mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java delete mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java delete mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java delete mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java delete mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/BaseTestParameterValidator.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ExecutableValidationResult.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ProtoValueParsing.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessor.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessorList.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotation.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorExtension.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorTest.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValidator.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValues.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameters.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParametersMethodProcessor.java delete mode 100644 junit5/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorJUnit5Test.java create mode 100644 junit5/src/test/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorJUnit5Test.java diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java deleted file mode 100644 index 692a713..0000000 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector.junit5; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkState; -import static java.lang.Math.min; - -import 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> 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 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 parameter : parameters) { - List 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 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>> getIndependentParameters( - Context context); -} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java deleted file mode 100644 index 4c0c40d..0000000 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector.junit5; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.collect.Iterables.getOnlyElement; - -import com.google.auto.value.AutoValue; -import com.google.common.collect.ImmutableList; -import java.util.Collection; - -/** - * Value class that captures the result of a validating a single constructor or test method. - * - *

          If the validation is not validated by any processor, it will be validated using the default - * validator. If a processor validates a constructor/test method, the remaining processors will - * *not* be called. - */ -@AutoValue -abstract class ExecutableValidationResult { - - /** Returns true if the properties of the given constructor/test method were validated. */ - public abstract boolean wasValidated(); - - /** Returns the validation errors, if any. */ - public abstract ImmutableList validationErrors(); - - static ExecutableValidationResult notValidated() { - return of(/* wasValidated= */ false, /* validationErrors= */ ImmutableList.of()); - } - - static ExecutableValidationResult validated(Collection errors) { - return of(/* wasValidated= */ true, /* validationErrors= */ errors); - } - - static ExecutableValidationResult validated(Throwable error) { - return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of(error)); - } - - static ExecutableValidationResult valid() { - return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of()); - } - - private static ExecutableValidationResult of( - boolean wasValidated, Collection validationErrors) { - checkArgument(wasValidated || validationErrors.isEmpty()); - return new AutoValue_ExecutableValidationResult( - wasValidated, ImmutableList.copyOf(validationErrors)); - } - - void assertValid() { - if (wasValidated() && !validationErrors().isEmpty()) { - if (validationErrors().size() == 1) { - throw new AssertionError(getOnlyElement(validationErrors())); - } else { - throw new AssertionError(String.format("Found validation errors: %s", validationErrors())); - } - } - } -} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java deleted file mode 100644 index 59d2351..0000000 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector.junit5; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.toMap; - -import com.google.common.collect.Lists; -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 > Enum parseEnum(String str, Class enumType) { - return Enum.valueOf((Class) 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) - .supportParsedType(String.class, Float::valueOf); - - yamlValueTransformer - .ifJavaType(Double.class) - .supportParsedType(Double.class, identity()) - .supportParsedType(Integer.class, Integer::doubleValue) - .supportParsedType(Long.class, Long::doubleValue) - .supportParsedType(String.class, Double::valueOf); - - yamlValueTransformer - .ifJavaType(Enum.class) - .supportParsedType( - String.class, str -> ParameterValueParsing.parseEnum(str, javaType.getRawType())); - - yamlValueTransformer - .ifJavaType(MessageLite.class) - .supportParsedType(String.class, str -> parseTextprotoMessage(str, javaType.getRawType())) - .supportParsedType( - Map.class, - map -> - getProtoValueParser() - .parseProtobufMessage((Map) 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 -> parseYamlMapToJavaMap(map, javaType)); - - return yamlValueTransformer.transformedJavaValue(); - } - - private static Map parseYamlMapToJavaMap(Map map, TypeToken javaType) { - return map.entrySet().stream() - .collect( - toMap( - entry -> - parseYamlObjectToJavaType( - entry.getKey(), getGenericParameterType(javaType, /* parameterIndex= */ 0)), - entry -> - parseYamlObjectToJavaType( - entry.getValue(), - getGenericParameterType(javaType, /* parameterIndex= */ 1)))); - } - - 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; - } - - SupportedJavaType ifJavaType(Class 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 { - - private final Class supportedJavaType; - - private SupportedJavaType(Class supportedJavaType) { - this.supportedJavaType = supportedJavaType; - } - - @SuppressWarnings("unchecked") - SupportedJavaType supportParsedType( - Class parsedYamlType, Function transformation) { - if (Primitives.wrap(supportedJavaType).isAssignableFrom(Primitives.wrap(javaType))) { - if (Primitives.wrap(parsedYamlType).isInstance(parsedYaml)) { - checkState( - transformedJavaValue == null, - "This case is already handled. This is a bug in" - + " testparameterinjector.TestParametersMethodProcessor."); - try { - transformedJavaValue = checkNotNull(transformation.apply((ParsedYamlT) parsedYaml)); - } catch (Exception e) { - throw new IllegalArgumentException( - String.format( - "Could not map YAML value %s (class = %s) to java class %s", - parsedYaml, parsedYaml.getClass(), javaType), - e); - } - } - } - - return this; - } - } - } - - static 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.junit5.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/junit5/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java deleted file mode 100644 index d270295..0000000 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector.junit5; - -import com.google.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 map, Class javaType); -} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java deleted file mode 100644 index c6b1cc3..0000000 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java +++ /dev/null @@ -1,309 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector.junit5; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static 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. - * - *

          See b/168325767 for the reason behind this. tl;dr the name is put into a Unix file with max - * 255 characters. The surrounding constant characters take up 31 characters. The max is reduced - * by an additional 24 characters to account for future changes. - */ - static final int MAX_TEST_NAME_LENGTH = 200; - - public abstract Method getMethod(); - - /** - * The test class that is being run. - * - *

          Note that this is not always the same as the class that declares {@link #getMethod()} - * because test methods can be inherited. - */ - public abstract Class getTestClass(); - - public final String getName() { - if (getParameters().isEmpty()) { - return getMethod().getName(); - } else { - return String.format( - "%s[%s]", - getMethod().getName(), - getParameters().stream().map(TestInfoParameter::getName).collect(joining(","))); - } - } - - abstract ImmutableList getParameters(); - - public abstract ImmutableList getAnnotations(); - - @Nullable - public final T getAnnotation(Class annotationClass) { - for (Annotation annotation : getAnnotations()) { - if (annotationClass.isInstance(annotation)) { - return annotationClass.cast(annotation); - } - } - return null; - } - - final TestInfo withExtraParameters(List parameters) { - return new AutoValue_TestInfo( - getMethod(), - getTestClass(), - ImmutableList.builder() - .addAll(this.getParameters()) - .addAll(parameters) - .build(), - getAnnotations()); - } - - final TestInfo withExtraAnnotation(Annotation annotation) { - ImmutableList newAnnotations = - ImmutableList.builder().addAll(this.getAnnotations()).add(annotation).build(); - return new AutoValue_TestInfo(getMethod(), getTestClass(), getParameters(), newAnnotations); - } - - /** - * Returns a new TestInfo instance with updated parameter names. - * - * @param parameterWithIndexToNewName A function of the parameter and its index in the {@link - * #getParameters()} list to the new name. - */ - private TestInfo withUpdatedParameterNames( - BiFunction parameterWithIndexToNewName) { - return new AutoValue_TestInfo( - getMethod(), - getTestClass(), - 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, Class testClass, String name, List annotations) { - return new AutoValue_TestInfo( - method, testClass, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations)); - } - - static TestInfo createWithoutParameters( - Method method, Class testClass, List annotations) { - return new AutoValue_TestInfo( - method, testClass, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations)); - } - - static ImmutableList shortenNamesIfNecessary(List testInfos) { - if (testInfos.stream().anyMatch(info -> info.getName().length() > MAX_TEST_NAME_LENGTH)) { - int numberOfParameters = testInfos.get(0).getParameters().size(); - - if (numberOfParameters == 0) { - return ImmutableList.copyOf(testInfos); - } else { - Set 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; - - // Subtract 4 characters to leave place for joining commas and the parameter index. - return maxLengthOfAllParameters / numberOfParameters - 4; - } - - static ImmutableList deduplicateTestNames(List 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 maybeAddTypesIfDuplicate(List testInfos) { - Multimap testNameToInfo = - MultimapBuilder.linkedHashKeys().arrayListValues().build(); - for (TestInfo testInfo : testInfos) { - testNameToInfo.put(testInfo.getName(), testInfo); - } - - return testNameToInfo.keySet().stream() - .flatMap( - testName -> { - Collection 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 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 deduplicateWithNumberPrefixes( - ImmutableList 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 Collector> 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(); - - final 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/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java deleted file mode 100644 index 48e9a5e..0000000 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector.junit5; - -import com.google.common.base.Optional; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.util.List; - -/** - * Interface to change the list of methods used in a test. - * - *

          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 calculateTestInfos(TestInfo originalTest); - - /** - * If this processor can handle the given constructor, returns the parameters with which it should - * be invoked. - * - *

          This method is never called for a parameterless constructor. - */ - Optional> maybeGetConstructorParameters( - Constructor constructor, TestInfo testInfo); - - /** - * If this processor can handle the given test, returns the parameters with which {@code - * testInfo.getMethod()} should be invoked. - * - *

          This method is never called for a parameterless {@code testInfo.getMethod()}. - */ - Optional> maybeGetTestMethodParameters(TestInfo testInfo); - - /** - * Optionally process the test instance right after construction to ready it for the given test - * instance. - */ - void postProcessTestInstance(Object testInstance, TestInfo testInfo); - - /** Optionally validates the given constructor. */ - ExecutableValidationResult validateConstructor(Constructor constructor); - - /** - * Optionally validates the given method. - * - *

          Note that the given method is not necessarily declared in the given class because test - * methods can be inherited. - */ - ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass); -} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java deleted file mode 100644 index aa2355d..0000000 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector.junit5; - -import static java.util.stream.Collectors.toList; - -import com.google.common.base.Optional; -import com.google.common.collect.ImmutableList; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.util.List; - -/** - * Combined version of all {@link TestMethodProcessor} implementations that this package supports. - */ -final class TestMethodProcessorList { - - private final ImmutableList testMethodProcessors; - - private TestMethodProcessorList(ImmutableList testMethodProcessors) { - this.testMethodProcessors = testMethodProcessors; - } - - /** - * Returns a TestMethodProcessorList that supports all features that this package supports, except - * the following legacy features: - * - *

            - *
          • No support for {@link org.junit.runners.Parameterized} - *
          • No support for class and method-level parameters, except for @TestParameters - *
          - */ - public static TestMethodProcessorList createNewParameterizedProcessors() { - return new TestMethodProcessorList( - ImmutableList.of( - new TestParametersMethodProcessor(), - TestParameterAnnotationMethodProcessor.onlyForFieldsAndParameters())); - } - - static TestMethodProcessorList empty() { - return new TestMethodProcessorList(ImmutableList.of()); - } - - /** - * Calculates the TestInfo instances for the given test method. Each TestInfo corresponds to a - * single test. - * - *

          The returned list always contains at least one element. If there is no parameterization, - * this would be the TestInfo for running the test method without parameters. - */ - public List calculateTestInfos(Method testMethod, Class testClass) { - List testInfos = - ImmutableList.of( - TestInfo.createWithoutParameters( - testMethod, testClass, ImmutableList.copyOf(testMethod.getAnnotations()))); - - for (final TestMethodProcessor testMethodProcessor : testMethodProcessors) { - testInfos = - testInfos.stream() - .flatMap( - lastTestInfo -> testMethodProcessor.calculateTestInfos(lastTestInfo).stream()) - .collect(toList()); - } - - testInfos = TestInfo.deduplicateTestNames(TestInfo.shortenNamesIfNecessary(testInfos)); - - return testInfos; - } - - /** - * Returns the parameters with which it should be invoked. - * - *

          This method is never called for a parameterless constructor. - */ - public List getConstructorParameters(Constructor constructor, TestInfo testInfo) { - return testMethodProcessors.stream() - .map(processor -> processor.maybeGetConstructorParameters(constructor, testInfo)) - .filter(Optional::isPresent) - .map(Optional::get) - .findFirst() - .orElseThrow( - () -> - new IllegalStateException( - String.format( - "Could not generate parameter values for %s. Did you forget an annotation?", - constructor))); - } - - /** - * Returns the parameters with which {@code testInfo.getMethod()} should be invoked. - * - *

          This method is never called for a parameterless {@code testInfo.getMethod()}. - */ - public List getTestMethodParameters(TestInfo testInfo) { - return testMethodProcessors.stream() - .map(processor -> processor.maybeGetTestMethodParameters(testInfo)) - .filter(Optional::isPresent) - .map(Optional::get) - .findFirst() - .orElseThrow( - () -> - new IllegalStateException( - String.format( - "Could not generate parameter values for %s. Did you forget an annotation?", - testInfo.getMethod()))); - } - - /** - * Optionally process the test instance right after construction to ready it for the given test. - */ - public void postProcessTestInstance(Object testInstance, TestInfo testInfo) { - for (TestMethodProcessor testMethodProcessor : testMethodProcessors) { - testMethodProcessor.postProcessTestInstance(testInstance, testInfo); - } - } - - /** Optionally validates the given constructor. */ - public ExecutableValidationResult validateConstructor(Constructor constructor) { - return testMethodProcessors.stream() - .map(processor -> processor.validateConstructor(constructor)) - .filter(ExecutableValidationResult::wasValidated) - .findFirst() - .orElse(ExecutableValidationResult.notValidated()); - } - - /** Optionally validates the given method. */ - public ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass) { - return testMethodProcessors.stream() - .map(processor -> processor.validateTestMethod(testMethod, testClass)) - .filter(ExecutableValidationResult::wasValidated) - .findFirst() - .orElse(ExecutableValidationResult.notValidated()); - } -} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java deleted file mode 100644 index f31854d..0000000 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector.junit5; - -import static com.google.common.base.Preconditions.checkState; -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.RetentionPolicy.RUNTIME; -import 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.junit5.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. - * - *

          For enums and booleans, the values can be automatically derived as all possible values: - * - *

          - * {@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 }
          - * 
          - * - *

          The values can be explicitly defined as a parsed string: - * - *

          - * 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)]
          - * }
          - * 
          - * - *

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

          Types that are supported: - * - *

            - *
          • String: No parsing happens - *
          • boolean: Specified as YAML boolean - *
          • long and int: Specified as YAML integer - *
          • float and double: Specified as YAML floating point or integer - *
          • Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()} - *
          • Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML bytes - * (example: "!!binary 'ZGF0YQ=='") - *
          - * - *

          For dynamic sets of parameters or parameter types that are not supported here, use {@link - * #valuesProvider()} and leave this field empty. - * - *

          For examples, see {@link TestParameter}. - */ - String[] value() default {}; - - /** - * Sets a provider that will return a list of parameter values. - * - *

          If this field is set, {@link #value()} must be empty and vice versa. - * - *

          Example - * - *

          -   * {@literal @}Test
          -   * public void matchesAllOf_throwsOnNull(
          -   *     {@literal @}TestParameter(valuesProvider = CharMatcherProvider.class)
          -   *         CharMatcher charMatcher) {
          -   *   assertThrows(NullPointerException.class, () -> charMatcher.matchesAllOf(null));
          -   * }
          -   *
          -   * private static final class CharMatcherProvider implements TestParameterValuesProvider {
          -   *   {@literal @}Override
          -   *   public {@literal List} provideValues() {
          -   *     return ImmutableList.of(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace());
          -   *   }
          -   * }
          -   * 
          - */ - Class 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 provideValues() { - return ImmutableList.of(); - } - } - - /** Implementation of this parameter annotation. */ - final class InternalImplementationOfThisParameter implements TestParameterValueProvider { - @Override - public List provideValues( - Annotation uncastAnnotation, Optional> 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 annotationType, Optional> 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 getValuesFromProvider( - Class valuesProvider) { - try { - Constructor 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/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java deleted file mode 100644 index e169eb3..0000000 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector.junit5; - -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.base.Verify.verify; -import static java.lang.annotation.ElementType.ANNOTATION_TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import com.google.common.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. - * - *

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

          {@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();
          - *     }
          - * }
          - * }
          - * - *

          An alternative is to use a method parameter for injection: - * - *

          {@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();
          - *     }
          - * }
          - * }
          - * - *

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

          {@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();
          - *     }
          - * }
          - * }
          - * - *

          Class constructors can also be annotated with @TestParameterAnnotation annotations, as shown - * below: - * - *

          {@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() {...}
          - * }
          - * }
          - * - *

          Each field that needs to be injected from a parameter requires its dedicated distinct - * annotation. - * - *

          If the same annotation is defined both on the class and method, the method parameter values - * take precedence. - * - *

          If the same annotation is defined both on the class and constructor, the constructor parameter - * values take precedence. - * - *

          Annotations cannot be duplicated between the constructor or constructor parameters and a - * method or method parameter. - * - *

          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 validator() default DefaultValidator.class; - - /** Specifies a value provider for the parameter to provide the values to test. */ - Class valueProvider() default DefaultValueProvider.class; - - /** Default {@link TestParameterValidator} implementation which skips no test. */ - class DefaultValidator implements TestParameterValidator { - - @Override - public boolean shouldSkip(Context context) { - return false; - } - } - - /** - * Default {@link TestParameterValueProvider} implementation that gets its values from the - * annotation's `value` method. - */ - class DefaultValueProvider implements TestParameterValueProvider { - - @Override - public List provideValues(Annotation annotation, Optional> 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 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 annotationType, Optional> 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 annotationType) { - Method valueMethod; - try { - valueMethod = annotationType.getMethod("value"); - } catch (NoSuchMethodException e) { - throw new RuntimeException( - "The @TestParameterAnnotation annotation should have a single value() method.", e); - } - Object parameters; - try { - parameters = valueMethod.invoke(annotation); - } catch (InvocationTargetException e) { - if (e.getCause() instanceof IllegalAccessError) { - // There seems to be a bug or at least something weird with the JVM that causes - // IllegalAccessError to be thrown because the return value is not visible when it is a - // non-public nested type. See - // http://mail.openjdk.java.net/pipermail/core-libs-dev/2014-January/024180.html for more - // info. - throw new RuntimeException( - String.format( - "Could not access %s.value(). This is probably because %s is not visible to the" - + " annotation proxy. To fix this, make %s public.", - annotationType.getSimpleName(), - valueMethod.getReturnType().getSimpleName(), - valueMethod.getReturnType().getSimpleName())); - // Note: Not chaining the exception to reduce the clutter for the reader - } else { - throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e); - } - } catch (Exception e) { - throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e); - } - return parameters; - } - } -} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java deleted file mode 100644 index c06cb3a..0000000 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ /dev/null @@ -1,1290 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector.junit5; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.base.Verify.verify; -import static 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.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -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.junit5.TestInfo.TestInfoParameter; -import java.io.Serializable; -import java.lang.annotation.Annotation; -import java.lang.annotation.Retention; -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; -import java.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; - -/** - * {@code TestMethodProcessor} implementation for supporting parameterized tests annotated with - * {@link TestParameterAnnotation}. - * - * @see TestParameterAnnotation - */ -final class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { - - /** - * Class to hold an annotation type and origin and one of the values as returned by the {@code - * value()} method. - */ - @AutoValue - abstract static class 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 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> 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 paramName(); - - /** - * Returns a String that represents this value and is fit for use in a test name (between - * brackets). - */ - String toTestNameString() { - Class 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 create( - AnnotationWithMetadata annotationWithMetadata, Origin origin) { - List 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(/* onlyForFieldsAndParameters= */ false) - .getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()).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 getTestParameterValue( - TestInfo testInfo, Class annotationType) { - return getTestParameterValues(testInfo).getValue(annotationType); - } - - private static List getParametersAnnotationValues( - AnnotationWithMetadata annotationWithMetadata) { - Annotation annotation = annotationWithMetadata.annotation(); - TestParameterAnnotation testParameter = - annotation.annotationType().getAnnotation(TestParameterAnnotation.class); - Class 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 matches(Class 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 annotationType(); - - /** Where the annotation was declared. */ - abstract Origin origin(); - - public static AnnotationTypeOrigin create( - Class 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> 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 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 boolean onlyForFieldsAndParameters; - private final LoadingCache, ImmutableList> - annotationTypeOriginsCache = - CacheBuilder.newBuilder() - .maximumSize(1000) - .build(CacheLoader.from(this::calculateAnnotationTypeOrigins)); - private final Cache>> parameterValuesCache = - CacheBuilder.newBuilder().maximumSize(1000).build(); - - private TestParameterAnnotationMethodProcessor(boolean onlyForFieldsAndParameters) { - this.onlyForFieldsAndParameters = onlyForFieldsAndParameters; - } - - /** - * Constructs a new {@link TestMethodProcessor} that handles {@link - * TestParameterAnnotation}-annotated annotations that are placed anywhere: - * - *
            - *
          • At a method / constructor parameter - *
          • At a field - *
          • At a method / constructor on the class - *
          • At the test class - *
          - */ - static TestMethodProcessor forAllAnnotationPlacements() { - return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ false); - } - - /** - * Constructs a new {@link TestMethodProcessor} that handles {@link - * TestParameterAnnotation}-annotated annotations that are placed at fields or parameters. - * - *

          Note that this excludes class and method-level annotations, as is the default (using the - * constructor). - */ - static TestMethodProcessor onlyForFieldsAndParameters() { - return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ true); - } - - private ImmutableList calculateAnnotationTypeOrigins(Class testClass) { - // Collect all annotations used in declared fields and methods that have themselves a - // @TestParameterAnnotation annotation. - List fieldAnnotations = - extractTestParameterAnnotations( - streamWithParents(testClass) - .flatMap(c -> stream(c.getDeclaredFields())) - .flatMap(field -> stream(field.getAnnotations())), - Origin.FIELD); - List methodAnnotations = - extractTestParameterAnnotations( - stream(testClass.getMethods()).flatMap(method -> stream(method.getAnnotations())), - Origin.METHOD); - List parameterAnnotations = - extractTestParameterAnnotations( - streamWithParents(testClass) - .flatMap(c -> stream(c.getDeclaredMethods())) - .flatMap(method -> stream(method.getParameterAnnotations()).flatMap(Stream::of)), - Origin.METHOD_PARAMETER); - List classAnnotations = - extractTestParameterAnnotations(stream(testClass.getAnnotations()), Origin.CLASS); - List constructorAnnotations = - extractTestParameterAnnotations( - stream(testClass.getDeclaredConstructors()) - .flatMap(constructor -> stream(constructor.getAnnotations())), - Origin.CONSTRUCTOR); - List constructorParameterAnnotations = - extractTestParameterAnnotations( - stream(testClass.getDeclaredConstructors()) - .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); - } - - return 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()); - } - - private ImmutableList getAnnotationTypeOrigins( - Class testClass, Origin firstOrigin, Origin... otherOrigins) { - Set originsToFilterBy = - ImmutableSet.builder().add(firstOrigin).add(otherOrigins).build(); - try { - return annotationTypeOriginsCache.getUnchecked(testClass).stream() - .filter(annotationTypeOrigin -> originsToFilterBy.contains(annotationTypeOrigin.origin())) - .collect(toImmutableList()); - } catch (UncheckedExecutionException e) { - Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); - throw e; - } - } - - private void checkDuplicatedFieldsAnnotations( - List methodAnnotations, List 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> 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 constructorAnnotations, - List classAnnotations, - List fieldAnnotations) { - ImmutableSet> classAnnotationTypes = - classAnnotations.stream() - .map(AnnotationTypeOrigin::annotationType) - .collect(toImmutableSet()); - - ImmutableSet> uniqueFieldAnnotations = - fieldAnnotations.stream() - .map(AnnotationTypeOrigin::annotationType) - .collect(toImmutableSet()); - ImmutableSet> 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 extractTestParameterAnnotations( - Stream 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 ExecutableValidationResult validateConstructor(Constructor constructor) { - Class[] parameterTypes = constructor.getParameterTypes(); - if (parameterTypes.length == 0) { - return ExecutableValidationResult.notValidated(); - } - // The constructor has parameters, they must be injected by a TestParameterAnnotation - // annotation. - Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); - Class testClass = constructor.getDeclaringClass(); - return ExecutableValidationResult.validated( - validateMethodOrConstructorParameters( - removeOverrides( - getAnnotationTypeOrigins( - testClass, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER), - testClass), - testClass, - constructor, - parameterTypes, - parameterAnnotations)); - } - - @Override - public ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass) { - Class[] methodParameterTypes = testMethod.getParameterTypes(); - if (methodParameterTypes.length == 0) { - return ExecutableValidationResult.notValidated(); - } else { - // The method has parameters, they must be injected by a TestParameterAnnotation annotation. - return ExecutableValidationResult.validated( - validateMethodOrConstructorParameters( - getAnnotationTypeOrigins( - testClass, Origin.CLASS, Origin.METHOD, Origin.METHOD_PARAMETER), - testClass, - testMethod, - methodParameterTypes, - testMethod.getParameterAnnotations())); - } - } - - private List validateMethodOrConstructorParameters( - List annotationTypeOrigins, - Class testClass, - AnnotatedElement methodOrConstructor, - Class[] parameterTypes, - Annotation[][] parametersAnnotations) { - List errors = new ArrayList<>(); - - for (int parameterIndex = 0; parameterIndex < parameterTypes.length; parameterIndex++) { - Class parameterType = parameterTypes[parameterIndex]; - Annotation[] parameterAnnotations = parametersAnnotations[parameterIndex]; - boolean matchingTestParameterAnnotationFound = false; - // First, handle the case where the method parameter specifies the test parameter explicitly, - // e.g. {@code public void test(@ColorParameter({...}) Color c)}. - for (AnnotationTypeOrigin testParameterAnnotationType : annotationTypeOrigins) { - for (Annotation parameterAnnotation : parameterAnnotations) { - if (parameterAnnotation - .annotationType() - .equals(testParameterAnnotationType.annotationType())) { - // Verify that the type is assignable with the return type of the 'value' method. - Class valueMethodReturnType = - getValueMethodReturnType( - testParameterAnnotationType.annotationType(), - /* paramClass = */ Optional.of(parameterType)); - if (!parameterType.isAssignableFrom(valueMethodReturnType)) { - errors.add( - new IllegalStateException( - String.format( - "Parameter of type %s annotated with %s does not match" - + " expected type %s in method/constructor %s", - parameterType.getName(), - testParameterAnnotationType.annotationType().getName(), - valueMethodReturnType.getName(), - methodOrConstructor))); - } else { - matchingTestParameterAnnotationFound = true; - } - } - } - } - // Second, handle the case where the method parameter does not specify the test parameter, - // and instead relies on the type matching, e.g. {@code public void test(Color c)}. - if (!matchingTestParameterAnnotationFound) { - List> testParameterAnnotationTypes = - getTestParameterAnnotations( - // Do not include METHOD_PARAMETER or CONSTRUCTOR_PARAMETER since they have already - // been evaluated. - filterAnnotationTypeOriginsByOrigin( - annotationTypeOrigins, Origin.CLASS, Origin.CONSTRUCTOR, Origin.METHOD), - testClass, - methodOrConstructor); - // If no annotation is present, simply compare the type. - for (Class testParameterAnnotationType : - testParameterAnnotationTypes) { - if (parameterType.isAssignableFrom( - getValueMethodReturnType( - testParameterAnnotationType, /* paramClass = */ Optional.absent()))) { - if (matchingTestParameterAnnotationFound) { - errors.add( - new IllegalStateException( - String.format( - "Ambiguous method/constructor parameter type, matching multiple" - + " annotations for parameter of type %s in method %s", - parameterType.getName(), methodOrConstructor))); - } - matchingTestParameterAnnotationFound = true; - } - } - } - if (!matchingTestParameterAnnotationFound) { - errors.add( - new IllegalStateException( - String.format( - "No matching test parameter annotation found" - + " for parameter of type %s in method/constructor %s", - parameterType.getName(), methodOrConstructor))); - } - } - return errors; - } - - @Override - public Optional> maybeGetConstructorParameters( - Constructor constructor, TestInfo testInfo) { - if (testInfo.getAnnotation(TestIndexHolder.class) == null - // Explicitly skip @TestParameters annotated methods to ensure compatibility. - // - // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported) - // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;). - // Without the @TestParameters check below, this class would try to find parameters for - // these methods. When there are no method parameters, this is a no-op, but when the method - // is annotated with @TestParameters, this throws an exception (because there are method - // parameters that this processor has no values for - they are provided by the - // @TestParameters processor). - || constructor.isAnnotationPresent(TestParameters.class)) { - return Optional.absent(); - } else { - TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); - List testParameterValues = - getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()); - - Class[] parameterTypes = constructor.getParameterTypes(); - Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); - List parameterValues = new ArrayList<>(/* initialCapacity= */ parameterTypes.length); - List> processedAnnotationTypes = new ArrayList<>(); - List parameterValuesForConstructor = - filterByOrigin( - testParameterValues, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER); - for (int i = 0; i < parameterTypes.length; i++) { - // Initialize each parameter value from the corresponding TestParameterAnnotation value. - parameterValues.add( - getParameterValue( - parameterValuesForConstructor, - parameterTypes[i], - parameterAnnotations[i], - processedAnnotationTypes)); - } - return Optional.of(parameterValues); - } - } - - @Override - public Optional> maybeGetTestMethodParameters(TestInfo testInfo) { - Method testMethod = testInfo.getMethod(); - if (testInfo.getAnnotation(TestIndexHolder.class) == null - // Explicitly skip @TestParameters annotated methods to ensure compatibility. - // - // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported) - // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;). - // Without the @TestParameters check below, this class would try to find parameters for - // these methods. When there are no method parameters, this is a no-op, but when the method - // is annotated with @TestParameters, this throws an exception (because there are method - // parameters that this processor has no values for - they are provided by the - // @TestParameters processor). - || testMethod.isAnnotationPresent(TestParameters.class)) { - return Optional.absent(); - } else { - TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); - checkState(testIndexHolder != null); - List testParameterValues = - filterByOrigin( - getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()), - Origin.CLASS, - Origin.METHOD, - Origin.METHOD_PARAMETER); - - Class[] parameterTypes = testMethod.getParameterTypes(); - Annotation[][] parametersAnnotations = testMethod.getParameterAnnotations(); - ArrayList parameterValues = - new ArrayList<>(/* initialCapacity= */ parameterTypes.length); - - List> processedAnnotationTypes = new ArrayList<>(); - for (int i = 0; i < parameterTypes.length; i++) { - parameterValues.add( - getParameterValue( - testParameterValues, - parameterTypes[i], - parametersAnnotations[i], - processedAnnotationTypes)); - } - - return Optional.of(parameterValues); - } - } - - /** - * Returns the {@link TestInfo}, one for each result of the cartesian product of each test - * parameter values. - * - *

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

          For multiple annotations (say, {@code @TestParameter("foo", "bar")} and - * {@code @ColorParameter({BLUE, WHITE})}), it will generate the following result: - * - *

            - *
          • ("foo", BLUE) - *
          • ("foo", WHITE) - *
          • ("bar", BLUE) - *
          • ("bar", WHITE) - *
          • - *
          - * - * corresponding to the cartesian product of both annotations. - */ - @Override - public List calculateTestInfos(TestInfo originalTest) { - List> parameterValuesForMethod = - getParameterValuesForMethod(originalTest.getMethod(), originalTest.getTestClass()); - - if (parameterValuesForMethod.equals(ImmutableList.of(ImmutableList.of()))) { - // This test is not parameterized - return ImmutableList.of(originalTest); - } - - ImmutableList.Builder testInfos = ImmutableList.builder(); - for (int parametersIndex = 0; - parametersIndex < parameterValuesForMethod.size(); - ++parametersIndex) { - List 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(originalTest.getTestClass()), - originalTest.getMethod()), - parametersIndex, - originalTest.getTestClass().getName()))); - } - - return testInfos.build(); - } - - private List> getParameterValuesForMethod( - Method method, Class testClass) { - try { - return parameterValuesCache.get( - method, - () -> { - List> testParameterValuesList = - getAnnotationValuesForUsedAnnotationTypes(method, testClass); - - 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 getParameterValuesForTest( - TestIndexHolder testIndexHolder, Class testClass) { - verify( - testIndexHolder.testClassName().equals(testClass.getName()), - "The class for which the given annotation was created (%s) is not the same as the test" - + " class that this runner is handling (%s)", - testIndexHolder.testClassName(), - testClass.getName()); - Method testMethod = getMethodsIncludingParents(testClass).get(testIndexHolder.methodIndex()); - return getParameterValuesForMethod(testMethod, testClass) - .get(testIndexHolder.parametersIndex()); - } - - /** - * Returns the list of annotation index for all annotations defined in a given test method and its - * class. - */ - private ImmutableList> getAnnotationValuesForUsedAnnotationTypes( - Method method, Class testClass) { - ImmutableList annotationTypes = - Stream.of( - getAnnotationTypeOrigins(testClass, Origin.CLASS).stream(), - getAnnotationTypeOrigins(testClass, Origin.FIELD).stream(), - getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR).stream(), - getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR_PARAMETER).stream(), - getAnnotationTypeOrigins(testClass, Origin.METHOD).stream(), - getAnnotationTypeOrigins(testClass, 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 annotationComparator( - Annotation[][] parameterAnnotations) { - ImmutableList 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}. - * - *

          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 removeOverrides( - List 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 removeOverrides( - List 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(); - default: - return true; - } - }) - .collect(toCollection(ArrayList::new)); - } - - /** - * Returns the given annotations defined either on the method parameters, method or the test - * class. - * - *

          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> getAnnotationFromParametersOrTestOrClass( - AnnotationTypeOrigin annotationTypeOrigin, Method method, Class testClass) { - Origin origin = annotationTypeOrigin.origin(); - Class annotationType = annotationTypeOrigin.annotationType(); - if (origin == Origin.CONSTRUCTOR_PARAMETER) { - Constructor constructor = getOnlyConstructor(testClass); - List 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 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 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> toTestParameterValueList( - List annotationWithMetadatas, Origin origin) { - return annotationWithMetadatas.stream() - .map(annotationWithMetadata -> TestParameterValue.create(annotationWithMetadata, origin)) - .collect(toImmutableList()); - } - - private static ImmutableList getAnnotationWithMetadataListWithType( - Method callable, Class annotationType) { - try { - return getAnnotationWithMetadataListWithType(callable.getParameters(), annotationType); - } catch (NoSuchMethodError ignored) { - return getAnnotationWithMetadataListWithType( - callable.getParameterTypes(), callable.getParameterAnnotations(), annotationType); - } - } - - private static ImmutableList getAnnotationWithMetadataListWithType( - Constructor callable, Class 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 getAnnotationWithMetadataListWithType( - Parameter[] parameters, Class 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 getAnnotationWithMetadataListWithType( - Class[] parameterTypes, - Annotation[][] annotations, - Class annotationType) { - checkArgument(parameterTypes.length == annotations.length); - - ImmutableList.Builder 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 getAnnotationListWithType( - Annotation[] annotations, Class annotationType) { - return stream(annotations) - .filter(annotation -> annotation.annotationType().equals(annotationType)) - .collect(toImmutableList()); - } - - private static Constructor getOnlyConstructor(Class testClass) { - Constructor[] constructors = testClass.getDeclaredConstructors(); - checkState( - constructors.length == 1, - "a single public constructor is required for class %s", - testClass); - return constructors[0]; - } - - @Override - public void postProcessTestInstance(Object testInstance, TestInfo testInfo) { - TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); - try { - if (testIndexHolder != null) { - List testParameterValues = - getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()); - - // Do not include {@link Origin#METHOD_PARAMETER} nor {@link Origin#CONSTRUCTOR_PARAMETER} - // annotations. - List testParameterValuesForFieldInjection = - filterByOrigin(testParameterValues, Origin.CLASS, Origin.FIELD, Origin.METHOD); - // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class - // in the example above. - List remainingTestParameterValuesForFieldInjection = - new ArrayList<>(testParameterValuesForFieldInjection); - for (Field declaredField : - streamWithParents(testInstance.getClass()) - .flatMap(c -> stream(c.getDeclaredFields())) - .collect(toImmutableList())) { - for (TestParameterValue testParameterValue : - remainingTestParameterValuesForFieldInjection) { - if (declaredField.isAnnotationPresent( - testParameterValue.annotationTypeOrigin().annotationType())) { - declaredField.setAccessible(true); - declaredField.set(testInstance, testParameterValue.value()); - remainingTestParameterValuesForFieldInjection.remove(testParameterValue); - break; - } - } - } - } - } catch (IllegalAccessException 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 filterByOrigin( - List testParameterValues, Origin... origins) { - Set 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 filterAnnotationTypeOriginsByOrigin( - List annotationTypeOrigins, Origin... origins) { - List originList = Arrays.asList(origins); - return annotationTypeOrigins.stream() - .filter(annotationTypeOrigin -> originList.contains(annotationTypeOrigin.origin())) - .collect(toImmutableList()); - } - - /** Returns a {@link TestParameterAnnotation}'s value for a method or constructor parameter. */ - private Object getParameterValue( - List testParameterValues, - Class methodParameterType, - Annotation[] parameterAnnotations, - List> processedAnnotationTypes) { - List> 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 annotationType = - testParameterValue.annotationTypeOrigin().annotationType(); - if (parameterAnnotation.annotationType().equals(annotationType)) { - // If multiple annotations exist, ensure that the proper one is selected. - // For instance, for: - // - // test(@FooParameter(1,2) Foo foo, @FooParameter(3,4) Foo bar) {} - // - // 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}. - */ - int parametersIndex(); - - /** - * The full name of the test class. Only used for verifying that assumptions about the above - * indices are valid. - */ - String testClassName(); - } - - /** Factory for {@link TestIndexHolder}. */ - static class TestIndexHolderFactory { - @AutoAnnotation - static TestIndexHolder create(int methodIndex, int parametersIndex, String testClassName) { - return new AutoAnnotation_TestParameterAnnotationMethodProcessor_TestIndexHolderFactory_create( - methodIndex, parametersIndex, testClassName); - } - - private TestIndexHolderFactory() {} - } - - /** - * Returns whether the test should be skipped according to the {@code annotationType}'s {@link - * TestParameterValidator} and the current list of {@link TestParameterValue}. - */ - private static boolean callShouldSkip( - Class annotationType, List testParameterValues) { - TestParameterAnnotation annotation = - annotationType.getAnnotation(TestParameterAnnotation.class); - Class 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 testParameterValues; - private final Set valueList; - - public ValidatorContext(List testParameterValues) { - this.testParameterValues = testParameterValues; - this.valueList = testParameterValues.stream().map(TestParameterValue::value).collect(toSet()); - } - - @Override - public boolean has(Class testParameter, Object value) { - return getValue(testParameter).transform(value::equals).or(false); - } - - @Override - public , U extends Enum> boolean has(T value1, U value2) { - return valueList.contains(value1) && valueList.contains(value2); - } - - @Override - public Optional getValue(Class testParameter) { - return getParameter(testParameter).transform(TestParameterValue::value); - } - - @Override - public List getSpecifiedValues(Class testParameter) { - return getParameter(testParameter) - .transform(TestParameterValue::specifiedValues) - .or(ImmutableList.of()); - } - - private Optional getParameter(Class 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 annotationType, Optional> paramClass) { - TestParameterAnnotation testParameter = - annotationType.getAnnotation(TestParameterAnnotation.class); - Class 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> getTestParameterAnnotations( - List annotationTypeOrigins, - final Class testClass, - AnnotatedElement methodOrConstructor) { - return annotationTypeOrigins.stream() - .map(AnnotationTypeOrigin::annotationType) - .filter( - annotationType -> - testClass.isAnnotationPresent(annotationType) - || methodOrConstructor.isAnnotationPresent(annotationType)) - .collect(toImmutableList()); - } - - private int strictIndexOf(List haystack, T needle) { - int index = haystack.indexOf(needle); - checkArgument(index >= 0, "Could not find '%s' in %s", needle, haystack); - return index; - } - - private ImmutableList getMethodsIncludingParents(Class clazz) { - ImmutableList.Builder resultBuilder = ImmutableList.builder(); - while (clazz != null) { - resultBuilder.add(clazz.getDeclaredMethods()); - clazz = clazz.getSuperclass(); - } - return resultBuilder.build(); - } - - private static Stream> streamWithParents(Class clazz) { - Stream.Builder> 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 Collector> toImmutableList() { - return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf); - } - - private static Collector> toImmutableSet() { - return Collectors.collectingAndThen(Collectors.toList(), ImmutableSet::copyOf); - } -} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorExtension.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorExtension.java deleted file mode 100644 index 6d6aa51..0000000 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorExtension.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector.junit5; - -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.collect.Iterables.getOnlyElement; - -import com.google.auto.value.AutoValue; -import com.google.auto.value.extension.memoized.Memoized; -import com.google.common.collect.ImmutableList; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.util.List; -import java.util.stream.Stream; -import org.junit.jupiter.api.extension.Extension; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.ParameterContext; -import org.junit.jupiter.api.extension.ParameterResolver; -import org.junit.jupiter.api.extension.TestInstancePostProcessor; -import org.junit.jupiter.api.extension.TestTemplateInvocationContext; -import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; - -/** Implements the TestParameterInjector logic for JUnit5 (Jupiter). */ -class TestParameterInjectorExtension implements TestTemplateInvocationContextProvider { - - private static final TestMethodProcessorList testMethodProcessors = - TestMethodProcessorList.createNewParameterizedProcessors(); - - @Override - public boolean supportsTestTemplate(ExtensionContext context) { - return true; - } - - @Override - public Stream provideTestTemplateInvocationContexts( - ExtensionContext extensionContext) { - validateTestMethodAndConstructor( - extensionContext.getRequiredTestMethod(), extensionContext.getRequiredTestClass()); - List testInfos = - testMethodProcessors.calculateTestInfos( - extensionContext.getRequiredTestMethod(), extensionContext.getRequiredTestClass()); - - return testInfos.stream().map(CustomInvocationContext::of); - } - - private void validateTestMethodAndConstructor(Method testMethod, Class testClass) { - checkState( - testClass.getDeclaredConstructors().length == 1, - "Only a single constructor is allowed, but found %s in %s", - testClass.getDeclaredConstructors().length, - testClass.getSimpleName()); - Constructor constructor = - getOnlyElement(ImmutableList.copyOf(testClass.getDeclaredConstructors())); - - testMethodProcessors.validateConstructor(constructor).assertValid(); - - testMethodProcessors.validateTestMethod(testMethod, testClass).assertValid(); - - checkState( - testMethod.getAnnotation(TestParameterInjectorTest.class) != null, - "Each test method handled by this extension should be annotated with" - + " @TestParameterInjectorTest"); - } - - @AutoValue - abstract static class CustomInvocationContext implements TestTemplateInvocationContext { - - abstract TestInfo testInfo(); - - @Memoized - List getConstructorParameters() { - Constructor constructor = - getOnlyElement(ImmutableList.copyOf(testInfo().getTestClass().getDeclaredConstructors())); - - return testMethodProcessors.getConstructorParameters(constructor, testInfo()); - } - - @Memoized - List getTestMethodParameters() { - return testMethodProcessors.getTestMethodParameters(testInfo()); - } - - static CustomInvocationContext of(TestInfo testInfo) { - return new AutoValue_TestParameterInjectorExtension_CustomInvocationContext(testInfo); - } - - @Override - public String getDisplayName(int invocationIndex) { - return testInfo().getName(); - } - - @Override - public List getAdditionalExtensions() { - return ImmutableList.of(new CustomAdditionalExtension()); - } - - class CustomAdditionalExtension implements ParameterResolver, TestInstancePostProcessor { - - @Override - public boolean supportsParameter( - ParameterContext parameterContext, ExtensionContext extensionContext) { - if (parameterContext.getDeclaringExecutable() instanceof Constructor) { - return true; - } else { - return parameterContext - .getDeclaringExecutable() - .isAnnotationPresent(TestParameterInjectorTest.class); - } - } - - @Override - public Object resolveParameter( - ParameterContext parameterContext, ExtensionContext extensionContext) { - if (parameterContext.getDeclaringExecutable() instanceof Constructor) { - return getConstructorParameters().get(parameterContext.getIndex()); - } else { - return getTestMethodParameters().get(parameterContext.getIndex()); - } - } - - @Override - public void postProcessTestInstance(Object testInstance, ExtensionContext extensionContext) - throws Exception { - testMethodProcessors.postProcessTestInstance(testInstance, testInfo()); - } - } - } -} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorTest.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorTest.java deleted file mode 100644 index e17179a..0000000 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorTest.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector.junit5; - -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; -import org.junit.jupiter.api.TestTemplate; -import org.junit.jupiter.api.extension.ExtendWith; - -/** - * Replacement for JUnit5's @Test for test methods that want to use @TestParameter[s]. - * - *

          Example: - * - *

          - * class MyTest {
          - *   {@literal @}TestParameterInjectorTest
          - *   void withParameter_success({@literal @}TestParameter boolean bool) {
          - *     // ...
          - *   }
          - *
          - *   {@literal @}TestParameterInjectorTest
          - *   {@literal @}TestParameters("{name: 1, number: 3.3}")
          - *   {@literal @}TestParameters("{name: abc, number: 5}")
          - *   void withParameters_success(String name, double number) {
          - *     // ...
          - *   }
          - * }
          - * 
          - */ -@TestTemplate -@ExtendWith(TestParameterInjectorExtension.class) -@Retention(RUNTIME) -@Target({METHOD}) -public @interface TestParameterInjectorTest {} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java deleted file mode 100644 index 70db746..0000000 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector.junit5; - -import com.google.common.base.Optional; -import java.lang.annotation.Annotation; -import java.util.List; - -/** - * Validator interface which allows {@link TestParameterAnnotation} annotations to validate the set - * of annotation values for a given test instance, and to selectively skip the test. - */ -interface TestParameterValidator { - - /** - * This interface allows to access information on the current testwhen implementing {@link - * TestParameterValidator}. - */ - interface Context { - - /** Returns whether the current test has the {@link TestParameterAnnotation} value(s). */ - boolean has(Class 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. - */ - , U extends Enum> boolean has(T value1, U value2); - - /** - * Returns all the current test value for a given {@link TestParameterAnnotation} annotated - * annotation. - */ - Optional getValue(Class testParameter); - - /** - * Returns all the values specified for a given {@link TestParameterAnnotation} annotated - * annotation in the test. - * - *

          For example, if the test annotates '@Foo(a,b,c)', getSpecifiedValues(Foo.class) will - * return [a,b,c]. - */ - List getSpecifiedValues(Class testParameter); - } - - /** - * Returns whether the test should be skipped based on the annotations' values. - * - *

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

          This method is not invoked in the context of a running test statement. - */ - boolean shouldSkip(Context context); -} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java deleted file mode 100644 index 47ed601..0000000 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector.junit5; - -import 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 provideValues(Annotation annotation, Optional> 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 annotationType, Optional> parameterClass); -} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java deleted file mode 100644 index b2c88a6..0000000 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector.junit5; - -import com.google.common.base.Optional; -import java.lang.annotation.Annotation; - -/** Interface to retrieve the {@link TestParameterAnnotation} values for a test. */ -interface TestParameterValues { - /** - * Returns a {@link TestParameterAnnotation} value for the current test as specified by {@code - * testInfo}, or {@link Optional#absent()} if the {@code annotationType} is not found. - */ - Optional getValue(Class annotationType); -} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java deleted file mode 100644 index 3a3c40c..0000000 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector.junit5; - -import static com.google.common.base.Preconditions.checkState; -import static java.lang.annotation.ElementType.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.Repeatable; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import javax.annotation.Nullable; - -/** - * Annotation that can be placed (repeatedly) on @Test-methods or a test constructor to indicate the - * sets of parameters that it should be invoked with. - * - *

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

          Note: If this annotation is used in a test class, the other methods in that class can still - * use other types of parameterization, such as {@linkplain TestParameter @TestParameter}. - * - *

          See {@link #value()} for simple examples. - * - *

          Warning: This annotation can only be used if the compiled java code contains the parameter - * names. This is typically done by passing the {@code -parameters} option to the Java compiler, - * which requires using Java 8 or higher and may not be available on Android. - */ -@Retention(RUNTIME) -@Target({CONSTRUCTOR, METHOD}) -@Repeatable(TestParameters.RepeatedTestParameters.class) -public @interface TestParameters { - - /** - * Specifies one or more stringified sets of parameters in YAML format. Each set corresponds to a - * single invocation of a test method. - * - *

          Each element in this array is a full parameter set, formatted as a YAML mapping. The mapping - * keys must match the parameter names and the mapping values will be converted to the parameter - * type if possible. See yaml.org for the YAML syntax and the section below on the supported - * parameter types. - * - *

          There are two distinct ways of using this annotation: repeated vs single: - * - *

          Recommended usage: Separate annotation per parameter set - * - *

          This approach uses multiple @TestParameters annotations, one for each set of parameters, for - * example: - * - *

          -   * {@literal @}Test
          -   * {@literal @}TestParameters("{age: 17, expectIsAdult: false}")
          -   * {@literal @}TestParameters("{age: 22, expectIsAdult: true}")
          -   * public void personIsAdult(int age, boolean expectIsAdult) { ... }
          -   *
          -   * {@literal @}Test
          -   * {@literal @}TestParameters("{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}")
          -   * {@literal @}TestParameters("{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}")
          -   * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... }
          -   * 
          - * - *

          Old discouraged usage: Single annotation with all parameter sets - * - *

          This approach uses a single @TestParameter annotation for all parameter sets, for example: - * - *

          -   * {@literal @}Test
          -   * {@literal @}TestParameters({
          -   *   "{age: 17, expectIsAdult: false}",
          -   *   "{age: 22, expectIsAdult: true}",
          -   * })
          -   * public void personIsAdult(int age, boolean expectIsAdult) { ... }
          -   *
          -   * {@literal @}Test
          -   * {@literal @}TestParameters({
          -   *   "{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}",
          -   *   "{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}",
          -   * })
          -   * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... }
          -   * 
          - * - *

          Supported parameter types - * - *

            - *
          • YAML primitives: - *
              - *
            • String: Specified as YAML string - *
            • boolean: Specified as YAML boolean - *
            • long and int: Specified as YAML integer - *
            • float and double: Specified as YAML floating point or integer - *
            - *
          • - *
          • Parsed types: - *
              - *
            • Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()} - *
            • Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML - * bytes (example: "!!binary 'ZGF0YQ=='") - *
            - *
          • - *
          - * - *

          For dynamic sets of parameters or parameter types that are not supported here, use {@link - * #valuesProvider()} and leave this field empty. - */ - String[] value() default {}; - - /** - * Overrides the name of the parameter set that is used in the test name. - * - *

          This can only be set if {@link #value()} has exactly one element. If not set, the YAML - * string in {@link #value()} is used in the test name. - * - *

          For example: If this name is set to "young adult", then the test name might be - * "personIsAdult[young adult]" where the default might have been "personIsAdult[{age: 17, - * expectIsAdult: false}]". - */ - String customName() default ""; - - /** - * Sets a provider that will return a list of parameter sets. Each element in the returned list - * corresponds to a single invocation of a test method. - * - *

          If this field is set, {@link #value()} must be empty and vice versa. - * - *

          Example - * - *

          -   * {@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} 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()
          -   *     );
          -   *   }
          -   * }
          -   * 
          - */ - Class valuesProvider() default - DefaultTestParametersValuesProvider.class; - - /** Interface for custom providers of test parameter values. */ - interface TestParametersValuesProvider { - List 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. - * - *

          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 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 parametersMap = new LinkedHashMap<>(); - - /** - * Sets a name for this set of parameters that will be used for describing this test. - * - *

          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 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 provideValues() { - return ImmutableList.of(); - } - } - - /** - * Holder annotation for multiple @TestParameters annotations. This should never be used directly. - */ - @Retention(RUNTIME) - @Target({CONSTRUCTOR, METHOD}) - @interface RepeatedTestParameters { - TestParameters[] value(); - } -} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java deleted file mode 100644 index 4879ca7..0000000 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java +++ /dev/null @@ -1,485 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector.junit5; - -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.base.Verify.verify; -import static com.google.common.collect.Iterables.getOnlyElement; -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toList; - -import com.google.auto.value.AutoAnnotation; -import com.google.common.base.Optional; -import com.google.common.base.Throwables; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Maps; -import com.google.common.primitives.Primitives; -import com.google.common.reflect.TypeToken; -import com.google.common.util.concurrent.UncheckedExecutionException; -import com.google.testing.junit.testparameterinjector.junit5.TestInfo.TestInfoParameter; -import com.google.testing.junit.testparameterinjector.junit5.TestParameters.DefaultTestParametersValuesProvider; -import com.google.testing.junit.testparameterinjector.junit5.TestParameters.RepeatedTestParameters; -import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValues; -import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValuesProvider; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.reflect.Constructor; -import java.lang.reflect.Executable; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.Parameter; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collector; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** {@code TestMethodProcessor} implementation for supporting {@link TestParameters}. */ -@SuppressWarnings("AndroidJdkLibsChecker") // Parameter is not available on old Android SDKs. -final class TestParametersMethodProcessor implements TestMethodProcessor { - - private final LoadingCache> - parameterValuesByConstructorOrMethodCache = - CacheBuilder.newBuilder() - .maximumSize(1000) - .build(CacheLoader.from(TestParametersMethodProcessor::toParameterValuesList)); - - @Override - public ExecutableValidationResult validateConstructor(Constructor constructor) { - if (hasRelevantAnnotation(constructor)) { - try { - // This method throws an exception if there is a validation error - getConstructorParameters(constructor); - } catch (Throwable t) { - return ExecutableValidationResult.validated(t); - } - return ExecutableValidationResult.valid(); - } else { - return ExecutableValidationResult.notValidated(); - } - } - - @Override - public ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass) { - if (hasRelevantAnnotation(testMethod)) { - try { - // This method throws an exception if there is a validation error - getMethodParameters(testMethod); - } catch (Throwable t) { - return ExecutableValidationResult.validated(t); - } - return ExecutableValidationResult.valid(); - } else { - return ExecutableValidationResult.notValidated(); - } - } - - @Override - public List calculateTestInfos(TestInfo originalTest) { - boolean constructorIsParameterized = - hasRelevantAnnotation(getOnlyConstructor(originalTest.getTestClass())); - boolean methodIsParameterized = hasRelevantAnnotation(originalTest.getMethod()); - - if (!constructorIsParameterized && !methodIsParameterized) { - return ImmutableList.of(originalTest); - } - - ImmutableList.Builder testInfos = ImmutableList.builder(); - - ImmutableList> constructorParametersList = - getConstructorParametersOrSingleAbsentElement(originalTest.getTestClass()); - ImmutableList> methodParametersList = - getMethodParametersOrSingleAbsentElement(originalTest.getMethod()); - for (int constructorParametersIndex = 0; - constructorParametersIndex < constructorParametersList.size(); - ++constructorParametersIndex) { - Optional constructorParameters = - constructorParametersList.get(constructorParametersIndex); - - for (int methodParametersIndex = 0; - methodParametersIndex < methodParametersList.size(); - ++methodParametersIndex) { - Optional 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> - getConstructorParametersOrSingleAbsentElement(Class testClass) { - Constructor constructor = getOnlyConstructor(testClass); - return hasRelevantAnnotation(constructor) - ? getConstructorParameters(constructor).stream() - .map(Optional::of) - .collect(toImmutableList()) - : ImmutableList.of(Optional.absent()); - } - - private ImmutableList> getMethodParametersOrSingleAbsentElement( - Method method) { - return hasRelevantAnnotation(method) - ? getMethodParameters(method).stream().map(Optional::of).collect(toImmutableList()) - : ImmutableList.of(Optional.absent()); - } - - @Override - public Optional> maybeGetConstructorParameters( - Constructor constructor, TestInfo testInfo) { - if (hasRelevantAnnotation(constructor)) { - ImmutableList parameterValuesList = - getConstructorParameters(constructor); - TestParametersValues parametersValues = - parameterValuesList.get( - testInfo.getAnnotation(TestIndexHolder.class).constructorParametersIndex()); - - return Optional.of(toParameterList(parametersValues, constructor.getParameters())); - } else { - return Optional.absent(); - } - } - - @Override - public Optional> maybeGetTestMethodParameters(TestInfo testInfo) { - Method testMethod = testInfo.getMethod(); - if (hasRelevantAnnotation(testMethod)) { - ImmutableList parameterValuesList = getMethodParameters(testMethod); - TestParametersValues parametersValues = - parameterValuesList.get( - testInfo.getAnnotation(TestIndexHolder.class).methodParametersIndex()); - - return Optional.of(toParameterList(parametersValues, testMethod.getParameters())); - } else { - return Optional.absent(); - } - } - - @Override - public void postProcessTestInstance(Object testInstance, TestInfo testInfo) {} - - private ImmutableList getConstructorParameters(Constructor constructor) { - try { - return parameterValuesByConstructorOrMethodCache.getUnchecked(constructor); - } catch (UncheckedExecutionException e) { - // Rethrow IllegalStateException because they can be caused by user mistakes and the user - // doesn't need to know that the caching layer is in between. - Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); - throw e; - } - } - - private ImmutableList getMethodParameters(Method method) { - try { - return parameterValuesByConstructorOrMethodCache.getUnchecked(method); - } catch (UncheckedExecutionException e) { - // Rethrow IllegalStateException because they can be caused by user mistakes and the user - // doesn't need to know that the caching layer is in between. - Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); - throw e; - } - } - - private static ImmutableList toParameterValuesList(Executable executable) { - checkParameterNamesArePresent(executable); - ImmutableList parametersList = ImmutableList.copyOf(executable.getParameters()); - - if (executable.isAnnotationPresent(TestParameters.class)) { - checkState( - !executable.isAnnotationPresent(RepeatedTestParameters.class), - "Unexpected situation: Both @TestParameters and @RepeatedTestParameters annotating the" - + " same method"); - TestParameters annotation = executable.getAnnotation(TestParameters.class); - boolean valueIsSet = annotation.value().length > 0; - boolean valuesProviderIsSet = - !annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class); - - checkState( - !(valueIsSet && valuesProviderIsSet), - "It is not allowed to specify both value and valuesProvider in @TestParameters(value=%s," - + " valuesProvider=%s) on %s()", - Arrays.toString(annotation.value()), - annotation.valuesProvider().getSimpleName(), - executable.getName()); - checkState( - valueIsSet || valuesProviderIsSet, - "Either a value or a valuesProvider must be set in @TestParameters on %s()", - executable.getName()); - if (!annotation.customName().isEmpty()) { - checkState( - annotation.value().length == 1, - "Setting @TestParameters.customName is only allowed if there is exactly one YAML string" - + " in @TestParameters.value (on %s())", - executable.getName()); - } - - if (valueIsSet) { - return stream(annotation.value()) - .map(yamlMap -> toParameterValues(yamlMap, parametersList, annotation.customName())) - .collect(toImmutableList()); - } else { - return toParameterValuesList(annotation.valuesProvider(), parametersList); - } - } else { // Not annotated with @TestParameters - verify( - executable.isAnnotationPresent(RepeatedTestParameters.class), - "This method should only be called for executables with at least one relevant" - + " annotation"); - - return stream(executable.getAnnotation(RepeatedTestParameters.class).value()) - .map( - annotation -> - toParameterValues( - validateAndGetSingleValueFromRepeatedAnnotation(annotation, executable), - parametersList, - annotation.customName())) - .collect(toImmutableList()); - } - } - - private static ImmutableList toParameterValuesList( - Class valuesProvider, List parameters) { - try { - Constructor 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 checkParameterNamesArePresent(Executable executable) { - checkState( - stream(executable.getParameters()).allMatch(Parameter::isNamePresent), - "" - + "No parameter name could be found for %s, which likely means that parameter names" - + " aren't available at runtime. Please ensure that the this test was built with the" - + " -parameters compiler option.\n" - + "\n" - + "In Maven, you do this by adding true to the" - + " maven-compiler-plugin's configuration. For example:\n" - + "\n" - + "\n" - + " \n" - + " \n" - + " org.apache.maven.plugins\n" - + " maven-compiler-plugin\n" - + " 3.8.1\n" - + " \n" - + " \n" - + " -parameters\n" - + " \n" - + " \n" - + " \n" - + " \n" - + "\n" - + "\n" - + "Don't forget to run `mvn clean` after making this change.", - executable.getName()); - } - - private static String validateAndGetSingleValueFromRepeatedAnnotation( - TestParameters annotation, Executable executable) { - checkState( - annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class), - "Setting a valuesProvider is not supported for methods/constructors with" - + " multiple @TestParameters annotations on %s()", - executable.getName()); - checkState( - annotation.value().length > 0, - "Either a value or a valuesProvider must be set in @TestParameters on %s()", - executable.getName()); - checkState( - annotation.value().length == 1, - "When specifying more than one @TestParameter for a method/constructor, each annotation" - + " must have exactly one value. Instead, got %s values on %s(): %s", - annotation.value().length, - executable.getName(), - Arrays.toString(annotation.value())); - - return annotation.value()[0]; - } - - private static void validateThatValuesMatchParameters( - TestParametersValues testParametersValues, List parameters) { - ImmutableMap 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 parameters, String maybeCustomName) { - Object yamlMapObject = ParameterValueParsing.parseYamlStringToObject(yamlString); - checkState( - yamlMapObject instanceof Map, - "Cannot map YAML string '%s' to parameters because it is not a mapping", - yamlString); - Map yamlMap = (Map) yamlMapObject; - - ImmutableMap parametersByName = - Maps.uniqueIndex(parameters, Parameter::getName); - checkState( - yamlMap.keySet().equals(parametersByName.keySet()), - "Cannot map YAML string '%s' to parameters %s", - yamlString, - parametersByName.keySet()); - - @SuppressWarnings("unchecked") - Map checkedYamlMap = (Map) yamlMap; - - return TestParametersValues.builder() - .name(maybeCustomName.isEmpty() ? yamlString : maybeCustomName) - .addParameters( - Maps.transformEntries( - checkedYamlMap, - (parameterName, parsedYaml) -> - ParameterValueParsing.parseYamlObjectToJavaType( - parsedYaml, - TypeToken.of(parametersByName.get(parameterName).getParameterizedType())))) - .build(); - } - - // Note: We're not using the Executable interface here because it isn't supported by Java 7 and - // this code is called even if only @TestParameter is used. In other places, Executable is usable - // because @TestParameters only works for Java 8 anyway. - private static boolean hasRelevantAnnotation(Constructor executable) { - return executable.isAnnotationPresent(TestParameters.class) - || executable.isAnnotationPresent(RepeatedTestParameters.class); - } - - private static boolean hasRelevantAnnotation(Method executable) { - return executable.isAnnotationPresent(TestParameters.class) - || executable.isAnnotationPresent(RepeatedTestParameters.class); - } - - private static List toParameterList( - TestParametersValues parametersValues, Parameter[] parameters) { - return stream(parameters) - .map(parameter -> parametersValues.parametersMap().get(parameter.getName())) - .collect(toList()); - } - - private static Constructor getOnlyConstructor(Class testClass) { - ImmutableList> constructors = - ImmutableList.copyOf(testClass.getDeclaredConstructors()); - checkState( - constructors.size() == 1, "Expected exactly one constructor, but got %s", constructors); - return getOnlyElement(constructors); - } - - // Immutable collectors are re-implemented here because they are missing from the Android - // collection library. - private static Collector> 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/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/BaseTestParameterValidator.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/BaseTestParameterValidator.java new file mode 100644 index 0000000..692a713 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/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.junit5; + +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> 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 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 parameter : parameters) { + List 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 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>> getIndependentParameters( + Context context); +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ExecutableValidationResult.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ExecutableValidationResult.java new file mode 100644 index 0000000..4c0c40d --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ExecutableValidationResult.java @@ -0,0 +1,72 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.Iterables.getOnlyElement; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import java.util.Collection; + +/** + * Value class that captures the result of a validating a single constructor or test method. + * + *

          If the validation is not validated by any processor, it will be validated using the default + * validator. If a processor validates a constructor/test method, the remaining processors will + * *not* be called. + */ +@AutoValue +abstract class ExecutableValidationResult { + + /** Returns true if the properties of the given constructor/test method were validated. */ + public abstract boolean wasValidated(); + + /** Returns the validation errors, if any. */ + public abstract ImmutableList validationErrors(); + + static ExecutableValidationResult notValidated() { + return of(/* wasValidated= */ false, /* validationErrors= */ ImmutableList.of()); + } + + static ExecutableValidationResult validated(Collection errors) { + return of(/* wasValidated= */ true, /* validationErrors= */ errors); + } + + static ExecutableValidationResult validated(Throwable error) { + return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of(error)); + } + + static ExecutableValidationResult valid() { + return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of()); + } + + private static ExecutableValidationResult of( + boolean wasValidated, Collection validationErrors) { + checkArgument(wasValidated || validationErrors.isEmpty()); + return new AutoValue_ExecutableValidationResult( + wasValidated, ImmutableList.copyOf(validationErrors)); + } + + void assertValid() { + if (wasValidated() && !validationErrors().isEmpty()) { + if (validationErrors().size() == 1) { + throw new AssertionError(getOnlyElement(validationErrors())); + } else { + throw new AssertionError(String.format("Found validation errors: %s", validationErrors())); + } + } + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java new file mode 100644 index 0000000..59d2351 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.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.junit5; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; + +import com.google.common.collect.Lists; +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 > Enum parseEnum(String str, Class enumType) { + return Enum.valueOf((Class) 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) + .supportParsedType(String.class, Float::valueOf); + + yamlValueTransformer + .ifJavaType(Double.class) + .supportParsedType(Double.class, identity()) + .supportParsedType(Integer.class, Integer::doubleValue) + .supportParsedType(Long.class, Long::doubleValue) + .supportParsedType(String.class, Double::valueOf); + + yamlValueTransformer + .ifJavaType(Enum.class) + .supportParsedType( + String.class, str -> ParameterValueParsing.parseEnum(str, javaType.getRawType())); + + yamlValueTransformer + .ifJavaType(MessageLite.class) + .supportParsedType(String.class, str -> parseTextprotoMessage(str, javaType.getRawType())) + .supportParsedType( + Map.class, + map -> + getProtoValueParser() + .parseProtobufMessage((Map) 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 -> parseYamlMapToJavaMap(map, javaType)); + + return yamlValueTransformer.transformedJavaValue(); + } + + private static Map parseYamlMapToJavaMap(Map map, TypeToken javaType) { + return map.entrySet().stream() + .collect( + toMap( + entry -> + parseYamlObjectToJavaType( + entry.getKey(), getGenericParameterType(javaType, /* parameterIndex= */ 0)), + entry -> + parseYamlObjectToJavaType( + entry.getValue(), + getGenericParameterType(javaType, /* parameterIndex= */ 1)))); + } + + 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; + } + + SupportedJavaType ifJavaType(Class 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 { + + private final Class supportedJavaType; + + private SupportedJavaType(Class supportedJavaType) { + this.supportedJavaType = supportedJavaType; + } + + @SuppressWarnings("unchecked") + SupportedJavaType supportParsedType( + Class parsedYamlType, Function transformation) { + if (Primitives.wrap(supportedJavaType).isAssignableFrom(Primitives.wrap(javaType))) { + if (Primitives.wrap(parsedYamlType).isInstance(parsedYaml)) { + checkState( + transformedJavaValue == null, + "This case is already handled. This is a bug in" + + " testparameterinjector.TestParametersMethodProcessor."); + try { + transformedJavaValue = checkNotNull(transformation.apply((ParsedYamlT) parsedYaml)); + } catch (Exception e) { + throw new IllegalArgumentException( + String.format( + "Could not map YAML value %s (class = %s) to java class %s", + parsedYaml, parsedYaml.getClass(), javaType), + e); + } + } + } + + return this; + } + } + } + + static 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.junit5.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/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ProtoValueParsing.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ProtoValueParsing.java new file mode 100644 index 0000000..d270295 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/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.junit5; + +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 map, Class javaType); +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java new file mode 100644 index 0000000..c6b1cc3 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java @@ -0,0 +1,309 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static 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. + * + *

          See b/168325767 for the reason behind this. tl;dr the name is put into a Unix file with max + * 255 characters. The surrounding constant characters take up 31 characters. The max is reduced + * by an additional 24 characters to account for future changes. + */ + static final int MAX_TEST_NAME_LENGTH = 200; + + public abstract Method getMethod(); + + /** + * The test class that is being run. + * + *

          Note that this is not always the same as the class that declares {@link #getMethod()} + * because test methods can be inherited. + */ + public abstract Class getTestClass(); + + public final String getName() { + if (getParameters().isEmpty()) { + return getMethod().getName(); + } else { + return String.format( + "%s[%s]", + getMethod().getName(), + getParameters().stream().map(TestInfoParameter::getName).collect(joining(","))); + } + } + + abstract ImmutableList getParameters(); + + public abstract ImmutableList getAnnotations(); + + @Nullable + public final T getAnnotation(Class annotationClass) { + for (Annotation annotation : getAnnotations()) { + if (annotationClass.isInstance(annotation)) { + return annotationClass.cast(annotation); + } + } + return null; + } + + final TestInfo withExtraParameters(List parameters) { + return new AutoValue_TestInfo( + getMethod(), + getTestClass(), + ImmutableList.builder() + .addAll(this.getParameters()) + .addAll(parameters) + .build(), + getAnnotations()); + } + + final TestInfo withExtraAnnotation(Annotation annotation) { + ImmutableList newAnnotations = + ImmutableList.builder().addAll(this.getAnnotations()).add(annotation).build(); + return new AutoValue_TestInfo(getMethod(), getTestClass(), getParameters(), newAnnotations); + } + + /** + * Returns a new TestInfo instance with updated parameter names. + * + * @param parameterWithIndexToNewName A function of the parameter and its index in the {@link + * #getParameters()} list to the new name. + */ + private TestInfo withUpdatedParameterNames( + BiFunction parameterWithIndexToNewName) { + return new AutoValue_TestInfo( + getMethod(), + getTestClass(), + 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, Class testClass, String name, List annotations) { + return new AutoValue_TestInfo( + method, testClass, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations)); + } + + static TestInfo createWithoutParameters( + Method method, Class testClass, List annotations) { + return new AutoValue_TestInfo( + method, testClass, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations)); + } + + static ImmutableList shortenNamesIfNecessary(List testInfos) { + if (testInfos.stream().anyMatch(info -> info.getName().length() > MAX_TEST_NAME_LENGTH)) { + int numberOfParameters = testInfos.get(0).getParameters().size(); + + if (numberOfParameters == 0) { + return ImmutableList.copyOf(testInfos); + } else { + Set 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; + + // Subtract 4 characters to leave place for joining commas and the parameter index. + return maxLengthOfAllParameters / numberOfParameters - 4; + } + + static ImmutableList deduplicateTestNames(List 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 maybeAddTypesIfDuplicate(List testInfos) { + Multimap testNameToInfo = + MultimapBuilder.linkedHashKeys().arrayListValues().build(); + for (TestInfo testInfo : testInfos) { + testNameToInfo.put(testInfo.getName(), testInfo); + } + + return testNameToInfo.keySet().stream() + .flatMap( + testName -> { + Collection 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 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 deduplicateWithNumberPrefixes( + ImmutableList 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 Collector> 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(); + + final 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/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessor.java new file mode 100644 index 0000000..48e9a5e --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessor.java @@ -0,0 +1,66 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import com.google.common.base.Optional; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; + +/** + * Interface to change the list of methods used in a test. + * + *

          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 calculateTestInfos(TestInfo originalTest); + + /** + * If this processor can handle the given constructor, returns the parameters with which it should + * be invoked. + * + *

          This method is never called for a parameterless constructor. + */ + Optional> maybeGetConstructorParameters( + Constructor constructor, TestInfo testInfo); + + /** + * If this processor can handle the given test, returns the parameters with which {@code + * testInfo.getMethod()} should be invoked. + * + *

          This method is never called for a parameterless {@code testInfo.getMethod()}. + */ + Optional> maybeGetTestMethodParameters(TestInfo testInfo); + + /** + * Optionally process the test instance right after construction to ready it for the given test + * instance. + */ + void postProcessTestInstance(Object testInstance, TestInfo testInfo); + + /** Optionally validates the given constructor. */ + ExecutableValidationResult validateConstructor(Constructor constructor); + + /** + * Optionally validates the given method. + * + *

          Note that the given method is not necessarily declared in the given class because test + * methods can be inherited. + */ + ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass); +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessorList.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessorList.java new file mode 100644 index 0000000..aa2355d --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessorList.java @@ -0,0 +1,146 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static java.util.stream.Collectors.toList; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; + +/** + * Combined version of all {@link TestMethodProcessor} implementations that this package supports. + */ +final class TestMethodProcessorList { + + private final ImmutableList testMethodProcessors; + + private TestMethodProcessorList(ImmutableList testMethodProcessors) { + this.testMethodProcessors = testMethodProcessors; + } + + /** + * Returns a TestMethodProcessorList that supports all features that this package supports, except + * the following legacy features: + * + *

            + *
          • No support for {@link org.junit.runners.Parameterized} + *
          • No support for class and method-level parameters, except for @TestParameters + *
          + */ + public static TestMethodProcessorList createNewParameterizedProcessors() { + return new TestMethodProcessorList( + ImmutableList.of( + new TestParametersMethodProcessor(), + TestParameterAnnotationMethodProcessor.onlyForFieldsAndParameters())); + } + + static TestMethodProcessorList empty() { + return new TestMethodProcessorList(ImmutableList.of()); + } + + /** + * Calculates the TestInfo instances for the given test method. Each TestInfo corresponds to a + * single test. + * + *

          The returned list always contains at least one element. If there is no parameterization, + * this would be the TestInfo for running the test method without parameters. + */ + public List calculateTestInfos(Method testMethod, Class testClass) { + List testInfos = + ImmutableList.of( + TestInfo.createWithoutParameters( + testMethod, testClass, ImmutableList.copyOf(testMethod.getAnnotations()))); + + for (final TestMethodProcessor testMethodProcessor : testMethodProcessors) { + testInfos = + testInfos.stream() + .flatMap( + lastTestInfo -> testMethodProcessor.calculateTestInfos(lastTestInfo).stream()) + .collect(toList()); + } + + testInfos = TestInfo.deduplicateTestNames(TestInfo.shortenNamesIfNecessary(testInfos)); + + return testInfos; + } + + /** + * Returns the parameters with which it should be invoked. + * + *

          This method is never called for a parameterless constructor. + */ + public List getConstructorParameters(Constructor constructor, TestInfo testInfo) { + return testMethodProcessors.stream() + .map(processor -> processor.maybeGetConstructorParameters(constructor, testInfo)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .orElseThrow( + () -> + new IllegalStateException( + String.format( + "Could not generate parameter values for %s. Did you forget an annotation?", + constructor))); + } + + /** + * Returns the parameters with which {@code testInfo.getMethod()} should be invoked. + * + *

          This method is never called for a parameterless {@code testInfo.getMethod()}. + */ + public List getTestMethodParameters(TestInfo testInfo) { + return testMethodProcessors.stream() + .map(processor -> processor.maybeGetTestMethodParameters(testInfo)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .orElseThrow( + () -> + new IllegalStateException( + String.format( + "Could not generate parameter values for %s. Did you forget an annotation?", + testInfo.getMethod()))); + } + + /** + * Optionally process the test instance right after construction to ready it for the given test. + */ + public void postProcessTestInstance(Object testInstance, TestInfo testInfo) { + for (TestMethodProcessor testMethodProcessor : testMethodProcessors) { + testMethodProcessor.postProcessTestInstance(testInstance, testInfo); + } + } + + /** Optionally validates the given constructor. */ + public ExecutableValidationResult validateConstructor(Constructor constructor) { + return testMethodProcessors.stream() + .map(processor -> processor.validateConstructor(constructor)) + .filter(ExecutableValidationResult::wasValidated) + .findFirst() + .orElse(ExecutableValidationResult.notValidated()); + } + + /** Optionally validates the given method. */ + public ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass) { + return testMethodProcessors.stream() + .map(processor -> processor.validateTestMethod(testMethod, testClass)) + .filter(ExecutableValidationResult::wasValidated) + .findFirst() + .orElse(ExecutableValidationResult.notValidated()); + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java new file mode 100644 index 0000000..f31854d --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/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.junit5; + +import static com.google.common.base.Preconditions.checkState; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import 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.junit5.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. + * + *

          For enums and booleans, the values can be automatically derived as all possible values: + * + *

          + * {@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 }
          + * 
          + * + *

          The values can be explicitly defined as a parsed string: + * + *

          + * 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)]
          + * }
          + * 
          + * + *

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

          Types that are supported: + * + *

            + *
          • String: No parsing happens + *
          • boolean: Specified as YAML boolean + *
          • long and int: Specified as YAML integer + *
          • float and double: Specified as YAML floating point or integer + *
          • Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()} + *
          • Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML bytes + * (example: "!!binary 'ZGF0YQ=='") + *
          + * + *

          For dynamic sets of parameters or parameter types that are not supported here, use {@link + * #valuesProvider()} and leave this field empty. + * + *

          For examples, see {@link TestParameter}. + */ + String[] value() default {}; + + /** + * Sets a provider that will return a list of parameter values. + * + *

          If this field is set, {@link #value()} must be empty and vice versa. + * + *

          Example + * + *

          +   * {@literal @}Test
          +   * public void matchesAllOf_throwsOnNull(
          +   *     {@literal @}TestParameter(valuesProvider = CharMatcherProvider.class)
          +   *         CharMatcher charMatcher) {
          +   *   assertThrows(NullPointerException.class, () -> charMatcher.matchesAllOf(null));
          +   * }
          +   *
          +   * private static final class CharMatcherProvider implements TestParameterValuesProvider {
          +   *   {@literal @}Override
          +   *   public {@literal List} provideValues() {
          +   *     return ImmutableList.of(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace());
          +   *   }
          +   * }
          +   * 
          + */ + Class 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 provideValues() { + return ImmutableList.of(); + } + } + + /** Implementation of this parameter annotation. */ + final class InternalImplementationOfThisParameter implements TestParameterValueProvider { + @Override + public List provideValues( + Annotation uncastAnnotation, Optional> 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 annotationType, Optional> 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 getValuesFromProvider( + Class valuesProvider) { + try { + Constructor 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/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotation.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotation.java new file mode 100644 index 0000000..e169eb3 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotation.java @@ -0,0 +1,251 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Verify.verify; +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.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. + * + *

          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: + * + *

          {@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();
          + *     }
          + * }
          + * }
          + * + *

          An alternative is to use a method parameter for injection: + * + *

          {@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();
          + *     }
          + * }
          + * }
          + * + *

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

          {@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();
          + *     }
          + * }
          + * }
          + * + *

          Class constructors can also be annotated with @TestParameterAnnotation annotations, as shown + * below: + * + *

          {@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() {...}
          + * }
          + * }
          + * + *

          Each field that needs to be injected from a parameter requires its dedicated distinct + * annotation. + * + *

          If the same annotation is defined both on the class and method, the method parameter values + * take precedence. + * + *

          If the same annotation is defined both on the class and constructor, the constructor parameter + * values take precedence. + * + *

          Annotations cannot be duplicated between the constructor or constructor parameters and a + * method or method parameter. + * + *

          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 validator() default DefaultValidator.class; + + /** Specifies a value provider for the parameter to provide the values to test. */ + Class valueProvider() default DefaultValueProvider.class; + + /** Default {@link TestParameterValidator} implementation which skips no test. */ + class DefaultValidator implements TestParameterValidator { + + @Override + public boolean shouldSkip(Context context) { + return false; + } + } + + /** + * Default {@link TestParameterValueProvider} implementation that gets its values from the + * annotation's `value` method. + */ + class DefaultValueProvider implements TestParameterValueProvider { + + @Override + public List provideValues(Annotation annotation, Optional> 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 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 annotationType, Optional> 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 annotationType) { + Method valueMethod; + try { + valueMethod = annotationType.getMethod("value"); + } catch (NoSuchMethodException e) { + throw new RuntimeException( + "The @TestParameterAnnotation annotation should have a single value() method.", e); + } + Object parameters; + try { + parameters = valueMethod.invoke(annotation); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof IllegalAccessError) { + // There seems to be a bug or at least something weird with the JVM that causes + // IllegalAccessError to be thrown because the return value is not visible when it is a + // non-public nested type. See + // http://mail.openjdk.java.net/pipermail/core-libs-dev/2014-January/024180.html for more + // info. + throw new RuntimeException( + String.format( + "Could not access %s.value(). This is probably because %s is not visible to the" + + " annotation proxy. To fix this, make %s public.", + annotationType.getSimpleName(), + valueMethod.getReturnType().getSimpleName(), + valueMethod.getReturnType().getSimpleName())); + // Note: Not chaining the exception to reduce the clutter for the reader + } else { + throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e); + } + } catch (Exception e) { + throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e); + } + return parameters; + } + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java new file mode 100644 index 0000000..c06cb3a --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java @@ -0,0 +1,1290 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Verify.verify; +import static 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.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +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.junit5.TestInfo.TestInfoParameter; +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.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; + +/** + * {@code TestMethodProcessor} implementation for supporting parameterized tests annotated with + * {@link TestParameterAnnotation}. + * + * @see TestParameterAnnotation + */ +final class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { + + /** + * Class to hold an annotation type and origin and one of the values as returned by the {@code + * value()} method. + */ + @AutoValue + abstract static class 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 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> 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 paramName(); + + /** + * Returns a String that represents this value and is fit for use in a test name (between + * brackets). + */ + String toTestNameString() { + Class 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 create( + AnnotationWithMetadata annotationWithMetadata, Origin origin) { + List 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(/* onlyForFieldsAndParameters= */ false) + .getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()).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 getTestParameterValue( + TestInfo testInfo, Class annotationType) { + return getTestParameterValues(testInfo).getValue(annotationType); + } + + private static List getParametersAnnotationValues( + AnnotationWithMetadata annotationWithMetadata) { + Annotation annotation = annotationWithMetadata.annotation(); + TestParameterAnnotation testParameter = + annotation.annotationType().getAnnotation(TestParameterAnnotation.class); + Class 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 matches(Class 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 annotationType(); + + /** Where the annotation was declared. */ + abstract Origin origin(); + + public static AnnotationTypeOrigin create( + Class 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> 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 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 boolean onlyForFieldsAndParameters; + private final LoadingCache, ImmutableList> + annotationTypeOriginsCache = + CacheBuilder.newBuilder() + .maximumSize(1000) + .build(CacheLoader.from(this::calculateAnnotationTypeOrigins)); + private final Cache>> parameterValuesCache = + CacheBuilder.newBuilder().maximumSize(1000).build(); + + private TestParameterAnnotationMethodProcessor(boolean onlyForFieldsAndParameters) { + this.onlyForFieldsAndParameters = onlyForFieldsAndParameters; + } + + /** + * Constructs a new {@link TestMethodProcessor} that handles {@link + * TestParameterAnnotation}-annotated annotations that are placed anywhere: + * + *
            + *
          • At a method / constructor parameter + *
          • At a field + *
          • At a method / constructor on the class + *
          • At the test class + *
          + */ + static TestMethodProcessor forAllAnnotationPlacements() { + return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ false); + } + + /** + * Constructs a new {@link TestMethodProcessor} that handles {@link + * TestParameterAnnotation}-annotated annotations that are placed at fields or parameters. + * + *

          Note that this excludes class and method-level annotations, as is the default (using the + * constructor). + */ + static TestMethodProcessor onlyForFieldsAndParameters() { + return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ true); + } + + private ImmutableList calculateAnnotationTypeOrigins(Class testClass) { + // Collect all annotations used in declared fields and methods that have themselves a + // @TestParameterAnnotation annotation. + List fieldAnnotations = + extractTestParameterAnnotations( + streamWithParents(testClass) + .flatMap(c -> stream(c.getDeclaredFields())) + .flatMap(field -> stream(field.getAnnotations())), + Origin.FIELD); + List methodAnnotations = + extractTestParameterAnnotations( + stream(testClass.getMethods()).flatMap(method -> stream(method.getAnnotations())), + Origin.METHOD); + List parameterAnnotations = + extractTestParameterAnnotations( + streamWithParents(testClass) + .flatMap(c -> stream(c.getDeclaredMethods())) + .flatMap(method -> stream(method.getParameterAnnotations()).flatMap(Stream::of)), + Origin.METHOD_PARAMETER); + List classAnnotations = + extractTestParameterAnnotations(stream(testClass.getAnnotations()), Origin.CLASS); + List constructorAnnotations = + extractTestParameterAnnotations( + stream(testClass.getDeclaredConstructors()) + .flatMap(constructor -> stream(constructor.getAnnotations())), + Origin.CONSTRUCTOR); + List constructorParameterAnnotations = + extractTestParameterAnnotations( + stream(testClass.getDeclaredConstructors()) + .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); + } + + return 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()); + } + + private ImmutableList getAnnotationTypeOrigins( + Class testClass, Origin firstOrigin, Origin... otherOrigins) { + Set originsToFilterBy = + ImmutableSet.builder().add(firstOrigin).add(otherOrigins).build(); + try { + return annotationTypeOriginsCache.getUnchecked(testClass).stream() + .filter(annotationTypeOrigin -> originsToFilterBy.contains(annotationTypeOrigin.origin())) + .collect(toImmutableList()); + } catch (UncheckedExecutionException e) { + Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); + throw e; + } + } + + private void checkDuplicatedFieldsAnnotations( + List methodAnnotations, List 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> 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 constructorAnnotations, + List classAnnotations, + List fieldAnnotations) { + ImmutableSet> classAnnotationTypes = + classAnnotations.stream() + .map(AnnotationTypeOrigin::annotationType) + .collect(toImmutableSet()); + + ImmutableSet> uniqueFieldAnnotations = + fieldAnnotations.stream() + .map(AnnotationTypeOrigin::annotationType) + .collect(toImmutableSet()); + ImmutableSet> 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 extractTestParameterAnnotations( + Stream 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 ExecutableValidationResult validateConstructor(Constructor constructor) { + Class[] parameterTypes = constructor.getParameterTypes(); + if (parameterTypes.length == 0) { + return ExecutableValidationResult.notValidated(); + } + // The constructor has parameters, they must be injected by a TestParameterAnnotation + // annotation. + Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); + Class testClass = constructor.getDeclaringClass(); + return ExecutableValidationResult.validated( + validateMethodOrConstructorParameters( + removeOverrides( + getAnnotationTypeOrigins( + testClass, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER), + testClass), + testClass, + constructor, + parameterTypes, + parameterAnnotations)); + } + + @Override + public ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass) { + Class[] methodParameterTypes = testMethod.getParameterTypes(); + if (methodParameterTypes.length == 0) { + return ExecutableValidationResult.notValidated(); + } else { + // The method has parameters, they must be injected by a TestParameterAnnotation annotation. + return ExecutableValidationResult.validated( + validateMethodOrConstructorParameters( + getAnnotationTypeOrigins( + testClass, Origin.CLASS, Origin.METHOD, Origin.METHOD_PARAMETER), + testClass, + testMethod, + methodParameterTypes, + testMethod.getParameterAnnotations())); + } + } + + private List validateMethodOrConstructorParameters( + List annotationTypeOrigins, + Class testClass, + AnnotatedElement methodOrConstructor, + Class[] parameterTypes, + Annotation[][] parametersAnnotations) { + List errors = new ArrayList<>(); + + for (int parameterIndex = 0; parameterIndex < parameterTypes.length; parameterIndex++) { + Class parameterType = parameterTypes[parameterIndex]; + Annotation[] parameterAnnotations = parametersAnnotations[parameterIndex]; + boolean matchingTestParameterAnnotationFound = false; + // First, handle the case where the method parameter specifies the test parameter explicitly, + // e.g. {@code public void test(@ColorParameter({...}) Color c)}. + for (AnnotationTypeOrigin testParameterAnnotationType : annotationTypeOrigins) { + for (Annotation parameterAnnotation : parameterAnnotations) { + if (parameterAnnotation + .annotationType() + .equals(testParameterAnnotationType.annotationType())) { + // Verify that the type is assignable with the return type of the 'value' method. + Class valueMethodReturnType = + getValueMethodReturnType( + testParameterAnnotationType.annotationType(), + /* paramClass = */ Optional.of(parameterType)); + if (!parameterType.isAssignableFrom(valueMethodReturnType)) { + errors.add( + new IllegalStateException( + String.format( + "Parameter of type %s annotated with %s does not match" + + " expected type %s in method/constructor %s", + parameterType.getName(), + testParameterAnnotationType.annotationType().getName(), + valueMethodReturnType.getName(), + methodOrConstructor))); + } else { + matchingTestParameterAnnotationFound = true; + } + } + } + } + // Second, handle the case where the method parameter does not specify the test parameter, + // and instead relies on the type matching, e.g. {@code public void test(Color c)}. + if (!matchingTestParameterAnnotationFound) { + List> testParameterAnnotationTypes = + getTestParameterAnnotations( + // Do not include METHOD_PARAMETER or CONSTRUCTOR_PARAMETER since they have already + // been evaluated. + filterAnnotationTypeOriginsByOrigin( + annotationTypeOrigins, Origin.CLASS, Origin.CONSTRUCTOR, Origin.METHOD), + testClass, + methodOrConstructor); + // If no annotation is present, simply compare the type. + for (Class testParameterAnnotationType : + testParameterAnnotationTypes) { + if (parameterType.isAssignableFrom( + getValueMethodReturnType( + testParameterAnnotationType, /* paramClass = */ Optional.absent()))) { + if (matchingTestParameterAnnotationFound) { + errors.add( + new IllegalStateException( + String.format( + "Ambiguous method/constructor parameter type, matching multiple" + + " annotations for parameter of type %s in method %s", + parameterType.getName(), methodOrConstructor))); + } + matchingTestParameterAnnotationFound = true; + } + } + } + if (!matchingTestParameterAnnotationFound) { + errors.add( + new IllegalStateException( + String.format( + "No matching test parameter annotation found" + + " for parameter of type %s in method/constructor %s", + parameterType.getName(), methodOrConstructor))); + } + } + return errors; + } + + @Override + public Optional> maybeGetConstructorParameters( + Constructor constructor, TestInfo testInfo) { + if (testInfo.getAnnotation(TestIndexHolder.class) == null + // Explicitly skip @TestParameters annotated methods to ensure compatibility. + // + // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported) + // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;). + // Without the @TestParameters check below, this class would try to find parameters for + // these methods. When there are no method parameters, this is a no-op, but when the method + // is annotated with @TestParameters, this throws an exception (because there are method + // parameters that this processor has no values for - they are provided by the + // @TestParameters processor). + || constructor.isAnnotationPresent(TestParameters.class)) { + return Optional.absent(); + } else { + TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); + List testParameterValues = + getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()); + + Class[] parameterTypes = constructor.getParameterTypes(); + Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); + List parameterValues = new ArrayList<>(/* initialCapacity= */ parameterTypes.length); + List> processedAnnotationTypes = new ArrayList<>(); + List parameterValuesForConstructor = + filterByOrigin( + testParameterValues, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER); + for (int i = 0; i < parameterTypes.length; i++) { + // Initialize each parameter value from the corresponding TestParameterAnnotation value. + parameterValues.add( + getParameterValue( + parameterValuesForConstructor, + parameterTypes[i], + parameterAnnotations[i], + processedAnnotationTypes)); + } + return Optional.of(parameterValues); + } + } + + @Override + public Optional> maybeGetTestMethodParameters(TestInfo testInfo) { + Method testMethod = testInfo.getMethod(); + if (testInfo.getAnnotation(TestIndexHolder.class) == null + // Explicitly skip @TestParameters annotated methods to ensure compatibility. + // + // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported) + // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;). + // Without the @TestParameters check below, this class would try to find parameters for + // these methods. When there are no method parameters, this is a no-op, but when the method + // is annotated with @TestParameters, this throws an exception (because there are method + // parameters that this processor has no values for - they are provided by the + // @TestParameters processor). + || testMethod.isAnnotationPresent(TestParameters.class)) { + return Optional.absent(); + } else { + TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); + checkState(testIndexHolder != null); + List testParameterValues = + filterByOrigin( + getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()), + Origin.CLASS, + Origin.METHOD, + Origin.METHOD_PARAMETER); + + Class[] parameterTypes = testMethod.getParameterTypes(); + Annotation[][] parametersAnnotations = testMethod.getParameterAnnotations(); + ArrayList parameterValues = + new ArrayList<>(/* initialCapacity= */ parameterTypes.length); + + List> processedAnnotationTypes = new ArrayList<>(); + for (int i = 0; i < parameterTypes.length; i++) { + parameterValues.add( + getParameterValue( + testParameterValues, + parameterTypes[i], + parametersAnnotations[i], + processedAnnotationTypes)); + } + + return Optional.of(parameterValues); + } + } + + /** + * Returns the {@link TestInfo}, one for each result of the cartesian product of each test + * parameter values. + * + *

          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)}). + * + *

          For multiple annotations (say, {@code @TestParameter("foo", "bar")} and + * {@code @ColorParameter({BLUE, WHITE})}), it will generate the following result: + * + *

            + *
          • ("foo", BLUE) + *
          • ("foo", WHITE) + *
          • ("bar", BLUE) + *
          • ("bar", WHITE) + *
          • + *
          + * + * corresponding to the cartesian product of both annotations. + */ + @Override + public List calculateTestInfos(TestInfo originalTest) { + List> parameterValuesForMethod = + getParameterValuesForMethod(originalTest.getMethod(), originalTest.getTestClass()); + + if (parameterValuesForMethod.equals(ImmutableList.of(ImmutableList.of()))) { + // This test is not parameterized + return ImmutableList.of(originalTest); + } + + ImmutableList.Builder testInfos = ImmutableList.builder(); + for (int parametersIndex = 0; + parametersIndex < parameterValuesForMethod.size(); + ++parametersIndex) { + List 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(originalTest.getTestClass()), + originalTest.getMethod()), + parametersIndex, + originalTest.getTestClass().getName()))); + } + + return testInfos.build(); + } + + private List> getParameterValuesForMethod( + Method method, Class testClass) { + try { + return parameterValuesCache.get( + method, + () -> { + List> testParameterValuesList = + getAnnotationValuesForUsedAnnotationTypes(method, testClass); + + 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 getParameterValuesForTest( + TestIndexHolder testIndexHolder, Class testClass) { + verify( + testIndexHolder.testClassName().equals(testClass.getName()), + "The class for which the given annotation was created (%s) is not the same as the test" + + " class that this runner is handling (%s)", + testIndexHolder.testClassName(), + testClass.getName()); + Method testMethod = getMethodsIncludingParents(testClass).get(testIndexHolder.methodIndex()); + return getParameterValuesForMethod(testMethod, testClass) + .get(testIndexHolder.parametersIndex()); + } + + /** + * Returns the list of annotation index for all annotations defined in a given test method and its + * class. + */ + private ImmutableList> getAnnotationValuesForUsedAnnotationTypes( + Method method, Class testClass) { + ImmutableList annotationTypes = + Stream.of( + getAnnotationTypeOrigins(testClass, Origin.CLASS).stream(), + getAnnotationTypeOrigins(testClass, Origin.FIELD).stream(), + getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR).stream(), + getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR_PARAMETER).stream(), + getAnnotationTypeOrigins(testClass, Origin.METHOD).stream(), + getAnnotationTypeOrigins(testClass, 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 annotationComparator( + Annotation[][] parameterAnnotations) { + ImmutableList 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}. + * + *

          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 removeOverrides( + List 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 removeOverrides( + List 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(); + default: + return true; + } + }) + .collect(toCollection(ArrayList::new)); + } + + /** + * Returns the given annotations defined either on the method parameters, method or the test + * class. + * + *

          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> getAnnotationFromParametersOrTestOrClass( + AnnotationTypeOrigin annotationTypeOrigin, Method method, Class testClass) { + Origin origin = annotationTypeOrigin.origin(); + Class annotationType = annotationTypeOrigin.annotationType(); + if (origin == Origin.CONSTRUCTOR_PARAMETER) { + Constructor constructor = getOnlyConstructor(testClass); + List 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 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 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> toTestParameterValueList( + List annotationWithMetadatas, Origin origin) { + return annotationWithMetadatas.stream() + .map(annotationWithMetadata -> TestParameterValue.create(annotationWithMetadata, origin)) + .collect(toImmutableList()); + } + + private static ImmutableList getAnnotationWithMetadataListWithType( + Method callable, Class annotationType) { + try { + return getAnnotationWithMetadataListWithType(callable.getParameters(), annotationType); + } catch (NoSuchMethodError ignored) { + return getAnnotationWithMetadataListWithType( + callable.getParameterTypes(), callable.getParameterAnnotations(), annotationType); + } + } + + private static ImmutableList getAnnotationWithMetadataListWithType( + Constructor callable, Class 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 getAnnotationWithMetadataListWithType( + Parameter[] parameters, Class 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 getAnnotationWithMetadataListWithType( + Class[] parameterTypes, + Annotation[][] annotations, + Class annotationType) { + checkArgument(parameterTypes.length == annotations.length); + + ImmutableList.Builder 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 getAnnotationListWithType( + Annotation[] annotations, Class annotationType) { + return stream(annotations) + .filter(annotation -> annotation.annotationType().equals(annotationType)) + .collect(toImmutableList()); + } + + private static Constructor getOnlyConstructor(Class testClass) { + Constructor[] constructors = testClass.getDeclaredConstructors(); + checkState( + constructors.length == 1, + "a single public constructor is required for class %s", + testClass); + return constructors[0]; + } + + @Override + public void postProcessTestInstance(Object testInstance, TestInfo testInfo) { + TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); + try { + if (testIndexHolder != null) { + List testParameterValues = + getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()); + + // Do not include {@link Origin#METHOD_PARAMETER} nor {@link Origin#CONSTRUCTOR_PARAMETER} + // annotations. + List testParameterValuesForFieldInjection = + filterByOrigin(testParameterValues, Origin.CLASS, Origin.FIELD, Origin.METHOD); + // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class + // in the example above. + List remainingTestParameterValuesForFieldInjection = + new ArrayList<>(testParameterValuesForFieldInjection); + for (Field declaredField : + streamWithParents(testInstance.getClass()) + .flatMap(c -> stream(c.getDeclaredFields())) + .collect(toImmutableList())) { + for (TestParameterValue testParameterValue : + remainingTestParameterValuesForFieldInjection) { + if (declaredField.isAnnotationPresent( + testParameterValue.annotationTypeOrigin().annotationType())) { + declaredField.setAccessible(true); + declaredField.set(testInstance, testParameterValue.value()); + remainingTestParameterValuesForFieldInjection.remove(testParameterValue); + break; + } + } + } + } + } catch (IllegalAccessException 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 filterByOrigin( + List testParameterValues, Origin... origins) { + Set 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 filterAnnotationTypeOriginsByOrigin( + List annotationTypeOrigins, Origin... origins) { + List originList = Arrays.asList(origins); + return annotationTypeOrigins.stream() + .filter(annotationTypeOrigin -> originList.contains(annotationTypeOrigin.origin())) + .collect(toImmutableList()); + } + + /** Returns a {@link TestParameterAnnotation}'s value for a method or constructor parameter. */ + private Object getParameterValue( + List testParameterValues, + Class methodParameterType, + Annotation[] parameterAnnotations, + List> processedAnnotationTypes) { + List> 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 annotationType = + testParameterValue.annotationTypeOrigin().annotationType(); + if (parameterAnnotation.annotationType().equals(annotationType)) { + // If multiple annotations exist, ensure that the proper one is selected. + // For instance, for: + // + // test(@FooParameter(1,2) Foo foo, @FooParameter(3,4) Foo bar) {} + // + // 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}. + */ + int parametersIndex(); + + /** + * The full name of the test class. Only used for verifying that assumptions about the above + * indices are valid. + */ + String testClassName(); + } + + /** Factory for {@link TestIndexHolder}. */ + static class TestIndexHolderFactory { + @AutoAnnotation + static TestIndexHolder create(int methodIndex, int parametersIndex, String testClassName) { + return new AutoAnnotation_TestParameterAnnotationMethodProcessor_TestIndexHolderFactory_create( + methodIndex, parametersIndex, testClassName); + } + + private TestIndexHolderFactory() {} + } + + /** + * Returns whether the test should be skipped according to the {@code annotationType}'s {@link + * TestParameterValidator} and the current list of {@link TestParameterValue}. + */ + private static boolean callShouldSkip( + Class annotationType, List testParameterValues) { + TestParameterAnnotation annotation = + annotationType.getAnnotation(TestParameterAnnotation.class); + Class 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 testParameterValues; + private final Set valueList; + + public ValidatorContext(List testParameterValues) { + this.testParameterValues = testParameterValues; + this.valueList = testParameterValues.stream().map(TestParameterValue::value).collect(toSet()); + } + + @Override + public boolean has(Class testParameter, Object value) { + return getValue(testParameter).transform(value::equals).or(false); + } + + @Override + public , U extends Enum> boolean has(T value1, U value2) { + return valueList.contains(value1) && valueList.contains(value2); + } + + @Override + public Optional getValue(Class testParameter) { + return getParameter(testParameter).transform(TestParameterValue::value); + } + + @Override + public List getSpecifiedValues(Class testParameter) { + return getParameter(testParameter) + .transform(TestParameterValue::specifiedValues) + .or(ImmutableList.of()); + } + + private Optional getParameter(Class 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 annotationType, Optional> paramClass) { + TestParameterAnnotation testParameter = + annotationType.getAnnotation(TestParameterAnnotation.class); + Class 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> getTestParameterAnnotations( + List annotationTypeOrigins, + final Class testClass, + AnnotatedElement methodOrConstructor) { + return annotationTypeOrigins.stream() + .map(AnnotationTypeOrigin::annotationType) + .filter( + annotationType -> + testClass.isAnnotationPresent(annotationType) + || methodOrConstructor.isAnnotationPresent(annotationType)) + .collect(toImmutableList()); + } + + private int strictIndexOf(List haystack, T needle) { + int index = haystack.indexOf(needle); + checkArgument(index >= 0, "Could not find '%s' in %s", needle, haystack); + return index; + } + + private ImmutableList getMethodsIncludingParents(Class clazz) { + ImmutableList.Builder resultBuilder = ImmutableList.builder(); + while (clazz != null) { + resultBuilder.add(clazz.getDeclaredMethods()); + clazz = clazz.getSuperclass(); + } + return resultBuilder.build(); + } + + private static Stream> streamWithParents(Class clazz) { + Stream.Builder> 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 Collector> toImmutableList() { + return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf); + } + + private static Collector> toImmutableSet() { + return Collectors.collectingAndThen(Collectors.toList(), ImmutableSet::copyOf); + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorExtension.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorExtension.java new file mode 100644 index 0000000..6d6aa51 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorExtension.java @@ -0,0 +1,140 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Iterables.getOnlyElement; + +import com.google.auto.value.AutoValue; +import com.google.auto.value.extension.memoized.Memoized; +import com.google.common.collect.ImmutableList; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.TestInstancePostProcessor; +import org.junit.jupiter.api.extension.TestTemplateInvocationContext; +import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; + +/** Implements the TestParameterInjector logic for JUnit5 (Jupiter). */ +class TestParameterInjectorExtension implements TestTemplateInvocationContextProvider { + + private static final TestMethodProcessorList testMethodProcessors = + TestMethodProcessorList.createNewParameterizedProcessors(); + + @Override + public boolean supportsTestTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideTestTemplateInvocationContexts( + ExtensionContext extensionContext) { + validateTestMethodAndConstructor( + extensionContext.getRequiredTestMethod(), extensionContext.getRequiredTestClass()); + List testInfos = + testMethodProcessors.calculateTestInfos( + extensionContext.getRequiredTestMethod(), extensionContext.getRequiredTestClass()); + + return testInfos.stream().map(CustomInvocationContext::of); + } + + private void validateTestMethodAndConstructor(Method testMethod, Class testClass) { + checkState( + testClass.getDeclaredConstructors().length == 1, + "Only a single constructor is allowed, but found %s in %s", + testClass.getDeclaredConstructors().length, + testClass.getSimpleName()); + Constructor constructor = + getOnlyElement(ImmutableList.copyOf(testClass.getDeclaredConstructors())); + + testMethodProcessors.validateConstructor(constructor).assertValid(); + + testMethodProcessors.validateTestMethod(testMethod, testClass).assertValid(); + + checkState( + testMethod.getAnnotation(TestParameterInjectorTest.class) != null, + "Each test method handled by this extension should be annotated with" + + " @TestParameterInjectorTest"); + } + + @AutoValue + abstract static class CustomInvocationContext implements TestTemplateInvocationContext { + + abstract TestInfo testInfo(); + + @Memoized + List getConstructorParameters() { + Constructor constructor = + getOnlyElement(ImmutableList.copyOf(testInfo().getTestClass().getDeclaredConstructors())); + + return testMethodProcessors.getConstructorParameters(constructor, testInfo()); + } + + @Memoized + List getTestMethodParameters() { + return testMethodProcessors.getTestMethodParameters(testInfo()); + } + + static CustomInvocationContext of(TestInfo testInfo) { + return new AutoValue_TestParameterInjectorExtension_CustomInvocationContext(testInfo); + } + + @Override + public String getDisplayName(int invocationIndex) { + return testInfo().getName(); + } + + @Override + public List getAdditionalExtensions() { + return ImmutableList.of(new CustomAdditionalExtension()); + } + + class CustomAdditionalExtension implements ParameterResolver, TestInstancePostProcessor { + + @Override + public boolean supportsParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) { + if (parameterContext.getDeclaringExecutable() instanceof Constructor) { + return true; + } else { + return parameterContext + .getDeclaringExecutable() + .isAnnotationPresent(TestParameterInjectorTest.class); + } + } + + @Override + public Object resolveParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) { + if (parameterContext.getDeclaringExecutable() instanceof Constructor) { + return getConstructorParameters().get(parameterContext.getIndex()); + } else { + return getTestMethodParameters().get(parameterContext.getIndex()); + } + } + + @Override + public void postProcessTestInstance(Object testInstance, ExtensionContext extensionContext) + throws Exception { + testMethodProcessors.postProcessTestInstance(testInstance, testInfo()); + } + } + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorTest.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorTest.java new file mode 100644 index 0000000..e17179a --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorTest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Replacement for JUnit5's @Test for test methods that want to use @TestParameter[s]. + * + *

          Example: + * + *

          + * class MyTest {
          + *   {@literal @}TestParameterInjectorTest
          + *   void withParameter_success({@literal @}TestParameter boolean bool) {
          + *     // ...
          + *   }
          + *
          + *   {@literal @}TestParameterInjectorTest
          + *   {@literal @}TestParameters("{name: 1, number: 3.3}")
          + *   {@literal @}TestParameters("{name: abc, number: 5}")
          + *   void withParameters_success(String name, double number) {
          + *     // ...
          + *   }
          + * }
          + * 
          + */ +@TestTemplate +@ExtendWith(TestParameterInjectorExtension.class) +@Retention(RUNTIME) +@Target({METHOD}) +public @interface TestParameterInjectorTest {} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValidator.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValidator.java new file mode 100644 index 0000000..70db746 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValidator.java @@ -0,0 +1,68 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import com.google.common.base.Optional; +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Validator interface which allows {@link TestParameterAnnotation} annotations to validate the set + * of annotation values for a given test instance, and to selectively skip the test. + */ +interface TestParameterValidator { + + /** + * This interface allows to access information on the current testwhen implementing {@link + * TestParameterValidator}. + */ + interface Context { + + /** Returns whether the current test has the {@link TestParameterAnnotation} value(s). */ + boolean has(Class 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. + */ + , U extends Enum> boolean has(T value1, U value2); + + /** + * Returns all the current test value for a given {@link TestParameterAnnotation} annotated + * annotation. + */ + Optional getValue(Class testParameter); + + /** + * Returns all the values specified for a given {@link TestParameterAnnotation} annotated + * annotation in the test. + * + *

          For example, if the test annotates '@Foo(a,b,c)', getSpecifiedValues(Foo.class) will + * return [a,b,c]. + */ + List getSpecifiedValues(Class testParameter); + } + + /** + * Returns whether the test should be skipped based on the annotations' values. + * + *

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

          This method is not invoked in the context of a running test statement. + */ + boolean shouldSkip(Context context); +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java new file mode 100644 index 0000000..47ed601 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/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.junit5; + +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 provideValues(Annotation annotation, Optional> 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 annotationType, Optional> parameterClass); +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValues.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValues.java new file mode 100644 index 0000000..b2c88a6 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValues.java @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import com.google.common.base.Optional; +import java.lang.annotation.Annotation; + +/** Interface to retrieve the {@link TestParameterAnnotation} values for a test. */ +interface TestParameterValues { + /** + * Returns a {@link TestParameterAnnotation} value for the current test as specified by {@code + * testInfo}, or {@link Optional#absent()} if the {@code annotationType} is not found. + */ + Optional getValue(Class annotationType); +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameters.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameters.java new file mode 100644 index 0000000..3a3c40c --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameters.java @@ -0,0 +1,259 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkState; +import static java.lang.annotation.ElementType.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.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * Annotation that can be placed (repeatedly) on @Test-methods or a test constructor to indicate the + * sets of parameters that it should be invoked with. + * + *

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

          Note: If this annotation is used in a test class, the other methods in that class can still + * use other types of parameterization, such as {@linkplain TestParameter @TestParameter}. + * + *

          See {@link #value()} for simple examples. + * + *

          Warning: This annotation can only be used if the compiled java code contains the parameter + * names. This is typically done by passing the {@code -parameters} option to the Java compiler, + * which requires using Java 8 or higher and may not be available on Android. + */ +@Retention(RUNTIME) +@Target({CONSTRUCTOR, METHOD}) +@Repeatable(TestParameters.RepeatedTestParameters.class) +public @interface TestParameters { + + /** + * Specifies one or more stringified sets of parameters in YAML format. Each set corresponds to a + * single invocation of a test method. + * + *

          Each element in this array is a full parameter set, formatted as a YAML mapping. The mapping + * keys must match the parameter names and the mapping values will be converted to the parameter + * type if possible. See yaml.org for the YAML syntax and the section below on the supported + * parameter types. + * + *

          There are two distinct ways of using this annotation: repeated vs single: + * + *

          Recommended usage: Separate annotation per parameter set + * + *

          This approach uses multiple @TestParameters annotations, one for each set of parameters, for + * example: + * + *

          +   * {@literal @}Test
          +   * {@literal @}TestParameters("{age: 17, expectIsAdult: false}")
          +   * {@literal @}TestParameters("{age: 22, expectIsAdult: true}")
          +   * public void personIsAdult(int age, boolean expectIsAdult) { ... }
          +   *
          +   * {@literal @}Test
          +   * {@literal @}TestParameters("{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}")
          +   * {@literal @}TestParameters("{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}")
          +   * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... }
          +   * 
          + * + *

          Old discouraged usage: Single annotation with all parameter sets + * + *

          This approach uses a single @TestParameter annotation for all parameter sets, for example: + * + *

          +   * {@literal @}Test
          +   * {@literal @}TestParameters({
          +   *   "{age: 17, expectIsAdult: false}",
          +   *   "{age: 22, expectIsAdult: true}",
          +   * })
          +   * public void personIsAdult(int age, boolean expectIsAdult) { ... }
          +   *
          +   * {@literal @}Test
          +   * {@literal @}TestParameters({
          +   *   "{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}",
          +   *   "{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}",
          +   * })
          +   * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... }
          +   * 
          + * + *

          Supported parameter types + * + *

            + *
          • YAML primitives: + *
              + *
            • String: Specified as YAML string + *
            • boolean: Specified as YAML boolean + *
            • long and int: Specified as YAML integer + *
            • float and double: Specified as YAML floating point or integer + *
            + *
          • + *
          • Parsed types: + *
              + *
            • Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()} + *
            • Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML + * bytes (example: "!!binary 'ZGF0YQ=='") + *
            + *
          • + *
          + * + *

          For dynamic sets of parameters or parameter types that are not supported here, use {@link + * #valuesProvider()} and leave this field empty. + */ + String[] value() default {}; + + /** + * Overrides the name of the parameter set that is used in the test name. + * + *

          This can only be set if {@link #value()} has exactly one element. If not set, the YAML + * string in {@link #value()} is used in the test name. + * + *

          For example: If this name is set to "young adult", then the test name might be + * "personIsAdult[young adult]" where the default might have been "personIsAdult[{age: 17, + * expectIsAdult: false}]". + */ + String customName() default ""; + + /** + * Sets a provider that will return a list of parameter sets. Each element in the returned list + * corresponds to a single invocation of a test method. + * + *

          If this field is set, {@link #value()} must be empty and vice versa. + * + *

          Example + * + *

          +   * {@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} 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()
          +   *     );
          +   *   }
          +   * }
          +   * 
          + */ + Class valuesProvider() default + DefaultTestParametersValuesProvider.class; + + /** Interface for custom providers of test parameter values. */ + interface TestParametersValuesProvider { + List 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. + * + *

          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 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 parametersMap = new LinkedHashMap<>(); + + /** + * Sets a name for this set of parameters that will be used for describing this test. + * + *

          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 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 provideValues() { + return ImmutableList.of(); + } + } + + /** + * Holder annotation for multiple @TestParameters annotations. This should never be used directly. + */ + @Retention(RUNTIME) + @Target({CONSTRUCTOR, METHOD}) + @interface RepeatedTestParameters { + TestParameters[] value(); + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParametersMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParametersMethodProcessor.java new file mode 100644 index 0000000..4879ca7 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParametersMethodProcessor.java @@ -0,0 +1,485 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Verify.verify; +import static com.google.common.collect.Iterables.getOnlyElement; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toList; + +import com.google.auto.value.AutoAnnotation; +import com.google.common.base.Optional; +import com.google.common.base.Throwables; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.common.primitives.Primitives; +import com.google.common.reflect.TypeToken; +import com.google.common.util.concurrent.UncheckedExecutionException; +import com.google.testing.junit.testparameterinjector.junit5.TestInfo.TestInfoParameter; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters.DefaultTestParametersValuesProvider; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters.RepeatedTestParameters; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValues; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValuesProvider; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** {@code TestMethodProcessor} implementation for supporting {@link TestParameters}. */ +@SuppressWarnings("AndroidJdkLibsChecker") // Parameter is not available on old Android SDKs. +final class TestParametersMethodProcessor implements TestMethodProcessor { + + private final LoadingCache> + parameterValuesByConstructorOrMethodCache = + CacheBuilder.newBuilder() + .maximumSize(1000) + .build(CacheLoader.from(TestParametersMethodProcessor::toParameterValuesList)); + + @Override + public ExecutableValidationResult validateConstructor(Constructor constructor) { + if (hasRelevantAnnotation(constructor)) { + try { + // This method throws an exception if there is a validation error + getConstructorParameters(constructor); + } catch (Throwable t) { + return ExecutableValidationResult.validated(t); + } + return ExecutableValidationResult.valid(); + } else { + return ExecutableValidationResult.notValidated(); + } + } + + @Override + public ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass) { + if (hasRelevantAnnotation(testMethod)) { + try { + // This method throws an exception if there is a validation error + getMethodParameters(testMethod); + } catch (Throwable t) { + return ExecutableValidationResult.validated(t); + } + return ExecutableValidationResult.valid(); + } else { + return ExecutableValidationResult.notValidated(); + } + } + + @Override + public List calculateTestInfos(TestInfo originalTest) { + boolean constructorIsParameterized = + hasRelevantAnnotation(getOnlyConstructor(originalTest.getTestClass())); + boolean methodIsParameterized = hasRelevantAnnotation(originalTest.getMethod()); + + if (!constructorIsParameterized && !methodIsParameterized) { + return ImmutableList.of(originalTest); + } + + ImmutableList.Builder testInfos = ImmutableList.builder(); + + ImmutableList> constructorParametersList = + getConstructorParametersOrSingleAbsentElement(originalTest.getTestClass()); + ImmutableList> methodParametersList = + getMethodParametersOrSingleAbsentElement(originalTest.getMethod()); + for (int constructorParametersIndex = 0; + constructorParametersIndex < constructorParametersList.size(); + ++constructorParametersIndex) { + Optional constructorParameters = + constructorParametersList.get(constructorParametersIndex); + + for (int methodParametersIndex = 0; + methodParametersIndex < methodParametersList.size(); + ++methodParametersIndex) { + Optional 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> + getConstructorParametersOrSingleAbsentElement(Class testClass) { + Constructor constructor = getOnlyConstructor(testClass); + return hasRelevantAnnotation(constructor) + ? getConstructorParameters(constructor).stream() + .map(Optional::of) + .collect(toImmutableList()) + : ImmutableList.of(Optional.absent()); + } + + private ImmutableList> getMethodParametersOrSingleAbsentElement( + Method method) { + return hasRelevantAnnotation(method) + ? getMethodParameters(method).stream().map(Optional::of).collect(toImmutableList()) + : ImmutableList.of(Optional.absent()); + } + + @Override + public Optional> maybeGetConstructorParameters( + Constructor constructor, TestInfo testInfo) { + if (hasRelevantAnnotation(constructor)) { + ImmutableList parameterValuesList = + getConstructorParameters(constructor); + TestParametersValues parametersValues = + parameterValuesList.get( + testInfo.getAnnotation(TestIndexHolder.class).constructorParametersIndex()); + + return Optional.of(toParameterList(parametersValues, constructor.getParameters())); + } else { + return Optional.absent(); + } + } + + @Override + public Optional> maybeGetTestMethodParameters(TestInfo testInfo) { + Method testMethod = testInfo.getMethod(); + if (hasRelevantAnnotation(testMethod)) { + ImmutableList parameterValuesList = getMethodParameters(testMethod); + TestParametersValues parametersValues = + parameterValuesList.get( + testInfo.getAnnotation(TestIndexHolder.class).methodParametersIndex()); + + return Optional.of(toParameterList(parametersValues, testMethod.getParameters())); + } else { + return Optional.absent(); + } + } + + @Override + public void postProcessTestInstance(Object testInstance, TestInfo testInfo) {} + + private ImmutableList getConstructorParameters(Constructor constructor) { + try { + return parameterValuesByConstructorOrMethodCache.getUnchecked(constructor); + } catch (UncheckedExecutionException e) { + // Rethrow IllegalStateException because they can be caused by user mistakes and the user + // doesn't need to know that the caching layer is in between. + Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); + throw e; + } + } + + private ImmutableList getMethodParameters(Method method) { + try { + return parameterValuesByConstructorOrMethodCache.getUnchecked(method); + } catch (UncheckedExecutionException e) { + // Rethrow IllegalStateException because they can be caused by user mistakes and the user + // doesn't need to know that the caching layer is in between. + Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); + throw e; + } + } + + private static ImmutableList toParameterValuesList(Executable executable) { + checkParameterNamesArePresent(executable); + ImmutableList parametersList = ImmutableList.copyOf(executable.getParameters()); + + if (executable.isAnnotationPresent(TestParameters.class)) { + checkState( + !executable.isAnnotationPresent(RepeatedTestParameters.class), + "Unexpected situation: Both @TestParameters and @RepeatedTestParameters annotating the" + + " same method"); + TestParameters annotation = executable.getAnnotation(TestParameters.class); + boolean valueIsSet = annotation.value().length > 0; + boolean valuesProviderIsSet = + !annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class); + + checkState( + !(valueIsSet && valuesProviderIsSet), + "It is not allowed to specify both value and valuesProvider in @TestParameters(value=%s," + + " valuesProvider=%s) on %s()", + Arrays.toString(annotation.value()), + annotation.valuesProvider().getSimpleName(), + executable.getName()); + checkState( + valueIsSet || valuesProviderIsSet, + "Either a value or a valuesProvider must be set in @TestParameters on %s()", + executable.getName()); + if (!annotation.customName().isEmpty()) { + checkState( + annotation.value().length == 1, + "Setting @TestParameters.customName is only allowed if there is exactly one YAML string" + + " in @TestParameters.value (on %s())", + executable.getName()); + } + + if (valueIsSet) { + return stream(annotation.value()) + .map(yamlMap -> toParameterValues(yamlMap, parametersList, annotation.customName())) + .collect(toImmutableList()); + } else { + return toParameterValuesList(annotation.valuesProvider(), parametersList); + } + } else { // Not annotated with @TestParameters + verify( + executable.isAnnotationPresent(RepeatedTestParameters.class), + "This method should only be called for executables with at least one relevant" + + " annotation"); + + return stream(executable.getAnnotation(RepeatedTestParameters.class).value()) + .map( + annotation -> + toParameterValues( + validateAndGetSingleValueFromRepeatedAnnotation(annotation, executable), + parametersList, + annotation.customName())) + .collect(toImmutableList()); + } + } + + private static ImmutableList toParameterValuesList( + Class valuesProvider, List parameters) { + try { + Constructor 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 checkParameterNamesArePresent(Executable executable) { + checkState( + stream(executable.getParameters()).allMatch(Parameter::isNamePresent), + "" + + "No parameter name could be found for %s, which likely means that parameter names" + + " aren't available at runtime. Please ensure that the this test was built with the" + + " -parameters compiler option.\n" + + "\n" + + "In Maven, you do this by adding true to the" + + " maven-compiler-plugin's configuration. For example:\n" + + "\n" + + "\n" + + " \n" + + " \n" + + " org.apache.maven.plugins\n" + + " maven-compiler-plugin\n" + + " 3.8.1\n" + + " \n" + + " \n" + + " -parameters\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + "\n" + + "Don't forget to run `mvn clean` after making this change.", + executable.getName()); + } + + private static String validateAndGetSingleValueFromRepeatedAnnotation( + TestParameters annotation, Executable executable) { + checkState( + annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class), + "Setting a valuesProvider is not supported for methods/constructors with" + + " multiple @TestParameters annotations on %s()", + executable.getName()); + checkState( + annotation.value().length > 0, + "Either a value or a valuesProvider must be set in @TestParameters on %s()", + executable.getName()); + checkState( + annotation.value().length == 1, + "When specifying more than one @TestParameter for a method/constructor, each annotation" + + " must have exactly one value. Instead, got %s values on %s(): %s", + annotation.value().length, + executable.getName(), + Arrays.toString(annotation.value())); + + return annotation.value()[0]; + } + + private static void validateThatValuesMatchParameters( + TestParametersValues testParametersValues, List parameters) { + ImmutableMap 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 parameters, String maybeCustomName) { + Object yamlMapObject = ParameterValueParsing.parseYamlStringToObject(yamlString); + checkState( + yamlMapObject instanceof Map, + "Cannot map YAML string '%s' to parameters because it is not a mapping", + yamlString); + Map yamlMap = (Map) yamlMapObject; + + ImmutableMap parametersByName = + Maps.uniqueIndex(parameters, Parameter::getName); + checkState( + yamlMap.keySet().equals(parametersByName.keySet()), + "Cannot map YAML string '%s' to parameters %s", + yamlString, + parametersByName.keySet()); + + @SuppressWarnings("unchecked") + Map checkedYamlMap = (Map) yamlMap; + + return TestParametersValues.builder() + .name(maybeCustomName.isEmpty() ? yamlString : maybeCustomName) + .addParameters( + Maps.transformEntries( + checkedYamlMap, + (parameterName, parsedYaml) -> + ParameterValueParsing.parseYamlObjectToJavaType( + parsedYaml, + TypeToken.of(parametersByName.get(parameterName).getParameterizedType())))) + .build(); + } + + // Note: We're not using the Executable interface here because it isn't supported by Java 7 and + // this code is called even if only @TestParameter is used. In other places, Executable is usable + // because @TestParameters only works for Java 8 anyway. + private static boolean hasRelevantAnnotation(Constructor executable) { + return executable.isAnnotationPresent(TestParameters.class) + || executable.isAnnotationPresent(RepeatedTestParameters.class); + } + + private static boolean hasRelevantAnnotation(Method executable) { + return executable.isAnnotationPresent(TestParameters.class) + || executable.isAnnotationPresent(RepeatedTestParameters.class); + } + + private static List toParameterList( + TestParametersValues parametersValues, Parameter[] parameters) { + return stream(parameters) + .map(parameter -> parametersValues.parametersMap().get(parameter.getName())) + .collect(toList()); + } + + private static Constructor getOnlyConstructor(Class testClass) { + ImmutableList> constructors = + ImmutableList.copyOf(testClass.getDeclaredConstructors()); + checkState( + constructors.size() == 1, "Expected exactly one constructor, but got %s", constructors); + return getOnlyElement(constructors); + } + + // Immutable collectors are re-implemented here because they are missing from the Android + // collection library. + private static Collector> 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/junit5/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorJUnit5Test.java b/junit5/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorJUnit5Test.java deleted file mode 100644 index 59ffb18..0000000 --- a/junit5/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorJUnit5Test.java +++ /dev/null @@ -1,607 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector.junit5_otherpackage; - -import static com.google.common.base.Preconditions.checkState; -import static com.google.common.collect.Iterables.getOnlyElement; -import static com.google.common.collect.Lists.newArrayList; -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; -import static java.lang.annotation.RetentionPolicy.RUNTIME; -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.joining; - -import com.google.common.base.CharMatcher; -import com.google.common.base.Throwables; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.testing.junit.testparameterinjector.junit5.TestParameter; -import com.google.testing.junit.testparameterinjector.junit5.TestParameter.TestParameterValuesProvider; -import com.google.testing.junit.testparameterinjector.junit5.TestParameterInjectorTest; -import com.google.testing.junit.testparameterinjector.junit5.TestParameters; -import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValues; -import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValuesProvider; -import java.lang.annotation.Retention; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Stream; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.platform.engine.TestExecutionResult; -import org.junit.platform.engine.TestExecutionResult.Status; -import org.junit.platform.engine.discovery.DiscoverySelectors; -import org.junit.platform.launcher.Launcher; -import org.junit.platform.launcher.LauncherDiscoveryRequest; -import org.junit.platform.launcher.TestExecutionListener; -import org.junit.platform.launcher.TestIdentifier; -import org.junit.platform.launcher.TestPlan; -import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; -import org.junit.platform.launcher.core.LauncherFactory; - -/** Tests the full feature set of TestParameterInjector with JUnit5 (Jupiter). */ -class TestParameterInjectorJUnit5Test { - - abstract static class SuccessfulTestCaseBase { - private static Map testNameToStringifiedParameters; - private static ImmutableMap expectedTestNameToStringifiedParameters; - private String testName; - - @BeforeAll - private static void checkStaticFieldAreNull() { - checkState(testNameToStringifiedParameters == null); - checkState(expectedTestNameToStringifiedParameters == null); - } - - @BeforeEach - private void storeTestName(org.junit.jupiter.api.TestInfo testInfo) { - testName = testInfo.getDisplayName(); - } - - final void storeTestParametersForThisTest(Object... params) { - if (testNameToStringifiedParameters == null) { - testNameToStringifiedParameters = new LinkedHashMap<>(); - // Copying this into a static field because @AfterAll methods have to be static - expectedTestNameToStringifiedParameters = expectedTestNameToStringifiedParameters(); - } - checkState( - !testNameToStringifiedParameters.containsKey(testName), - "Parameters for the test with name '%s' are already stored. This might mean that there" - + " are duplicate test names", - testName); - testNameToStringifiedParameters.put( - testName, stream(params).map(String::valueOf).collect(joining(":"))); - } - - abstract ImmutableMap expectedTestNameToStringifiedParameters(); - - @AfterAll - private static void completedAllTests() { - try { - assertWithMessage(toCopyPastableJavaString(testNameToStringifiedParameters)) - .that(testNameToStringifiedParameters) - .isEqualTo(expectedTestNameToStringifiedParameters); - } finally { - testNameToStringifiedParameters = null; - expectedTestNameToStringifiedParameters = null; - } - } - } - - @RunAsTest - static class SimpleCases_WithoutExplicitConstructor extends SuccessfulTestCaseBase { - @Test - void withoutCustomAnnotation() { - storeTestParametersForThisTest(); - } - - @TestParameterInjectorTest - void withoutParameters() { - storeTestParametersForThisTest(); - } - - @TestParameterInjectorTest - @TestParameters("{name: 1, number: 3.3}") - @TestParameters("{name: abc, number: 5}") - void withParameters_success(String name, double number) { - storeTestParametersForThisTest(name, number); - } - - @TestParameterInjectorTest - void withParameter_success( - @TestParameter({"2", "xyz"}) String name, @TestParameter boolean bool) { - storeTestParametersForThisTest(name, bool); - } - - @Override - ImmutableMap expectedTestNameToStringifiedParameters() { - return ImmutableMap.builder() - .put("withoutCustomAnnotation()", "") - .put("withoutParameters", "") - .put("withParameters_success[{name: 1, number: 3.3}]", "1:3.3") - .put("withParameters_success[{name: abc, number: 5}]", "abc:5.0") - .put("withParameter_success[2,bool=false]", "2:false") - .put("withParameter_success[2,bool=true]", "2:true") - .put("withParameter_success[xyz,bool=false]", "xyz:false") - .put("withParameter_success[xyz,bool=true]", "xyz:true") - .build(); - } - } - - @RunAsTest - static class SimpleCases_WithParameterizedConstructor_TestParameter - extends SuccessfulTestCaseBase { - private final boolean constr; - - @TestParameter({"AAA", "BBB"}) - private String field; - - SimpleCases_WithParameterizedConstructor_TestParameter(@TestParameter boolean constr) { - this.constr = constr; - } - - @TestParameterInjectorTest - void withoutParameters() { - storeTestParametersForThisTest(constr, field); - } - - @TestParameterInjectorTest - @TestParameters("{name: 1}") - @TestParameters("{name: abc}") - void withParameters_success(String name) { - storeTestParametersForThisTest(constr, field, name); - } - - @TestParameterInjectorTest - void withParameter_success(@TestParameter({"2", "xyz"}) String name) { - storeTestParametersForThisTest(constr, field, name); - } - - @Override - ImmutableMap expectedTestNameToStringifiedParameters() { - return ImmutableMap.builder() - .put("withParameters_success[{name: 1},AAA,constr=false]", "false:AAA:1") - .put("withParameters_success[{name: 1},AAA,constr=true]", "true:AAA:1") - .put("withParameters_success[{name: 1},BBB,constr=false]", "false:BBB:1") - .put("withParameters_success[{name: 1},BBB,constr=true]", "true:BBB:1") - .put("withParameters_success[{name: abc},AAA,constr=false]", "false:AAA:abc") - .put("withParameters_success[{name: abc},AAA,constr=true]", "true:AAA:abc") - .put("withParameters_success[{name: abc},BBB,constr=false]", "false:BBB:abc") - .put("withParameters_success[{name: abc},BBB,constr=true]", "true:BBB:abc") - .put("withParameter_success[AAA,constr=false,2]", "false:AAA:2") - .put("withParameter_success[AAA,constr=false,xyz]", "false:AAA:xyz") - .put("withParameter_success[AAA,constr=true,2]", "true:AAA:2") - .put("withParameter_success[AAA,constr=true,xyz]", "true:AAA:xyz") - .put("withParameter_success[BBB,constr=false,2]", "false:BBB:2") - .put("withParameter_success[BBB,constr=false,xyz]", "false:BBB:xyz") - .put("withParameter_success[BBB,constr=true,2]", "true:BBB:2") - .put("withParameter_success[BBB,constr=true,xyz]", "true:BBB:xyz") - .put("withoutParameters[AAA,constr=false]", "false:AAA") - .put("withoutParameters[AAA,constr=true]", "true:AAA") - .put("withoutParameters[BBB,constr=false]", "false:BBB") - .put("withoutParameters[BBB,constr=true]", "true:BBB") - .build(); - } - } - - @RunAsTest - static class SimpleCases_WithParameterizedConstructor_TestParameters - extends SuccessfulTestCaseBase { - private final boolean constr; - - @TestParameter({"AAA", "BBB"}) - private String field; - - @TestParameters("{constr: true}") - @TestParameters("{constr: false}") - SimpleCases_WithParameterizedConstructor_TestParameters(boolean constr) { - this.constr = constr; - } - - @TestParameterInjectorTest - void withoutParameters() { - storeTestParametersForThisTest(constr, field); - } - - @TestParameterInjectorTest - @TestParameters("{name: 1}") - @TestParameters("{name: abc}") - void withParameters_success(String name) { - storeTestParametersForThisTest(constr, field, name); - } - - @TestParameterInjectorTest - void withParameter_success(@TestParameter({"2", "xyz"}) String name) { - storeTestParametersForThisTest(constr, field, name); - } - - @Override - ImmutableMap expectedTestNameToStringifiedParameters() { - return ImmutableMap.builder() - .put("withParameters_success[{constr: true},{name: 1},AAA]", "true:AAA:1") - .put("withParameters_success[{constr: true},{name: 1},BBB]", "true:BBB:1") - .put("withParameters_success[{constr: true},{name: abc},AAA]", "true:AAA:abc") - .put("withParameters_success[{constr: true},{name: abc},BBB]", "true:BBB:abc") - .put("withParameters_success[{constr: false},{name: 1},AAA]", "false:AAA:1") - .put("withParameters_success[{constr: false},{name: 1},BBB]", "false:BBB:1") - .put("withParameters_success[{constr: false},{name: abc},AAA]", "false:AAA:abc") - .put("withParameters_success[{constr: false},{name: abc},BBB]", "false:BBB:abc") - .put("withParameter_success[{constr: true},AAA,2]", "true:AAA:2") - .put("withParameter_success[{constr: true},AAA,xyz]", "true:AAA:xyz") - .put("withParameter_success[{constr: true},BBB,2]", "true:BBB:2") - .put("withParameter_success[{constr: true},BBB,xyz]", "true:BBB:xyz") - .put("withParameter_success[{constr: false},AAA,2]", "false:AAA:2") - .put("withParameter_success[{constr: false},AAA,xyz]", "false:AAA:xyz") - .put("withParameter_success[{constr: false},BBB,2]", "false:BBB:2") - .put("withParameter_success[{constr: false},BBB,xyz]", "false:BBB:xyz") - .put("withoutParameters[{constr: true},AAA]", "true:AAA") - .put("withoutParameters[{constr: true},BBB]", "true:BBB") - .put("withoutParameters[{constr: false},AAA]", "false:AAA") - .put("withoutParameters[{constr: false},BBB]", "false:BBB") - .build(); - } - } - - @RunAsTest - public static class AdvancedCases_WithValuesProvider extends SuccessfulTestCaseBase { - private final TestEnum testEnum; - - @TestParameters(valuesProvider = TestEnumValuesProvider.class) - public AdvancedCases_WithValuesProvider(TestEnum testEnum) { - this.testEnum = testEnum; - } - - @TestParameterInjectorTest - void test1() { - storeTestParametersForThisTest(testEnum); - } - - @Override - ImmutableMap expectedTestNameToStringifiedParameters() { - return ImmutableMap.builder() - .put("test1[one]", "ONE") - .put("test1[two]", "TWO") - .put("test1[null-case]", "null") - .build(); - } - } - - @RunAsTest - public static class AdvancedCases_WithValueProvider extends SuccessfulTestCaseBase { - @TestParameterInjectorTest - void stringTest(@TestParameter(valuesProvider = TestStringProvider.class) String string) { - storeTestParametersForThisTest(string); - } - - @TestParameterInjectorTest - void charMatcherTest( - @TestParameter(valuesProvider = CharMatcherProvider.class) CharMatcher charMatcher) { - storeTestParametersForThisTest(charMatcher); - } - - @Override - ImmutableMap expectedTestNameToStringifiedParameters() { - return ImmutableMap.builder() - .put("stringTest[A]", "A") - .put("stringTest[B]", "B") - .put("stringTest[null]", "null") - .put("charMatcherTest[CharMatcher.any()]", "CharMatcher.any()") - .put("charMatcherTest[CharMatcher.ascii()]", "CharMatcher.ascii()") - .put("charMatcherTest[CharMatcher.whitespace()]", "CharMatcher.whitespace()") - .build(); - } - - private static final class TestStringProvider implements TestParameterValuesProvider { - @Override - public List provideValues() { - return newArrayList("A", "B", null); - } - } - - private static final class CharMatcherProvider implements TestParameterValuesProvider { - @Override - public List provideValues() { - return newArrayList(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace()); - } - } - } - - public abstract static class BaseClassWithTestParametersMethod extends SuccessfulTestCaseBase { - @TestParameterInjectorTest - @TestParameters("{testEnum: ONE}") - @TestParameters("{testEnum: TWO}") - void testInBase(TestEnum testEnum) { - storeTestParametersForThisTest(testEnum); - } - } - - @RunAsTest - public static class AdvancedCases_WithBaseClass_TestParametersMethodInBase - extends BaseClassWithTestParametersMethod { - @TestParameterInjectorTest - @TestParameters({"{testEnum: TWO}", "{testEnum: THREE}"}) - void testInChild(TestEnum testEnum) { - storeTestParametersForThisTest(testEnum); - } - - @Override - ImmutableMap expectedTestNameToStringifiedParameters() { - return ImmutableMap.builder() - .put("testInBase[{testEnum: ONE}]", "ONE") - .put("testInBase[{testEnum: TWO}]", "TWO") - .put("testInChild[{testEnum: TWO}]", "TWO") - .put("testInChild[{testEnum: THREE}]", "THREE") - .build(); - } - } - - public abstract static class BaseClassWithTestParameterMethod extends SuccessfulTestCaseBase { - @TestParameterInjectorTest - void testInBase(@TestParameter({"ONE", "TWO"}) TestEnum testEnum) { - storeTestParametersForThisTest(testEnum); - } - } - - @RunAsTest - public static class AdvancedCases_WithBaseClass_TestParameterMethodInBase - extends BaseClassWithTestParameterMethod { - @TestParameterInjectorTest - @TestParameters({"{testEnum: TWO}", "{testEnum: THREE}"}) - void testInChild(TestEnum testEnum) { - storeTestParametersForThisTest(testEnum); - } - - @Override - ImmutableMap expectedTestNameToStringifiedParameters() { - return ImmutableMap.builder() - .put("testInBase[ONE]", "ONE") - .put("testInBase[TWO]", "TWO") - .put("testInChild[{testEnum: TWO}]", "TWO") - .put("testInChild[{testEnum: THREE}]", "THREE") - .build(); - } - } - - public abstract static class BaseClassWithTestParameterField extends SuccessfulTestCaseBase { - @TestParameter TestEnum fieldInBase; - } - - @RunAsTest - public static class AdvancedCases_WithBaseClass_TestParameterFieldInBase - extends BaseClassWithTestParameterField { - @TestParameterInjectorTest - void testInChild() { - storeTestParametersForThisTest(fieldInBase); - } - - @Override - ImmutableMap expectedTestNameToStringifiedParameters() { - return ImmutableMap.builder() - .put("testInChild[ONE]", "ONE") - .put("testInChild[TWO]", "TWO") - .put("testInChild[THREE]", "THREE") - .build(); - } - } - - @RunAsTest( - failsWithMessage = - "Either a value or a valuesProvider must be set in @TestParameters on test1()") - public static class InvalidTest_TestParameters_EmptyAnnotation { - @TestParameterInjectorTest - @TestParameters - void test1() {} - } - - @RunAsTest( - failsWithMessage = "Either a value or a valuesProvider must be set in @TestParameters on ") - public static class InvalidTest_TestParameters_EmptyAnnotationOnConstructor { - @TestParameters - public InvalidTest_TestParameters_EmptyAnnotationOnConstructor() {} - - @TestParameterInjectorTest - void test1() {} - } - - @RunAsTest( - failsWithMessage = - "It is not allowed to specify both value and valuesProvider in" - + " @TestParameters(value=[{testEnum: ONE}], valuesProvider=TestEnumValuesProvider)" - + " on test1()") - public static class InvalidTest_TestParameters_CombiningValueWithProvider { - @TestParameterInjectorTest - @TestParameters(value = "{testEnum: ONE}", valuesProvider = TestEnumValuesProvider.class) - void test1(TestEnum testEnum) {} - } - - @RunAsTest( - failsWithMessage = - "Either a value or a valuesProvider must be set in @TestParameters on test1()") - public static class InvalidTest_TestParameters_RepeatedAnnotationIsEmpty { - @TestParameterInjectorTest - @TestParameters(value = "{testEnum: ONE}") - @TestParameters - void test1(TestEnum testEnum) {} - } - - @RunAsTest( - failsWithMessage = - "When specifying more than one @TestParameter for a method/constructor, each annotation" - + " must have exactly one value. Instead, got 2 values on test1(): [{testEnum: TWO}," - + " {testEnum: THREE}]") - public static class InvalidTest_TestParameters_RepeatedAnnotationHasMultipleValues { - @TestParameterInjectorTest - @TestParameters(value = "{testEnum: ONE}") - @TestParameters(value = {"{testEnum: TWO}", "{testEnum: THREE}"}) - void test1(TestEnum testEnum) {} - } - - @RunAsTest( - failsWithMessage = - "Setting a valuesProvider is not supported for methods/constructors with" - + " multiple @TestParameters annotations on test1()") - public static class InvalidTest_TestParameters_RepeatedAnnotationHasProvider { - @TestParameterInjectorTest - @TestParameters(valuesProvider = TestEnumValuesProvider.class) - @TestParameters(valuesProvider = TestEnumValuesProvider.class) - void test1(TestEnum testEnum) {} - } - - @RunAsTest( - failsWithMessage = - "Setting @TestParameters.customName is only allowed if there is exactly one YAML string" - + " in @TestParameters.value (on test1())") - public static class InvalidTest_TestParameters_NamedAnnotationHasMultipleValues { - @TestParameterInjectorTest - @TestParameters( - customName = "custom", - value = {"{testEnum: TWO}", "{testEnum: THREE}"}) - void test1(TestEnum testEnum) {} - } - - @RunAsTest( - failsWithMessage = - "Could not find a no-arg constructor for NonStaticProvider, probably because it is a" - + " not-static inner class. You can fix this by making NonStaticProvider static.") - public static class InvalidTest_TestParameter_NonStaticProviderClass { - @TestParameterInjectorTest - void test(@TestParameter(valuesProvider = NonStaticProvider.class) int i) {} - - @SuppressWarnings("ClassCanBeStatic") - class NonStaticProvider implements TestParameterValuesProvider { - @Override - public List provideValues() { - return ImmutableList.of(); - } - } - } - - @ParameterizedTest(name = "{0}") - @MethodSource("provideTestClassesThatExpectSuccess") - void runTest_success(Class testClass) { - FailureListener listener = new FailureListener(); - LauncherDiscoveryRequest request = - LauncherDiscoveryRequestBuilder.request() - .selectors(DiscoverySelectors.selectClass(testClass)) - .build(); - Launcher launcher = LauncherFactory.create(); - launcher.registerTestExecutionListeners(listener); - launcher.execute(request); - - assertNoFailures(listener.failures); - } - - @ParameterizedTest(name = "{0} fails with '{1}'") - @MethodSource("provideTestClassesThatExpectFailure") - void runTest_failure(Class testClass, String failureMessage) { - FailureListener listener = new FailureListener(); - LauncherDiscoveryRequest request = - LauncherDiscoveryRequestBuilder.request() - .selectors(DiscoverySelectors.selectClass(testClass)) - .build(); - Launcher launcher = LauncherFactory.create(); - TestPlan testPlan = launcher.discover(request); - launcher.registerTestExecutionListeners(listener); - launcher.execute(request); - - assertThat(listener.failures).hasSize(1); - assertThat(getOnlyElement(listener.failures)).contains(failureMessage); - } - - private static Stream> provideTestClassesThatExpectSuccess() { - return stream(TestParameterInjectorJUnit5Test.class.getDeclaredClasses()) - .filter( - cls -> - cls.isAnnotationPresent(RunAsTest.class) - && cls.getAnnotation(RunAsTest.class).failsWithMessage().isEmpty()); - } - - private static Stream provideTestClassesThatExpectFailure() { - return stream(TestParameterInjectorJUnit5Test.class.getDeclaredClasses()) - .filter( - cls -> - cls.isAnnotationPresent(RunAsTest.class) - && !cls.getAnnotation(RunAsTest.class).failsWithMessage().isEmpty()) - .map(cls -> Arguments.of(cls, cls.getAnnotation(RunAsTest.class).failsWithMessage())); - } - - private static void assertNoFailures(List failures) { - if (failures.size() == 1) { - throw new AssertionError(getOnlyElement(failures)); - } else if (failures.size() > 1) { - throw new AssertionError( - String.format( - "Test failed unexpectedly:\n\n%s", - failures.stream().collect(joining("\n------------------------------------\n")))); - } - } - - private static String toCopyPastableJavaString(Map map) { - StringBuilder resultBuilder = new StringBuilder(); - resultBuilder.append("\n----------------------\n"); - resultBuilder.append("ImmutableMap.builder()\n"); - map.forEach( - (key, value) -> - resultBuilder.append(String.format(" .put(\"%s\", \"%s\")\n", key, value))); - resultBuilder.append(" .build()\n"); - resultBuilder.append("----------------------\n"); - return resultBuilder.toString(); - } - - class FailureListener implements TestExecutionListener { - final List failures = new ArrayList<>(); - - @Override - public void executionFinished( - TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { - if (testExecutionResult.getStatus() != Status.SUCCESSFUL) { - failures.add( - String.format( - "%s --> %s", - testIdentifier.getDisplayName(), - testExecutionResult.getThrowable().isPresent() - ? Throwables.getStackTraceAsString(testExecutionResult.getThrowable().get()) - : testExecutionResult)); - } - } - } - - @Retention(RUNTIME) - @interface RunAsTest { - String failsWithMessage() default ""; - } - - public enum TestEnum { - ONE, - TWO, - THREE; - } - - private static final class TestEnumValuesProvider implements TestParametersValuesProvider { - @Override - public List provideValues() { - return ImmutableList.of( - TestParametersValues.builder().name("one").addParameter("testEnum", TestEnum.ONE).build(), - TestParametersValues.builder().name("two").addParameter("testEnum", TestEnum.TWO).build(), - TestParametersValues.builder().name("null-case").addParameter("testEnum", null).build()); - } - } -} diff --git a/junit5/src/test/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorJUnit5Test.java b/junit5/src/test/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorJUnit5Test.java new file mode 100644 index 0000000..59ffb18 --- /dev/null +++ b/junit5/src/test/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorJUnit5Test.java @@ -0,0 +1,607 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5_otherpackage; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.testing.junit.testparameterinjector.junit5.TestParameter; +import com.google.testing.junit.testparameterinjector.junit5.TestParameter.TestParameterValuesProvider; +import com.google.testing.junit.testparameterinjector.junit5.TestParameterInjectorTest; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValues; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValuesProvider; +import java.lang.annotation.Retention; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.TestExecutionResult.Status; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestIdentifier; +import org.junit.platform.launcher.TestPlan; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.launcher.core.LauncherFactory; + +/** Tests the full feature set of TestParameterInjector with JUnit5 (Jupiter). */ +class TestParameterInjectorJUnit5Test { + + abstract static class SuccessfulTestCaseBase { + private static Map testNameToStringifiedParameters; + private static ImmutableMap expectedTestNameToStringifiedParameters; + private String testName; + + @BeforeAll + private static void checkStaticFieldAreNull() { + checkState(testNameToStringifiedParameters == null); + checkState(expectedTestNameToStringifiedParameters == null); + } + + @BeforeEach + private void storeTestName(org.junit.jupiter.api.TestInfo testInfo) { + testName = testInfo.getDisplayName(); + } + + final void storeTestParametersForThisTest(Object... params) { + if (testNameToStringifiedParameters == null) { + testNameToStringifiedParameters = new LinkedHashMap<>(); + // Copying this into a static field because @AfterAll methods have to be static + expectedTestNameToStringifiedParameters = expectedTestNameToStringifiedParameters(); + } + checkState( + !testNameToStringifiedParameters.containsKey(testName), + "Parameters for the test with name '%s' are already stored. This might mean that there" + + " are duplicate test names", + testName); + testNameToStringifiedParameters.put( + testName, stream(params).map(String::valueOf).collect(joining(":"))); + } + + abstract ImmutableMap expectedTestNameToStringifiedParameters(); + + @AfterAll + private static void completedAllTests() { + try { + assertWithMessage(toCopyPastableJavaString(testNameToStringifiedParameters)) + .that(testNameToStringifiedParameters) + .isEqualTo(expectedTestNameToStringifiedParameters); + } finally { + testNameToStringifiedParameters = null; + expectedTestNameToStringifiedParameters = null; + } + } + } + + @RunAsTest + static class SimpleCases_WithoutExplicitConstructor extends SuccessfulTestCaseBase { + @Test + void withoutCustomAnnotation() { + storeTestParametersForThisTest(); + } + + @TestParameterInjectorTest + void withoutParameters() { + storeTestParametersForThisTest(); + } + + @TestParameterInjectorTest + @TestParameters("{name: 1, number: 3.3}") + @TestParameters("{name: abc, number: 5}") + void withParameters_success(String name, double number) { + storeTestParametersForThisTest(name, number); + } + + @TestParameterInjectorTest + void withParameter_success( + @TestParameter({"2", "xyz"}) String name, @TestParameter boolean bool) { + storeTestParametersForThisTest(name, bool); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("withoutCustomAnnotation()", "") + .put("withoutParameters", "") + .put("withParameters_success[{name: 1, number: 3.3}]", "1:3.3") + .put("withParameters_success[{name: abc, number: 5}]", "abc:5.0") + .put("withParameter_success[2,bool=false]", "2:false") + .put("withParameter_success[2,bool=true]", "2:true") + .put("withParameter_success[xyz,bool=false]", "xyz:false") + .put("withParameter_success[xyz,bool=true]", "xyz:true") + .build(); + } + } + + @RunAsTest + static class SimpleCases_WithParameterizedConstructor_TestParameter + extends SuccessfulTestCaseBase { + private final boolean constr; + + @TestParameter({"AAA", "BBB"}) + private String field; + + SimpleCases_WithParameterizedConstructor_TestParameter(@TestParameter boolean constr) { + this.constr = constr; + } + + @TestParameterInjectorTest + void withoutParameters() { + storeTestParametersForThisTest(constr, field); + } + + @TestParameterInjectorTest + @TestParameters("{name: 1}") + @TestParameters("{name: abc}") + void withParameters_success(String name) { + storeTestParametersForThisTest(constr, field, name); + } + + @TestParameterInjectorTest + void withParameter_success(@TestParameter({"2", "xyz"}) String name) { + storeTestParametersForThisTest(constr, field, name); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("withParameters_success[{name: 1},AAA,constr=false]", "false:AAA:1") + .put("withParameters_success[{name: 1},AAA,constr=true]", "true:AAA:1") + .put("withParameters_success[{name: 1},BBB,constr=false]", "false:BBB:1") + .put("withParameters_success[{name: 1},BBB,constr=true]", "true:BBB:1") + .put("withParameters_success[{name: abc},AAA,constr=false]", "false:AAA:abc") + .put("withParameters_success[{name: abc},AAA,constr=true]", "true:AAA:abc") + .put("withParameters_success[{name: abc},BBB,constr=false]", "false:BBB:abc") + .put("withParameters_success[{name: abc},BBB,constr=true]", "true:BBB:abc") + .put("withParameter_success[AAA,constr=false,2]", "false:AAA:2") + .put("withParameter_success[AAA,constr=false,xyz]", "false:AAA:xyz") + .put("withParameter_success[AAA,constr=true,2]", "true:AAA:2") + .put("withParameter_success[AAA,constr=true,xyz]", "true:AAA:xyz") + .put("withParameter_success[BBB,constr=false,2]", "false:BBB:2") + .put("withParameter_success[BBB,constr=false,xyz]", "false:BBB:xyz") + .put("withParameter_success[BBB,constr=true,2]", "true:BBB:2") + .put("withParameter_success[BBB,constr=true,xyz]", "true:BBB:xyz") + .put("withoutParameters[AAA,constr=false]", "false:AAA") + .put("withoutParameters[AAA,constr=true]", "true:AAA") + .put("withoutParameters[BBB,constr=false]", "false:BBB") + .put("withoutParameters[BBB,constr=true]", "true:BBB") + .build(); + } + } + + @RunAsTest + static class SimpleCases_WithParameterizedConstructor_TestParameters + extends SuccessfulTestCaseBase { + private final boolean constr; + + @TestParameter({"AAA", "BBB"}) + private String field; + + @TestParameters("{constr: true}") + @TestParameters("{constr: false}") + SimpleCases_WithParameterizedConstructor_TestParameters(boolean constr) { + this.constr = constr; + } + + @TestParameterInjectorTest + void withoutParameters() { + storeTestParametersForThisTest(constr, field); + } + + @TestParameterInjectorTest + @TestParameters("{name: 1}") + @TestParameters("{name: abc}") + void withParameters_success(String name) { + storeTestParametersForThisTest(constr, field, name); + } + + @TestParameterInjectorTest + void withParameter_success(@TestParameter({"2", "xyz"}) String name) { + storeTestParametersForThisTest(constr, field, name); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("withParameters_success[{constr: true},{name: 1},AAA]", "true:AAA:1") + .put("withParameters_success[{constr: true},{name: 1},BBB]", "true:BBB:1") + .put("withParameters_success[{constr: true},{name: abc},AAA]", "true:AAA:abc") + .put("withParameters_success[{constr: true},{name: abc},BBB]", "true:BBB:abc") + .put("withParameters_success[{constr: false},{name: 1},AAA]", "false:AAA:1") + .put("withParameters_success[{constr: false},{name: 1},BBB]", "false:BBB:1") + .put("withParameters_success[{constr: false},{name: abc},AAA]", "false:AAA:abc") + .put("withParameters_success[{constr: false},{name: abc},BBB]", "false:BBB:abc") + .put("withParameter_success[{constr: true},AAA,2]", "true:AAA:2") + .put("withParameter_success[{constr: true},AAA,xyz]", "true:AAA:xyz") + .put("withParameter_success[{constr: true},BBB,2]", "true:BBB:2") + .put("withParameter_success[{constr: true},BBB,xyz]", "true:BBB:xyz") + .put("withParameter_success[{constr: false},AAA,2]", "false:AAA:2") + .put("withParameter_success[{constr: false},AAA,xyz]", "false:AAA:xyz") + .put("withParameter_success[{constr: false},BBB,2]", "false:BBB:2") + .put("withParameter_success[{constr: false},BBB,xyz]", "false:BBB:xyz") + .put("withoutParameters[{constr: true},AAA]", "true:AAA") + .put("withoutParameters[{constr: true},BBB]", "true:BBB") + .put("withoutParameters[{constr: false},AAA]", "false:AAA") + .put("withoutParameters[{constr: false},BBB]", "false:BBB") + .build(); + } + } + + @RunAsTest + public static class AdvancedCases_WithValuesProvider extends SuccessfulTestCaseBase { + private final TestEnum testEnum; + + @TestParameters(valuesProvider = TestEnumValuesProvider.class) + public AdvancedCases_WithValuesProvider(TestEnum testEnum) { + this.testEnum = testEnum; + } + + @TestParameterInjectorTest + void test1() { + storeTestParametersForThisTest(testEnum); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test1[one]", "ONE") + .put("test1[two]", "TWO") + .put("test1[null-case]", "null") + .build(); + } + } + + @RunAsTest + public static class AdvancedCases_WithValueProvider extends SuccessfulTestCaseBase { + @TestParameterInjectorTest + void stringTest(@TestParameter(valuesProvider = TestStringProvider.class) String string) { + storeTestParametersForThisTest(string); + } + + @TestParameterInjectorTest + void charMatcherTest( + @TestParameter(valuesProvider = CharMatcherProvider.class) CharMatcher charMatcher) { + storeTestParametersForThisTest(charMatcher); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("stringTest[A]", "A") + .put("stringTest[B]", "B") + .put("stringTest[null]", "null") + .put("charMatcherTest[CharMatcher.any()]", "CharMatcher.any()") + .put("charMatcherTest[CharMatcher.ascii()]", "CharMatcher.ascii()") + .put("charMatcherTest[CharMatcher.whitespace()]", "CharMatcher.whitespace()") + .build(); + } + + private static final class TestStringProvider implements TestParameterValuesProvider { + @Override + public List provideValues() { + return newArrayList("A", "B", null); + } + } + + private static final class CharMatcherProvider implements TestParameterValuesProvider { + @Override + public List provideValues() { + return newArrayList(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace()); + } + } + } + + public abstract static class BaseClassWithTestParametersMethod extends SuccessfulTestCaseBase { + @TestParameterInjectorTest + @TestParameters("{testEnum: ONE}") + @TestParameters("{testEnum: TWO}") + void testInBase(TestEnum testEnum) { + storeTestParametersForThisTest(testEnum); + } + } + + @RunAsTest + public static class AdvancedCases_WithBaseClass_TestParametersMethodInBase + extends BaseClassWithTestParametersMethod { + @TestParameterInjectorTest + @TestParameters({"{testEnum: TWO}", "{testEnum: THREE}"}) + void testInChild(TestEnum testEnum) { + storeTestParametersForThisTest(testEnum); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("testInBase[{testEnum: ONE}]", "ONE") + .put("testInBase[{testEnum: TWO}]", "TWO") + .put("testInChild[{testEnum: TWO}]", "TWO") + .put("testInChild[{testEnum: THREE}]", "THREE") + .build(); + } + } + + public abstract static class BaseClassWithTestParameterMethod extends SuccessfulTestCaseBase { + @TestParameterInjectorTest + void testInBase(@TestParameter({"ONE", "TWO"}) TestEnum testEnum) { + storeTestParametersForThisTest(testEnum); + } + } + + @RunAsTest + public static class AdvancedCases_WithBaseClass_TestParameterMethodInBase + extends BaseClassWithTestParameterMethod { + @TestParameterInjectorTest + @TestParameters({"{testEnum: TWO}", "{testEnum: THREE}"}) + void testInChild(TestEnum testEnum) { + storeTestParametersForThisTest(testEnum); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("testInBase[ONE]", "ONE") + .put("testInBase[TWO]", "TWO") + .put("testInChild[{testEnum: TWO}]", "TWO") + .put("testInChild[{testEnum: THREE}]", "THREE") + .build(); + } + } + + public abstract static class BaseClassWithTestParameterField extends SuccessfulTestCaseBase { + @TestParameter TestEnum fieldInBase; + } + + @RunAsTest + public static class AdvancedCases_WithBaseClass_TestParameterFieldInBase + extends BaseClassWithTestParameterField { + @TestParameterInjectorTest + void testInChild() { + storeTestParametersForThisTest(fieldInBase); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("testInChild[ONE]", "ONE") + .put("testInChild[TWO]", "TWO") + .put("testInChild[THREE]", "THREE") + .build(); + } + } + + @RunAsTest( + failsWithMessage = + "Either a value or a valuesProvider must be set in @TestParameters on test1()") + public static class InvalidTest_TestParameters_EmptyAnnotation { + @TestParameterInjectorTest + @TestParameters + void test1() {} + } + + @RunAsTest( + failsWithMessage = "Either a value or a valuesProvider must be set in @TestParameters on ") + public static class InvalidTest_TestParameters_EmptyAnnotationOnConstructor { + @TestParameters + public InvalidTest_TestParameters_EmptyAnnotationOnConstructor() {} + + @TestParameterInjectorTest + void test1() {} + } + + @RunAsTest( + failsWithMessage = + "It is not allowed to specify both value and valuesProvider in" + + " @TestParameters(value=[{testEnum: ONE}], valuesProvider=TestEnumValuesProvider)" + + " on test1()") + public static class InvalidTest_TestParameters_CombiningValueWithProvider { + @TestParameterInjectorTest + @TestParameters(value = "{testEnum: ONE}", valuesProvider = TestEnumValuesProvider.class) + void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "Either a value or a valuesProvider must be set in @TestParameters on test1()") + public static class InvalidTest_TestParameters_RepeatedAnnotationIsEmpty { + @TestParameterInjectorTest + @TestParameters(value = "{testEnum: ONE}") + @TestParameters + void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "When specifying more than one @TestParameter for a method/constructor, each annotation" + + " must have exactly one value. Instead, got 2 values on test1(): [{testEnum: TWO}," + + " {testEnum: THREE}]") + public static class InvalidTest_TestParameters_RepeatedAnnotationHasMultipleValues { + @TestParameterInjectorTest + @TestParameters(value = "{testEnum: ONE}") + @TestParameters(value = {"{testEnum: TWO}", "{testEnum: THREE}"}) + void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "Setting a valuesProvider is not supported for methods/constructors with" + + " multiple @TestParameters annotations on test1()") + public static class InvalidTest_TestParameters_RepeatedAnnotationHasProvider { + @TestParameterInjectorTest + @TestParameters(valuesProvider = TestEnumValuesProvider.class) + @TestParameters(valuesProvider = TestEnumValuesProvider.class) + void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "Setting @TestParameters.customName is only allowed if there is exactly one YAML string" + + " in @TestParameters.value (on test1())") + public static class InvalidTest_TestParameters_NamedAnnotationHasMultipleValues { + @TestParameterInjectorTest + @TestParameters( + customName = "custom", + value = {"{testEnum: TWO}", "{testEnum: THREE}"}) + void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "Could not find a no-arg constructor for NonStaticProvider, probably because it is a" + + " not-static inner class. You can fix this by making NonStaticProvider static.") + public static class InvalidTest_TestParameter_NonStaticProviderClass { + @TestParameterInjectorTest + void test(@TestParameter(valuesProvider = NonStaticProvider.class) int i) {} + + @SuppressWarnings("ClassCanBeStatic") + class NonStaticProvider implements TestParameterValuesProvider { + @Override + public List provideValues() { + return ImmutableList.of(); + } + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("provideTestClassesThatExpectSuccess") + void runTest_success(Class testClass) { + FailureListener listener = new FailureListener(); + LauncherDiscoveryRequest request = + LauncherDiscoveryRequestBuilder.request() + .selectors(DiscoverySelectors.selectClass(testClass)) + .build(); + Launcher launcher = LauncherFactory.create(); + launcher.registerTestExecutionListeners(listener); + launcher.execute(request); + + assertNoFailures(listener.failures); + } + + @ParameterizedTest(name = "{0} fails with '{1}'") + @MethodSource("provideTestClassesThatExpectFailure") + void runTest_failure(Class testClass, String failureMessage) { + FailureListener listener = new FailureListener(); + LauncherDiscoveryRequest request = + LauncherDiscoveryRequestBuilder.request() + .selectors(DiscoverySelectors.selectClass(testClass)) + .build(); + Launcher launcher = LauncherFactory.create(); + TestPlan testPlan = launcher.discover(request); + launcher.registerTestExecutionListeners(listener); + launcher.execute(request); + + assertThat(listener.failures).hasSize(1); + assertThat(getOnlyElement(listener.failures)).contains(failureMessage); + } + + private static Stream> provideTestClassesThatExpectSuccess() { + return stream(TestParameterInjectorJUnit5Test.class.getDeclaredClasses()) + .filter( + cls -> + cls.isAnnotationPresent(RunAsTest.class) + && cls.getAnnotation(RunAsTest.class).failsWithMessage().isEmpty()); + } + + private static Stream provideTestClassesThatExpectFailure() { + return stream(TestParameterInjectorJUnit5Test.class.getDeclaredClasses()) + .filter( + cls -> + cls.isAnnotationPresent(RunAsTest.class) + && !cls.getAnnotation(RunAsTest.class).failsWithMessage().isEmpty()) + .map(cls -> Arguments.of(cls, cls.getAnnotation(RunAsTest.class).failsWithMessage())); + } + + private static void assertNoFailures(List failures) { + if (failures.size() == 1) { + throw new AssertionError(getOnlyElement(failures)); + } else if (failures.size() > 1) { + throw new AssertionError( + String.format( + "Test failed unexpectedly:\n\n%s", + failures.stream().collect(joining("\n------------------------------------\n")))); + } + } + + private static String toCopyPastableJavaString(Map map) { + StringBuilder resultBuilder = new StringBuilder(); + resultBuilder.append("\n----------------------\n"); + resultBuilder.append("ImmutableMap.builder()\n"); + map.forEach( + (key, value) -> + resultBuilder.append(String.format(" .put(\"%s\", \"%s\")\n", key, value))); + resultBuilder.append(" .build()\n"); + resultBuilder.append("----------------------\n"); + return resultBuilder.toString(); + } + + class FailureListener implements TestExecutionListener { + final List failures = new ArrayList<>(); + + @Override + public void executionFinished( + TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { + if (testExecutionResult.getStatus() != Status.SUCCESSFUL) { + failures.add( + String.format( + "%s --> %s", + testIdentifier.getDisplayName(), + testExecutionResult.getThrowable().isPresent() + ? Throwables.getStackTraceAsString(testExecutionResult.getThrowable().get()) + : testExecutionResult)); + } + } + } + + @Retention(RUNTIME) + @interface RunAsTest { + String failsWithMessage() default ""; + } + + public enum TestEnum { + ONE, + TWO, + THREE; + } + + private static final class TestEnumValuesProvider implements TestParametersValuesProvider { + @Override + public List 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()); + } + } +} -- cgit v1.2.3 From 1f2de4ca26e4e7e8672f8789c0a93e5f82d7ab98 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Thu, 20 Jan 2022 09:34:10 +0000 Subject: Github README: Add instructions for JUnit5 Fixes https://github.com/google/TestParameterInjector/issues/11 --- README.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f1507bb..967dc3c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ TestParameterInjector ## Introduction -`TestParameterInjector` is a JUnit4 test runner that runs its test methods for +`TestParameterInjector` is a JUnit4 and JUnit5 test runner that runs its test methods for different combinations of field/parameter values. Parameterized tests are a great way to avoid code duplication between tests and @@ -23,6 +23,8 @@ frameworks used at Google. ## Getting started +### JUnit4 + To start using `TestParameterInjector` right away, copy the following snippet: ```java @@ -52,7 +54,7 @@ And add the following dependency to your `.pom` file: com.google.testparameterinjector test-parameter-injector - 1.7 + 1.8 ``` @@ -60,9 +62,56 @@ or see [this maven.org page](https://search.maven.org/artifact/com.google.testparameterinjector/test-parameter-injector) for instructions for other build tools. +### JUnit5 (Jupiter) +
          +Click to expand + +To start using `TestParameterInjector` right away, copy the following snippet: + +```java +import com.google.testing.junit.testparameterinjector.junit5.TestParameterInjectorTest; +import com.google.testing.junit.testparameterinjector.junit5.TestParameter; + +class MyTest { + + @TestParameter boolean isDryRun; + + @TestParameterInjectorTest + void test1(@TestParameter boolean enableFlag) { + // ... + } + + @TestParameterInjectorTest + void test2(@TestParameter MyEnum myEnum) { + // ... + } + + enum MyEnum { VALUE_A, VALUE_B, VALUE_C } +} +``` + +And add the following dependency to your `.pom` file: + +```xml + + com.google.testparameterinjector + test-parameter-injector-junit5 + 1.8 + +``` + +or see [this maven.org +page](https://search.maven.org/artifact/com.google.testparameterinjector/test-parameter-injector-junit5) +for instructions for other build tools. + +
          ## Basics +**Note about JUnit4 vs JUnit5:**
          +The code below assumes you're using JUnit4. For JUnit5 users, simply remove the +`@RunWith` annotation and replace `@Test` by `@TestParameterInjectorTest`. + ### `@TestParameter` for testing all combinations #### Parameterizing a single test method @@ -243,6 +292,10 @@ tests will be run for the given parameter sets. ## Advanced usage +**Note about JUnit4 vs JUnit5:**
          +The code below assumes you're using JUnit4. For JUnit5 users, simply remove the +`@RunWith` annotation and replace `@Test` by `@TestParameterInjectorTest`. + ### Dynamic parameter generation for `@TestParameter` Instead of providing a list of parsable strings, you can implement your own -- cgit v1.2.3 From 162eef62993c7b5fe07fc59a09962769676013db Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Thu, 20 Jan 2022 10:41:55 +0000 Subject: README: Add test to maven targets --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 967dc3c..107b985 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ And add the following dependency to your `.pom` file: com.google.testparameterinjector test-parameter-injector 1.8 + test ``` @@ -97,6 +98,7 @@ And add the following dependency to your `.pom` file: com.google.testparameterinjector test-parameter-injector-junit5 1.8 + test ``` -- cgit v1.2.3 From 1c175712b54f7b988f6c3cb7272c45b0592f8408 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Thu, 20 Jan 2022 10:43:14 +0000 Subject: CHANGELOG: Add that there is now support for JUnit5 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9557846..3fe2b1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.8 + +- Add support for JUnit5 (Jupiter) + ## 1.7 - Remove `TestParameterInjector` support for `org.junit.runners.Parameterized`, -- cgit v1.2.3 From a25f43d9c8b7cd9ebdfa6e9702ccfca92ee0908a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Mar 2022 01:16:46 +0000 Subject: Bump actions/checkout from 2.4.0 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 2.4.0 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2.4.0...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yaml | 2 +- .github/workflows/release.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 8eefb23..d7421b0 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.4.0 + - uses: actions/checkout@v3 - uses: actions/setup-java@v2 with: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b81dfb7..a14d996 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.4.0 + - uses: actions/checkout@v3 - uses: actions/setup-java@v2 with: -- cgit v1.2.3 From a9fc69dd6cdc06ee5502d2d407bb2dcc2ed65851 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Mon, 7 Mar 2022 12:47:28 +0000 Subject: Add a section to the README about filtering unwanted test cases. This fixes a request in https://github.com/google/TestParameterInjector/pull/19 --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 107b985..630b736 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,29 @@ tests will be run for the given parameter sets. > MyTest#personIsAdult[young adult] > ``` +### Filtering unwanted parameters + +Sometimes, you want to exclude a parameter or a combination of parameters. We +recommend doing this via JUnit assumptions which is also supported by +[Truth](https://truth.dev/): + +```java +import static com.google.common.truth.TruthJUnit.assume; + +@Test +public void myTest(@TestParameter Fruit fruit) { + assume().that(fruit).isNotEqualTo(Fruit.BANANA); + + // At this point, the test will only run for APPLE and CHERRY. + // The BANANA case will silently be ignored. +} + +enum Fruit { APPLE, BANANA, CHERRY } +``` + +Note that the above works regardless of what parameterization framework you +choose. + ## Advanced usage **Note about JUnit4 vs JUnit5:**
          -- cgit v1.2.3 From daa7c584755ed09866ba6b5c4c676e5ce59af610 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Apr 2022 01:25:45 +0000 Subject: Bump actions/setup-java from 2 to 3 Bumps [actions/setup-java](https://github.com/actions/setup-java) from 2 to 3. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/setup-java dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yaml | 2 +- .github/workflows/release.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index d7421b0..d4f17e4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -29,7 +29,7 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: 11 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a14d996..c790b2b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -26,7 +26,7 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: 11 -- cgit v1.2.3 From 4d0fe8852d4eda4e2179ac2e82d988e3588e92b9 Mon Sep 17 00:00:00 2001 From: GediminasZukas Date: Mon, 11 Apr 2022 15:56:08 +0300 Subject: Bump protobuf dependency 3.0.1 -> 3.20.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index c739fe2..8936586 100644 --- a/pom.xml +++ b/pom.xml @@ -141,8 +141,8 @@ com.google.protobuf - protobuf-lite - 3.0.1 + protobuf-javalite + 3.20.0 org.yaml -- cgit v1.2.3 From 732412d509c4ae8570d6fc45607fcf7ac80ef152 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 15 Apr 2022 20:07:40 +0000 Subject: Internal implementation detail: Rename getOuterTestRules() to getExtraTestRules() --- .../testing/junit/testparameterinjector/PluggableTestRunner.java | 4 ++-- .../testparameterinjector/TestParameterAnnotationMethodProcessor.java | 4 +++- .../junit5/TestParameterAnnotationMethodProcessor.java | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java index 1905ab1..3b32a1c 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java @@ -128,7 +128,7 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { * {@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 getOuterTestRules() { + protected List getExtraTestRules() { return ImmutableList.of(); } @@ -282,7 +282,7 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { Stream.of( getInnerTestRules().stream(), getTestRules(target).stream(), - getOuterTestRules().stream()) + getExtraTestRules().stream()) .flatMap(x -> x) .collect(toImmutableList()); diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index b2be9b6..33ef406 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -843,7 +843,9 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso testClass); } - /** @see #removeOverrides(List, Class) */ + /** + * @see #removeOverrides(List, Class) + */ private List removeOverrides( List annotationTypeOrigins, Class testClass) { return annotationTypeOrigins.stream() diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java index c06cb3a..a51ceb8 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java @@ -843,7 +843,9 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso testClass); } - /** @see #removeOverrides(List, Class) */ + /** + * @see #removeOverrides(List, Class) + */ private List removeOverrides( List annotationTypeOrigins, Class testClass) { return annotationTypeOrigins.stream() -- cgit v1.2.3 From f7ffdf87585f77f8809cba7e10c8059989754aa8 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 15 Apr 2022 20:28:21 +0000 Subject: Internal implementation detail: Remove unused overridable methods --- .../testparameterinjector/PluggableTestRunner.java | 38 ++-------------------- 1 file changed, 2 insertions(+), 36 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java index 3b32a1c..38d859e 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java @@ -20,8 +20,6 @@ import static java.util.stream.Collectors.joining; import com.google.common.annotations.VisibleForTesting; 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 java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Method; @@ -116,14 +114,6 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { 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 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. @@ -132,22 +122,6 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { 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 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 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. @@ -279,19 +253,11 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { /** Modifies the statement with each {@link MethodRule} and {@link TestRule} */ private Statement withRules(FrameworkMethod method, Object target, Statement statement) { ImmutableList testRules = - Stream.of( - getInnerTestRules().stream(), - getTestRules(target).stream(), - getExtraTestRules().stream()) + Stream.of(getTestRules(target).stream(), getExtraTestRules().stream()) .flatMap(x -> x) .collect(toImmutableList()); - Iterable methodRules = - Iterables.concat( - Lists.reverse(getInnerMethodRules()), - rules(target), - Lists.reverse(getOuterMethodRules())); - for (MethodRule methodRule : methodRules) { + for (MethodRule methodRule : rules(target)) { // For rules that implement both TestRule and MethodRule, only apply the TestRule. if (!testRules.contains(methodRule)) { statement = methodRule.apply(statement, method, target); -- cgit v1.2.3 From 91901ed4a7d6b59ca27ca3538e93de55604e0c3a Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Thu, 21 Apr 2022 08:34:30 +0000 Subject: Bugfix: Apply explicit rule ordering if specified. This retains the original behavior when no explicit ordering is specified (reverse field order). This original behavior is also consistent with the default JUnit4 runner. --- .../testparameterinjector/PluggableTestRunner.java | 66 +++++++++++-- .../PluggableTestRunnerTest.java | 108 ++++++++++++++++++--- 2 files changed, 153 insertions(+), 21 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java index 38d859e..f1822ef 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java @@ -20,6 +20,8 @@ import static java.util.stream.Collectors.joining; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Lists; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Method; @@ -28,6 +30,7 @@ import java.util.List; import java.util.stream.Collector; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.junit.Rule; import org.junit.Test; import org.junit.internal.runners.model.ReflectiveCallable; import org.junit.internal.runners.statements.Fail; @@ -40,7 +43,9 @@ 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.MemberValueConsumer; import org.junit.runners.model.Statement; +import org.junit.runners.model.TestClass; /** * Class to substitute JUnit4 runner in JUnit4 tests, adding additional functionality. @@ -252,22 +257,63 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { /** Modifies the statement with each {@link MethodRule} and {@link TestRule} */ private Statement withRules(FrameworkMethod method, Object target, Statement statement) { - ImmutableList testRules = - Stream.of(getTestRules(target).stream(), getExtraTestRules().stream()) - .flatMap(x -> x) + Description testDescription = describeChild(method); + TestClass testClass = getTestClass(); + + LinkedListMultimap orderToRulesMultimap = LinkedListMultimap.create(); + MemberValueConsumer collector = + (frameworkMember, rule) -> { + Rule ruleAnnotation = frameworkMember.getAnnotation(Rule.class); + int order = ruleAnnotation == null ? Rule.DEFAULT_ORDER : ruleAnnotation.order(); + if (orderToRulesMultimap.containsValue(rule) + && rule instanceof MethodRule + && rule instanceof TestRule) { + // This rule was already added because it is both a MethodRule and a TestRule. + // For legacy reasons, we need to put the new rule at the end of the list. + orderToRulesMultimap.remove(order, rule); + } + orderToRulesMultimap.put(order, rule); + }; + + testClass.collectAnnotatedMethodValues(target, Rule.class, MethodRule.class, collector::accept); + testClass.collectAnnotatedFieldValues(target, Rule.class, MethodRule.class, collector::accept); + testClass.collectAnnotatedMethodValues(target, Rule.class, TestRule.class, collector::accept); + testClass.collectAnnotatedFieldValues(target, Rule.class, TestRule.class, collector::accept); + + ImmutableList orderedRules = + orderToRulesMultimap.keySet().stream() + .sorted() + .flatMap( + // Execute the rules in the reverse order of when the fields occurred. This may look + // counter-intuitive, but that is what the default JUnit4 runner does, and there is + // no reason to deviate from that here. + key -> Lists.reverse(orderToRulesMultimap.get(key)).stream()) .collect(toImmutableList()); - for (MethodRule methodRule : rules(target)) { - // For rules that implement both TestRule and MethodRule, only apply the TestRule. - if (!testRules.contains(methodRule)) { - statement = methodRule.apply(statement, method, target); + // Note: The perceived order* is the reverse of the order in which the code below applies the + // rules to the statements because each subsequent rule wraps the previous statement. + // + // [*] The rule implementation can add its logic both before or after the base statement, so the + // order depends on the rule implementation. If all rules put their logic before the base + // statement, the order matches that of `orderedRules`. + + for (Object rule : Lists.reverse(orderedRules)) { + if (rule instanceof TestRule) { + statement = ((TestRule) rule).apply(statement, testDescription); + } else if (rule instanceof MethodRule) { + statement = ((MethodRule) rule).apply(statement, method, target); + } else { + throw new AssertionError(rule); } } - Description testDescription = describeChild(method); - for (TestRule testRule : testRules) { + + // Apply extra rules + for (TestRule testRule : getExtraTestRules()) { statement = testRule.apply(statement, testDescription); } - return new ContextMethodRule().apply(statement, method, target); + statement = new ContextMethodRule().apply(statement, method, target); + + return statement; } private Object createTestForMethod(FrameworkMethod method) throws Exception { diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java index a44df08..3060e37 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java @@ -24,6 +24,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.MethodRule; @@ -36,24 +37,52 @@ import org.junit.runners.model.Statement; @RunWith(JUnit4.class) public class PluggableTestRunnerTest { + + private static ArrayList ruleInvocations; + private static int testMethodInvocationCount; + private static List testOrder; + + @Before + public void setUp() { + ruleInvocations = new ArrayList<>(); + testMethodInvocationCount = 0; + testOrder = new ArrayList<>(); + } + @Retention(RetentionPolicy.RUNTIME) - private static @interface CustomTest {} + private @interface CustomTest {} - private static int ruleInvocationCount = 0; - private static int testMethodInvocationCount = 0; + static class TestAndMethodRule implements MethodRule, TestRule { + private final String name; - public static class TestAndMethodRule implements MethodRule, TestRule { + TestAndMethodRule() { + this("DEFAULT_NAME"); + } + + TestAndMethodRule(String name) { + this.name = name; + } @Override public Statement apply(Statement base, Description description) { - ruleInvocationCount++; - return base; + return new Statement() { + @Override + public void evaluate() throws Throwable { + ruleInvocations.add(name); + base.evaluate(); + } + }; } @Override public Statement apply(Statement base, FrameworkMethod method, Object target) { - ruleInvocationCount++; - return base; + return new Statement() { + @Override + public void evaluate() throws Throwable { + ruleInvocations.add(name); + base.evaluate(); + } + }; } } @@ -78,7 +107,66 @@ public class PluggableTestRunnerTest { } }); - assertThat(ruleInvocationCount).isEqualTo(1); + assertThat(ruleInvocations).hasSize(1); + } + + @RunWith(PluggableTestRunner.class) + public static class RuleOrderingTestClassWithExplicitOrder { + + @Rule(order = 3) + public TestAndMethodRule ruleA = new TestAndMethodRule("A"); + + @Rule(order = 1) + public TestAndMethodRule ruleB = new TestAndMethodRule("B"); + + @Rule(order = 2) + public TestAndMethodRule ruleC = new TestAndMethodRule("C"); + + @Test + public void test() { + // no-op + } + } + + @Test + public void rulesAreSortedCorrectly_withExplicitOrder() throws Exception { + PluggableTestRunner.run( + new PluggableTestRunner(RuleOrderingTestClassWithExplicitOrder.class) { + @Override + protected TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.empty(); + } + }); + + assertThat(ruleInvocations).containsExactly("B", "C", "A").inOrder(); + } + + @RunWith(PluggableTestRunner.class) + public static class RuleOrderingTestClassWithImplicitOrder { + + @Rule public TestAndMethodRule ruleC = new TestAndMethodRule("C"); + @Rule public TestAndMethodRule ruleA = new TestAndMethodRule("A"); + @Rule public TestAndMethodRule ruleB = new TestAndMethodRule("B"); + + @Test + public void test() { + // no-op + } + } + + @Test + public void rulesAreSortedCorrectly_withImplicitOrder() throws Exception { + PluggableTestRunner.run( + new PluggableTestRunner(RuleOrderingTestClassWithImplicitOrder.class) { + @Override + protected TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.empty(); + } + }); + + // This might look counter-intuitive, but JUnit4 behaves in this reverse order way. So for + // consistency, PluggableTestRunner should do the same. + assertThat(ruleInvocations).containsExactly("B", "A", "C").inOrder(); } @RunWith(PluggableTestRunner.class) @@ -114,8 +202,6 @@ public class PluggableTestRunnerTest { assertThat(testMethodInvocationCount).isEqualTo(2); } - private static final List testOrder = new ArrayList<>(); - @RunWith(PluggableTestRunner.class) public static class SortedPluggableTestRunnerTestClass { @Test -- cgit v1.2.3 From 7360b1eb025dafe13635df9f06dd0f96d83da141 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Thu, 21 Apr 2022 09:04:06 +0000 Subject: Remove tests that are dependent on the build tool. --- .../PluggableTestRunnerTest.java | 28 ---------------------- 1 file changed, 28 deletions(-) diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java index 3060e37..2627bec 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java @@ -141,34 +141,6 @@ public class PluggableTestRunnerTest { assertThat(ruleInvocations).containsExactly("B", "C", "A").inOrder(); } - @RunWith(PluggableTestRunner.class) - public static class RuleOrderingTestClassWithImplicitOrder { - - @Rule public TestAndMethodRule ruleC = new TestAndMethodRule("C"); - @Rule public TestAndMethodRule ruleA = new TestAndMethodRule("A"); - @Rule public TestAndMethodRule ruleB = new TestAndMethodRule("B"); - - @Test - public void test() { - // no-op - } - } - - @Test - public void rulesAreSortedCorrectly_withImplicitOrder() throws Exception { - PluggableTestRunner.run( - new PluggableTestRunner(RuleOrderingTestClassWithImplicitOrder.class) { - @Override - protected TestMethodProcessorList createTestMethodProcessorList() { - return TestMethodProcessorList.empty(); - } - }); - - // This might look counter-intuitive, but JUnit4 behaves in this reverse order way. So for - // consistency, PluggableTestRunner should do the same. - assertThat(ruleInvocations).containsExactly("B", "A", "C").inOrder(); - } - @RunWith(PluggableTestRunner.class) public static class CustomTestAnnotationTestClass { @SuppressWarnings("JUnit4TestNotRun") -- cgit v1.2.3 From 50632bd1c3667a9ba728a07a28e2cdcaf7383d67 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 22 Apr 2022 11:17:56 +0000 Subject: Add rule ordering fix to CHANGELOG --- CHANGELOG.md | 4 ++++ README.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fe2b1d..9fe13b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.9 + +- Bugfix: Support explicit ordering by the JUnit4 `@Rule`. For example: `@Rule(ordering=3)`. + ## 1.8 - Add support for JUnit5 (Jupiter) diff --git a/README.md b/README.md index 630b736..6b6e94a 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,10 @@ public class MyTest { In this example, both `test1` and `test2` will be run twice (once for each parameter value). +The test runner will set these fields before calling any methods, so it is safe +to use such `@TestParameter`-annotated fields for setting up other test values +and behavior in `@Before` methods. + #### Supported types The following examples show most of the supported types. See the `@TestParameter` javadoc for more details. -- cgit v1.2.3 From 7f5674859b0c415d244597246818e4db5a9a7711 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 3 May 2022 09:53:27 +0000 Subject: TestParameterInjector: Move PluggableTestRunner.run() into shared test-only class This test also makes some tests a bit stricter because they now check that there are no test failures. --- .../testparameterinjector/PluggableTestRunner.java | 23 ------- .../PluggableTestRunnerTest.java | 8 +-- .../SharedTestUtilitiesJUnit4.java | 77 ++++++++++++++++++++++ ...TestParameterAnnotationMethodProcessorTest.java | 33 ++-------- .../testparameterinjector/TestParameterTest.java | 39 ++--------- .../TestParametersMethodProcessorTest.java | 30 ++------- 6 files changed, 97 insertions(+), 113 deletions(-) create mode 100644 junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java index f1822ef..21e3ea6 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java @@ -17,7 +17,6 @@ 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.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.LinkedListMultimap; @@ -37,9 +36,6 @@ 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; @@ -127,25 +123,6 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { 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 run(PluggableTestRunner testRunner) throws Exception { - final ImmutableList.Builder 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 computeTestMethods() { Stream processedMethods = diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java index 2627bec..6c9064a 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java @@ -99,7 +99,7 @@ public class PluggableTestRunnerTest { @Test public void ruleThatIsBothTestRuleAndMethodRuleIsInvokedOnceOnly() throws Exception { - PluggableTestRunner.run( + SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures( new PluggableTestRunner(TestAndMethodRuleTestClass.class) { @Override protected TestMethodProcessorList createTestMethodProcessorList() { @@ -130,7 +130,7 @@ public class PluggableTestRunnerTest { @Test public void rulesAreSortedCorrectly_withExplicitOrder() throws Exception { - PluggableTestRunner.run( + SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures( new PluggableTestRunner(RuleOrderingTestClassWithExplicitOrder.class) { @Override protected TestMethodProcessorList createTestMethodProcessorList() { @@ -158,7 +158,7 @@ public class PluggableTestRunnerTest { @Test public void testMarkedWithCustomClassIsInvoked() throws Exception { testMethodInvocationCount = 0; - PluggableTestRunner.run( + SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures( new PluggableTestRunner(CustomTestAnnotationTestClass.class) { @Override protected TestMethodProcessorList createTestMethodProcessorList() { @@ -195,7 +195,7 @@ public class PluggableTestRunnerTest { @Test public void testsAreSortedCorrectly() throws Exception { testOrder.clear(); - PluggableTestRunner.run( + SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures( new PluggableTestRunner(SortedPluggableTestRunnerTestClass.class) { @Override protected TestMethodProcessorList createTestMethodProcessorList() { diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java new file mode 100644 index 0000000..0351f80 --- /dev/null +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java @@ -0,0 +1,77 @@ +/* + * 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.Iterables.getOnlyElement; +import static java.util.stream.Collectors.joining; + +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import org.junit.runner.Runner; +import org.junit.runner.notification.Failure; +import org.junit.runner.notification.RunListener; +import org.junit.runner.notification.RunNotifier; + +/** Shared utility code for TestParameterInjector (JUnit4) tests. */ +class SharedTestUtilitiesJUnit4 { + + /** + * Runs the given test runner. + * + * @throws AssertionError if the test instance reports any failures + */ + static void runTestsAndAssertNoFailures(Runner testRunner) { + ImmutableList failures = runTestsAndGetFailures(testRunner); + + if (failures.size() == 1) { + throw new AssertionError(getOnlyElement(failures).getException()); + } else if (failures.size() > 1) { + throw new AssertionError( + String.format( + "Test failed unexpectedly:\n\n%s", + failures.stream() + .map( + f -> + String.format( + "<<%s>> %s", + f.getDescription(), + Throwables.getStackTraceAsString(f.getException()))) + .collect(joining("\n------------------------------------\n")))); + } + } + + /** + * Runs the given test runner. + * + * @return all failures reported by the test instance. + */ + static ImmutableList runTestsAndGetFailures(Runner testRunner) { + final ImmutableList.Builder failures = ImmutableList.builder(); + RunNotifier notifier = new RunNotifier(); + notifier.addFirstListener( + new RunListener() { + @Override + public void testFailure(Failure failure) throws Exception { + failures.add(failure); + } + }); + + testRunner.run(notifier); + + return failures.build(); + } + + private SharedTestUtilitiesJUnit4() {} +} diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java index 9af29ae..4736811 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java @@ -15,15 +15,12 @@ package com.google.testing.junit.testparameterinjector; import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.truth.Truth.assertThat; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import static java.util.stream.Collectors.joining; import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; -import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider; import com.google.testing.junit.testparameterinjector.TestParameterAnnotationMethodProcessorTest.ErrorNonStaticProviderClass.NonStaticProvider; @@ -44,7 +41,6 @@ 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; @@ -956,17 +952,16 @@ public class TestParameterAnnotationMethodProcessorTest { public void test() throws Exception { switch (result) { case SUCCESS_ALWAYS: - assertNoFailures( - PluggableTestRunner.run( - newTestRunnerWithParameterizedSupport( - testClass -> TestMethodProcessorList.createNewParameterizedProcessors()))); + SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures( + newTestRunnerWithParameterizedSupport( + testClass -> TestMethodProcessorList.createNewParameterizedProcessors())); break; case SUCCESS_FOR_ALL_PLACEMENTS_ONLY: assertThrows( RuntimeException.class, () -> - PluggableTestRunner.run( + SharedTestUtilitiesJUnit4.runTestsAndGetFailures( newTestRunnerWithParameterizedSupport( testClass -> TestMethodProcessorList.createNewParameterizedProcessors()))); break; @@ -975,7 +970,7 @@ public class TestParameterAnnotationMethodProcessorTest { assertThrows( RuntimeException.class, () -> - PluggableTestRunner.run( + SharedTestUtilitiesJUnit4.runTestsAndGetFailures( newTestRunnerWithParameterizedSupport( testClass -> TestMethodProcessorList.createNewParameterizedProcessors()))); break; @@ -991,22 +986,4 @@ public class TestParameterAnnotationMethodProcessorTest { } }; } - - private static void assertNoFailures(List failures) { - if (failures.size() == 1) { - throw new AssertionError(getOnlyElement(failures).getException()); - } else if (failures.size() > 1) { - throw new AssertionError( - String.format( - "Test failed unexpectedly:\n\n%s", - failures.stream() - .map( - f -> - String.format( - "<<%s>> %s", - f.getDescription(), - Throwables.getStackTraceAsString(f.getException()))) - .collect(joining("\n------------------------------------\n")))); - } - } } diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java index 85cb686..f68d5ca 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java @@ -15,15 +15,12 @@ package com.google.testing.junit.testparameterinjector; import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.collect.Lists.newArrayList; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import static java.util.stream.Collectors.joining; import com.google.common.base.CharMatcher; -import com.google.common.base.Throwables; import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider; import java.lang.annotation.Retention; import java.util.ArrayList; @@ -34,7 +31,6 @@ 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; @@ -211,33 +207,12 @@ public class TestParameterTest { @Test public void test() throws Exception { - List failures = - PluggableTestRunner.run( - new PluggableTestRunner(testClass) { - @Override - protected TestMethodProcessorList createTestMethodProcessorList() { - return TestMethodProcessorList.createNewParameterizedProcessors(); - } - }); - - assertNoFailures(failures); - } - - private static void assertNoFailures(List failures) { - if (failures.size() == 1) { - throw new AssertionError(getOnlyElement(failures).getException()); - } else if (failures.size() > 1) { - throw new AssertionError( - String.format( - "Test failed unexpectedly:\n\n%s", - failures.stream() - .map( - f -> - String.format( - "<<%s>> %s", - f.getDescription(), - Throwables.getStackTraceAsString(f.getException()))) - .collect(joining("\n------------------------------------\n")))); - } + SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures( + new PluggableTestRunner(testClass) { + @Override + protected TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.createNewParameterizedProcessors(); + } + }); } } diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java index 3e15277..77b1e2a 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java @@ -15,14 +15,11 @@ package com.google.testing.junit.testparameterinjector; import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.TruthJUnit.assume; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import static java.util.stream.Collectors.joining; import static org.junit.Assert.assertThrows; -import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValues; import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValuesProvider; @@ -42,7 +39,6 @@ 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; @@ -577,9 +573,7 @@ public class TestParametersMethodProcessorTest { public void test_success() throws Exception { assume().that(maybeFailureMessage.isPresent()).isFalse(); - List failures = PluggableTestRunner.run(newTestRunner()); - - assertNoFailures(failures); + SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures(newTestRunner()); } @Test @@ -587,7 +581,9 @@ public class TestParametersMethodProcessorTest { assume().that(maybeFailureMessage.isPresent()).isTrue(); IllegalStateException exception = - assertThrows(IllegalStateException.class, () -> PluggableTestRunner.run(newTestRunner())); + assertThrows( + IllegalStateException.class, + () -> SharedTestUtilitiesJUnit4.runTestsAndGetFailures(newTestRunner())); assertThat(exception).hasMessageThat().isEqualTo(maybeFailureMessage.get()); } @@ -600,22 +596,4 @@ public class TestParametersMethodProcessorTest { } }; } - - private static void assertNoFailures(List failures) { - if (failures.size() == 1) { - throw new AssertionError(getOnlyElement(failures).getException()); - } else if (failures.size() > 1) { - throw new AssertionError( - String.format( - "Test failed unexpectedly:\n\n%s", - failures.stream() - .map( - f -> - String.format( - "<<%s>> %s", - f.getDescription(), - Throwables.getStackTraceAsString(f.getException()))) - .collect(joining("\n------------------------------------\n")))); - } - } } -- cgit v1.2.3 From 944fcc0d0acb173583279c9a0fb525c7ca484eab Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 3 May 2022 10:19:14 +0000 Subject: TestParameterInjector: Add SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase and apply it it simplify the JUnit4 tests --- .../SharedTestUtilitiesJUnit4.java | 69 +++ ...TestParameterAnnotationMethodProcessorTest.java | 688 +++++++++------------ 2 files changed, 348 insertions(+), 409 deletions(-) diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java index 0351f80..863551d 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java @@ -14,11 +14,21 @@ package com.google.testing.junit.testparameterinjector; +import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.util.Arrays.stream; import static java.util.stream.Collectors.joining; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.rules.TestName; import org.junit.runner.Runner; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunListener; @@ -73,5 +83,64 @@ class SharedTestUtilitiesJUnit4 { return failures.build(); } + private static String toCopyPastableJavaString(Map map) { + StringBuilder resultBuilder = new StringBuilder(); + resultBuilder.append("\n----------------------\n"); + resultBuilder.append("ImmutableMap.builder()\n"); + map.forEach( + (key, value) -> + resultBuilder.append(String.format(" .put(\"%s\", \"%s\")\n", key, value))); + resultBuilder.append(" .build()\n"); + resultBuilder.append("----------------------\n"); + return resultBuilder.toString(); + } + + /** + * Base class for a test class that acts as a test case testing a single property of a + * TestParameterInjector-run test. + */ + abstract static class SuccessfulTestCaseBase { + + @Rule public TestName testName = new TestName(); + + private static Map testNameToStringifiedParameters; + private static ImmutableMap expectedTestNameToStringifiedParameters; + + @BeforeClass + public static void checkStaticFieldAreNull() { + checkState(testNameToStringifiedParameters == null); + checkState(expectedTestNameToStringifiedParameters == null); + } + + final void storeTestParametersForThisTest(Object... params) { + if (testNameToStringifiedParameters == null) { + testNameToStringifiedParameters = new LinkedHashMap<>(); + // Copying this into a static field because @AfterAll methods have to be static + expectedTestNameToStringifiedParameters = expectedTestNameToStringifiedParameters(); + } + checkState( + !testNameToStringifiedParameters.containsKey(testName.getMethodName()), + "Parameters for the test with name '%s' are already stored. This might mean that there" + + " are duplicate test names", + testName.getMethodName()); + testNameToStringifiedParameters.put( + testName.getMethodName(), stream(params).map(String::valueOf).collect(joining(":"))); + } + + abstract ImmutableMap expectedTestNameToStringifiedParameters(); + + @AfterClass + public static void completedAllTests() { + try { + assertWithMessage(toCopyPastableJavaString(testNameToStringifiedParameters)) + .that(testNameToStringifiedParameters) + .isEqualTo(expectedTestNameToStringifiedParameters); + } finally { + testNameToStringifiedParameters = null; + expectedTestNameToStringifiedParameters = null; + } + } + } + private SharedTestUtilitiesJUnit4() {} } diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java index 4736811..dd3f1c5 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java @@ -16,30 +16,20 @@ 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.common.collect.ImmutableMap; +import com.google.testing.junit.testparameterinjector.SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase; import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider; -import com.google.testing.junit.testparameterinjector.TestParameterAnnotationMethodProcessorTest.ErrorNonStaticProviderClass.NonStaticProvider; 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.Rule; import org.junit.Test; -import org.junit.rules.TestName; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; @@ -84,205 +74,166 @@ public class TestParameterAnnotationMethodProcessorTest { } @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class SingleAnnotationClass { - - private static List testedParameters; + public static class SingleAnnotationClass extends SuccessfulTestCaseBase { @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) TestEnum enumParameter; - @BeforeClass - public static void resetStaticState() { - testedParameters = new ArrayList<>(); - } - @Test public void test() { - testedParameters.add(enumParameter); + storeTestParametersForThisTest(enumParameter); } - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO, TestEnum.THREE); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test[ONE]", "ONE") + .put("test[TWO]", "TWO") + .put("test[THREE]", "THREE") + .build(); } } @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class MultipleAllEnumValuesAnnotationClass { + public static class MultipleAllEnumValuesAnnotationClass extends SuccessfulTestCaseBase { - private static List testedParameters; + @TestParameter({"ONE", "THREE"}) + TestEnum enumParameter1; - @TestParameter TestEnum enumParameter1; + @TestParameter TestEnum2 enumParameter2; - @BeforeClass - public static void resetStaticState() { - testedParameters = new ArrayList<>(); + @Test + public void test(@TestParameter TestEnum2 enumParameter3) { + storeTestParametersForThisTest(enumParameter1, enumParameter2, enumParameter3); } - @Test - public void test(@TestParameter TestEnum enumParameter2) { - testedParameters.add(enumParameter1 + ":" + enumParameter2); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test[ONE,A,A]", "ONE:A:A") + .put("test[ONE,A,B]", "ONE:A:B") + .put("test[ONE,B,A]", "ONE:B:A") + .put("test[ONE,B,B]", "ONE:B:B") + .put("test[THREE,A,A]", "THREE:A:A") + .put("test[THREE,A,B]", "THREE:A:B") + .put("test[THREE,B,A]", "THREE:B:A") + .put("test[THREE,B,B]", "THREE:B:B") + .build(); } - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testedParameters).hasSize(TestEnum.values().length * TestEnum.values().length); + enum TestEnum2 { + A, + B; } } @ClassTestResult(Result.SUCCESS_FOR_ALL_PLACEMENTS_ONLY) - public static class SingleParameterAnnotationClass { - - private static List testedParameters; - - @BeforeClass - public static void resetStaticState() { - testedParameters = new ArrayList<>(); - } + public static class SingleParameterAnnotationClass extends SuccessfulTestCaseBase { @Test @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) public void test(TestEnum enumParameter) { - testedParameters.add(enumParameter); + storeTestParametersForThisTest(enumParameter); } - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO, TestEnum.THREE); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test[ONE]", "ONE") + .put("test[TWO]", "TWO") + .put("test[THREE]", "THREE") + .build(); } } @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class SingleAnnotatedParameterAnnotationClass { - - private static List testedParameters; - - @BeforeClass - public static void resetStaticState() { - testedParameters = new ArrayList<>(); - } + public static class SingleAnnotatedParameterAnnotationClass extends SuccessfulTestCaseBase { @Test public void test( @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) TestEnum enumParameter) { - testedParameters.add(enumParameter); + storeTestParametersForThisTest(enumParameter); } - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO, TestEnum.THREE); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test[ONE]", "ONE") + .put("test[TWO]", "TWO") + .put("test[THREE]", "THREE") + .build(); } } @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class AnnotatedSuperclassParameter { - - private static List testedParameters; - - @BeforeClass - public static void resetStaticState() { - testedParameters = new ArrayList<>(); - } + public static class AnnotatedSuperclassParameter extends SuccessfulTestCaseBase { @Test public void test( @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) Object enumParameter) { - testedParameters.add(enumParameter); + storeTestParametersForThisTest(enumParameter); } - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO, TestEnum.THREE); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test[ONE]", "ONE") + .put("test[TWO]", "TWO") + .put("test[THREE]", "THREE") + .build(); } } @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class DuplicatedAnnotatedParameterAnnotationClass { - - private static List> testedParameters; - - @BeforeClass - public static void resetStaticState() { - testedParameters = new ArrayList<>(); - } + public static class DuplicatedAnnotatedParameterAnnotationClass extends SuccessfulTestCaseBase { @Test public void test( @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) TestEnum enumParameter, @EnumParameter({TestEnum.FOUR, TestEnum.FIVE}) TestEnum enumParameter2) { - testedParameters.add(ImmutableList.of(enumParameter, enumParameter2)); + storeTestParametersForThisTest(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)); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test[ONE,FOUR]", "ONE:FOUR") + .put("test[ONE,FIVE]", "ONE:FIVE") + .put("test[TWO,FOUR]", "TWO:FOUR") + .put("test[TWO,FIVE]", "TWO:FIVE") + .put("test[THREE,FOUR]", "THREE:FOUR") + .put("test[THREE,FIVE]", "THREE:FIVE") + .build(); } } @ClassTestResult(Result.FAILURE) public static class SingleAnnotatedParameterAnnotationClassWithMissingValue { - private static List 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); - } + public void test(@EnumParameter TestEnum enumParameter) {} } @ClassTestResult(Result.SUCCESS_FOR_ALL_PLACEMENTS_ONLY) - public static class MultipleAnnotationTestClass { - - private static List testedParameters; + public static class MultipleAnnotationTestClass extends SuccessfulTestCaseBase { @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); + storeTestParametersForThisTest(enumParameter); } - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testedParameters).containsExactly(TestEnum.THREE); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder().put("parameterized[THREE]", "THREE").build(); } } @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class TooLongTestNamesShortened { - - @Rule public TestName testName = new TestName(); - - private static List allTestNames; - - @BeforeClass - public static void resetStaticState() { - allTestNames = new ArrayList<>(); - } + public static class TooLongTestNamesShortened extends SuccessfulTestCaseBase { @Test public void test1( @@ -294,38 +245,31 @@ public class TestParameterAnnotationMethodProcessorTest { + "===================================" }) String testString) { - allTestNames.add(testName.getMethodName()); + storeTestParametersForThisTest(testString); } - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(allTestNames) - .containsExactly( - "test1[1.ABC]", + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test1[1.ABC]", "ABC") + .put( "test1[2.This is a very long string (240 characters) that would normally cause" + " Sponge+Tin to exceed the filename limit of 255 characters." - + " =========================================================...]"); + + " =========================================================...]", + "This is a very long string (240 characters) that would normally cause Sponge+Tin to" + + " exceed the filename limit of 255 characters." + + " ============================================================================" + + "==================================") + .build(); } } @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class DuplicateTestNames { - - @Rule public TestName testName = new TestName(); - - private static List allTestNames; - private static List allTestParameterValues; - - @BeforeClass - public static void resetStaticState() { - allTestNames = new ArrayList<>(); - allTestParameterValues = new ArrayList<>(); - } + public static class DuplicateTestNames extends SuccessfulTestCaseBase { @Test public void test1(@TestParameter({"ABC", "ABC"}) String testString) { - allTestNames.add(testName.getMethodName()); - allTestParameterValues.add(testString); + storeTestParametersForThisTest(testString); } private static final class Test2Provider implements TestParameterValuesProvider { @@ -337,28 +281,24 @@ public class TestParameterAnnotationMethodProcessorTest { @Test public void test2(@TestParameter(valuesProvider = Test2Provider.class) Object testObject) { - allTestNames.add(testName.getMethodName()); - allTestParameterValues.add(testObject); + storeTestParametersForThisTest(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); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test1[1.ABC]", "ABC") + .put("test1[2.ABC]", "ABC") + .put("test2[123 (Integer)]", "123") + .put("test2[123 (String)]", "123") + .put("test2[null (String)]", "null") + .put("test2[null (null reference)]", "null") + .build(); } } @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class DuplicateFieldAnnotationTestClass { - - private static List testedParameters; + public static class DuplicateFieldAnnotationTestClass extends SuccessfulTestCaseBase { @TestParameter({"foo", "bar"}) String stringParameter; @@ -366,26 +306,24 @@ public class TestParameterAnnotationMethodProcessorTest { @TestParameter({"baz", "qux"}) String stringParameter2; - @BeforeClass - public static void resetStaticState() { - testedParameters = new ArrayList<>(); - } - @Test public void test() { - testedParameters.add(stringParameter + ":" + stringParameter2); + storeTestParametersForThisTest(stringParameter, stringParameter2); } - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testedParameters).containsExactly("foo:baz", "foo:qux", "bar:baz", "bar:qux"); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test[foo,baz]", "foo:baz") + .put("test[foo,qux]", "foo:qux") + .put("test[bar,baz]", "bar:baz") + .put("test[bar,qux]", "bar:qux") + .build(); } } @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class DuplicateIdenticalFieldAnnotationTestClass { - - private static List testedParameters; + public static class DuplicateIdenticalFieldAnnotationTestClass extends SuccessfulTestCaseBase { @TestParameter({"foo", "bar"}) String stringParameter; @@ -393,19 +331,19 @@ public class TestParameterAnnotationMethodProcessorTest { @TestParameter({"foo", "bar"}) String stringParameter2; - @BeforeClass - public static void resetStaticState() { - testedParameters = new ArrayList<>(); - } - @Test public void test() { - testedParameters.add(stringParameter + ":" + stringParameter2); + storeTestParametersForThisTest(stringParameter, stringParameter2); } - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testedParameters).containsExactly("foo:foo", "foo:bar", "bar:foo", "bar:bar"); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test[foo,foo]", "foo:foo") + .put("test[foo,bar]", "foo:bar") + .put("test[bar,foo]", "bar:foo") + .put("test[bar,bar]", "bar:bar") + .build(); } } @@ -437,117 +375,83 @@ public class TestParameterAnnotationMethodProcessorTest { } @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class SingleAnnotationTestClassWithAnnotation { - - private static List testedParameters; + public static class SingleAnnotationTestClassWithAnnotation extends SuccessfulTestCaseBase { @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) TestEnum enumParameter; - @BeforeClass - public static void resetStaticState() { - testedParameters = new ArrayList<>(); - } - @Test public void test() { - testedParameters.add(enumParameter); + storeTestParametersForThisTest(enumParameter); } - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO, TestEnum.THREE); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test[ONE]", "ONE") + .put("test[TWO]", "TWO") + .put("test[THREE]", "THREE") + .build(); } } @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class MultipleAnnotationTestClassWithAnnotation { - - private static List testedParameters; + public static class MultipleAnnotationTestClassWithAnnotation extends SuccessfulTestCaseBase { @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); + storeTestParametersForThisTest(enumParameter, stringParameter); } @Test public void nonParameterized() { - testedParameters.add("none:" + enumParameter); + storeTestParametersForThisTest(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"); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("parameterized[ONE,foo]", "ONE:foo") + .put("parameterized[ONE,bar]", "ONE:bar") + .put("parameterized[TWO,foo]", "TWO:foo") + .put("parameterized[TWO,bar]", "TWO:bar") + .put("parameterized[THREE,foo]", "THREE:foo") + .put("parameterized[THREE,bar]", "THREE:bar") + .put("nonParameterized[ONE]", "ONE") + .put("nonParameterized[TWO]", "TWO") + .put("nonParameterized[THREE]", "THREE") + .build(); } } - public abstract static class BaseClassWithSingleTest { - @Rule public TestName testName = new TestName(); - - static List allTestNames; - - @BeforeClass - public static void resetStaticState() { - allTestNames = new ArrayList<>(); - } - + public abstract static class BaseClassWithSingleTest extends SuccessfulTestCaseBase { @Test public void testInBase(@TestParameter boolean b) { - allTestNames.add(testName.getMethodName()); - } - - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(allTestNames).containsExactly("testInBase[b=true]", "testInBase[b=false]"); + storeTestParametersForThisTest(b); } } @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class SimpleTestInheritedFromBaseClass extends BaseClassWithSingleTest {} - - public abstract static class BaseClassWithAnnotations { - @Rule public TestName testName = new TestName(); - - static List allTestNames; - - @TestParameter boolean boolInBase; - - @BeforeClass - public static void resetStaticState() { - allTestNames = new ArrayList<>(); + public static class SimpleTestInheritedFromBaseClass extends BaseClassWithSingleTest { + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("testInBase[b=false]", "false") + .put("testInBase[b=true]", "true") + .build(); } + } - @Before - public void setUp() { - assertThat(allTestNames).doesNotContain(testName.getMethodName()); - } + public abstract static class BaseClassWithAnnotations extends SuccessfulTestCaseBase { - @After - public void tearDown() { - assertThat(allTestNames).contains(testName.getMethodName()); - } + @TestParameter boolean boolInBase; @Test public void testInBase(@TestParameter({"ONE", "TWO"}) TestEnum enumInBase) { - allTestNames.add(testName.getMethodName()); + storeTestParametersForThisTest(boolInBase, enumInBase); } @Test @@ -566,47 +470,47 @@ public class TestParameterAnnotationMethodProcessorTest { @Test public void testInChild(@TestParameter({"TWO", "THREE"}) TestEnum enumInChild) { - allTestNames.add(testName.getMethodName()); + storeTestParametersForThisTest(boolInBase, boolInChild, enumInChild); } @Override public void abstractTestInBase() { - allTestNames.add(testName.getMethodName()); + storeTestParametersForThisTest(boolInBase, boolInChild); } @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]"); + storeTestParametersForThisTest(boolInBase, boolInChild); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("testInChild[boolInChild=false,boolInBase=false,TWO]", "false:false:TWO") + .put("testInChild[boolInChild=false,boolInBase=false,THREE]", "false:false:THREE") + .put("testInChild[boolInChild=false,boolInBase=true,TWO]", "true:false:TWO") + .put("testInChild[boolInChild=false,boolInBase=true,THREE]", "true:false:THREE") + .put("testInChild[boolInChild=true,boolInBase=false,TWO]", "false:true:TWO") + .put("testInChild[boolInChild=true,boolInBase=false,THREE]", "false:true:THREE") + .put("testInChild[boolInChild=true,boolInBase=true,TWO]", "true:true:TWO") + .put("testInChild[boolInChild=true,boolInBase=true,THREE]", "true:true:THREE") + .put("abstractTestInBase[boolInChild=false,boolInBase=false]", "false:false") + .put("abstractTestInBase[boolInChild=false,boolInBase=true]", "true:false") + .put("abstractTestInBase[boolInChild=true,boolInBase=false]", "false:true") + .put("abstractTestInBase[boolInChild=true,boolInBase=true]", "true:true") + .put("overridableTestInBase[boolInChild=false,boolInBase=false]", "false:false") + .put("overridableTestInBase[boolInChild=false,boolInBase=true]", "true:false") + .put("overridableTestInBase[boolInChild=true,boolInBase=false]", "false:true") + .put("overridableTestInBase[boolInChild=true,boolInBase=true]", "true:true") + .put("testInBase[boolInChild=false,boolInBase=false,ONE]", "false:ONE") + .put("testInBase[boolInChild=false,boolInBase=false,TWO]", "false:TWO") + .put("testInBase[boolInChild=false,boolInBase=true,ONE]", "true:ONE") + .put("testInBase[boolInChild=false,boolInBase=true,TWO]", "true:TWO") + .put("testInBase[boolInChild=true,boolInBase=false,ONE]", "false:ONE") + .put("testInBase[boolInChild=true,boolInBase=false,TWO]", "false:TWO") + .put("testInBase[boolInChild=true,boolInBase=true,ONE]", "true:ONE") + .put("testInBase[boolInChild=true,boolInBase=true,TWO]", "true:TWO") + .build(); } } @@ -625,63 +529,46 @@ public class TestParameterAnnotationMethodProcessorTest { } @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class MethodEvaluatorClass { - - private static List testedParameters; + public static class MethodEvaluatorClass extends SuccessfulTestCaseBase { @Test public void test( @EnumEvaluatorParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) TestEnum value) { - if (value == TestEnum.THREE) { - fail(); - } else { - testedParameters.add(value); - } + storeTestParametersForThisTest(value); } - @BeforeClass - public static void resetStaticState() { - testedParameters = new ArrayList<>(); - } - - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test[ONE]", "ONE") + .put("test[TWO]", "TWO") + .build(); } } @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class FieldEvaluatorClass { - - private static List testedParameters; + public static class FieldEvaluatorClass extends SuccessfulTestCaseBase { @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); - } + storeTestParametersForThisTest(value); } - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test[ONE]", "ONE") + .put("test[TWO]", "TWO") + .build(); } } @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class ConstructorClass { + public static class ConstructorClass extends SuccessfulTestCaseBase { - private static List testedParameters; final TestEnum enumParameter; public ConstructorClass( @@ -689,51 +576,46 @@ public class TestParameterAnnotationMethodProcessorTest { this.enumParameter = enumParameter; } - @BeforeClass - public static void resetStaticState() { - testedParameters = new ArrayList<>(); - } - @Test public void test() { - testedParameters.add(enumParameter); + storeTestParametersForThisTest(enumParameter); } - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO, TestEnum.THREE); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test[ONE]", "ONE") + .put("test[TWO]", "TWO") + .put("test[THREE]", "THREE") + .build(); } } @ClassTestResult(Result.SUCCESS_FOR_ALL_PLACEMENTS_ONLY) - public static class MethodFieldOverrideClass { - - private static List testedParameters; + public static class MethodFieldOverrideClass extends SuccessfulTestCaseBase { @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); + storeTestParametersForThisTest(enumParameter); } - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO, TestEnum.THREE); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test[ONE]", "ONE") + .put("test[TWO]", "TWO") + .put("test[THREE]", "THREE") + .build(); } } @ClassTestResult(Result.SUCCESS_FOR_ALL_PLACEMENTS_ONLY) - public static class ErrorDuplicatedConstructorMethodAnnotation { + public static class ErrorDuplicatedConstructorMethodAnnotation extends SuccessfulTestCaseBase { - private static List testedParameters; final TestEnum enumParameter; @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) @@ -741,21 +623,22 @@ public class TestParameterAnnotationMethodProcessorTest { 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); + storeTestParametersForThisTest(enumParameter, otherParameter); } - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testedParameters) - .containsExactly("ONE:ONE", "ONE:TWO", "TWO:ONE", "TWO:TWO", "THREE:ONE", "THREE:TWO"); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test[ONE,ONE]", "ONE:ONE") + .put("test[ONE,TWO]", "ONE:TWO") + .put("test[TWO,ONE]", "TWO:ONE") + .put("test[TWO,TWO]", "TWO:TWO") + .put("test[THREE,ONE]", "THREE:ONE") + .put("test[THREE,TWO]", "THREE:TWO") + .build(); } } @@ -763,25 +646,11 @@ public class TestParameterAnnotationMethodProcessorTest { @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) public static class ErrorDuplicatedClassFieldAnnotation { - private static List 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); - } + public void test() {} } @ClassTestResult(Result.FAILURE) @@ -850,46 +719,29 @@ public class TestParameterAnnotationMethodProcessorTest { } @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class IndependentAnnotation { + public static class IndependentAnnotation extends SuccessfulTestCaseBase { @EnumAParameter EnumA enumA; @EnumBParameter EnumB enumB; @EnumCParameter EnumC enumC; - private static List> 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()); + storeTestParametersForThisTest(enumA, enumB, enumC); } - private static > void assertAllEnumsAreIncluded(Enum[] values) { - Set> enumSet = new HashSet<>(Arrays.asList(values)); - for (List enumList : testedParameters) { - enumSet.removeAll(enumList); - } - assertThat(enumSet).isEmpty(); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test[A1,B1,C1]", "A1:B1:C1") + .put("test[A2,B2,C2]", "A2:B2:C2") + .put("test[A2,B2,C3]", "A2:B2:C3") + .build(); } } @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class TestNamesTest { - - @Rule public TestName name = new TestName(); + public static class TestNamesTest extends SuccessfulTestCaseBase { @TestParameter("8") long fieldParam; @@ -897,33 +749,51 @@ public class TestParameterAnnotationMethodProcessorTest { @Test public void withPrimitives( @TestParameter("true") boolean param1, @TestParameter("2") int param2) { - assertThat(name.getMethodName()) - .isEqualTo("withPrimitives[fieldParam=8,param1=true,param2=2]"); + storeTestParametersForThisTest(fieldParam, param1, param2); } @Test public void withString(@TestParameter("AAA") String param1) { - assertThat(name.getMethodName()).isEqualTo("withString[fieldParam=8,AAA]"); + storeTestParametersForThisTest(fieldParam, param1); } @Test public void withEnum(@EnumParameter(TestEnum.TWO) TestEnum param1) { - assertThat(name.getMethodName()).isEqualTo("withEnum[fieldParam=8,TWO]"); + storeTestParametersForThisTest(fieldParam, param1); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("withString[fieldParam=8,AAA]", "8:AAA") + .put("withEnum[fieldParam=8,TWO]", "8:TWO") + .put("withPrimitives[fieldParam=8,param1=true,param2=2]", "8:true:2") + .build(); } } @ClassTestResult(Result.SUCCESS_ALWAYS) - public static class MethodNameContainsOrderedParameterNames { - - @Rule public TestName name = new TestName(); + public static class MethodNameContainsOrderedParameterNames extends SuccessfulTestCaseBase { @Test - public void pretest(@TestParameter({"a", "b"}) String foo) {} + public void pretest(@TestParameter({"a", "b"}) String foo) { + storeTestParametersForThisTest(foo); + } @Test public void test( @EnumParameter({TestEnum.ONE, TestEnum.TWO}) TestEnum e, @TestParameter({"c"}) String foo) { - assertThat(name.getMethodName()).isEqualTo("test[" + e.name() + "," + foo + "]"); + storeTestParametersForThisTest(e, foo); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("pretest[a]", "a") + .put("pretest[b]", "b") + .put("test[ONE,c]", "ONE:c") + .put("test[TWO,c]", "TWO:c") + .build(); } } -- cgit v1.2.3 From 8f9eaff2a21bed29a6661b01e6a4e178bbaa74f6 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 3 May 2022 13:10:46 +0000 Subject: TestParameterInjector: Apply SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase to TestParametersMethodProcessorTest --- .../SharedTestUtilitiesJUnit4.java | 3 + .../TestParametersMethodProcessorTest.java | 354 ++++++++------------- 2 files changed, 137 insertions(+), 220 deletions(-) diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java index 863551d..f047e21 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java @@ -14,6 +14,7 @@ package com.google.testing.junit.testparameterinjector; +import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.truth.Truth.assertWithMessage; @@ -131,6 +132,8 @@ class SharedTestUtilitiesJUnit4 { @AfterClass public static void completedAllTests() { + checkNotNull( + testNameToStringifiedParameters, "storeTestParametersForThisTest() was never called"); try { assertWithMessage(toCopyPastableJavaString(testNameToStringifiedParameters)) .that(testNameToStringifiedParameters) diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java index 77b1e2a..a8f5cff 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java @@ -21,23 +21,16 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.testing.junit.testparameterinjector.SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase; import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValues; import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValuesProvider; import java.lang.annotation.Retention; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.Optional; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.TestName; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; @@ -67,24 +60,14 @@ public class TestParametersMethodProcessorTest { } @RunAsTest - public static class SimpleMethodAnnotation { - @Rule public TestName testName = new TestName(); - - private static Map testNameToStringifiedParametersMap; - - @BeforeClass - public static void resetStaticState() { - testNameToStringifiedParametersMap = new LinkedHashMap<>(); - } + public static class SimpleMethodAnnotation extends SuccessfulTestCaseBase { @Test @TestParameters("{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}") @TestParameters("{testEnum: TWO,\ntestLong: 22,\ntestBoolean: true,\r\n\r\n testString: 'DEF'}") @TestParameters("{testEnum: null, testLong: 33, testBoolean: false, testString: null}") public void test(TestEnum testEnum, long testLong, boolean testBoolean, String testString) { - testNameToStringifiedParametersMap.put( - testName.getMethodName(), - String.format("%s,%s,%s,%s", testEnum, testLong, testBoolean, testString)); + storeTestParametersForThisTest(testEnum, testLong, testBoolean, testString); } @Test @@ -95,9 +78,7 @@ public class TestParametersMethodProcessorTest { }) public void test_singleAnnotation( TestEnum testEnum, long testLong, boolean testBoolean, String testString) { - testNameToStringifiedParametersMap.put( - testName.getMethodName(), - String.format("%s,%s,%s,%s", testEnum, testLong, testBoolean, testString)); + storeTestParametersForThisTest(testEnum, testLong, testBoolean, testString); } @Test @@ -108,7 +89,7 @@ public class TestParametersMethodProcessorTest { + " =================================================================================" + "=============='}") public void test2_withLongNames(String testString) { - testNameToStringifiedParametersMap.put(testName.getMethodName(), testString); + storeTestParametersForThisTest(testString); } @Test @@ -123,9 +104,7 @@ public class TestParametersMethodProcessorTest { List testLongs, List testBooleans, List testStrings) { - testNameToStringifiedParametersMap.put( - testName.getMethodName(), - String.format("%s,%s,%s,%s", testEnums, testLongs, testBooleans, testStrings)); + storeTestParametersForThisTest(testEnums, testLongs, testBooleans, testStrings); } @Test @@ -133,61 +112,62 @@ public class TestParametersMethodProcessorTest { @TestParameters("{testEnum: TWO}") @TestParameters(customName = "custom3", value = "{testEnum: THREE}") public void test4_withCustomName(TestEnum testEnum) { - testNameToStringifiedParametersMap.put(testName.getMethodName(), String.valueOf(testEnum)); + storeTestParametersForThisTest(testEnum); } - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testNameToStringifiedParametersMap) - .containsExactly( + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put( "test[{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}]", - "ONE,11,false,ABC", + "ONE:11:false:ABC") + .put( "test[{testEnum: TWO, testLong: 22, testBoolean: true, testString: 'DEF'}]", - "TWO,22,true,DEF", + "TWO:22:true:DEF") + .put( "test[{testEnum: null, testLong: 33, testBoolean: false, testString: null}]", - "null,33,false,null", + "null:33:false:null") + .put( "test_singleAnnotation[{testEnum: ONE, testLong: 11, testBoolean: false, testString:" + " ABC}]", - "ONE,11,false,ABC", + "ONE:11:false:ABC") + .put( "test_singleAnnotation[{testEnum: TWO, testLong: 22, testBoolean: true, testString:" + " 'DEF'}]", - "TWO,22,true,DEF", + "TWO:22:true:DEF") + .put( "test_singleAnnotation[{testEnum: null, testLong: 33, testBoolean: false, testString:" + " null}]", - "null,33,false,null", - "test2_withLongNames[1.{testString: ABC}]", - "ABC", + "null:33:false:null") + .put("test2_withLongNames[1.{testString: ABC}]", "ABC") + .put( "test2_withLongNames[2.{testString: 'This is a very long string (240 characters) that" + " would normally cause Sponge+Tin to exceed the filename limit of 255" + " characters. =============================...]", "This is a very long string (240 characters) that would normally cause Sponge+Tin to" + " exceed the filename limit of 255 characters." - + " =================================================================================" - + "==============", + + " ===============================================================================================") + .put( "test3_withRepeatedParams[{testEnums: [ONE, TWO, THREE], testLongs: [11, 4]," + " testBooleans: [false, true], testStrings: [ABC, '123']}]", - "[ONE, TWO, THREE],[11, 4],[false, true],[ABC, 123]", + "[ONE, TWO, THREE]:[11, 4]:[false, true]:[ABC, 123]") + .put( "test3_withRepeatedParams[{testEnums: [TWO], testLongs: [22], testBooleans: [true]," + " testStrings: ['DEF']}]", - "[TWO],[22],[true],[DEF]", + "[TWO]:[22]:[true]:[DEF]") + .put( "test3_withRepeatedParams[{testEnums: [], testLongs: [], testBooleans: []," + " testStrings: []}]", - "[],[],[],[]", - "test4_withCustomName[custom1]", - "ONE", - "test4_withCustomName[{testEnum: TWO}]", - "TWO", - "test4_withCustomName[custom3]", - "THREE"); + "[]:[]:[]:[]") + .put("test4_withCustomName[custom1]", "ONE") + .put("test4_withCustomName[{testEnum: TWO}]", "TWO") + .put("test4_withCustomName[custom3]", "THREE") + .build(); } } @RunAsTest - public static class SimpleConstructorAnnotation { - - @Rule public TestName testName = new TestName(); - - private static Map testNameToStringifiedParametersMap; + public static class SimpleConstructorAnnotation extends SuccessfulTestCaseBase { private final TestEnum testEnum; private final long testLong; @@ -207,50 +187,43 @@ public class TestParametersMethodProcessorTest { 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)); + storeTestParametersForThisTest(testEnum, testLong, testBoolean, testString); } @Test public void test2() { - testNameToStringifiedParametersMap.put( - testName.getMethodName(), - String.format("%s,%s,%s,%s", testEnum, testLong, testBoolean, testString)); + storeTestParametersForThisTest(testEnum, testLong, testBoolean, testString); } - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testNameToStringifiedParametersMap) - .containsExactly( + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put( "test1[{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}]", - "ONE,11,false,ABC", + "ONE:11:false:ABC") + .put( "test1[{testEnum: TWO, testLong: 22, testBoolean: true, testString: DEF}]", - "TWO,22,true,DEF", + "TWO:22:true:DEF") + .put( "test1[{testEnum: null, testLong: 33, testBoolean: false, testString: null}]", - "null,33,false,null", + "null:33:false:null") + .put( "test2[{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}]", - "ONE,11,false,ABC", + "ONE:11:false:ABC") + .put( "test2[{testEnum: TWO, testLong: 22, testBoolean: true, testString: DEF}]", - "TWO,22,true,DEF", + "TWO:22:true:DEF") + .put( "test2[{testEnum: null, testLong: 33, testBoolean: false, testString: null}]", - "null,33,false,null"); + "null:33:false:null") + .build(); } } @RunAsTest - public static class ConstructorAnnotationWithProvider { - - @Rule public TestName testName = new TestName(); - - private static Map testNameToParameterMap; + public static class ConstructorAnnotationWithProvider extends SuccessfulTestCaseBase { private final TestEnum testEnum; @@ -259,59 +232,28 @@ public class TestParametersMethodProcessorTest { 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); + public void test() { + storeTestParametersForThisTest(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); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test[one]", "ONE") + .put("test[two]", "TWO") + .put("test[null-case]", "null") + .build(); } } - public abstract static class BaseClassWithMethodAnnotation { - @Rule public TestName testName = new TestName(); - - static List 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()); - } + public abstract static class BaseClassWithMethodAnnotation extends SuccessfulTestCaseBase { @Test @TestParameters("{testEnum: ONE}") @TestParameters("{testEnum: TWO}") public void testInBase(TestEnum testEnum) { - allTestNames.add(testName.getMethodName()); + storeTestParametersForThisTest(testEnum); } } @@ -321,62 +263,41 @@ public class TestParametersMethodProcessorTest { @Test @TestParameters({"{testEnum: TWO}", "{testEnum: THREE}"}) public void testInChild(TestEnum testEnum) { - allTestNames.add(testName.getMethodName()); + storeTestParametersForThisTest(testEnum); } - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(allTestNames) - .containsExactly( - "testInBase[{testEnum: ONE}]", - "testInBase[{testEnum: TWO}]", - "testInChild[{testEnum: TWO}]", - "testInChild[{testEnum: THREE}]"); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("testInChild[{testEnum: TWO}]", "TWO") + .put("testInChild[{testEnum: THREE}]", "THREE") + .put("testInBase[{testEnum: ONE}]", "ONE") + .put("testInBase[{testEnum: TWO}]", "TWO") + .build(); } } @RunAsTest - public static class MixedWithTestParameterMethodAnnotation { - @Rule public TestName testName = new TestName(); + public static class MixedWithTestParameterMethodAnnotation extends SuccessfulTestCaseBase { - private static List allTestNames; - private static List testNamesThatInvokedBefore; - private static List testNamesThatInvokedAfter; + private final TestEnum testEnumFromConstructor; @TestParameters("{testEnum: ONE}") @TestParameters("{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()); + public MixedWithTestParameterMethodAnnotation(TestEnum testEnum) { + this.testEnumFromConstructor = testEnum; } @Test public void test1(@TestParameter TestEnum testEnum) { - assertThat(testNamesThatInvokedBefore).contains(testName.getMethodName()); - allTestNames.add(testName.getMethodName()); + storeTestParametersForThisTest(testEnumFromConstructor, testEnum); } @Test @TestParameters("{testString: ABC}") @TestParameters("{testString: DEF}") public void test2(String testString) { - allTestNames.add(testName.getMethodName()); + storeTestParametersForThisTest(testEnumFromConstructor, testString); } @Test @@ -387,84 +308,77 @@ public class TestParametersMethodProcessorTest { + " =================================================================================" + "=============='}") public void test3_withLongNames(String testString) { - allTestNames.add(testName.getMethodName()); + storeTestParametersForThisTest(testEnumFromConstructor, testString); } - @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(); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test1[{testEnum: ONE},ONE]", "ONE:ONE") + .put("test1[{testEnum: ONE},TWO]", "ONE:TWO") + .put("test1[{testEnum: ONE},THREE]", "ONE:THREE") + .put("test1[{testEnum: TWO},ONE]", "TWO:ONE") + .put("test1[{testEnum: TWO},TWO]", "TWO:TWO") + .put("test1[{testEnum: TWO},THREE]", "TWO:THREE") + .put("test2[{testEnum: ONE},{testString: ABC}]", "ONE:ABC") + .put("test2[{testEnum: ONE},{testString: DEF}]", "ONE:DEF") + .put("test2[{testEnum: TWO},{testString: ABC}]", "TWO:ABC") + .put("test2[{testEnum: TWO},{testString: DEF}]", "TWO:DEF") + .put("test3_withLongNames[{testEnum: ONE},1.{testString: ABC}]", "ONE:ABC") + .put( + "test3_withLongNames[{testEnum: ONE},2.{testString: 'This is a very long string (240" + + " characters) that would normally caus...]", + "ONE:This is a very long string (240 characters) that would normally cause Sponge+Tin" + + " to exceed the filename limit of 255 characters." + + " ===================================================================" + + "============================") + .put("test3_withLongNames[{testEnum: TWO},1.{testString: ABC}]", "TWO:ABC") + .put( + "test3_withLongNames[{testEnum: TWO},2.{testString: 'This is a very long string (240" + + " characters) that would normally caus...]", + "TWO:This is a very long string (240 characters) that would normally cause Sponge+Tin" + + " to exceed the filename limit of 255 characters." + + " ======================================================================" + + "=========================") + .build(); } } @RunAsTest - public static class MixedWithTestParameterFieldAnnotation { - @Rule public TestName testName = new TestName(); + public static class MixedWithTestParameterFieldAnnotation extends SuccessfulTestCaseBase { - private static List allTestNames; + private final TestEnum testEnumB; @TestParameter TestEnum testEnumA; @TestParameters("{testEnumB: ONE}") @TestParameters("{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()); + public MixedWithTestParameterFieldAnnotation(TestEnum testEnumB) { + this.testEnumB = testEnumB; } @Test @TestParameters({"{testString: ABC}", "{testString: DEF}"}) public void test1(String testString) { - allTestNames.add(testName.getMethodName()); + storeTestParametersForThisTest(testEnumA, testEnumB, testString); } - @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]"); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test1[{testEnumB: ONE},{testString: ABC},ONE]", "ONE:ONE:ABC") + .put("test1[{testEnumB: ONE},{testString: ABC},TWO]", "TWO:ONE:ABC") + .put("test1[{testEnumB: ONE},{testString: ABC},THREE]", "THREE:ONE:ABC") + .put("test1[{testEnumB: ONE},{testString: DEF},ONE]", "ONE:ONE:DEF") + .put("test1[{testEnumB: ONE},{testString: DEF},TWO]", "TWO:ONE:DEF") + .put("test1[{testEnumB: ONE},{testString: DEF},THREE]", "THREE:ONE:DEF") + .put("test1[{testEnumB: TWO},{testString: ABC},ONE]", "ONE:TWO:ABC") + .put("test1[{testEnumB: TWO},{testString: ABC},TWO]", "TWO:TWO:ABC") + .put("test1[{testEnumB: TWO},{testString: ABC},THREE]", "THREE:TWO:ABC") + .put("test1[{testEnumB: TWO},{testString: DEF},ONE]", "ONE:TWO:DEF") + .put("test1[{testEnumB: TWO},{testString: DEF},TWO]", "TWO:TWO:DEF") + .put("test1[{testEnumB: TWO},{testString: DEF},THREE]", "THREE:TWO:DEF") + .build(); } } -- cgit v1.2.3 From 192024aa1c297234bae21b1b6e8d8279a7c13b73 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 3 May 2022 13:11:54 +0000 Subject: TestInfoParameter: Rename ambiguous 'name' to 'valueInTestName'. 'name' was ambiguous because it might also have referred tot the parameter name. But actually, it's the stringified version of the value, optionally prefixed with the parameter name (and some some complex things) --- .../junit/testparameterinjector/TestInfo.java | 39 +++++++---- .../junit/testparameterinjector/TestInfoTest.java | 79 ++++++++++++++-------- .../testparameterinjector/junit5/TestInfo.java | 39 +++++++---- 3 files changed, 100 insertions(+), 57 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java index 69777d2..daefeb9 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java @@ -64,7 +64,9 @@ abstract class TestInfo { return String.format( "%s[%s]", getMethod().getName(), - getParameters().stream().map(TestInfoParameter::getName).collect(joining(","))); + getParameters().stream() + .map(TestInfoParameter::getValueInTestName) + .collect(joining(","))); } } @@ -114,7 +116,7 @@ abstract class TestInfo { .mapToObj( parameterIndex -> { TestInfoParameter parameter = getParameters().get(parameterIndex); - return parameter.withName( + return parameter.withValueInTestName( parameterWithIndexToNewName.apply(parameter, parameterIndex)); }) .collect(toImmutableList()), @@ -147,7 +149,10 @@ abstract class TestInfo { testInfos.stream() .anyMatch( info -> - info.getParameters().get(parameterIndex).getName().length() + info.getParameters() + .get(parameterIndex) + .getValueInTestName() + .length() > getMaxCharactersPerParameter(info, numberOfParameters))) .boxed() .collect(toSet()); @@ -161,7 +166,7 @@ abstract class TestInfo { ? getShortenedName( parameter, getMaxCharactersPerParameter(info, numberOfParameters)) - : info.getParameters().get(parameterIndex).getName())) + : info.getParameters().get(parameterIndex).getValueInTestName())) .collect(toImmutableList()); } } else { @@ -195,9 +200,9 @@ abstract class TestInfo { return String.valueOf(parameter.getIndexInValueSource() + 1); } else { String shortenedName = - parameter.getName().length() > maxCharactersPerParameter - ? parameter.getName().substring(0, maxCharactersPerParameter - 3) + "..." - : parameter.getName(); + parameter.getValueInTestName().length() > maxCharactersPerParameter + ? parameter.getValueInTestName().substring(0, maxCharactersPerParameter - 3) + "..." + : parameter.getValueInTestName(); return String.format("%s.%s", parameter.getIndexInValueSource() + 1, shortenedName); } } @@ -244,8 +249,9 @@ abstract class TestInfo { testInfo.withUpdatedParameterNames( (parameter, parameterIndex) -> indicesThatShouldGetSuffix.contains(parameterIndex) - ? parameter.getName() + getTypeSuffix(parameter.getValue()) - : parameter.getName())); + ? parameter.getValueInTestName() + + getTypeSuffix(parameter.getValue()) + : parameter.getValueInTestName())); } }) .collect(toImmutableList()); @@ -273,7 +279,9 @@ abstract class TestInfo { testInfo.withUpdatedParameterNames( (parameter, parameterIndex) -> String.format( - "%s.%s", parameter.getIndexInValueSource() + 1, parameter.getName()))) + "%s.%s", + parameter.getIndexInValueSource() + 1, + parameter.getValueInTestName()))) .collect(toImmutableList()); } } @@ -285,7 +293,7 @@ abstract class TestInfo { @AutoValue abstract static class TestInfoParameter { - abstract String getName(); + abstract String getValueInTestName(); @Nullable abstract Object getValue(); @@ -296,14 +304,15 @@ abstract class TestInfo { */ abstract int getIndexInValueSource(); - final TestInfoParameter withName(String newName) { - return create(newName, getValue(), getIndexInValueSource()); + final TestInfoParameter withValueInTestName(String newValueInTestName) { + return create(newValueInTestName, getValue(), getIndexInValueSource()); } - static TestInfoParameter create(String name, @Nullable Object value, int indexInValueSource) { + static TestInfoParameter create( + String valueInTestName, @Nullable Object value, int indexInValueSource) { checkArgument(indexInValueSource >= 0); return new AutoValue_TestInfo_TestInfoParameter( - checkNotNull(name), value, indexInValueSource); + checkNotNull(valueInTestName), value, indexInValueSource); } } } diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java index f6fd512..46af6c4 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java @@ -70,14 +70,18 @@ public class TestInfoTest { ImmutableList.of( fakeTestInfo( TestInfoParameter.create( - /* name= */ "short", /* value= */ 1, /* indexInValueSource= */ 1), + /* valueInTestName= */ "short", /* value= */ 1, /* indexInValueSource= */ 1), TestInfoParameter.create( - /* name= */ "shorter", /* value= */ null, /* indexInValueSource= */ 3)), + /* valueInTestName= */ "shorter", + /* value= */ null, + /* indexInValueSource= */ 3)), fakeTestInfo( TestInfoParameter.create( - /* name= */ "short", /* value= */ 1, /* indexInValueSource= */ 1), + /* valueInTestName= */ "short", /* value= */ 1, /* indexInValueSource= */ 1), TestInfoParameter.create( - /* name= */ "shortest", /* value= */ 20, /* indexInValueSource= */ 0))); + /* valueInTestName= */ "shortest", + /* value= */ 20, + /* indexInValueSource= */ 0))); ImmutableList result = TestInfo.shortenNamesIfNecessary(givenTestInfos); @@ -91,14 +95,20 @@ public class TestInfoTest { ImmutableList.of( fakeTestInfo( TestInfoParameter.create( - /* name= */ "short", /* value= */ 1, /* indexInValueSource= */ 0), + /* valueInTestName= */ "short", + /* value= */ 1, + /* indexInValueSource= */ 0), TestInfoParameter.create( - /* name= */ "shorter", /* value= */ null, /* indexInValueSource= */ 0)), + /* valueInTestName= */ "shorter", + /* value= */ null, + /* indexInValueSource= */ 0)), fakeTestInfo( TestInfoParameter.create( - /* name= */ "short", /* value= */ 1, /* indexInValueSource= */ 0), + /* valueInTestName= */ "short", + /* value= */ 1, + /* indexInValueSource= */ 0), TestInfoParameter.create( - /* name= */ "very long parameter name for test" + /* valueInTestName= */ "very long parameter name for test" + " 00000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000", @@ -120,10 +130,12 @@ public class TestInfoTest { ImmutableList.of( fakeTestInfo( TestInfoParameter.create( - /* name= */ "shorter", /* value= */ null, /* indexInValueSource= */ 0)), + /* valueInTestName= */ "shorter", + /* value= */ null, + /* indexInValueSource= */ 0)), fakeTestInfo( TestInfoParameter.create( - /* name= */ "very long parameter name for test" + /* valueInTestName= */ "very long parameter name for test" + " 00000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000", @@ -147,7 +159,9 @@ public class TestInfoTest { .mapToObj( i -> TestInfoParameter.create( - /* name= */ "short", /* value= */ i, /* indexInValueSource= */ i)) + /* valueInTestName= */ "short", + /* value= */ i, + /* indexInValueSource= */ i)) .toArray(TestInfoParameter[]::new)); ImmutableList result = @@ -165,18 +179,21 @@ public class TestInfoTest { ImmutableList.of( fakeTestInfo( TestInfoParameter.create( - /* name= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1), + /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1), TestInfoParameter.create( - /* name= */ "bbb", /* value= */ null, /* indexInValueSource= */ 3)), + /* valueInTestName= */ "bbb", /* value= */ null, /* indexInValueSource= */ 3)), fakeTestInfo( TestInfoParameter.create( - /* name= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1), + /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1), TestInfoParameter.create( - /* name= */ "ccc", /* value= */ 1, /* indexInValueSource= */ 0))); + /* valueInTestName= */ "ccc", /* value= */ 1, /* indexInValueSource= */ 0))); ImmutableList result = TestInfo.deduplicateTestNames(givenTestInfos); assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder(); + assertThatTestNamesOf(result) + .containsExactly("toLowerCase[aaa,bbb]", "toLowerCase[aaa,ccc]") + .inOrder(); } @Test @@ -186,19 +203,25 @@ public class TestInfoTest { ImmutableList.of( fakeTestInfo( TestInfoParameter.create( - /* name= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1), + /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1), TestInfoParameter.create( - /* name= */ "null", /* value= */ null, /* indexInValueSource= */ 3)), + /* valueInTestName= */ "null", + /* value= */ null, + /* indexInValueSource= */ 3)), fakeTestInfo( TestInfoParameter.create( - /* name= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1), + /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1), TestInfoParameter.create( - /* name= */ "null", /* value= */ "null", /* indexInValueSource= */ 0)), + /* valueInTestName= */ "null", + /* value= */ "null", + /* indexInValueSource= */ 0)), fakeTestInfo( TestInfoParameter.create( - /* name= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1), + /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1), TestInfoParameter.create( - /* name= */ "bbb", /* value= */ "b", /* indexInValueSource= */ 0)))); + /* valueInTestName= */ "bbb", + /* value= */ "b", + /* indexInValueSource= */ 0)))); assertThatTestNamesOf(result) .containsExactly( @@ -215,19 +238,21 @@ public class TestInfoTest { ImmutableList.of( fakeTestInfo( TestInfoParameter.create( - /* name= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 0), + /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 0), TestInfoParameter.create( - /* name= */ "bbb", /* value= */ 1, /* indexInValueSource= */ 0)), + /* valueInTestName= */ "bbb", /* value= */ 1, /* indexInValueSource= */ 0)), fakeTestInfo( TestInfoParameter.create( - /* name= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 0), + /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 0), TestInfoParameter.create( - /* name= */ "bbb", /* value= */ 1, /* indexInValueSource= */ 1)), + /* valueInTestName= */ "bbb", /* value= */ 1, /* indexInValueSource= */ 1)), fakeTestInfo( TestInfoParameter.create( - /* name= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 0), + /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 0), TestInfoParameter.create( - /* name= */ "ccc", /* value= */ "b", /* indexInValueSource= */ 2)))); + /* valueInTestName= */ "ccc", + /* value= */ "b", + /* indexInValueSource= */ 2)))); assertThatTestNamesOf(result) .containsExactly( diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java index c6b1cc3..93b0c9a 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java @@ -64,7 +64,9 @@ abstract class TestInfo { return String.format( "%s[%s]", getMethod().getName(), - getParameters().stream().map(TestInfoParameter::getName).collect(joining(","))); + getParameters().stream() + .map(TestInfoParameter::getValueInTestName) + .collect(joining(","))); } } @@ -114,7 +116,7 @@ abstract class TestInfo { .mapToObj( parameterIndex -> { TestInfoParameter parameter = getParameters().get(parameterIndex); - return parameter.withName( + return parameter.withValueInTestName( parameterWithIndexToNewName.apply(parameter, parameterIndex)); }) .collect(toImmutableList()), @@ -147,7 +149,10 @@ abstract class TestInfo { testInfos.stream() .anyMatch( info -> - info.getParameters().get(parameterIndex).getName().length() + info.getParameters() + .get(parameterIndex) + .getValueInTestName() + .length() > getMaxCharactersPerParameter(info, numberOfParameters))) .boxed() .collect(toSet()); @@ -161,7 +166,7 @@ abstract class TestInfo { ? getShortenedName( parameter, getMaxCharactersPerParameter(info, numberOfParameters)) - : info.getParameters().get(parameterIndex).getName())) + : info.getParameters().get(parameterIndex).getValueInTestName())) .collect(toImmutableList()); } } else { @@ -195,9 +200,9 @@ abstract class TestInfo { return String.valueOf(parameter.getIndexInValueSource() + 1); } else { String shortenedName = - parameter.getName().length() > maxCharactersPerParameter - ? parameter.getName().substring(0, maxCharactersPerParameter - 3) + "..." - : parameter.getName(); + parameter.getValueInTestName().length() > maxCharactersPerParameter + ? parameter.getValueInTestName().substring(0, maxCharactersPerParameter - 3) + "..." + : parameter.getValueInTestName(); return String.format("%s.%s", parameter.getIndexInValueSource() + 1, shortenedName); } } @@ -244,8 +249,9 @@ abstract class TestInfo { testInfo.withUpdatedParameterNames( (parameter, parameterIndex) -> indicesThatShouldGetSuffix.contains(parameterIndex) - ? parameter.getName() + getTypeSuffix(parameter.getValue()) - : parameter.getName())); + ? parameter.getValueInTestName() + + getTypeSuffix(parameter.getValue()) + : parameter.getValueInTestName())); } }) .collect(toImmutableList()); @@ -273,7 +279,9 @@ abstract class TestInfo { testInfo.withUpdatedParameterNames( (parameter, parameterIndex) -> String.format( - "%s.%s", parameter.getIndexInValueSource() + 1, parameter.getName()))) + "%s.%s", + parameter.getIndexInValueSource() + 1, + parameter.getValueInTestName()))) .collect(toImmutableList()); } } @@ -285,7 +293,7 @@ abstract class TestInfo { @AutoValue abstract static class TestInfoParameter { - abstract String getName(); + abstract String getValueInTestName(); @Nullable abstract Object getValue(); @@ -296,14 +304,15 @@ abstract class TestInfo { */ abstract int getIndexInValueSource(); - final TestInfoParameter withName(String newName) { - return create(newName, getValue(), getIndexInValueSource()); + final TestInfoParameter withValueInTestName(String newValueInTestName) { + return create(newValueInTestName, getValue(), getIndexInValueSource()); } - static TestInfoParameter create(String name, @Nullable Object value, int indexInValueSource) { + static TestInfoParameter create( + String valueInTestName, @Nullable Object value, int indexInValueSource) { checkArgument(indexInValueSource >= 0); return new AutoValue_TestInfo_TestInfoParameter( - checkNotNull(name), value, indexInValueSource); + checkNotNull(valueInTestName), value, indexInValueSource); } } } -- cgit v1.2.3 From e390bab7bf4c621e54ae7f814ebd405e49d60829 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 4 May 2022 07:53:58 +0000 Subject: TestParameterInjector: Support byte arrays in test name. Before this change, the byte array would be printed as default in java with the memory reference. --- .../TestParameterAnnotationMethodProcessor.java | 35 ++++++++++++++++++---- .../TestParameterAnnotationMethodProcessor.java | 35 ++++++++++++++++++---- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 33ef406..62e60c7 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -35,11 +35,14 @@ 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.protobuf.ByteString; import com.google.testing.junit.testparameterinjector.TestInfo.TestInfoParameter; +import com.google.testing.junit.testparameterinjector.TestParameterAnnotationMethodProcessor.TestParameterValue; import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -124,14 +127,34 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso && 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 + // 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+", " "); + return String.format("%s=%s", paramName().get(), valueAsString()) + .trim() + .replaceAll("\\s+", " "); } else { - return MessageFormat.format(namePattern, value()).trim().replaceAll("\\s+", " "); + return MessageFormat.format(namePattern, valueAsString()).trim().replaceAll("\\s+", " "); + } + } + + private String valueAsString() { + if (value() != null && value().getClass().isArray()) { + StringBuilder resultBuider = new StringBuilder(); + resultBuider.append("["); + for (int i = 0; i < Array.getLength(value()); i++) { + if (i > 0) { + resultBuider.append(", "); + } + resultBuider.append(Array.get(value(), i)); + } + resultBuider.append("]"); + return resultBuider.toString(); + } else if (value() instanceof ByteString) { + return Arrays.toString(((ByteString) value()).toByteArray()); + } else { + return String.valueOf(value()); } } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java index a51ceb8..1b6300e 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java @@ -35,11 +35,14 @@ 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.protobuf.ByteString; import com.google.testing.junit.testparameterinjector.junit5.TestInfo.TestInfoParameter; +import com.google.testing.junit.testparameterinjector.junit5.TestParameterAnnotationMethodProcessor.TestParameterValue; import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -124,14 +127,34 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso && 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 + // 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+", " "); + return String.format("%s=%s", paramName().get(), valueAsString()) + .trim() + .replaceAll("\\s+", " "); } else { - return MessageFormat.format(namePattern, value()).trim().replaceAll("\\s+", " "); + return MessageFormat.format(namePattern, valueAsString()).trim().replaceAll("\\s+", " "); + } + } + + private String valueAsString() { + if (value() != null && value().getClass().isArray()) { + StringBuilder resultBuider = new StringBuilder(); + resultBuider.append("["); + for (int i = 0; i < Array.getLength(value()); i++) { + if (i > 0) { + resultBuider.append(", "); + } + resultBuider.append(Array.get(value(), i)); + } + resultBuider.append("]"); + return resultBuider.toString(); + } else if (value() instanceof ByteString) { + return Arrays.toString(((ByteString) value()).toByteArray()); + } else { + return String.valueOf(value()); } } -- cgit v1.2.3 From 2290212e106570f0986d0a2ae3400f08dbcdfc68 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 4 May 2022 08:26:35 +0000 Subject: TestParameterInjector: Apply SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase to TestParameterTest --- .../testparameterinjector/TestParameterTest.java | 153 +++++++++------------ 1 file changed, 65 insertions(+), 88 deletions(-) diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java index f68d5ca..e099521 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java @@ -16,19 +16,16 @@ 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.common.collect.ImmutableMap; +import com.google.testing.junit.testparameterinjector.SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase; 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.runners.Parameterized; @@ -48,132 +45,112 @@ public class TestParameterTest { } @RunAsTest - public static class AnnotatedField { - private static List testedParameters; + public static class AnnotatedField extends SuccessfulTestCaseBase { @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); + storeTestParametersForThisTest(enumParameter); } - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testedParameters).containsExactly(TestEnum.ONE, TestEnum.TWO, TestEnum.THREE); + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test[ONE]", "ONE") + .put("test[TWO]", "TWO") + .put("test[THREE]", "THREE") + .build(); } } @RunAsTest - public static class AnnotatedConstructorParameter { - private static List testedParameters; + public static class AnnotatedConstructorParameter extends SuccessfulTestCaseBase { private final TestEnum constructorEnum; - public AnnotatedConstructorParameter(@TestParameter TestEnum constructorEnum) { - this.constructorEnum = constructorEnum; - } - @TestParameter TestEnum fieldEnum; - @BeforeClass - public static void initializeStaticFields() { - assertWithMessage("Expecting this class to be run only once").that(testedParameters).isNull(); - testedParameters = new ArrayList<>(); + public AnnotatedConstructorParameter(@TestParameter TestEnum constructorEnum) { + this.constructorEnum = constructorEnum; } @Test public void test() { - testedParameters.add(String.format("%s:%s", fieldEnum, constructorEnum)); - } - - @AfterClass - public static void completedAllParameterizedTests() { - assertThat(testedParameters) - .containsExactly( - "ONE:ONE", - "ONE:TWO", - "ONE:THREE", - "TWO:ONE", - "TWO:TWO", - "TWO:THREE", - "THREE:ONE", - "THREE:TWO", - "THREE:THREE"); + storeTestParametersForThisTest(fieldEnum, constructorEnum); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test[ONE,ONE]", "ONE:ONE") + .put("test[ONE,TWO]", "ONE:TWO") + .put("test[ONE,THREE]", "ONE:THREE") + .put("test[TWO,ONE]", "TWO:ONE") + .put("test[TWO,TWO]", "TWO:TWO") + .put("test[TWO,THREE]", "TWO:THREE") + .put("test[THREE,ONE]", "THREE:ONE") + .put("test[THREE,TWO]", "THREE:TWO") + .put("test[THREE,THREE]", "THREE:THREE") + .build(); } } @RunAsTest - public static class MultipleAnnotatedParameters { - private static List testedParameters; - - @BeforeClass - public static void initializeStaticFields() { - assertWithMessage("Expecting this class to be run only once").that(testedParameters).isNull(); - testedParameters = new ArrayList<>(); - } + public static class MultipleAnnotatedParameters extends SuccessfulTestCaseBase { @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"); + storeTestParametersForThisTest(enumParameterA, enumParameterB, new String(bytes)); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test[ONE,TWO,[100, 97, 116, 97]]", "ONE:TWO:data") + .put("test[ONE,TWO,[100, 97, 116, 97, 50]]", "ONE:TWO:data2") + .put("test[ONE,THREE,[100, 97, 116, 97]]", "ONE:THREE:data") + .put("test[ONE,THREE,[100, 97, 116, 97, 50]]", "ONE:THREE:data2") + .put("test[TWO,TWO,[100, 97, 116, 97]]", "TWO:TWO:data") + .put("test[TWO,TWO,[100, 97, 116, 97, 50]]", "TWO:TWO:data2") + .put("test[TWO,THREE,[100, 97, 116, 97]]", "TWO:THREE:data") + .put("test[TWO,THREE,[100, 97, 116, 97, 50]]", "TWO:THREE:data2") + .put("test[THREE,TWO,[100, 97, 116, 97]]", "THREE:TWO:data") + .put("test[THREE,TWO,[100, 97, 116, 97, 50]]", "THREE:TWO:data2") + .put("test[THREE,THREE,[100, 97, 116, 97]]", "THREE:THREE:data") + .put("test[THREE,THREE,[100, 97, 116, 97, 50]]", "THREE:THREE:data2") + .build(); } } @RunAsTest - public static class WithValuesProvider { - private static List testedParameters; - - @BeforeClass - public static void initializeStaticFields() { - assertWithMessage("Expecting this class to be run only once").that(testedParameters).isNull(); - testedParameters = new ArrayList<>(); - } + public static class WithValuesProvider extends SuccessfulTestCaseBase { @Test public void stringTest( @TestParameter(valuesProvider = TestStringProvider.class) String string) { - testedParameters.add(string); + storeTestParametersForThisTest(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()); + storeTestParametersForThisTest(charMatcher); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("stringTest[A]", "A") + .put("stringTest[B]", "B") + .put("stringTest[null]", "null") + .put("charMatcherTest[CharMatcher.any()]", "CharMatcher.any()") + .put("charMatcherTest[CharMatcher.ascii()]", "CharMatcher.ascii()") + .put("charMatcherTest[CharMatcher.whitespace()]", "CharMatcher.whitespace()") + .build(); } private static final class TestStringProvider implements TestParameterValuesProvider { -- cgit v1.2.3 From 9d34787a36b92605a876cba361c1f252cc181ae1 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 5 Oct 2022 12:53:38 +0000 Subject: Remove dependencies on Java 8, to allow older Android versions to use it --- .../testparameterinjector/ParameterValueParsing.java | 2 ++ .../junit/testparameterinjector/TestParameter.java | 19 ++++++++++--------- .../TestParameterAnnotation.java | 2 +- .../TestParameterAnnotationMethodProcessor.java | 9 ++------- .../TestParameterValueProvider.java | 2 +- .../junit5/ParameterValueParsing.java | 2 ++ .../testparameterinjector/junit5/TestParameter.java | 19 ++++++++++--------- .../junit5/TestParameterAnnotation.java | 2 +- .../TestParameterAnnotationMethodProcessor.java | 9 ++------- .../junit5/TestParameterValueProvider.java | 2 +- 10 files changed, 32 insertions(+), 36 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java index f7c7cd6..604b899 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java @@ -23,6 +23,7 @@ import static java.util.stream.Collectors.toMap; import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; import com.google.common.reflect.TypeToken; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.protobuf.ByteString; import com.google.protobuf.MessageLite; import java.lang.reflect.ParameterizedType; @@ -204,6 +205,7 @@ final class ParameterValueParsing { } @SuppressWarnings("unchecked") + @CanIgnoreReturnValue SupportedJavaType supportParsedType( Class parsedYamlType, Function transformation) { if (Primitives.wrap(supportedJavaType).isAssignableFrom(Primitives.wrap(javaType))) { diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java index 6725d16..4c03903 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java @@ -21,6 +21,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; import static java.util.Arrays.stream; import static java.util.stream.Collectors.toList; +import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.primitives.Primitives; import com.google.protobuf.MessageLite; @@ -32,7 +33,6 @@ 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. @@ -169,14 +169,15 @@ public @interface TestParameter { @Override public Class getValueType( Class annotationType, Optional> 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))); + if (parameterClass.isPresent()) { + return parameterClass.get(); + } + throw new AssertionError( + String.format( + "An empty parameter class should not be possible since" + + " @TestParameter can only target FIELD or PARAMETER, both" + + " of which are supported for annotation %s.", + annotationType)); } private static Object parseStringValue(String value, Class parameterClass) { diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java index 8c04bc0..9a2cc46 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java @@ -19,6 +19,7 @@ import static com.google.common.base.Verify.verify; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; +import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.primitives.Primitives; import java.lang.annotation.Annotation; @@ -29,7 +30,6 @@ 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 diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 62e60c7..4f6c052 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -218,9 +218,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return valueProvider .getConstructor() .newInstance() - .provideValues( - annotation, - java.util.Optional.ofNullable(annotationWithMetadata.paramClass().orNull())); + .provideValues(annotation, annotationWithMetadata.paramClass()); } catch (ReflectiveOperationException e) { throw new RuntimeException( "Unexpected exception while invoking value provider " + valueProvider, e); @@ -1252,10 +1250,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso annotationType.getAnnotation(TestParameterAnnotation.class); Class valueProvider = testParameter.valueProvider(); try { - return valueProvider - .getConstructor() - .newInstance() - .getValueType(annotationType, java.util.Optional.ofNullable(paramClass.orNull())); + return valueProvider.getConstructor().newInstance().getValueType(annotationType, paramClass); } catch (Exception e) { throw new RuntimeException( "Unexpected exception while invoking value provider " + valueProvider, e); diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java index 6c398aa..dbb334c 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java @@ -14,9 +14,9 @@ package com.google.testing.junit.testparameterinjector; +import com.google.common.base.Optional; 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 diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java index 59d2351..2794e70 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java @@ -23,6 +23,7 @@ import static java.util.stream.Collectors.toMap; import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; import com.google.common.reflect.TypeToken; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.protobuf.ByteString; import com.google.protobuf.MessageLite; import java.lang.reflect.ParameterizedType; @@ -204,6 +205,7 @@ final class ParameterValueParsing { } @SuppressWarnings("unchecked") + @CanIgnoreReturnValue SupportedJavaType supportParsedType( Class parsedYamlType, Function transformation) { if (Primitives.wrap(supportedJavaType).isAssignableFrom(Primitives.wrap(javaType))) { diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java index f31854d..3bc3170 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java @@ -21,6 +21,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; import static java.util.Arrays.stream; import static java.util.stream.Collectors.toList; +import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.primitives.Primitives; import com.google.protobuf.MessageLite; @@ -32,7 +33,6 @@ 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. @@ -169,14 +169,15 @@ public @interface TestParameter { @Override public Class getValueType( Class annotationType, Optional> 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))); + if (parameterClass.isPresent()) { + return parameterClass.get(); + } + throw new AssertionError( + String.format( + "An empty parameter class should not be possible since" + + " @TestParameter can only target FIELD or PARAMETER, both" + + " of which are supported for annotation %s.", + annotationType)); } private static Object parseStringValue(String value, Class parameterClass) { diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotation.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotation.java index e169eb3..8d9051e 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotation.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotation.java @@ -19,6 +19,7 @@ import static com.google.common.base.Verify.verify; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; +import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.primitives.Primitives; import java.lang.annotation.Annotation; @@ -29,7 +30,6 @@ 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 diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java index 1b6300e..37cb36c 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java @@ -218,9 +218,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return valueProvider .getConstructor() .newInstance() - .provideValues( - annotation, - java.util.Optional.ofNullable(annotationWithMetadata.paramClass().orNull())); + .provideValues(annotation, annotationWithMetadata.paramClass()); } catch (ReflectiveOperationException e) { throw new RuntimeException( "Unexpected exception while invoking value provider " + valueProvider, e); @@ -1252,10 +1250,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso annotationType.getAnnotation(TestParameterAnnotation.class); Class valueProvider = testParameter.valueProvider(); try { - return valueProvider - .getConstructor() - .newInstance() - .getValueType(annotationType, java.util.Optional.ofNullable(paramClass.orNull())); + return valueProvider.getConstructor().newInstance().getValueType(annotationType, paramClass); } catch (Exception e) { throw new RuntimeException( "Unexpected exception while invoking value provider " + valueProvider, e); diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java index 47ed601..3c42eec 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java @@ -14,9 +14,9 @@ package com.google.testing.junit.testparameterinjector.junit5; +import com.google.common.base.Optional; 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 -- cgit v1.2.3 From bac0007024c56b8e0c9be356ec05153ab698c60a Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 5 Oct 2022 13:13:02 +0000 Subject: Bump version to v1.9 in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6b6e94a..19d25c1 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ And add the following dependency to your `.pom` file: com.google.testparameterinjector test-parameter-injector - 1.8 + 1.9 test ``` @@ -97,7 +97,7 @@ And add the following dependency to your `.pom` file: com.google.testparameterinjector test-parameter-injector-junit5 - 1.8 + 1.9 test ``` -- cgit v1.2.3 From cd1575831b100667e8ee68d4408587c8cab5d1db Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Mon, 17 Oct 2022 11:27:48 +0000 Subject: Add entry to 1.9 changelog: Test names are no longer dependent on the locale Fixes https://github.com/google/TestParameterInjector/issues/25 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fe13b7..8a5f9ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## 1.9 - Bugfix: Support explicit ordering by the JUnit4 `@Rule`. For example: `@Rule(ordering=3)`. +- Potential test name change: Test names are no longer dependent on the locale of the machine + running it (e.g. doubles with integer values are always formatted with a trailing `.0`) ## 1.8 -- cgit v1.2.3 From 42468a079bab05bafdba9fa3f208547e62aeb703 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Mon, 17 Oct 2022 11:28:25 +0000 Subject: Remove java 8 dependencies across the codebase --- .../BaseTestParameterValidator.java | 15 +- .../ParameterValueParsing.java | 48 +-- .../testparameterinjector/PluggableTestRunner.java | 76 ++-- .../junit/testparameterinjector/TestInfo.java | 93 ++--- .../TestMethodProcessorList.java | 74 ++-- .../junit/testparameterinjector/TestParameter.java | 16 +- .../TestParameterAnnotationMethodProcessor.java | 387 +++++++++++---------- .../TestParametersMethodProcessor.java | 87 +++-- .../PluggableTestRunnerTest.java | 9 +- .../SharedTestUtilitiesJUnit4.java | 22 +- .../junit5/BaseTestParameterValidator.java | 15 +- .../junit5/ParameterValueParsing.java | 48 +-- .../testparameterinjector/junit5/TestInfo.java | 93 ++--- .../junit5/TestMethodProcessorList.java | 74 ++-- .../junit5/TestParameter.java | 16 +- .../TestParameterAnnotationMethodProcessor.java | 387 +++++++++++---------- .../junit5/TestParametersMethodProcessor.java | 87 +++-- 17 files changed, 795 insertions(+), 752 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java index ab5003e..6c23efa 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java @@ -18,8 +18,8 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static java.lang.Math.min; +import com.google.common.collect.FluentIterable; import java.lang.annotation.Annotation; -import java.util.Comparator; import java.util.List; /** @@ -45,10 +45,17 @@ abstract class BaseTestParameterValidator implements TestParameterValidator { // 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 leadingParameter = - parameters.stream() - .max(Comparator.comparing(parameter -> context.getSpecifiedValues(parameter).size())) - .get(); + FluentIterable.from(parameters) + .toSortedList( + (o1, o2) -> + Integer.compare( + context.getSpecifiedValues(o1).size(), + context.getSpecifiedValues(o2).size())) + .reverse() + .get(0); + // Second, determine which index is the current value in the specified value list of // the leading parameter. In the example above, the index of the current value 'c2' of the // leading parameter 'C' would be '1', given the specified values (c1, c2, c3). diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java index 604b899..d40454d 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java @@ -17,9 +17,8 @@ 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 static java.util.stream.Collectors.toMap; +import com.google.common.base.Function; import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; import com.google.common.reflect.TypeToken; @@ -27,10 +26,11 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.protobuf.ByteString; import com.google.protobuf.MessageLite; import java.lang.reflect.ParameterizedType; -import java.nio.charset.StandardCharsets; +import java.nio.charset.Charset; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.function.Function; +import java.util.Map.Entry; import javax.annotation.Nullable; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; @@ -64,7 +64,7 @@ final class ParameterValueParsing { return new Yaml(new SafeConstructor()).load(yamlString); } - @SuppressWarnings("unchecked") + @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) { @@ -76,7 +76,7 @@ final class ParameterValueParsing { yamlValueTransformer .ifJavaType(String.class) - .supportParsedType(String.class, identity()) + .supportParsedType(String.class, self -> self) // Also support other primitives because it's easy to accidentally write e.g. a number when // a string was intended in YAML .supportParsedType(Boolean.class, Object::toString) @@ -84,25 +84,25 @@ final class ParameterValueParsing { .supportParsedType(Long.class, Object::toString) .supportParsedType(Double.class, Object::toString); - yamlValueTransformer.ifJavaType(Boolean.class).supportParsedType(Boolean.class, identity()); + yamlValueTransformer.ifJavaType(Boolean.class).supportParsedType(Boolean.class, self -> self); - yamlValueTransformer.ifJavaType(Integer.class).supportParsedType(Integer.class, identity()); + yamlValueTransformer.ifJavaType(Integer.class).supportParsedType(Integer.class, self -> self); yamlValueTransformer .ifJavaType(Long.class) - .supportParsedType(Long.class, identity()) + .supportParsedType(Long.class, self -> self) .supportParsedType(Integer.class, Integer::longValue); yamlValueTransformer .ifJavaType(Float.class) - .supportParsedType(Float.class, identity()) + .supportParsedType(Float.class, self -> self) .supportParsedType(Double.class, Double::floatValue) .supportParsedType(Integer.class, Integer::floatValue) .supportParsedType(String.class, Float::valueOf); yamlValueTransformer .ifJavaType(Double.class) - .supportParsedType(Double.class, identity()) + .supportParsedType(Double.class, self -> self) .supportParsedType(Integer.class, Integer::doubleValue) .supportParsedType(Long.class, Long::doubleValue) .supportParsedType(String.class, Double::valueOf); @@ -123,8 +123,11 @@ final class ParameterValueParsing { yamlValueTransformer .ifJavaType(byte[].class) - .supportParsedType(byte[].class, identity()) - .supportParsedType(String.class, s -> s.getBytes(StandardCharsets.UTF_8)); + .supportParsedType(byte[].class, self -> self) + // Uses String based charset because StandardCharsets was not introduced until later + // versions of Android + // See https://developer.android.com/reference/java/nio/charset/StandardCharsets. + .supportParsedType(String.class, s -> s.getBytes(Charset.forName("UTF-8"))); yamlValueTransformer .ifJavaType(ByteString.class) @@ -150,16 +153,15 @@ final class ParameterValueParsing { } private static Map parseYamlMapToJavaMap(Map map, TypeToken javaType) { - return map.entrySet().stream() - .collect( - toMap( - entry -> - parseYamlObjectToJavaType( - entry.getKey(), getGenericParameterType(javaType, /* parameterIndex= */ 0)), - entry -> - parseYamlObjectToJavaType( - entry.getValue(), - getGenericParameterType(javaType, /* parameterIndex= */ 1)))); + Map returnedMap = new LinkedHashMap<>(); + for (Entry entry : map.entrySet()) { + returnedMap.put( + parseYamlObjectToJavaType( + entry.getKey(), getGenericParameterType(javaType, /* parameterIndex= */ 0)), + parseYamlObjectToJavaType( + entry.getValue(), getGenericParameterType(javaType, /* parameterIndex= */ 1))); + } + return returnedMap; } private static TypeToken getGenericParameterType(TypeToken typeToken, int parameterIndex) { diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java index 21e3ea6..10cd52a 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java @@ -14,10 +14,10 @@ package com.google.testing.junit.testparameterinjector; -import static java.util.Comparator.comparing; -import static java.util.stream.Collectors.joining; - +import com.google.common.base.Joiner; import com.google.common.base.Throwables; +import com.google.common.collect.ComparisonChain; +import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.Lists; @@ -25,10 +25,9 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.stream.Collector; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.junit.Rule; import org.junit.Test; import org.junit.internal.runners.model.ReflectiveCallable; @@ -96,14 +95,17 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { *

          This should be deterministic. The order should not change, even when tests are added/removed * or between releases. */ - protected Stream sortTestMethods(Stream methods) { + protected ImmutableList sortTestMethods(ImmutableList methods) { if (!shouldSortTestMethodsDeterministically()) { return methods; } - - return methods.sorted( - comparing((FrameworkMethod method) -> method.getName().hashCode()) - .thenComparing(FrameworkMethod::getName)); + return FluentIterable.from(methods) + .toSortedList( + (o1, o2) -> + ComparisonChain.start() + .compare(o1.getName().hashCode(), o2.getName().hashCode()) + .compare(o1.getName(), o2.getName()) + .result()); } /** @@ -125,14 +127,11 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { @Override protected final ImmutableList computeTestMethods() { - Stream processedMethods = - getSupportedTestAnnotations().stream() - .flatMap(annotation -> getTestClass().getAnnotatedMethods(annotation).stream()) - .flatMap(method -> processMethod(method).stream()); - - processedMethods = sortTestMethods(processedMethods); - - return processedMethods.collect(toImmutableList()); + return sortTestMethods( + FluentIterable.from(getSupportedTestAnnotations()) + .transformAndConcat(annotation -> getTestClass().getAnnotatedMethods(annotation)) + .transformAndConcat(this::processMethod) + .toList()); } /** Implementation of a JUnit FrameworkMethod where the name and annotation list is overridden. */ @@ -182,11 +181,13 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { } private ImmutableList processMethod(FrameworkMethod initialMethod) { - return getTestMethodProcessors() - .calculateTestInfos(initialMethod.getMethod(), getTestClass().getJavaClass()) - .stream() - .map(testInfo -> new OverriddenFrameworkMethod(testInfo.getMethod(), testInfo)) - .collect(toImmutableList()); + return FluentIterable.from( + getTestMethodProcessors() + .calculateTestInfos(initialMethod.getMethod(), getTestClass().getJavaClass())) + .transform( + testInfo -> + (FrameworkMethod) new OverriddenFrameworkMethod(testInfo.getMethod(), testInfo)) + .toList(); } // Note: This is a copy of the parent implementation, except that instead of calling @@ -257,15 +258,16 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { testClass.collectAnnotatedMethodValues(target, Rule.class, TestRule.class, collector::accept); testClass.collectAnnotatedFieldValues(target, Rule.class, TestRule.class, collector::accept); + ArrayList keys = new ArrayList<>(orderToRulesMultimap.keySet()); + Collections.sort(keys); ImmutableList orderedRules = - orderToRulesMultimap.keySet().stream() - .sorted() - .flatMap( + FluentIterable.from(keys) + .transformAndConcat( // Execute the rules in the reverse order of when the fields occurred. This may look // counter-intuitive, but that is what the default JUnit4 runner does, and there is // no reason to deviate from that here. - key -> Lists.reverse(orderToRulesMultimap.get(key)).stream()) - .collect(toImmutableList()); + key -> Lists.reverse(orderToRulesMultimap.get(key))) + .toList(); // Note: The perceived order* is the reverse of the order in which the code below applies the // rules to the statements because each subsequent rule wraps the previous statement. @@ -334,9 +336,9 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { @Override protected final void validateTestMethods(List errorsReturned) { List testMethods = - getSupportedTestAnnotations().stream() - .flatMap(annotation -> getTestClass().getAnnotatedMethods(annotation).stream()) - .collect(toImmutableList()); + FluentIterable.from(getSupportedTestAnnotations()) + .transformAndConcat(annotation -> getTestClass().getAnnotatedMethods(annotation)) + .toList(); for (FrameworkMethod testMethod : testMethods) { ExecutableValidationResult validationResult = getTestMethodProcessors() @@ -371,9 +373,9 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { 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 - ")))); + FluentIterable.from(errors) + .transform(Throwables::getStackTraceAsString) + .join(Joiner.on("\n\n\n - ")))); } } @@ -414,8 +416,4 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { }; } } - - private static Collector> toImmutableList() { - return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf); - } } diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java index daefeb9..965d41a 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java @@ -16,22 +16,21 @@ 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.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toSet; import com.google.auto.value.AutoValue; +import com.google.common.base.Joiner; +import com.google.common.collect.ContiguousSet; +import com.google.common.collect.DiscreteDomain; +import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.Multimap; import com.google.common.collect.MultimapBuilder; +import com.google.common.collect.Range; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.Collection; import java.util.List; import java.util.Set; -import 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). */ @@ -64,9 +63,9 @@ abstract class TestInfo { return String.format( "%s[%s]", getMethod().getName(), - getParameters().stream() - .map(TestInfoParameter::getValueInTestName) - .collect(joining(","))); + FluentIterable.from(getParameters()) + .transform(TestInfoParameter::getValueInTestName) + .join(Joiner.on(","))); } } @@ -108,18 +107,20 @@ abstract class TestInfo { * #getParameters()} list to the new name. */ private TestInfo withUpdatedParameterNames( - BiFunction parameterWithIndexToNewName) { + Java8BiFunction parameterWithIndexToNewName) { return new AutoValue_TestInfo( getMethod(), getTestClass(), - IntStream.range(0, getParameters().size()) - .mapToObj( + FluentIterable.from( + ContiguousSet.create( + Range.closedOpen(0, getParameters().size()), DiscreteDomain.integers())) + .transform( parameterIndex -> { TestInfoParameter parameter = getParameters().get(parameterIndex); return parameter.withValueInTestName( parameterWithIndexToNewName.apply(parameter, parameterIndex)); }) - .collect(toImmutableList()), + .toList(), getAnnotations()); } @@ -136,17 +137,20 @@ abstract class TestInfo { } static ImmutableList shortenNamesIfNecessary(List testInfos) { - if (testInfos.stream().anyMatch(info -> info.getName().length() > MAX_TEST_NAME_LENGTH)) { + if (FluentIterable.from(testInfos) + .anyMatch(info -> info.getName().length() > MAX_TEST_NAME_LENGTH)) { int numberOfParameters = testInfos.get(0).getParameters().size(); if (numberOfParameters == 0) { return ImmutableList.copyOf(testInfos); } else { Set parameterIndicesThatNeedUpdate = - IntStream.range(0, numberOfParameters) + FluentIterable.from( + ContiguousSet.create( + Range.closedOpen(0, numberOfParameters), DiscreteDomain.integers())) .filter( parameterIndex -> - testInfos.stream() + FluentIterable.from(testInfos) .anyMatch( info -> info.getParameters() @@ -154,11 +158,10 @@ abstract class TestInfo { .getValueInTestName() .length() > getMaxCharactersPerParameter(info, numberOfParameters))) - .boxed() - .collect(toSet()); + .toSet(); - return testInfos.stream() - .map( + return FluentIterable.from(testInfos) + .transform( info -> info.withUpdatedParameterNames( (parameter, parameterIndex) -> @@ -167,7 +170,7 @@ abstract class TestInfo { parameter, getMaxCharactersPerParameter(info, numberOfParameters)) : info.getParameters().get(parameterIndex).getValueInTestName())) - .collect(toImmutableList()); + .toList(); } } else { return ImmutableList.copyOf(testInfos); @@ -184,7 +187,8 @@ abstract class TestInfo { } static ImmutableList deduplicateTestNames(List testInfos) { - long uniqueTestNameCount = testInfos.stream().map(TestInfo::getName).distinct().count(); + long uniqueTestNameCount = + FluentIterable.from(testInfos).transform(TestInfo::getName).toSet().size(); if (testInfos.size() == uniqueTestNameCount) { // Return early if there are no duplicates return ImmutableList.copyOf(testInfos); @@ -214,37 +218,38 @@ abstract class TestInfo { testNameToInfo.put(testInfo.getName(), testInfo); } - return testNameToInfo.keySet().stream() - .flatMap( + return FluentIterable.from(testNameToInfo.keySet()) + .transformAndConcat( testName -> { Collection matchedInfos = testNameToInfo.get(testName); if (matchedInfos.size() == 1) { // There was only one method with this name, so no deduplication is necessary - return matchedInfos.stream(); + return matchedInfos; } else { // Found tests with duplicate test names int numParameters = matchedInfos.iterator().next().getParameters().size(); Set indicesThatShouldGetSuffix = // Find parameter indices for which a suffix would allow the reader to // differentiate - IntStream.range(0, numParameters) + FluentIterable.from( + ContiguousSet.create( + Range.closedOpen(0, numParameters), DiscreteDomain.integers())) .filter( parameterIndex -> - matchedInfos.stream() - .map( + FluentIterable.from(matchedInfos) + .transform( info -> getTypeSuffix( info.getParameters() .get(parameterIndex) .getValue())) - .distinct() - .count() + .toSet() + .size() > 1) - .boxed() - .collect(toSet()); + .toSet(); - return matchedInfos.stream() - .map( + return FluentIterable.from(matchedInfos) + .transform( testInfo -> testInfo.withUpdatedParameterNames( (parameter, parameterIndex) -> @@ -254,7 +259,7 @@ abstract class TestInfo { : parameter.getValueInTestName())); } }) - .collect(toImmutableList()); + .toList(); } private static String getTypeSuffix(@Nullable Object value) { @@ -267,14 +272,15 @@ abstract class TestInfo { private static ImmutableList deduplicateWithNumberPrefixes( ImmutableList testInfos) { - long uniqueTestNameCount = testInfos.stream().map(TestInfo::getName).distinct().count(); + long uniqueTestNameCount = + FluentIterable.from(testInfos).transform(TestInfo::getName).toSet().size(); if (testInfos.size() == uniqueTestNameCount) { return ImmutableList.copyOf(testInfos); } else { // There are still duplicates, even after adding type suffixes. As a last resort: add a // counter to all parameters to guarantee that each case is unique. - return testInfos.stream() - .map( + return FluentIterable.from(testInfos) + .transform( testInfo -> testInfo.withUpdatedParameterNames( (parameter, parameterIndex) -> @@ -282,14 +288,10 @@ abstract class TestInfo { "%s.%s", parameter.getIndexInValueSource() + 1, parameter.getValueInTestName()))) - .collect(toImmutableList()); + .toList(); } } - private static Collector> toImmutableList() { - return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf); - } - @AutoValue abstract static class TestInfoParameter { @@ -315,4 +317,9 @@ abstract class TestInfo { checkNotNull(valueInTestName), value, indexInValueSource); } } + + /** Copy of Java8's java.util.BiFunction which is not available in older versions of the JDK */ + interface Java8BiFunction { + K apply(I a, J b); + } } diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java index 867d994..2caf531 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java @@ -14,12 +14,12 @@ package com.google.testing.junit.testparameterinjector; -import static java.util.stream.Collectors.toList; - import com.google.common.base.Optional; +import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.List; /** @@ -67,11 +67,11 @@ final class TestMethodProcessorList { testMethod, testClass, ImmutableList.copyOf(testMethod.getAnnotations()))); for (final TestMethodProcessor testMethodProcessor : testMethodProcessors) { - testInfos = - testInfos.stream() - .flatMap( - lastTestInfo -> testMethodProcessor.calculateTestInfos(lastTestInfo).stream()) - .collect(toList()); + List list = new ArrayList<>(); + for (TestInfo lastTestInfo : testInfos) { + list.addAll(testMethodProcessor.calculateTestInfos(lastTestInfo)); + } + testInfos = list; } testInfos = TestInfo.deduplicateTestNames(TestInfo.shortenNamesIfNecessary(testInfos)); @@ -85,17 +85,18 @@ final class TestMethodProcessorList { *

          This method is never called for a parameterless constructor. */ public List getConstructorParameters(Constructor constructor, TestInfo testInfo) { - return testMethodProcessors.stream() - .map(processor -> processor.maybeGetConstructorParameters(constructor, testInfo)) + return FluentIterable.from(testMethodProcessors) + .transform(processor -> processor.maybeGetConstructorParameters(constructor, testInfo)) .filter(Optional::isPresent) - .map(Optional::get) - .findFirst() - .orElseThrow( - () -> - new IllegalStateException( - String.format( - "Could not generate parameter values for %s. Did you forget an annotation?", - constructor))); + .transform(Optional::get) + .first() + .or( + () -> { + throw new IllegalStateException( + String.format( + "Could not generate parameter values for %s. Did you forget an annotation?", + constructor)); + }); } /** @@ -104,17 +105,18 @@ final class TestMethodProcessorList { *

          This method is never called for a parameterless {@code testInfo.getMethod()}. */ public List getTestMethodParameters(TestInfo testInfo) { - return testMethodProcessors.stream() - .map(processor -> processor.maybeGetTestMethodParameters(testInfo)) + return FluentIterable.from(testMethodProcessors) + .transform(processor -> processor.maybeGetTestMethodParameters(testInfo)) .filter(Optional::isPresent) - .map(Optional::get) - .findFirst() - .orElseThrow( - () -> - new IllegalStateException( - String.format( - "Could not generate parameter values for %s. Did you forget an annotation?", - testInfo.getMethod()))); + .transform(Optional::get) + .first() + .or( + () -> { + throw new IllegalStateException( + String.format( + "Could not generate parameter values for %s. Did you forget an annotation?", + testInfo.getMethod())); + }); } /** @@ -128,19 +130,17 @@ final class TestMethodProcessorList { /** Optionally validates the given constructor. */ public ExecutableValidationResult validateConstructor(Constructor constructor) { - return testMethodProcessors.stream() - .map(processor -> processor.validateConstructor(constructor)) - .filter(ExecutableValidationResult::wasValidated) - .findFirst() - .orElse(ExecutableValidationResult.notValidated()); + return FluentIterable.from(testMethodProcessors) + .transform(processor -> processor.validateConstructor(constructor)) + .firstMatch(ExecutableValidationResult::wasValidated) + .or(ExecutableValidationResult.notValidated()); } /** Optionally validates the given method. */ public ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass) { - return testMethodProcessors.stream() - .map(processor -> processor.validateTestMethod(testMethod, testClass)) - .filter(ExecutableValidationResult::wasValidated) - .findFirst() - .orElse(ExecutableValidationResult.notValidated()); + return FluentIterable.from(testMethodProcessors) + .transform(processor -> processor.validateTestMethod(testMethod, testClass)) + .firstMatch(ExecutableValidationResult::wasValidated) + .or(ExecutableValidationResult.notValidated()); } } diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java index 4c03903..e08c7b8 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java @@ -18,11 +18,11 @@ 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.base.Optional; +import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; import com.google.protobuf.MessageLite; import com.google.testing.junit.testparameterinjector.TestParameter.InternalImplementationOfThisParameter; @@ -32,6 +32,7 @@ import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** @@ -146,16 +147,17 @@ public @interface TestParameter { annotation); if (valueIsSet) { - return stream(annotation.value()) - .map(v -> parseStringValue(v, parameterClass)) - .collect(toList()); + return Lists.newArrayList( + FluentIterable.from(annotation.value()) + .transform(v -> parseStringValue(v, parameterClass)) + .toArray(Object.class)); } else if (valuesProviderIsSet) { return getValuesFromProvider(annotation.valuesProvider()); } else { if (Enum.class.isAssignableFrom(parameterClass)) { - return ImmutableList.copyOf(parameterClass.asSubclass(Enum.class).getEnumConstants()); + return Arrays.asList((Object[]) parameterClass.asSubclass(Enum.class).getEnumConstants()); } else if (Primitives.wrap(parameterClass).equals(Boolean.class)) { - return ImmutableList.of(false, true); + return Arrays.asList(false, true); } else { throw new IllegalStateException( String.format( diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 4f6c052..18be553 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -18,9 +18,6 @@ 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; @@ -30,14 +27,17 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; +import com.google.common.collect.ContiguousSet; +import com.google.common.collect.DiscreteDomain; +import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; +import com.google.common.collect.Range; import com.google.common.primitives.Primitives; import com.google.common.util.concurrent.UncheckedExecutionException; import com.google.protobuf.ByteString; import com.google.testing.junit.testparameterinjector.TestInfo.TestInfoParameter; -import com.google.testing.junit.testparameterinjector.TestParameterAnnotationMethodProcessor.TestParameterValue; import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; @@ -52,15 +52,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.ExecutionException; -import 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; /** @@ -165,18 +161,21 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso !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( + return FluentIterable.from( + ContiguousSet.create( + Range.closedOpen(0, specifiedValues.size()), DiscreteDomain.integers())) + .transform( valueIndex -> - new AutoValue_TestParameterAnnotationMethodProcessor_TestParameterValue( - AnnotationTypeOrigin.create( - annotationWithMetadata.annotation().annotationType(), origin), - specifiedValues.get(valueIndex), - valueIndex, - new ArrayList<>(specifiedValues), - annotationWithMetadata.paramClass(), - annotationWithMetadata.paramName())) - .collect(toImmutableList()); + (TestParameterValue) + new AutoValue_TestParameterAnnotationMethodProcessor_TestParameterValue( + AnnotationTypeOrigin.create( + annotationWithMetadata.annotation().annotationType(), origin), + specifiedValues.get(valueIndex), + valueIndex, + new ArrayList<>(specifiedValues), + annotationWithMetadata.paramClass(), + annotationWithMetadata.paramName())) + .toList(); } } /** @@ -189,13 +188,18 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return annotationType -> Optional.absent(); } else { return annotationType -> - Optional.fromNullable( - new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ false) - .getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()).stream() - .filter(matches(annotationType)) - .map(TestParameterValue::value) - .findFirst() - .orElse(null)); + FluentIterable.from( + new TestParameterAnnotationMethodProcessor( + /* onlyForFieldsAndParameters= */ false) + .getParameterValuesForTest(testIndexHolder, testInfo.getTestClass())) + .filter( + testParameterValue -> + testParameterValue + .annotationTypeOrigin() + .annotationType() + .equals(annotationType)) + .transform(TestParameterValue::value) + .first(); } } @@ -225,11 +229,6 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } } - private static Predicate matches(Class annotationType) { - return testParameterValue -> - testParameterValue.annotationTypeOrigin().annotationType().equals(annotationType); - } - /** The origin of an annotation type. */ enum Origin { CLASS, @@ -347,33 +346,41 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso // @TestParameterAnnotation annotation. List fieldAnnotations = extractTestParameterAnnotations( - streamWithParents(testClass) - .flatMap(c -> stream(c.getDeclaredFields())) - .flatMap(field -> stream(field.getAnnotations())), + FluentIterable.from(listWithParents(testClass)) + .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields())) + .transformAndConcat(field -> Arrays.asList(field.getAnnotations())) + .toList(), Origin.FIELD); List methodAnnotations = extractTestParameterAnnotations( - stream(testClass.getMethods()).flatMap(method -> stream(method.getAnnotations())), + FluentIterable.from(testClass.getMethods()) + .transformAndConcat(method -> Arrays.asList(method.getAnnotations())) + .toList(), Origin.METHOD); List parameterAnnotations = extractTestParameterAnnotations( - streamWithParents(testClass) - .flatMap(c -> stream(c.getDeclaredMethods())) - .flatMap(method -> stream(method.getParameterAnnotations()).flatMap(Stream::of)), + FluentIterable.from(listWithParents(testClass)) + .transformAndConcat(c -> Arrays.asList(c.getDeclaredMethods())) + .transformAndConcat(method -> Arrays.asList(method.getParameterAnnotations())) + .transformAndConcat(Arrays::asList) + .toList(), Origin.METHOD_PARAMETER); List classAnnotations = - extractTestParameterAnnotations(stream(testClass.getAnnotations()), Origin.CLASS); + extractTestParameterAnnotations(Arrays.asList(testClass.getAnnotations()), Origin.CLASS); List constructorAnnotations = extractTestParameterAnnotations( - stream(testClass.getDeclaredConstructors()) - .flatMap(constructor -> stream(constructor.getAnnotations())), + FluentIterable.from(testClass.getDeclaredConstructors()) + .transformAndConcat(constructor -> Arrays.asList(constructor.getAnnotations())) + .toList(), Origin.CONSTRUCTOR); List constructorParameterAnnotations = extractTestParameterAnnotations( - stream(testClass.getDeclaredConstructors()) - .flatMap( + FluentIterable.from(testClass.getDeclaredConstructors()) + .transformAndConcat( constructor -> - stream(constructor.getParameterAnnotations()).flatMap(Stream::of)), + FluentIterable.from(Arrays.asList(constructor.getParameterAnnotations())) + .transformAndConcat(Arrays::asList)) + .toList(), Origin.CONSTRUCTOR_PARAMETER); checkDuplicatedClassAndFieldAnnotations( @@ -382,11 +389,11 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso checkDuplicatedFieldsAnnotations(methodAnnotations, fieldAnnotations); checkState( - constructorAnnotations.stream().distinct().count() == constructorAnnotations.size(), + FluentIterable.from(constructorAnnotations).toSet().size() == constructorAnnotations.size(), "Annotations should not be duplicated on the constructor."); checkState( - classAnnotations.stream().distinct().count() == classAnnotations.size(), + FluentIterable.from(classAnnotations).toSet().size() == classAnnotations.size(), "Annotations should not be duplicated on the class."); if (onlyForFieldsAndParameters) { @@ -410,18 +417,16 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso constructorAnnotations); } - return 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()); + // The order matters, since it will determine which annotation processor is + // called first. + return FluentIterable.from(classAnnotations) + .append(fieldAnnotations) + .append(constructorAnnotations) + .append(constructorParameterAnnotations) + .append(methodAnnotations) + .append(parameterAnnotations) + .toSet() + .asList(); } private ImmutableList getAnnotationTypeOrigins( @@ -429,9 +434,9 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso Set originsToFilterBy = ImmutableSet.builder().add(firstOrigin).add(otherOrigins).build(); try { - return annotationTypeOriginsCache.getUnchecked(testClass).stream() + return FluentIterable.from(annotationTypeOriginsCache.getUnchecked(testClass)) .filter(annotationTypeOrigin -> originsToFilterBy.contains(annotationTypeOrigin.origin())) - .collect(toImmutableList()); + .toList(); } catch (UncheckedExecutionException e) { Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); throw e; @@ -442,14 +447,17 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso List methodAnnotations, List 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()) { + if (FluentIterable.from(fieldAnnotations).toSet().size() != fieldAnnotations.size()) { List> methodOrFieldAnnotations = - Stream.concat(methodAnnotations.stream(), fieldAnnotations.stream().distinct()) - .map(AnnotationTypeOrigin::annotationType) - .collect(toCollection(ArrayList::new)); + new ArrayList<>( + FluentIterable.from(methodAnnotations) + .append(new HashSet<>(fieldAnnotations)) + .transform(AnnotationTypeOrigin::annotationType) + .toList()); checkState( - methodOrFieldAnnotations.stream().distinct().count() == methodOrFieldAnnotations.size(), + FluentIterable.from(methodOrFieldAnnotations).toSet().size() + == methodOrFieldAnnotations.size(), "Annotations should not be duplicated on a method and field" + " if they are present on multiple fields"); } @@ -460,18 +468,18 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso List classAnnotations, List fieldAnnotations) { ImmutableSet> classAnnotationTypes = - classAnnotations.stream() - .map(AnnotationTypeOrigin::annotationType) - .collect(toImmutableSet()); - - ImmutableSet> uniqueFieldAnnotations = - fieldAnnotations.stream() - .map(AnnotationTypeOrigin::annotationType) - .collect(toImmutableSet()); - ImmutableSet> uniqueConstructorAnnotations = - constructorAnnotations.stream() - .map(AnnotationTypeOrigin::annotationType) - .collect(toImmutableSet()); + FluentIterable.from(classAnnotations) + .transform(AnnotationTypeOrigin::annotationType) + .toSet(); + + ImmutableSet> uniqueFieldAnnotations = + FluentIterable.from(fieldAnnotations) + .transform(AnnotationTypeOrigin::annotationType) + .toSet(); + ImmutableSet> uniqueConstructorAnnotations = + FluentIterable.from(constructorAnnotations) + .transform(AnnotationTypeOrigin::annotationType) + .toSet(); checkState( Collections.disjoint(classAnnotationTypes, uniqueFieldAnnotations), @@ -486,14 +494,15 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso "Annotations should not be duplicated on a field and constructor"); } - /** Returns a list of annotation types that are a {@link TestParameterAnnotation}. */ private List extractTestParameterAnnotations( - Stream annotations, Origin origin) { - return annotations - .map(Annotation::annotationType) - .filter(annotationType -> annotationType.isAnnotationPresent(TestParameterAnnotation.class)) - .map(annotationType -> AnnotationTypeOrigin.create(annotationType, origin)) - .collect(toCollection(ArrayList::new)); + List annotations, Origin origin) { + return new ArrayList<>( + FluentIterable.from(annotations) + .transform(Annotation::annotationType) + .filter( + annotationType -> annotationType.isAnnotationPresent(TestParameterAnnotation.class)) + .transform(annotationType -> AnnotationTypeOrigin.create(annotationType, origin)) + .toList()); } @Override @@ -579,7 +588,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso // 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> testParameterAnnotationTypes = + ImmutableList> testParameterAnnotationTypes = getTestParameterAnnotations( // Do not include METHOD_PARAMETER or CONSTRUCTOR_PARAMETER since they have already // been evaluated. @@ -740,12 +749,12 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso testInfos.add( originalTest .withExtraParameters( - testParameterValues.stream() - .map( + FluentIterable.from(testParameterValues) + .transform( param -> TestInfoParameter.create( param.toTestNameString(), param.value(), param.valueIndex())) - .collect(toImmutableList())) + .toList()) .withExtraAnnotation( TestIndexHolderFactory.create( /* methodIndex= */ strictIndexOf( @@ -767,18 +776,19 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso List> testParameterValuesList = getAnnotationValuesForUsedAnnotationTypes(method, testClass); - return Lists.cartesianProduct(testParameterValuesList).stream() + return FluentIterable.from(Lists.cartesianProduct(testParameterValuesList)) .filter( // Skip tests based on the annotations' {@link Validator#shouldSkip} return // value. testParameterValues -> - testParameterValues.stream() - .noneMatch( + FluentIterable.from(testParameterValues) + .filter( testParameterValue -> callShouldSkip( testParameterValue.annotationTypeOrigin().annotationType(), - testParameterValues))) - .collect(toImmutableList()); + testParameterValues)) + .isEmpty()) + .toList(); }); } catch (ExecutionException | UncheckedExecutionException e) { Throwables.throwIfUnchecked(e.getCause()); @@ -806,35 +816,38 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso private ImmutableList> getAnnotationValuesForUsedAnnotationTypes( Method method, Class testClass) { ImmutableList annotationTypes = - Stream.of( - getAnnotationTypeOrigins(testClass, Origin.CLASS).stream(), - getAnnotationTypeOrigins(testClass, Origin.FIELD).stream(), - getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR).stream(), - getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR_PARAMETER).stream(), - getAnnotationTypeOrigins(testClass, Origin.METHOD).stream(), - getAnnotationTypeOrigins(testClass, Origin.METHOD_PARAMETER).stream() - .sorted(annotationComparator(method.getParameterAnnotations()))) - .flatMap(x -> x) - .collect(toImmutableList()); - - return removeOverrides(annotationTypes, testClass, method).stream() - .map( + FluentIterable.from(getAnnotationTypeOrigins(testClass, Origin.CLASS)) + .append(getAnnotationTypeOrigins(testClass, Origin.FIELD)) + .append(getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR)) + .append(getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR_PARAMETER)) + .append(getAnnotationTypeOrigins(testClass, Origin.METHOD)) + .append( + ImmutableList.sortedCopyOf( + annotationComparator(method.getParameterAnnotations()), + getAnnotationTypeOrigins(testClass, Origin.METHOD_PARAMETER))) + .toList(); + + return FluentIterable.from(removeOverrides(annotationTypes, testClass, method)) + .transform( annotationTypeOrigin -> getAnnotationFromParametersOrTestOrClass(annotationTypeOrigin, method, testClass)) .filter(l -> !l.isEmpty()) - .flatMap(List::stream) - .collect(toImmutableList()); + .transformAndConcat(i -> i) + .toList(); } private Comparator annotationComparator( Annotation[][] parameterAnnotations) { ImmutableList annotationOrdering = - stream(parameterAnnotations) - .flatMap(Arrays::stream) - .map(Annotation::annotationType) - .map(Class::getName) - .collect(toImmutableList()); - return Comparator.comparingInt(o -> annotationOrdering.indexOf(o.annotationType().getName())); + FluentIterable.from(parameterAnnotations) + .transformAndConcat(Arrays::asList) + .transform(Annotation::annotationType) + .transform(Class::getName) + .toList(); + return (annotationTypeOrigin, t1) -> + Integer.compare( + annotationOrdering.indexOf(annotationTypeOrigin.annotationType().getName()), + annotationOrdering.indexOf(t1.annotationType().getName())); } /** @@ -847,43 +860,45 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso private List removeOverrides( List annotationTypeOrigins, Class testClass, Method method) { return removeOverrides( - annotationTypeOrigins.stream() + new ArrayList<>( + FluentIterable.from(annotationTypeOrigins) + .filter( + annotationTypeOrigin -> { + switch (annotationTypeOrigin.origin()) { + case FIELD: // Fall through. + case CLASS: + return getAnnotationListWithType( + method.getAnnotations(), annotationTypeOrigin.annotationType()) + .isEmpty(); + default: + return true; + } + }) + .toList()), + testClass); + } + + /** + * @see #removeOverrides(List, Class) + */ + private List removeOverrides( + List annotationTypeOrigins, Class testClass) { + return new ArrayList<>( + FluentIterable.from(annotationTypeOrigins) .filter( annotationTypeOrigin -> { switch (annotationTypeOrigin.origin()) { case FIELD: // Fall through. case CLASS: return getAnnotationListWithType( - method.getAnnotations(), annotationTypeOrigin.annotationType()) + getOnlyConstructor(testClass).getAnnotations(), + annotationTypeOrigin.annotationType()) .isEmpty(); default: return true; } }) - .collect(toCollection(ArrayList::new)), - testClass); - } - - /** - * @see #removeOverrides(List, Class) - */ - private List removeOverrides( - List 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(); - default: - return true; - } - }) - .collect(toCollection(ArrayList::new)); + .toList()); } /** @@ -928,16 +943,18 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } } else if (origin == Origin.FIELD) { List 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)); + new ArrayList<>( + FluentIterable.from(listWithParents(testClass)) + .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields())) + .transformAndConcat( + field -> + FluentIterable.from( + getAnnotationListWithType(field.getAnnotations(), annotationType)) + .transform( + annotation -> + AnnotationWithMetadata.withMetadata( + annotation, field.getType(), field.getName()))) + .toList()); if (!annotations.isEmpty()) { return toTestParameterValueList(annotations, origin); } @@ -953,9 +970,12 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso private static ImmutableList> toTestParameterValueList( List annotationWithMetadatas, Origin origin) { - return annotationWithMetadatas.stream() - .map(annotationWithMetadata -> TestParameterValue.create(annotationWithMetadata, origin)) - .collect(toImmutableList()); + return FluentIterable.from(annotationWithMetadatas) + .transform( + annotationWithMetadata -> + (List) + new ArrayList<>(TestParameterValue.create(annotationWithMetadata, origin))) + .toList(); } private static ImmutableList getAnnotationWithMetadataListWithType( @@ -984,8 +1004,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso @SuppressWarnings("AndroidJdkLibsChecker") private static ImmutableList getAnnotationWithMetadataListWithType( Parameter[] parameters, Class annotationType) { - return stream(parameters) - .map( + return FluentIterable.from(parameters) + .transform( parameter -> { Annotation annotation = parameter.getAnnotation(annotationType); return annotation == null @@ -996,7 +1016,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso : AnnotationWithMetadata.withMetadata(annotation, parameter.getType()); }) .filter(Objects::nonNull) - .collect(toImmutableList()); + .toList(); } private static ImmutableList getAnnotationWithMetadataListWithType( @@ -1018,9 +1038,9 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso private ImmutableList getAnnotationListWithType( Annotation[] annotations, Class annotationType) { - return stream(annotations) + return FluentIterable.from(annotations) .filter(annotation -> annotation.annotationType().equals(annotationType)) - .collect(toImmutableList()); + .toList(); } private static Constructor getOnlyConstructor(Class testClass) { @@ -1049,9 +1069,9 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso List remainingTestParameterValuesForFieldInjection = new ArrayList<>(testParameterValuesForFieldInjection); for (Field declaredField : - streamWithParents(testInstance.getClass()) - .flatMap(c -> stream(c.getDeclaredFields())) - .collect(toImmutableList())) { + FluentIterable.from(listWithParents(testInstance.getClass())) + .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields())) + .toList()) { for (TestParameterValue testParameterValue : remainingTestParameterValuesForFieldInjection) { if (declaredField.isAnnotationPresent( @@ -1076,11 +1096,11 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso private static ImmutableList filterByOrigin( List testParameterValues, Origin... origins) { Set originsToFilterBy = ImmutableSet.copyOf(origins); - return testParameterValues.stream() + return FluentIterable.from(testParameterValues) .filter( testParameterValue -> originsToFilterBy.contains(testParameterValue.annotationTypeOrigin().origin())) - .collect(toImmutableList()); + .toList(); } /** @@ -1090,9 +1110,9 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso private static ImmutableList filterAnnotationTypeOriginsByOrigin( List annotationTypeOrigins, Origin... origins) { List originList = Arrays.asList(origins); - return annotationTypeOrigins.stream() + return FluentIterable.from(annotationTypeOrigins) .filter(annotationTypeOrigin -> originList.contains(annotationTypeOrigin.origin())) - .collect(toImmutableList()); + .toList(); } /** Returns a {@link TestParameterAnnotation}'s value for a method or constructor parameter. */ @@ -1200,7 +1220,11 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso public ValidatorContext(List testParameterValues) { this.testParameterValues = testParameterValues; - this.valueList = testParameterValues.stream().map(TestParameterValue::value).collect(toSet()); + this.valueList = + FluentIterable.from(testParameterValues) + .transform(TestParameterValue::value) + .filter(Objects::nonNull) + .toSet(); } @Override @@ -1226,11 +1250,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } private Optional getParameter(Class testParameter) { - return Optional.fromNullable( - testParameterValues.stream() - .filter(value -> value.annotationTypeOrigin().annotationType().equals(testParameter)) - .findAny() - .orElse(null)); + return FluentIterable.from(testParameterValues) + .firstMatch(value -> value.annotationTypeOrigin().annotationType().equals(testParameter)); } } @@ -1258,17 +1279,17 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } /** Returns the TestParameterAnnotation annotation types defined for a method or constructor. */ - private ImmutableList> getTestParameterAnnotations( + private ImmutableList> getTestParameterAnnotations( List annotationTypeOrigins, final Class testClass, AnnotatedElement methodOrConstructor) { - return annotationTypeOrigins.stream() - .map(AnnotationTypeOrigin::annotationType) + return FluentIterable.from(annotationTypeOrigins) + .transform(AnnotationTypeOrigin::annotationType) .filter( annotationType -> testClass.isAnnotationPresent(annotationType) || methodOrConstructor.isAnnotationPresent(annotationType)) - .collect(toImmutableList()); + .toList(); } private int strictIndexOf(List haystack, T needle) { @@ -1286,8 +1307,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return resultBuilder.build(); } - private static Stream> streamWithParents(Class clazz) { - Stream.Builder> resultBuilder = Stream.builder(); + private static ImmutableList> listWithParents(Class clazz) { + ImmutableList.Builder> resultBuilder = ImmutableList.builder(); Class currentClass = clazz; while (currentClass != null) { @@ -1297,14 +1318,4 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return resultBuilder.build(); } - - // Immutable collectors are re-implemented here because they are missing from the Android - // collection library. - private static Collector> toImmutableList() { - return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf); - } - - private static Collector> toImmutableSet() { - return Collectors.collectingAndThen(Collectors.toList(), ImmutableSet::copyOf); - } } diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java index bffc3b4..1a8d022 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java @@ -17,8 +17,6 @@ package com.google.testing.junit.testparameterinjector; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Verify.verify; import static com.google.common.collect.Iterables.getOnlyElement; -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toList; import com.google.auto.value.AutoAnnotation; import com.google.common.base.Optional; @@ -26,6 +24,7 @@ import com.google.common.base.Throwables; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; +import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; @@ -47,10 +46,6 @@ import java.lang.reflect.Parameter; import java.util.Arrays; 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; /** {@code TestMethodProcessor} implementation for supporting {@link TestParameters}. */ @SuppressWarnings("AndroidJdkLibsChecker") // Parameter is not available on old Android SDKs. @@ -127,25 +122,22 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { 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())) + FluentIterable.of( + constructorParameters.transform( + param -> + TestInfoParameter.create( + param.name(), + param.parametersMap(), + constructorParametersIndexCopy)), + methodParameters.transform( + param -> + TestInfoParameter.create( + param.name(), + param.parametersMap(), + methodParametersIndexCopy))) + .filter(Optional::isPresent) + .transform(Optional::get) + .toList()) .withExtraAnnotation( TestIndexHolderFactory.create( constructorParametersIndex, methodParametersIndex))); @@ -158,16 +150,16 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { getConstructorParametersOrSingleAbsentElement(Class testClass) { Constructor constructor = getOnlyConstructor(testClass); return hasRelevantAnnotation(constructor) - ? getConstructorParameters(constructor).stream() - .map(Optional::of) - .collect(toImmutableList()) + ? FluentIterable.from(getConstructorParameters(constructor)) + .transform(Optional::of) + .toList() : ImmutableList.of(Optional.absent()); } private ImmutableList> getMethodParametersOrSingleAbsentElement( Method method) { return hasRelevantAnnotation(method) - ? getMethodParameters(method).stream().map(Optional::of).collect(toImmutableList()) + ? FluentIterable.from(getMethodParameters(method)).transform(Optional::of).toList() : ImmutableList.of(Optional.absent()); } @@ -261,9 +253,10 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { } if (valueIsSet) { - return stream(annotation.value()) - .map(yamlMap -> toParameterValues(yamlMap, parametersList, annotation.customName())) - .collect(toImmutableList()); + return FluentIterable.from(annotation.value()) + .transform( + yamlMap -> toParameterValues(yamlMap, parametersList, annotation.customName())) + .toList(); } else { return toParameterValuesList(annotation.valuesProvider(), parametersList); } @@ -273,14 +266,14 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { "This method should only be called for executables with at least one relevant" + " annotation"); - return stream(executable.getAnnotation(RepeatedTestParameters.class).value()) - .map( + return FluentIterable.from(executable.getAnnotation(RepeatedTestParameters.class).value()) + .transform( annotation -> toParameterValues( validateAndGetSingleValueFromRepeatedAnnotation(annotation, executable), parametersList, annotation.customName())) - .collect(toImmutableList()); + .toList(); } } @@ -290,9 +283,11 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { Constructor constructor = valuesProvider.getDeclaredConstructor(); constructor.setAccessible(true); - return constructor.newInstance().provideValues().stream() - .peek(values -> validateThatValuesMatchParameters(values, parameters)) - .collect(toImmutableList()); + List testParametersValues = constructor.newInstance().provideValues(); + for (TestParametersValues testParametersValue : testParametersValues) { + validateThatValuesMatchParameters(testParametersValue, parameters); + } + return ImmutableList.copyOf(testParametersValues); } catch (NoSuchMethodException e) { if (!Modifier.isStatic(valuesProvider.getModifiers()) && valuesProvider.isMemberClass()) { throw new IllegalStateException( @@ -314,7 +309,7 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { private static void checkParameterNamesArePresent(Executable executable) { checkState( - stream(executable.getParameters()).allMatch(Parameter::isNamePresent), + FluentIterable.from(executable.getParameters()).allMatch(Parameter::isNamePresent), "" + "No parameter name could be found for %s, which likely means that parameter names" + " aren't available at runtime. Please ensure that the this test was built with the" @@ -442,9 +437,11 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { private static List toParameterList( TestParametersValues parametersValues, Parameter[] parameters) { - return stream(parameters) - .map(parameter -> parametersValues.parametersMap().get(parameter.getName())) - .collect(toList()); + return Arrays.asList( + FluentIterable.from(Arrays.asList(parameters)) + .transform(Parameter::getName) + .transform(name -> parametersValues.parametersMap().get(name)) + .toArray(Object.class)); } private static Constructor getOnlyConstructor(Class testClass) { @@ -455,12 +452,6 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { return getOnlyElement(constructors); } - // Immutable collectors are re-implemented here because they are missing from the Android - // collection library. - private static Collector> 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. diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java index 6c9064a..f7afd79 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java @@ -15,15 +15,14 @@ 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.FluentIterable; import com.google.common.collect.ImmutableList; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; -import java.util.stream.Stream; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -203,8 +202,10 @@ public class PluggableTestRunnerTest { } @Override - protected Stream sortTestMethods(Stream methods) { - return methods.sorted(comparing(FrameworkMethod::getName).reversed()); + protected ImmutableList sortTestMethods( + ImmutableList methods) { + return FluentIterable.from(methods) + .toSortedList((o1, o2) -> o2.getName().compareTo(o1.getName())); // reversed } }); assertThat(testOrder).containsExactly("c", "b", "a"); diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java index f047e21..5dfe610 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java @@ -18,14 +18,15 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.truth.Truth.assertWithMessage; -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.joining; +import com.google.common.base.Joiner; import com.google.common.base.Throwables; +import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Map.Entry; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Rule; @@ -52,14 +53,14 @@ class SharedTestUtilitiesJUnit4 { throw new AssertionError( String.format( "Test failed unexpectedly:\n\n%s", - failures.stream() - .map( + FluentIterable.from(failures) + .transform( f -> String.format( "<<%s>> %s", f.getDescription(), Throwables.getStackTraceAsString(f.getException()))) - .collect(joining("\n------------------------------------\n")))); + .join(Joiner.on("\n------------------------------------\n")))); } } @@ -88,9 +89,11 @@ class SharedTestUtilitiesJUnit4 { StringBuilder resultBuilder = new StringBuilder(); resultBuilder.append("\n----------------------\n"); resultBuilder.append("ImmutableMap.builder()\n"); - map.forEach( - (key, value) -> - resultBuilder.append(String.format(" .put(\"%s\", \"%s\")\n", key, value))); + for (Entry entry : map.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + resultBuilder.append(String.format(" .put(\"%s\", \"%s\")\n", key, value)); + } resultBuilder.append(" .build()\n"); resultBuilder.append("----------------------\n"); return resultBuilder.toString(); @@ -125,7 +128,8 @@ class SharedTestUtilitiesJUnit4 { + " are duplicate test names", testName.getMethodName()); testNameToStringifiedParameters.put( - testName.getMethodName(), stream(params).map(String::valueOf).collect(joining(":"))); + testName.getMethodName(), + FluentIterable.from(params).transform(String::valueOf).join(Joiner.on(":"))); } abstract ImmutableMap expectedTestNameToStringifiedParameters(); diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/BaseTestParameterValidator.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/BaseTestParameterValidator.java index 692a713..2386278 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/BaseTestParameterValidator.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/BaseTestParameterValidator.java @@ -18,8 +18,8 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static java.lang.Math.min; +import com.google.common.collect.FluentIterable; import java.lang.annotation.Annotation; -import java.util.Comparator; import java.util.List; /** @@ -45,10 +45,17 @@ abstract class BaseTestParameterValidator implements TestParameterValidator { // 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 leadingParameter = - parameters.stream() - .max(Comparator.comparing(parameter -> context.getSpecifiedValues(parameter).size())) - .get(); + FluentIterable.from(parameters) + .toSortedList( + (o1, o2) -> + Integer.compare( + context.getSpecifiedValues(o1).size(), + context.getSpecifiedValues(o2).size())) + .reverse() + .get(0); + // Second, determine which index is the current value in the specified value list of // the leading parameter. In the example above, the index of the current value 'c2' of the // leading parameter 'C' would be '1', given the specified values (c1, c2, c3). diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java index 2794e70..531ee1e 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java @@ -17,9 +17,8 @@ package com.google.testing.junit.testparameterinjector.junit5; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.toMap; +import com.google.common.base.Function; import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; import com.google.common.reflect.TypeToken; @@ -27,10 +26,11 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.protobuf.ByteString; import com.google.protobuf.MessageLite; import java.lang.reflect.ParameterizedType; -import java.nio.charset.StandardCharsets; +import java.nio.charset.Charset; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.function.Function; +import java.util.Map.Entry; import javax.annotation.Nullable; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; @@ -64,7 +64,7 @@ final class ParameterValueParsing { return new Yaml(new SafeConstructor()).load(yamlString); } - @SuppressWarnings("unchecked") + @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) { @@ -76,7 +76,7 @@ final class ParameterValueParsing { yamlValueTransformer .ifJavaType(String.class) - .supportParsedType(String.class, identity()) + .supportParsedType(String.class, self -> self) // Also support other primitives because it's easy to accidentally write e.g. a number when // a string was intended in YAML .supportParsedType(Boolean.class, Object::toString) @@ -84,25 +84,25 @@ final class ParameterValueParsing { .supportParsedType(Long.class, Object::toString) .supportParsedType(Double.class, Object::toString); - yamlValueTransformer.ifJavaType(Boolean.class).supportParsedType(Boolean.class, identity()); + yamlValueTransformer.ifJavaType(Boolean.class).supportParsedType(Boolean.class, self -> self); - yamlValueTransformer.ifJavaType(Integer.class).supportParsedType(Integer.class, identity()); + yamlValueTransformer.ifJavaType(Integer.class).supportParsedType(Integer.class, self -> self); yamlValueTransformer .ifJavaType(Long.class) - .supportParsedType(Long.class, identity()) + .supportParsedType(Long.class, self -> self) .supportParsedType(Integer.class, Integer::longValue); yamlValueTransformer .ifJavaType(Float.class) - .supportParsedType(Float.class, identity()) + .supportParsedType(Float.class, self -> self) .supportParsedType(Double.class, Double::floatValue) .supportParsedType(Integer.class, Integer::floatValue) .supportParsedType(String.class, Float::valueOf); yamlValueTransformer .ifJavaType(Double.class) - .supportParsedType(Double.class, identity()) + .supportParsedType(Double.class, self -> self) .supportParsedType(Integer.class, Integer::doubleValue) .supportParsedType(Long.class, Long::doubleValue) .supportParsedType(String.class, Double::valueOf); @@ -123,8 +123,11 @@ final class ParameterValueParsing { yamlValueTransformer .ifJavaType(byte[].class) - .supportParsedType(byte[].class, identity()) - .supportParsedType(String.class, s -> s.getBytes(StandardCharsets.UTF_8)); + .supportParsedType(byte[].class, self -> self) + // Uses String based charset because StandardCharsets was not introduced until later + // versions of Android + // See https://developer.android.com/reference/java/nio/charset/StandardCharsets. + .supportParsedType(String.class, s -> s.getBytes(Charset.forName("UTF-8"))); yamlValueTransformer .ifJavaType(ByteString.class) @@ -150,16 +153,15 @@ final class ParameterValueParsing { } private static Map parseYamlMapToJavaMap(Map map, TypeToken javaType) { - return map.entrySet().stream() - .collect( - toMap( - entry -> - parseYamlObjectToJavaType( - entry.getKey(), getGenericParameterType(javaType, /* parameterIndex= */ 0)), - entry -> - parseYamlObjectToJavaType( - entry.getValue(), - getGenericParameterType(javaType, /* parameterIndex= */ 1)))); + Map returnedMap = new LinkedHashMap<>(); + for (Entry entry : map.entrySet()) { + returnedMap.put( + parseYamlObjectToJavaType( + entry.getKey(), getGenericParameterType(javaType, /* parameterIndex= */ 0)), + parseYamlObjectToJavaType( + entry.getValue(), getGenericParameterType(javaType, /* parameterIndex= */ 1))); + } + return returnedMap; } private static TypeToken getGenericParameterType(TypeToken typeToken, int parameterIndex) { diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java index 93b0c9a..7ed3412 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java @@ -16,22 +16,21 @@ package com.google.testing.junit.testparameterinjector.junit5; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toSet; import com.google.auto.value.AutoValue; +import com.google.common.base.Joiner; +import com.google.common.collect.ContiguousSet; +import com.google.common.collect.DiscreteDomain; +import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.Multimap; import com.google.common.collect.MultimapBuilder; +import com.google.common.collect.Range; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.Collection; import java.util.List; import java.util.Set; -import 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). */ @@ -64,9 +63,9 @@ abstract class TestInfo { return String.format( "%s[%s]", getMethod().getName(), - getParameters().stream() - .map(TestInfoParameter::getValueInTestName) - .collect(joining(","))); + FluentIterable.from(getParameters()) + .transform(TestInfoParameter::getValueInTestName) + .join(Joiner.on(","))); } } @@ -108,18 +107,20 @@ abstract class TestInfo { * #getParameters()} list to the new name. */ private TestInfo withUpdatedParameterNames( - BiFunction parameterWithIndexToNewName) { + Java8BiFunction parameterWithIndexToNewName) { return new AutoValue_TestInfo( getMethod(), getTestClass(), - IntStream.range(0, getParameters().size()) - .mapToObj( + FluentIterable.from( + ContiguousSet.create( + Range.closedOpen(0, getParameters().size()), DiscreteDomain.integers())) + .transform( parameterIndex -> { TestInfoParameter parameter = getParameters().get(parameterIndex); return parameter.withValueInTestName( parameterWithIndexToNewName.apply(parameter, parameterIndex)); }) - .collect(toImmutableList()), + .toList(), getAnnotations()); } @@ -136,17 +137,20 @@ abstract class TestInfo { } static ImmutableList shortenNamesIfNecessary(List testInfos) { - if (testInfos.stream().anyMatch(info -> info.getName().length() > MAX_TEST_NAME_LENGTH)) { + if (FluentIterable.from(testInfos) + .anyMatch(info -> info.getName().length() > MAX_TEST_NAME_LENGTH)) { int numberOfParameters = testInfos.get(0).getParameters().size(); if (numberOfParameters == 0) { return ImmutableList.copyOf(testInfos); } else { Set parameterIndicesThatNeedUpdate = - IntStream.range(0, numberOfParameters) + FluentIterable.from( + ContiguousSet.create( + Range.closedOpen(0, numberOfParameters), DiscreteDomain.integers())) .filter( parameterIndex -> - testInfos.stream() + FluentIterable.from(testInfos) .anyMatch( info -> info.getParameters() @@ -154,11 +158,10 @@ abstract class TestInfo { .getValueInTestName() .length() > getMaxCharactersPerParameter(info, numberOfParameters))) - .boxed() - .collect(toSet()); + .toSet(); - return testInfos.stream() - .map( + return FluentIterable.from(testInfos) + .transform( info -> info.withUpdatedParameterNames( (parameter, parameterIndex) -> @@ -167,7 +170,7 @@ abstract class TestInfo { parameter, getMaxCharactersPerParameter(info, numberOfParameters)) : info.getParameters().get(parameterIndex).getValueInTestName())) - .collect(toImmutableList()); + .toList(); } } else { return ImmutableList.copyOf(testInfos); @@ -184,7 +187,8 @@ abstract class TestInfo { } static ImmutableList deduplicateTestNames(List testInfos) { - long uniqueTestNameCount = testInfos.stream().map(TestInfo::getName).distinct().count(); + long uniqueTestNameCount = + FluentIterable.from(testInfos).transform(TestInfo::getName).toSet().size(); if (testInfos.size() == uniqueTestNameCount) { // Return early if there are no duplicates return ImmutableList.copyOf(testInfos); @@ -214,37 +218,38 @@ abstract class TestInfo { testNameToInfo.put(testInfo.getName(), testInfo); } - return testNameToInfo.keySet().stream() - .flatMap( + return FluentIterable.from(testNameToInfo.keySet()) + .transformAndConcat( testName -> { Collection matchedInfos = testNameToInfo.get(testName); if (matchedInfos.size() == 1) { // There was only one method with this name, so no deduplication is necessary - return matchedInfos.stream(); + return matchedInfos; } else { // Found tests with duplicate test names int numParameters = matchedInfos.iterator().next().getParameters().size(); Set indicesThatShouldGetSuffix = // Find parameter indices for which a suffix would allow the reader to // differentiate - IntStream.range(0, numParameters) + FluentIterable.from( + ContiguousSet.create( + Range.closedOpen(0, numParameters), DiscreteDomain.integers())) .filter( parameterIndex -> - matchedInfos.stream() - .map( + FluentIterable.from(matchedInfos) + .transform( info -> getTypeSuffix( info.getParameters() .get(parameterIndex) .getValue())) - .distinct() - .count() + .toSet() + .size() > 1) - .boxed() - .collect(toSet()); + .toSet(); - return matchedInfos.stream() - .map( + return FluentIterable.from(matchedInfos) + .transform( testInfo -> testInfo.withUpdatedParameterNames( (parameter, parameterIndex) -> @@ -254,7 +259,7 @@ abstract class TestInfo { : parameter.getValueInTestName())); } }) - .collect(toImmutableList()); + .toList(); } private static String getTypeSuffix(@Nullable Object value) { @@ -267,14 +272,15 @@ abstract class TestInfo { private static ImmutableList deduplicateWithNumberPrefixes( ImmutableList testInfos) { - long uniqueTestNameCount = testInfos.stream().map(TestInfo::getName).distinct().count(); + long uniqueTestNameCount = + FluentIterable.from(testInfos).transform(TestInfo::getName).toSet().size(); if (testInfos.size() == uniqueTestNameCount) { return ImmutableList.copyOf(testInfos); } else { // There are still duplicates, even after adding type suffixes. As a last resort: add a // counter to all parameters to guarantee that each case is unique. - return testInfos.stream() - .map( + return FluentIterable.from(testInfos) + .transform( testInfo -> testInfo.withUpdatedParameterNames( (parameter, parameterIndex) -> @@ -282,14 +288,10 @@ abstract class TestInfo { "%s.%s", parameter.getIndexInValueSource() + 1, parameter.getValueInTestName()))) - .collect(toImmutableList()); + .toList(); } } - private static Collector> toImmutableList() { - return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf); - } - @AutoValue abstract static class TestInfoParameter { @@ -315,4 +317,9 @@ abstract class TestInfo { checkNotNull(valueInTestName), value, indexInValueSource); } } + + /** Copy of Java8's java.util.BiFunction which is not available in older versions of the JDK */ + interface Java8BiFunction { + K apply(I a, J b); + } } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessorList.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessorList.java index aa2355d..d1020c8 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessorList.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessorList.java @@ -14,12 +14,12 @@ package com.google.testing.junit.testparameterinjector.junit5; -import static java.util.stream.Collectors.toList; - import com.google.common.base.Optional; +import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.List; /** @@ -67,11 +67,11 @@ final class TestMethodProcessorList { testMethod, testClass, ImmutableList.copyOf(testMethod.getAnnotations()))); for (final TestMethodProcessor testMethodProcessor : testMethodProcessors) { - testInfos = - testInfos.stream() - .flatMap( - lastTestInfo -> testMethodProcessor.calculateTestInfos(lastTestInfo).stream()) - .collect(toList()); + List list = new ArrayList<>(); + for (TestInfo lastTestInfo : testInfos) { + list.addAll(testMethodProcessor.calculateTestInfos(lastTestInfo)); + } + testInfos = list; } testInfos = TestInfo.deduplicateTestNames(TestInfo.shortenNamesIfNecessary(testInfos)); @@ -85,17 +85,18 @@ final class TestMethodProcessorList { *

          This method is never called for a parameterless constructor. */ public List getConstructorParameters(Constructor constructor, TestInfo testInfo) { - return testMethodProcessors.stream() - .map(processor -> processor.maybeGetConstructorParameters(constructor, testInfo)) + return FluentIterable.from(testMethodProcessors) + .transform(processor -> processor.maybeGetConstructorParameters(constructor, testInfo)) .filter(Optional::isPresent) - .map(Optional::get) - .findFirst() - .orElseThrow( - () -> - new IllegalStateException( - String.format( - "Could not generate parameter values for %s. Did you forget an annotation?", - constructor))); + .transform(Optional::get) + .first() + .or( + () -> { + throw new IllegalStateException( + String.format( + "Could not generate parameter values for %s. Did you forget an annotation?", + constructor)); + }); } /** @@ -104,17 +105,18 @@ final class TestMethodProcessorList { *

          This method is never called for a parameterless {@code testInfo.getMethod()}. */ public List getTestMethodParameters(TestInfo testInfo) { - return testMethodProcessors.stream() - .map(processor -> processor.maybeGetTestMethodParameters(testInfo)) + return FluentIterable.from(testMethodProcessors) + .transform(processor -> processor.maybeGetTestMethodParameters(testInfo)) .filter(Optional::isPresent) - .map(Optional::get) - .findFirst() - .orElseThrow( - () -> - new IllegalStateException( - String.format( - "Could not generate parameter values for %s. Did you forget an annotation?", - testInfo.getMethod()))); + .transform(Optional::get) + .first() + .or( + () -> { + throw new IllegalStateException( + String.format( + "Could not generate parameter values for %s. Did you forget an annotation?", + testInfo.getMethod())); + }); } /** @@ -128,19 +130,17 @@ final class TestMethodProcessorList { /** Optionally validates the given constructor. */ public ExecutableValidationResult validateConstructor(Constructor constructor) { - return testMethodProcessors.stream() - .map(processor -> processor.validateConstructor(constructor)) - .filter(ExecutableValidationResult::wasValidated) - .findFirst() - .orElse(ExecutableValidationResult.notValidated()); + return FluentIterable.from(testMethodProcessors) + .transform(processor -> processor.validateConstructor(constructor)) + .firstMatch(ExecutableValidationResult::wasValidated) + .or(ExecutableValidationResult.notValidated()); } /** Optionally validates the given method. */ public ExecutableValidationResult validateTestMethod(Method testMethod, Class testClass) { - return testMethodProcessors.stream() - .map(processor -> processor.validateTestMethod(testMethod, testClass)) - .filter(ExecutableValidationResult::wasValidated) - .findFirst() - .orElse(ExecutableValidationResult.notValidated()); + return FluentIterable.from(testMethodProcessors) + .transform(processor -> processor.validateTestMethod(testMethod, testClass)) + .firstMatch(ExecutableValidationResult::wasValidated) + .or(ExecutableValidationResult.notValidated()); } } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java index 3bc3170..6423b7a 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java @@ -18,11 +18,11 @@ 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.base.Optional; +import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; import com.google.protobuf.MessageLite; import com.google.testing.junit.testparameterinjector.junit5.TestParameter.InternalImplementationOfThisParameter; @@ -32,6 +32,7 @@ import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** @@ -146,16 +147,17 @@ public @interface TestParameter { annotation); if (valueIsSet) { - return stream(annotation.value()) - .map(v -> parseStringValue(v, parameterClass)) - .collect(toList()); + return Lists.newArrayList( + FluentIterable.from(annotation.value()) + .transform(v -> parseStringValue(v, parameterClass)) + .toArray(Object.class)); } else if (valuesProviderIsSet) { return getValuesFromProvider(annotation.valuesProvider()); } else { if (Enum.class.isAssignableFrom(parameterClass)) { - return ImmutableList.copyOf(parameterClass.asSubclass(Enum.class).getEnumConstants()); + return Arrays.asList((Object[]) parameterClass.asSubclass(Enum.class).getEnumConstants()); } else if (Primitives.wrap(parameterClass).equals(Boolean.class)) { - return ImmutableList.of(false, true); + return Arrays.asList(false, true); } else { throw new IllegalStateException( String.format( diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java index 37cb36c..08c79b3 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java @@ -18,9 +18,6 @@ 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; @@ -30,14 +27,17 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; +import com.google.common.collect.ContiguousSet; +import com.google.common.collect.DiscreteDomain; +import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; +import com.google.common.collect.Range; import com.google.common.primitives.Primitives; import com.google.common.util.concurrent.UncheckedExecutionException; import com.google.protobuf.ByteString; import com.google.testing.junit.testparameterinjector.junit5.TestInfo.TestInfoParameter; -import com.google.testing.junit.testparameterinjector.junit5.TestParameterAnnotationMethodProcessor.TestParameterValue; import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; @@ -52,15 +52,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.ExecutionException; -import 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; /** @@ -165,18 +161,21 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso !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( + return FluentIterable.from( + ContiguousSet.create( + Range.closedOpen(0, specifiedValues.size()), DiscreteDomain.integers())) + .transform( valueIndex -> - new AutoValue_TestParameterAnnotationMethodProcessor_TestParameterValue( - AnnotationTypeOrigin.create( - annotationWithMetadata.annotation().annotationType(), origin), - specifiedValues.get(valueIndex), - valueIndex, - new ArrayList<>(specifiedValues), - annotationWithMetadata.paramClass(), - annotationWithMetadata.paramName())) - .collect(toImmutableList()); + (TestParameterValue) + new AutoValue_TestParameterAnnotationMethodProcessor_TestParameterValue( + AnnotationTypeOrigin.create( + annotationWithMetadata.annotation().annotationType(), origin), + specifiedValues.get(valueIndex), + valueIndex, + new ArrayList<>(specifiedValues), + annotationWithMetadata.paramClass(), + annotationWithMetadata.paramName())) + .toList(); } } /** @@ -189,13 +188,18 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return annotationType -> Optional.absent(); } else { return annotationType -> - Optional.fromNullable( - new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ false) - .getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()).stream() - .filter(matches(annotationType)) - .map(TestParameterValue::value) - .findFirst() - .orElse(null)); + FluentIterable.from( + new TestParameterAnnotationMethodProcessor( + /* onlyForFieldsAndParameters= */ false) + .getParameterValuesForTest(testIndexHolder, testInfo.getTestClass())) + .filter( + testParameterValue -> + testParameterValue + .annotationTypeOrigin() + .annotationType() + .equals(annotationType)) + .transform(TestParameterValue::value) + .first(); } } @@ -225,11 +229,6 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } } - private static Predicate matches(Class annotationType) { - return testParameterValue -> - testParameterValue.annotationTypeOrigin().annotationType().equals(annotationType); - } - /** The origin of an annotation type. */ enum Origin { CLASS, @@ -347,33 +346,41 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso // @TestParameterAnnotation annotation. List fieldAnnotations = extractTestParameterAnnotations( - streamWithParents(testClass) - .flatMap(c -> stream(c.getDeclaredFields())) - .flatMap(field -> stream(field.getAnnotations())), + FluentIterable.from(listWithParents(testClass)) + .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields())) + .transformAndConcat(field -> Arrays.asList(field.getAnnotations())) + .toList(), Origin.FIELD); List methodAnnotations = extractTestParameterAnnotations( - stream(testClass.getMethods()).flatMap(method -> stream(method.getAnnotations())), + FluentIterable.from(testClass.getMethods()) + .transformAndConcat(method -> Arrays.asList(method.getAnnotations())) + .toList(), Origin.METHOD); List parameterAnnotations = extractTestParameterAnnotations( - streamWithParents(testClass) - .flatMap(c -> stream(c.getDeclaredMethods())) - .flatMap(method -> stream(method.getParameterAnnotations()).flatMap(Stream::of)), + FluentIterable.from(listWithParents(testClass)) + .transformAndConcat(c -> Arrays.asList(c.getDeclaredMethods())) + .transformAndConcat(method -> Arrays.asList(method.getParameterAnnotations())) + .transformAndConcat(Arrays::asList) + .toList(), Origin.METHOD_PARAMETER); List classAnnotations = - extractTestParameterAnnotations(stream(testClass.getAnnotations()), Origin.CLASS); + extractTestParameterAnnotations(Arrays.asList(testClass.getAnnotations()), Origin.CLASS); List constructorAnnotations = extractTestParameterAnnotations( - stream(testClass.getDeclaredConstructors()) - .flatMap(constructor -> stream(constructor.getAnnotations())), + FluentIterable.from(testClass.getDeclaredConstructors()) + .transformAndConcat(constructor -> Arrays.asList(constructor.getAnnotations())) + .toList(), Origin.CONSTRUCTOR); List constructorParameterAnnotations = extractTestParameterAnnotations( - stream(testClass.getDeclaredConstructors()) - .flatMap( + FluentIterable.from(testClass.getDeclaredConstructors()) + .transformAndConcat( constructor -> - stream(constructor.getParameterAnnotations()).flatMap(Stream::of)), + FluentIterable.from(Arrays.asList(constructor.getParameterAnnotations())) + .transformAndConcat(Arrays::asList)) + .toList(), Origin.CONSTRUCTOR_PARAMETER); checkDuplicatedClassAndFieldAnnotations( @@ -382,11 +389,11 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso checkDuplicatedFieldsAnnotations(methodAnnotations, fieldAnnotations); checkState( - constructorAnnotations.stream().distinct().count() == constructorAnnotations.size(), + FluentIterable.from(constructorAnnotations).toSet().size() == constructorAnnotations.size(), "Annotations should not be duplicated on the constructor."); checkState( - classAnnotations.stream().distinct().count() == classAnnotations.size(), + FluentIterable.from(classAnnotations).toSet().size() == classAnnotations.size(), "Annotations should not be duplicated on the class."); if (onlyForFieldsAndParameters) { @@ -410,18 +417,16 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso constructorAnnotations); } - return 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()); + // The order matters, since it will determine which annotation processor is + // called first. + return FluentIterable.from(classAnnotations) + .append(fieldAnnotations) + .append(constructorAnnotations) + .append(constructorParameterAnnotations) + .append(methodAnnotations) + .append(parameterAnnotations) + .toSet() + .asList(); } private ImmutableList getAnnotationTypeOrigins( @@ -429,9 +434,9 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso Set originsToFilterBy = ImmutableSet.builder().add(firstOrigin).add(otherOrigins).build(); try { - return annotationTypeOriginsCache.getUnchecked(testClass).stream() + return FluentIterable.from(annotationTypeOriginsCache.getUnchecked(testClass)) .filter(annotationTypeOrigin -> originsToFilterBy.contains(annotationTypeOrigin.origin())) - .collect(toImmutableList()); + .toList(); } catch (UncheckedExecutionException e) { Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); throw e; @@ -442,14 +447,17 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso List methodAnnotations, List 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()) { + if (FluentIterable.from(fieldAnnotations).toSet().size() != fieldAnnotations.size()) { List> methodOrFieldAnnotations = - Stream.concat(methodAnnotations.stream(), fieldAnnotations.stream().distinct()) - .map(AnnotationTypeOrigin::annotationType) - .collect(toCollection(ArrayList::new)); + new ArrayList<>( + FluentIterable.from(methodAnnotations) + .append(new HashSet<>(fieldAnnotations)) + .transform(AnnotationTypeOrigin::annotationType) + .toList()); checkState( - methodOrFieldAnnotations.stream().distinct().count() == methodOrFieldAnnotations.size(), + FluentIterable.from(methodOrFieldAnnotations).toSet().size() + == methodOrFieldAnnotations.size(), "Annotations should not be duplicated on a method and field" + " if they are present on multiple fields"); } @@ -460,18 +468,18 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso List classAnnotations, List fieldAnnotations) { ImmutableSet> classAnnotationTypes = - classAnnotations.stream() - .map(AnnotationTypeOrigin::annotationType) - .collect(toImmutableSet()); - - ImmutableSet> uniqueFieldAnnotations = - fieldAnnotations.stream() - .map(AnnotationTypeOrigin::annotationType) - .collect(toImmutableSet()); - ImmutableSet> uniqueConstructorAnnotations = - constructorAnnotations.stream() - .map(AnnotationTypeOrigin::annotationType) - .collect(toImmutableSet()); + FluentIterable.from(classAnnotations) + .transform(AnnotationTypeOrigin::annotationType) + .toSet(); + + ImmutableSet> uniqueFieldAnnotations = + FluentIterable.from(fieldAnnotations) + .transform(AnnotationTypeOrigin::annotationType) + .toSet(); + ImmutableSet> uniqueConstructorAnnotations = + FluentIterable.from(constructorAnnotations) + .transform(AnnotationTypeOrigin::annotationType) + .toSet(); checkState( Collections.disjoint(classAnnotationTypes, uniqueFieldAnnotations), @@ -486,14 +494,15 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso "Annotations should not be duplicated on a field and constructor"); } - /** Returns a list of annotation types that are a {@link TestParameterAnnotation}. */ private List extractTestParameterAnnotations( - Stream annotations, Origin origin) { - return annotations - .map(Annotation::annotationType) - .filter(annotationType -> annotationType.isAnnotationPresent(TestParameterAnnotation.class)) - .map(annotationType -> AnnotationTypeOrigin.create(annotationType, origin)) - .collect(toCollection(ArrayList::new)); + List annotations, Origin origin) { + return new ArrayList<>( + FluentIterable.from(annotations) + .transform(Annotation::annotationType) + .filter( + annotationType -> annotationType.isAnnotationPresent(TestParameterAnnotation.class)) + .transform(annotationType -> AnnotationTypeOrigin.create(annotationType, origin)) + .toList()); } @Override @@ -579,7 +588,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso // 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> testParameterAnnotationTypes = + ImmutableList> testParameterAnnotationTypes = getTestParameterAnnotations( // Do not include METHOD_PARAMETER or CONSTRUCTOR_PARAMETER since they have already // been evaluated. @@ -740,12 +749,12 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso testInfos.add( originalTest .withExtraParameters( - testParameterValues.stream() - .map( + FluentIterable.from(testParameterValues) + .transform( param -> TestInfoParameter.create( param.toTestNameString(), param.value(), param.valueIndex())) - .collect(toImmutableList())) + .toList()) .withExtraAnnotation( TestIndexHolderFactory.create( /* methodIndex= */ strictIndexOf( @@ -767,18 +776,19 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso List> testParameterValuesList = getAnnotationValuesForUsedAnnotationTypes(method, testClass); - return Lists.cartesianProduct(testParameterValuesList).stream() + return FluentIterable.from(Lists.cartesianProduct(testParameterValuesList)) .filter( // Skip tests based on the annotations' {@link Validator#shouldSkip} return // value. testParameterValues -> - testParameterValues.stream() - .noneMatch( + FluentIterable.from(testParameterValues) + .filter( testParameterValue -> callShouldSkip( testParameterValue.annotationTypeOrigin().annotationType(), - testParameterValues))) - .collect(toImmutableList()); + testParameterValues)) + .isEmpty()) + .toList(); }); } catch (ExecutionException | UncheckedExecutionException e) { Throwables.throwIfUnchecked(e.getCause()); @@ -806,35 +816,38 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso private ImmutableList> getAnnotationValuesForUsedAnnotationTypes( Method method, Class testClass) { ImmutableList annotationTypes = - Stream.of( - getAnnotationTypeOrigins(testClass, Origin.CLASS).stream(), - getAnnotationTypeOrigins(testClass, Origin.FIELD).stream(), - getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR).stream(), - getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR_PARAMETER).stream(), - getAnnotationTypeOrigins(testClass, Origin.METHOD).stream(), - getAnnotationTypeOrigins(testClass, Origin.METHOD_PARAMETER).stream() - .sorted(annotationComparator(method.getParameterAnnotations()))) - .flatMap(x -> x) - .collect(toImmutableList()); - - return removeOverrides(annotationTypes, testClass, method).stream() - .map( + FluentIterable.from(getAnnotationTypeOrigins(testClass, Origin.CLASS)) + .append(getAnnotationTypeOrigins(testClass, Origin.FIELD)) + .append(getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR)) + .append(getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR_PARAMETER)) + .append(getAnnotationTypeOrigins(testClass, Origin.METHOD)) + .append( + ImmutableList.sortedCopyOf( + annotationComparator(method.getParameterAnnotations()), + getAnnotationTypeOrigins(testClass, Origin.METHOD_PARAMETER))) + .toList(); + + return FluentIterable.from(removeOverrides(annotationTypes, testClass, method)) + .transform( annotationTypeOrigin -> getAnnotationFromParametersOrTestOrClass(annotationTypeOrigin, method, testClass)) .filter(l -> !l.isEmpty()) - .flatMap(List::stream) - .collect(toImmutableList()); + .transformAndConcat(i -> i) + .toList(); } private Comparator annotationComparator( Annotation[][] parameterAnnotations) { ImmutableList annotationOrdering = - stream(parameterAnnotations) - .flatMap(Arrays::stream) - .map(Annotation::annotationType) - .map(Class::getName) - .collect(toImmutableList()); - return Comparator.comparingInt(o -> annotationOrdering.indexOf(o.annotationType().getName())); + FluentIterable.from(parameterAnnotations) + .transformAndConcat(Arrays::asList) + .transform(Annotation::annotationType) + .transform(Class::getName) + .toList(); + return (annotationTypeOrigin, t1) -> + Integer.compare( + annotationOrdering.indexOf(annotationTypeOrigin.annotationType().getName()), + annotationOrdering.indexOf(t1.annotationType().getName())); } /** @@ -847,43 +860,45 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso private List removeOverrides( List annotationTypeOrigins, Class testClass, Method method) { return removeOverrides( - annotationTypeOrigins.stream() + new ArrayList<>( + FluentIterable.from(annotationTypeOrigins) + .filter( + annotationTypeOrigin -> { + switch (annotationTypeOrigin.origin()) { + case FIELD: // Fall through. + case CLASS: + return getAnnotationListWithType( + method.getAnnotations(), annotationTypeOrigin.annotationType()) + .isEmpty(); + default: + return true; + } + }) + .toList()), + testClass); + } + + /** + * @see #removeOverrides(List, Class) + */ + private List removeOverrides( + List annotationTypeOrigins, Class testClass) { + return new ArrayList<>( + FluentIterable.from(annotationTypeOrigins) .filter( annotationTypeOrigin -> { switch (annotationTypeOrigin.origin()) { case FIELD: // Fall through. case CLASS: return getAnnotationListWithType( - method.getAnnotations(), annotationTypeOrigin.annotationType()) + getOnlyConstructor(testClass).getAnnotations(), + annotationTypeOrigin.annotationType()) .isEmpty(); default: return true; } }) - .collect(toCollection(ArrayList::new)), - testClass); - } - - /** - * @see #removeOverrides(List, Class) - */ - private List removeOverrides( - List 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(); - default: - return true; - } - }) - .collect(toCollection(ArrayList::new)); + .toList()); } /** @@ -928,16 +943,18 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } } else if (origin == Origin.FIELD) { List 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)); + new ArrayList<>( + FluentIterable.from(listWithParents(testClass)) + .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields())) + .transformAndConcat( + field -> + FluentIterable.from( + getAnnotationListWithType(field.getAnnotations(), annotationType)) + .transform( + annotation -> + AnnotationWithMetadata.withMetadata( + annotation, field.getType(), field.getName()))) + .toList()); if (!annotations.isEmpty()) { return toTestParameterValueList(annotations, origin); } @@ -953,9 +970,12 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso private static ImmutableList> toTestParameterValueList( List annotationWithMetadatas, Origin origin) { - return annotationWithMetadatas.stream() - .map(annotationWithMetadata -> TestParameterValue.create(annotationWithMetadata, origin)) - .collect(toImmutableList()); + return FluentIterable.from(annotationWithMetadatas) + .transform( + annotationWithMetadata -> + (List) + new ArrayList<>(TestParameterValue.create(annotationWithMetadata, origin))) + .toList(); } private static ImmutableList getAnnotationWithMetadataListWithType( @@ -984,8 +1004,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso @SuppressWarnings("AndroidJdkLibsChecker") private static ImmutableList getAnnotationWithMetadataListWithType( Parameter[] parameters, Class annotationType) { - return stream(parameters) - .map( + return FluentIterable.from(parameters) + .transform( parameter -> { Annotation annotation = parameter.getAnnotation(annotationType); return annotation == null @@ -996,7 +1016,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso : AnnotationWithMetadata.withMetadata(annotation, parameter.getType()); }) .filter(Objects::nonNull) - .collect(toImmutableList()); + .toList(); } private static ImmutableList getAnnotationWithMetadataListWithType( @@ -1018,9 +1038,9 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso private ImmutableList getAnnotationListWithType( Annotation[] annotations, Class annotationType) { - return stream(annotations) + return FluentIterable.from(annotations) .filter(annotation -> annotation.annotationType().equals(annotationType)) - .collect(toImmutableList()); + .toList(); } private static Constructor getOnlyConstructor(Class testClass) { @@ -1049,9 +1069,9 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso List remainingTestParameterValuesForFieldInjection = new ArrayList<>(testParameterValuesForFieldInjection); for (Field declaredField : - streamWithParents(testInstance.getClass()) - .flatMap(c -> stream(c.getDeclaredFields())) - .collect(toImmutableList())) { + FluentIterable.from(listWithParents(testInstance.getClass())) + .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields())) + .toList()) { for (TestParameterValue testParameterValue : remainingTestParameterValuesForFieldInjection) { if (declaredField.isAnnotationPresent( @@ -1076,11 +1096,11 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso private static ImmutableList filterByOrigin( List testParameterValues, Origin... origins) { Set originsToFilterBy = ImmutableSet.copyOf(origins); - return testParameterValues.stream() + return FluentIterable.from(testParameterValues) .filter( testParameterValue -> originsToFilterBy.contains(testParameterValue.annotationTypeOrigin().origin())) - .collect(toImmutableList()); + .toList(); } /** @@ -1090,9 +1110,9 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso private static ImmutableList filterAnnotationTypeOriginsByOrigin( List annotationTypeOrigins, Origin... origins) { List originList = Arrays.asList(origins); - return annotationTypeOrigins.stream() + return FluentIterable.from(annotationTypeOrigins) .filter(annotationTypeOrigin -> originList.contains(annotationTypeOrigin.origin())) - .collect(toImmutableList()); + .toList(); } /** Returns a {@link TestParameterAnnotation}'s value for a method or constructor parameter. */ @@ -1200,7 +1220,11 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso public ValidatorContext(List testParameterValues) { this.testParameterValues = testParameterValues; - this.valueList = testParameterValues.stream().map(TestParameterValue::value).collect(toSet()); + this.valueList = + FluentIterable.from(testParameterValues) + .transform(TestParameterValue::value) + .filter(Objects::nonNull) + .toSet(); } @Override @@ -1226,11 +1250,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } private Optional getParameter(Class testParameter) { - return Optional.fromNullable( - testParameterValues.stream() - .filter(value -> value.annotationTypeOrigin().annotationType().equals(testParameter)) - .findAny() - .orElse(null)); + return FluentIterable.from(testParameterValues) + .firstMatch(value -> value.annotationTypeOrigin().annotationType().equals(testParameter)); } } @@ -1258,17 +1279,17 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } /** Returns the TestParameterAnnotation annotation types defined for a method or constructor. */ - private ImmutableList> getTestParameterAnnotations( + private ImmutableList> getTestParameterAnnotations( List annotationTypeOrigins, final Class testClass, AnnotatedElement methodOrConstructor) { - return annotationTypeOrigins.stream() - .map(AnnotationTypeOrigin::annotationType) + return FluentIterable.from(annotationTypeOrigins) + .transform(AnnotationTypeOrigin::annotationType) .filter( annotationType -> testClass.isAnnotationPresent(annotationType) || methodOrConstructor.isAnnotationPresent(annotationType)) - .collect(toImmutableList()); + .toList(); } private int strictIndexOf(List haystack, T needle) { @@ -1286,8 +1307,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return resultBuilder.build(); } - private static Stream> streamWithParents(Class clazz) { - Stream.Builder> resultBuilder = Stream.builder(); + private static ImmutableList> listWithParents(Class clazz) { + ImmutableList.Builder> resultBuilder = ImmutableList.builder(); Class currentClass = clazz; while (currentClass != null) { @@ -1297,14 +1318,4 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return resultBuilder.build(); } - - // Immutable collectors are re-implemented here because they are missing from the Android - // collection library. - private static Collector> toImmutableList() { - return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf); - } - - private static Collector> toImmutableSet() { - return Collectors.collectingAndThen(Collectors.toList(), ImmutableSet::copyOf); - } } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParametersMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParametersMethodProcessor.java index 4879ca7..3ada177 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParametersMethodProcessor.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParametersMethodProcessor.java @@ -17,8 +17,6 @@ package com.google.testing.junit.testparameterinjector.junit5; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Verify.verify; import static com.google.common.collect.Iterables.getOnlyElement; -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toList; import com.google.auto.value.AutoAnnotation; import com.google.common.base.Optional; @@ -26,6 +24,7 @@ import com.google.common.base.Throwables; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; +import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; @@ -47,10 +46,6 @@ import java.lang.reflect.Parameter; import java.util.Arrays; 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; /** {@code TestMethodProcessor} implementation for supporting {@link TestParameters}. */ @SuppressWarnings("AndroidJdkLibsChecker") // Parameter is not available on old Android SDKs. @@ -127,25 +122,22 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { 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())) + FluentIterable.of( + constructorParameters.transform( + param -> + TestInfoParameter.create( + param.name(), + param.parametersMap(), + constructorParametersIndexCopy)), + methodParameters.transform( + param -> + TestInfoParameter.create( + param.name(), + param.parametersMap(), + methodParametersIndexCopy))) + .filter(Optional::isPresent) + .transform(Optional::get) + .toList()) .withExtraAnnotation( TestIndexHolderFactory.create( constructorParametersIndex, methodParametersIndex))); @@ -158,16 +150,16 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { getConstructorParametersOrSingleAbsentElement(Class testClass) { Constructor constructor = getOnlyConstructor(testClass); return hasRelevantAnnotation(constructor) - ? getConstructorParameters(constructor).stream() - .map(Optional::of) - .collect(toImmutableList()) + ? FluentIterable.from(getConstructorParameters(constructor)) + .transform(Optional::of) + .toList() : ImmutableList.of(Optional.absent()); } private ImmutableList> getMethodParametersOrSingleAbsentElement( Method method) { return hasRelevantAnnotation(method) - ? getMethodParameters(method).stream().map(Optional::of).collect(toImmutableList()) + ? FluentIterable.from(getMethodParameters(method)).transform(Optional::of).toList() : ImmutableList.of(Optional.absent()); } @@ -261,9 +253,10 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { } if (valueIsSet) { - return stream(annotation.value()) - .map(yamlMap -> toParameterValues(yamlMap, parametersList, annotation.customName())) - .collect(toImmutableList()); + return FluentIterable.from(annotation.value()) + .transform( + yamlMap -> toParameterValues(yamlMap, parametersList, annotation.customName())) + .toList(); } else { return toParameterValuesList(annotation.valuesProvider(), parametersList); } @@ -273,14 +266,14 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { "This method should only be called for executables with at least one relevant" + " annotation"); - return stream(executable.getAnnotation(RepeatedTestParameters.class).value()) - .map( + return FluentIterable.from(executable.getAnnotation(RepeatedTestParameters.class).value()) + .transform( annotation -> toParameterValues( validateAndGetSingleValueFromRepeatedAnnotation(annotation, executable), parametersList, annotation.customName())) - .collect(toImmutableList()); + .toList(); } } @@ -290,9 +283,11 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { Constructor constructor = valuesProvider.getDeclaredConstructor(); constructor.setAccessible(true); - return constructor.newInstance().provideValues().stream() - .peek(values -> validateThatValuesMatchParameters(values, parameters)) - .collect(toImmutableList()); + List testParametersValues = constructor.newInstance().provideValues(); + for (TestParametersValues testParametersValue : testParametersValues) { + validateThatValuesMatchParameters(testParametersValue, parameters); + } + return ImmutableList.copyOf(testParametersValues); } catch (NoSuchMethodException e) { if (!Modifier.isStatic(valuesProvider.getModifiers()) && valuesProvider.isMemberClass()) { throw new IllegalStateException( @@ -314,7 +309,7 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { private static void checkParameterNamesArePresent(Executable executable) { checkState( - stream(executable.getParameters()).allMatch(Parameter::isNamePresent), + FluentIterable.from(executable.getParameters()).allMatch(Parameter::isNamePresent), "" + "No parameter name could be found for %s, which likely means that parameter names" + " aren't available at runtime. Please ensure that the this test was built with the" @@ -442,9 +437,11 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { private static List toParameterList( TestParametersValues parametersValues, Parameter[] parameters) { - return stream(parameters) - .map(parameter -> parametersValues.parametersMap().get(parameter.getName())) - .collect(toList()); + return Arrays.asList( + FluentIterable.from(Arrays.asList(parameters)) + .transform(Parameter::getName) + .transform(name -> parametersValues.parametersMap().get(name)) + .toArray(Object.class)); } private static Constructor getOnlyConstructor(Class testClass) { @@ -455,12 +452,6 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { return getOnlyElement(constructors); } - // Immutable collectors are re-implemented here because they are missing from the Android - // collection library. - private static Collector> 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. -- cgit v1.2.3 From 6371cc426d9ee28b61dc33279d9a922d8d5d2c5c Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 26 Oct 2022 07:45:38 +0000 Subject: Add a Kotlin test to supplement the Java/JVM and Java/Android tests. This test will be used to test whether https://github.com/google/TestParameterInjector/issues/22 is fixed --- .../TestParameterInjectorKotlinTest.kt | 188 +++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt new file mode 100644 index 0000000..4316bed --- /dev/null +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt @@ -0,0 +1,188 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import com.google.testing.junit.testparameterinjector.SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase +import java.util.Arrays +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters + +@RunWith(Parameterized::class) +class TestParameterInjectorKotlinTest { + + // ********** Test cases ********** // + // These test classes are all expected to run successfully + + @RunAsTest + internal class TestParameter_MethodParam : SuccessfulTestCaseBase() { + @Test + fun testString(@TestParameter("a", "b") param: String) { + storeTestParametersForThisTest(param) + } + + @Test + fun testEnum_selection(@TestParameter("RED", "GREEN") param: Color) { + storeTestParametersForThisTest(param) + } + + @Test + fun testEnum_all(@TestParameter param: Color) { + storeTestParametersForThisTest(param) + } + + @Test + fun testMultiple( + @TestParameter("1", "8") width: Int, + @TestParameter("1", "5.5") height: Double, + ) { + storeTestParametersForThisTest(width, height) + } + + override fun expectedTestNameToStringifiedParameters(): ImmutableMap { + return ImmutableMap.builder() + .put("testString[a]", "a") + .put("testString[b]", "b") + .put("testEnum_selection[RED]", "RED") + .put("testEnum_selection[GREEN]", "GREEN") + .put("testEnum_all[RED]", "RED") + .put("testEnum_all[BLUE]", "BLUE") + .put("testEnum_all[GREEN]", "GREEN") + .put("testMultiple[width=1,height=1.0]", "1:1.0") + .put("testMultiple[width=1,height=5.5]", "1:5.5") + .put("testMultiple[width=8,height=1.0]", "8:1.0") + .put("testMultiple[width=8,height=5.5]", "8:5.5") + .buildOrThrow() + } + } + + @RunAsTest + internal class TestParameter_Field : SuccessfulTestCaseBase() { + @TestParameter("1", "2") var width: Int? = null + + @Test + fun test() { + storeTestParametersForThisTest(width) + } + + override fun expectedTestNameToStringifiedParameters(): ImmutableMap { + return ImmutableMap.builder() + .put("test[width=1]", "1") + .put("test[width=2]", "2") + .buildOrThrow() + } + } + + @RunAsTest + internal class TestParameter_ConstructorParam : SuccessfulTestCaseBase { + val width: Int + + constructor(@TestParameter("1", "2") width: Int) { + this.width = width + } + + @Test + fun test() { + storeTestParametersForThisTest(width) + } + + override fun expectedTestNameToStringifiedParameters(): ImmutableMap { + return ImmutableMap.builder() + .put("test[width=1]", "1") + .put("test[width=2]", "2") + .buildOrThrow() + } + } + + @RunAsTest + internal class TestParameters_MethodParam : SuccessfulTestCaseBase() { + @TestParameters("{width: 3, height: 8}") + @TestParameters("{width: 5, height: 2.5}") + @Test + fun test(width: Int, height: Double) { + storeTestParametersForThisTest(width, height) + } + + override fun expectedTestNameToStringifiedParameters(): ImmutableMap { + return ImmutableMap.builder() + .put("test[{width: 3, height: 8}]", "3:8.0") + .put("test[{width: 5, height: 2.5}]", "5:2.5") + .buildOrThrow() + } + } + + @RunAsTest + internal class TestParameters_ConstructorParam : SuccessfulTestCaseBase { + val width: Int + + @TestParameters("{width: 1}") + @TestParameters("{width: 2}") + constructor(width: Int) { + this.width = width + } + + @Test + fun test() { + storeTestParametersForThisTest(width) + } + + override fun expectedTestNameToStringifiedParameters(): ImmutableMap { + return ImmutableMap.builder() + .put("test[{width: 1}]", "1") + .put("test[{width: 2}]", "2") + .buildOrThrow() + } + } + + // ********** Test infrastructure ********** // + + private val testClass: Class<*> + + constructor(@Suppress("UNUSED_PARAMETER") testName: String?, testClass: Class<*>) { + this.testClass = testClass + } + + @Test + fun test_success() { + SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures( + object : PluggableTestRunner(testClass) { + override fun createTestMethodProcessorList(): TestMethodProcessorList { + return TestMethodProcessorList.createNewParameterizedProcessors() + } + } + ) + } + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun parameters(): Collection> { + return Arrays.stream(TestParameterInjectorKotlinTest::class.java.classes) + .filter { cls: Class<*> -> cls.isAnnotationPresent(RunAsTest::class.java) } + .map { cls: Class<*> -> arrayOf(cls.simpleName, cls) } + .collect(ImmutableList.toImmutableList()) + } + } + annotation class RunAsTest + + enum class Color { + RED, + BLUE, + GREEN + } +} -- cgit v1.2.3 From ea2ccf7bf26cdb739c6adc8ad4922ee32735a326 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 26 Oct 2022 13:03:00 +0000 Subject: Add support for Kotlin @JvmInline value classes as @TestParameter method parameters Related to https://github.com/google/TestParameterInjector/issues/22 --- .../junit/testparameterinjector/TestInfo.java | 21 ++++++++-- .../TestParameterInjectorKotlinTest.kt | 45 ++++++++++++++++++++++ .../testparameterinjector/junit5/TestInfo.java | 21 ++++++++-- 3 files changed, 81 insertions(+), 6 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java index 965d41a..b2f9c03 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.auto.value.AutoValue; import com.google.common.base.Joiner; +import com.google.common.base.Splitter; import com.google.common.collect.ContiguousSet; import com.google.common.collect.DiscreteDomain; import com.google.common.collect.FluentIterable; @@ -58,11 +59,11 @@ abstract class TestInfo { public final String getName() { if (getParameters().isEmpty()) { - return getMethod().getName(); + return getRealMethodName(); } else { return String.format( "%s[%s]", - getMethod().getName(), + getRealMethodName(), FluentIterable.from(getParameters()) .transform(TestInfoParameter::getValueInTestName) .join(Joiner.on(","))); @@ -124,6 +125,20 @@ abstract class TestInfo { getAnnotations()); } + private String getRealMethodName() { + String candidate = getMethod().getName(); + if (candidate.contains("-")) { + // Kotlin hack: + // Method names can normally not contain the '-' character. However, when a Kotlin method gets + // a @JvmInline value class as parameter, a method with a hash suffix will show up in the + // TestParameterInjector's reflection results. These are of the form realMethodName-fiSAjMM(). + // The code below strips off this suffix. + return Splitter.on('-').omitEmptyStrings().split(candidate).iterator().next(); + } else { + return candidate; + } + } + public static TestInfo legacyCreate( Method method, Class testClass, String name, List annotations) { return new AutoValue_TestInfo( @@ -180,7 +195,7 @@ abstract class TestInfo { 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; + MAX_TEST_NAME_LENGTH - testInfo.getRealMethodName().length() - 2; // Subtract 4 characters to leave place for joining commas and the parameter index. return maxLengthOfAllParameters / numberOfParameters - 4; diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt index 4316bed..b69bbc1 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt @@ -71,6 +71,48 @@ class TestParameterInjectorKotlinTest { } } + @RunAsTest + internal class TestParameter_MethodParam_WithValueClasses : SuccessfulTestCaseBase() { + @Test + fun testString(@TestParameter("a", "b") param: StringValueClass) { + storeTestParametersForThisTest(param.onlyValue) + } + + @Test + fun testEnum_selection(@TestParameter("RED", "GREEN") param: ColorValueClass) { + storeTestParametersForThisTest(param.onlyValue) + } + + @Test + fun testEnum_all(@TestParameter param: ColorValueClass) { + storeTestParametersForThisTest(param.onlyValue) + } + + @Test + fun testMixed( + @TestParameter("1", "8") width: Int, + @TestParameter("1", "5.5") height: DoubleValueClass, + ) { + storeTestParametersForThisTest(width, height.onlyValue) + } + + override fun expectedTestNameToStringifiedParameters(): ImmutableMap { + return ImmutableMap.builder() + .put("testString[a]", "a") + .put("testString[b]", "b") + .put("testEnum_selection[RED]", "RED") + .put("testEnum_selection[GREEN]", "GREEN") + .put("testEnum_all[RED]", "RED") + .put("testEnum_all[BLUE]", "BLUE") + .put("testEnum_all[GREEN]", "GREEN") + .put("testMixed[width=1,height=1.0]", "1:1.0") + .put("testMixed[width=1,height=5.5]", "1:5.5") + .put("testMixed[width=8,height=1.0]", "8:1.0") + .put("testMixed[width=8,height=5.5]", "8:5.5") + .buildOrThrow() + } + } + @RunAsTest internal class TestParameter_Field : SuccessfulTestCaseBase() { @TestParameter("1", "2") var width: Int? = null @@ -185,4 +227,7 @@ class TestParameterInjectorKotlinTest { BLUE, GREEN } + @JvmInline value class ColorValueClass(val onlyValue: Color) + @JvmInline value class StringValueClass(val onlyValue: String) + @JvmInline value class DoubleValueClass(val onlyValue: Double) } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java index 7ed3412..7b0b9b0 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.auto.value.AutoValue; import com.google.common.base.Joiner; +import com.google.common.base.Splitter; import com.google.common.collect.ContiguousSet; import com.google.common.collect.DiscreteDomain; import com.google.common.collect.FluentIterable; @@ -58,11 +59,11 @@ abstract class TestInfo { public final String getName() { if (getParameters().isEmpty()) { - return getMethod().getName(); + return getRealMethodName(); } else { return String.format( "%s[%s]", - getMethod().getName(), + getRealMethodName(), FluentIterable.from(getParameters()) .transform(TestInfoParameter::getValueInTestName) .join(Joiner.on(","))); @@ -124,6 +125,20 @@ abstract class TestInfo { getAnnotations()); } + private String getRealMethodName() { + String candidate = getMethod().getName(); + if (candidate.contains("-")) { + // Kotlin hack: + // Method names can normally not contain the '-' character. However, when a Kotlin method gets + // a @JvmInline value class as parameter, a method with a hash suffix will show up in the + // TestParameterInjector's reflection results. These are of the form realMethodName-fiSAjMM(). + // The code below strips off this suffix. + return Splitter.on('-').omitEmptyStrings().split(candidate).iterator().next(); + } else { + return candidate; + } + } + public static TestInfo legacyCreate( Method method, Class testClass, String name, List annotations) { return new AutoValue_TestInfo( @@ -180,7 +195,7 @@ abstract class TestInfo { 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; + MAX_TEST_NAME_LENGTH - testInfo.getRealMethodName().length() - 2; // Subtract 4 characters to leave place for joining commas and the parameter index. return maxLengthOfAllParameters / numberOfParameters - 4; -- cgit v1.2.3 From 8ace594a087a509e5eabf03e24886c1ae3ed825e Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 26 Oct 2022 13:07:24 +0000 Subject: Add a test to show support for Kotlin @JvmInline value classes as @TestParameter field parameters It doesn't work by string parsing though. I think this is almost impossible to fix because the Kotlin field has the wrapper type instead of the wrapped type when requested via reflection. Related to https://github.com/google/TestParameterInjector/issues/22. --- .../TestParameterInjectorKotlinTest.kt | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt index b69bbc1..8901415 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt @@ -17,6 +17,7 @@ package com.google.testing.junit.testparameterinjector import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap import com.google.testing.junit.testparameterinjector.SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase +import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider import java.util.Arrays import org.junit.Test import org.junit.runner.RunWith @@ -130,6 +131,30 @@ class TestParameterInjectorKotlinTest { } } + @RunAsTest + internal class TestParameter_Field_WithValueClass : SuccessfulTestCaseBase() { + @TestParameter(valuesProvider = DoubleValueClassProvider::class) + var width: DoubleValueClass? = null + + @Test + fun test() { + storeTestParametersForThisTest(width?.onlyValue) + } + + override fun expectedTestNameToStringifiedParameters(): ImmutableMap { + return ImmutableMap.builder() + .put("test[DoubleValueClass(onlyValue=1.0)]", "1.0") + .put("test[DoubleValueClass(onlyValue=2.5)]", "2.5") + .buildOrThrow() + } + + private class DoubleValueClassProvider : TestParameterValuesProvider { + override fun provideValues(): List { + return ImmutableList.of(DoubleValueClass(1.0), DoubleValueClass(2.5)) + } + } + } + @RunAsTest internal class TestParameter_ConstructorParam : SuccessfulTestCaseBase { val width: Int -- cgit v1.2.3 From 6669a2dc4acebc71d92c512f3a7a70d5e06efda5 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Thu, 27 Oct 2022 14:07:21 +0000 Subject: Roll back earlier change that adds support for Kotlin @JvmInline value classes because the hack broke real use cases Example use case: @Test fun `return NYC-5TH`() { .. } --- .../junit/testparameterinjector/TestInfo.java | 21 ++--------- .../TestParameterInjectorKotlinTest.kt | 42 ---------------------- .../testparameterinjector/junit5/TestInfo.java | 21 ++--------- 3 files changed, 6 insertions(+), 78 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java index b2f9c03..965d41a 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java @@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.auto.value.AutoValue; import com.google.common.base.Joiner; -import com.google.common.base.Splitter; import com.google.common.collect.ContiguousSet; import com.google.common.collect.DiscreteDomain; import com.google.common.collect.FluentIterable; @@ -59,11 +58,11 @@ abstract class TestInfo { public final String getName() { if (getParameters().isEmpty()) { - return getRealMethodName(); + return getMethod().getName(); } else { return String.format( "%s[%s]", - getRealMethodName(), + getMethod().getName(), FluentIterable.from(getParameters()) .transform(TestInfoParameter::getValueInTestName) .join(Joiner.on(","))); @@ -125,20 +124,6 @@ abstract class TestInfo { getAnnotations()); } - private String getRealMethodName() { - String candidate = getMethod().getName(); - if (candidate.contains("-")) { - // Kotlin hack: - // Method names can normally not contain the '-' character. However, when a Kotlin method gets - // a @JvmInline value class as parameter, a method with a hash suffix will show up in the - // TestParameterInjector's reflection results. These are of the form realMethodName-fiSAjMM(). - // The code below strips off this suffix. - return Splitter.on('-').omitEmptyStrings().split(candidate).iterator().next(); - } else { - return candidate; - } - } - public static TestInfo legacyCreate( Method method, Class testClass, String name, List annotations) { return new AutoValue_TestInfo( @@ -195,7 +180,7 @@ abstract class TestInfo { private static int getMaxCharactersPerParameter(TestInfo testInfo, int numberOfParameters) { int maxLengthOfAllParameters = // Subtract 2 characters for square brackets - MAX_TEST_NAME_LENGTH - testInfo.getRealMethodName().length() - 2; + MAX_TEST_NAME_LENGTH - testInfo.getMethod().getName().length() - 2; // Subtract 4 characters to leave place for joining commas and the parameter index. return maxLengthOfAllParameters / numberOfParameters - 4; diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt index 8901415..e19c527 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt @@ -72,48 +72,6 @@ class TestParameterInjectorKotlinTest { } } - @RunAsTest - internal class TestParameter_MethodParam_WithValueClasses : SuccessfulTestCaseBase() { - @Test - fun testString(@TestParameter("a", "b") param: StringValueClass) { - storeTestParametersForThisTest(param.onlyValue) - } - - @Test - fun testEnum_selection(@TestParameter("RED", "GREEN") param: ColorValueClass) { - storeTestParametersForThisTest(param.onlyValue) - } - - @Test - fun testEnum_all(@TestParameter param: ColorValueClass) { - storeTestParametersForThisTest(param.onlyValue) - } - - @Test - fun testMixed( - @TestParameter("1", "8") width: Int, - @TestParameter("1", "5.5") height: DoubleValueClass, - ) { - storeTestParametersForThisTest(width, height.onlyValue) - } - - override fun expectedTestNameToStringifiedParameters(): ImmutableMap { - return ImmutableMap.builder() - .put("testString[a]", "a") - .put("testString[b]", "b") - .put("testEnum_selection[RED]", "RED") - .put("testEnum_selection[GREEN]", "GREEN") - .put("testEnum_all[RED]", "RED") - .put("testEnum_all[BLUE]", "BLUE") - .put("testEnum_all[GREEN]", "GREEN") - .put("testMixed[width=1,height=1.0]", "1:1.0") - .put("testMixed[width=1,height=5.5]", "1:5.5") - .put("testMixed[width=8,height=1.0]", "8:1.0") - .put("testMixed[width=8,height=5.5]", "8:5.5") - .buildOrThrow() - } - } - @RunAsTest internal class TestParameter_Field : SuccessfulTestCaseBase() { @TestParameter("1", "2") var width: Int? = null diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java index 7b0b9b0..7ed3412 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java @@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.auto.value.AutoValue; import com.google.common.base.Joiner; -import com.google.common.base.Splitter; import com.google.common.collect.ContiguousSet; import com.google.common.collect.DiscreteDomain; import com.google.common.collect.FluentIterable; @@ -59,11 +58,11 @@ abstract class TestInfo { public final String getName() { if (getParameters().isEmpty()) { - return getRealMethodName(); + return getMethod().getName(); } else { return String.format( "%s[%s]", - getRealMethodName(), + getMethod().getName(), FluentIterable.from(getParameters()) .transform(TestInfoParameter::getValueInTestName) .join(Joiner.on(","))); @@ -125,20 +124,6 @@ abstract class TestInfo { getAnnotations()); } - private String getRealMethodName() { - String candidate = getMethod().getName(); - if (candidate.contains("-")) { - // Kotlin hack: - // Method names can normally not contain the '-' character. However, when a Kotlin method gets - // a @JvmInline value class as parameter, a method with a hash suffix will show up in the - // TestParameterInjector's reflection results. These are of the form realMethodName-fiSAjMM(). - // The code below strips off this suffix. - return Splitter.on('-').omitEmptyStrings().split(candidate).iterator().next(); - } else { - return candidate; - } - } - public static TestInfo legacyCreate( Method method, Class testClass, String name, List annotations) { return new AutoValue_TestInfo( @@ -195,7 +180,7 @@ abstract class TestInfo { private static int getMaxCharactersPerParameter(TestInfo testInfo, int numberOfParameters) { int maxLengthOfAllParameters = // Subtract 2 characters for square brackets - MAX_TEST_NAME_LENGTH - testInfo.getRealMethodName().length() - 2; + MAX_TEST_NAME_LENGTH - testInfo.getMethod().getName().length() - 2; // Subtract 4 characters to leave place for joining commas and the parameter index. return maxLengthOfAllParameters / numberOfParameters - 4; -- cgit v1.2.3 From 5bd07d2fa5125a78d29ec9f470b3d95590a0029c Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Thu, 27 Oct 2022 15:54:01 +0000 Subject: Add a test to show support for Kotlin @JvmInline value classes as @TestParameters method parameters Related to https://github.com/google/TestParameterInjector/issues/22 --- .../TestParameterInjectorKotlinTest.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt index e19c527..b903668 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt @@ -151,6 +151,23 @@ class TestParameterInjectorKotlinTest { } } + @RunAsTest + internal class TestParameters_MethodParam_WithValueClasses : SuccessfulTestCaseBase() { + @TestParameters("{width: 3, height: 8}") + @TestParameters("{width: 5, height: 2.5}") + @Test + fun test(width: Int, height: DoubleValueClass) { + storeTestParametersForThisTest(width, height.onlyValue) + } + + override fun expectedTestNameToStringifiedParameters(): ImmutableMap { + return ImmutableMap.builder() + .put("test-lvZ97mM[{width: 3, height: 8}]", "3:8.0") + .put("test-lvZ97mM[{width: 5, height: 2.5}]", "5:2.5") + .buildOrThrow() + } + } + @RunAsTest internal class TestParameters_ConstructorParam : SuccessfulTestCaseBase { val width: Int -- cgit v1.2.3 From bac47b76953e68383ebfc1934a74ca19b150efe3 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 28 Oct 2022 07:41:27 +0000 Subject: Add a test to show support for Kotlin @JvmInline value classes as @TestParameter method parameters Related to https://github.com/google/TestParameterInjector/issues/22 --- .../TestParameterInjectorKotlinTest.kt | 42 ++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt index b903668..e015533 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt @@ -72,6 +72,48 @@ class TestParameterInjectorKotlinTest { } } + @RunAsTest + internal class TestParameter_MethodParam_WithValueClasses : SuccessfulTestCaseBase() { + @Test + fun testString(@TestParameter("a", "b") param: StringValueClass) { + storeTestParametersForThisTest(param.onlyValue) + } + + @Test + fun testEnum_selection(@TestParameter("RED", "GREEN") param: ColorValueClass) { + storeTestParametersForThisTest(param.onlyValue) + } + + @Test + fun testEnum_all(@TestParameter param: ColorValueClass) { + storeTestParametersForThisTest(param.onlyValue) + } + + @Test + fun testMixed( + @TestParameter("1", "8") width: Int, + @TestParameter("1", "5.5") height: DoubleValueClass, + ) { + storeTestParametersForThisTest(width, height.onlyValue) + } + + override fun expectedTestNameToStringifiedParameters(): ImmutableMap { + return ImmutableMap.builder() + .put("testString-HMW45e8[a]", "a") + .put("testString-HMW45e8[b]", "b") + .put("testEnum_selection-fiSAjMM[RED]", "RED") + .put("testEnum_selection-fiSAjMM[GREEN]", "GREEN") + .put("testEnum_all-fiSAjMM[RED]", "RED") + .put("testEnum_all-fiSAjMM[BLUE]", "BLUE") + .put("testEnum_all-fiSAjMM[GREEN]", "GREEN") + .put("testMixed-lvZ97mM[width=1,height=1.0]", "1:1.0") + .put("testMixed-lvZ97mM[width=1,height=5.5]", "1:5.5") + .put("testMixed-lvZ97mM[width=8,height=1.0]", "8:1.0") + .put("testMixed-lvZ97mM[width=8,height=5.5]", "8:5.5") + .buildOrThrow() + } + } + @RunAsTest internal class TestParameter_Field : SuccessfulTestCaseBase() { @TestParameter("1", "2") var width: Int? = null -- cgit v1.2.3 From 26e5c4d4c1d9336293169fd82851be6e43c0dfae Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Thu, 10 Nov 2022 09:07:40 +0000 Subject: Remove dependency on MessageLite This is a first step towards fixing https://github.com/google/TestParameterInjector/issues/24 --- .../ParameterValueParsing.java | 30 ---------------------- .../testparameterinjector/ProtoValueParsing.java | 25 ------------------ .../junit/testparameterinjector/TestParameter.java | 7 ----- .../junit5/ParameterValueParsing.java | 30 ---------------------- .../junit5/ProtoValueParsing.java | 25 ------------------ .../junit5/TestParameter.java | 7 ----- 6 files changed, 124 deletions(-) delete mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java delete mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ProtoValueParsing.java diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java index d40454d..670d17d 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java @@ -24,7 +24,6 @@ import com.google.common.primitives.Primitives; import com.google.common.reflect.TypeToken; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.protobuf.ByteString; -import com.google.protobuf.MessageLite; import java.lang.reflect.ParameterizedType; import java.nio.charset.Charset; import java.util.LinkedHashMap; @@ -43,10 +42,6 @@ final class ParameterValueParsing { return Enum.valueOf((Class) 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); @@ -112,15 +107,6 @@ final class ParameterValueParsing { .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) map, javaType.getRawType())); - yamlValueTransformer .ifJavaType(byte[].class) .supportParsedType(byte[].class, self -> self) @@ -233,21 +219,5 @@ final class ParameterValueParsing { } } - 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/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java deleted file mode 100644 index 61cf13b..0000000 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ProtoValueParsing.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 map, Class javaType); -} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java index e08c7b8..7f9d93e 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java @@ -24,7 +24,6 @@ import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; 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; @@ -187,12 +186,6 @@ public @interface TestParameter { 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); } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java index 531ee1e..3511308 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java @@ -24,7 +24,6 @@ import com.google.common.primitives.Primitives; import com.google.common.reflect.TypeToken; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.protobuf.ByteString; -import com.google.protobuf.MessageLite; import java.lang.reflect.ParameterizedType; import java.nio.charset.Charset; import java.util.LinkedHashMap; @@ -43,10 +42,6 @@ final class ParameterValueParsing { return Enum.valueOf((Class) 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); @@ -112,15 +107,6 @@ final class ParameterValueParsing { .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) map, javaType.getRawType())); - yamlValueTransformer .ifJavaType(byte[].class) .supportParsedType(byte[].class, self -> self) @@ -233,21 +219,5 @@ final class ParameterValueParsing { } } - 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.junit5.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/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ProtoValueParsing.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ProtoValueParsing.java deleted file mode 100644 index d270295..0000000 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ProtoValueParsing.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2021 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package com.google.testing.junit.testparameterinjector.junit5; - -import com.google.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 map, Class javaType); -} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java index 6423b7a..dca6325 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java @@ -24,7 +24,6 @@ import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; -import com.google.protobuf.MessageLite; import com.google.testing.junit.testparameterinjector.junit5.TestParameter.InternalImplementationOfThisParameter; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; @@ -187,12 +186,6 @@ public @interface TestParameter { 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); } -- cgit v1.2.3 From 481c9c463e9d1f52e7ba1730964b78870b69ac7f Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Thu, 10 Nov 2022 09:08:49 +0000 Subject: Replace dependency on protobuf.ByteString by reflection in TestParameterAnnotationMethodProcessor This is the second step towards fixing https://github.com/google/TestParameterInjector/issues/24 The next and last step is to do the same for ParameterValueParsing, after which protobuf-javalite can be removed from the dependencies --- .../ByteStringReflection.java | 67 ++++++++++++++++++++++ .../TestParameterAnnotationMethodProcessor.java | 5 +- .../junit5/ByteStringReflection.java | 67 ++++++++++++++++++++++ .../TestParameterAnnotationMethodProcessor.java | 5 +- 4 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/ByteStringReflection.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ByteStringReflection.java diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ByteStringReflection.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ByteStringReflection.java new file mode 100644 index 0000000..56203a8 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ByteStringReflection.java @@ -0,0 +1,67 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import com.google.common.base.Optional; +import java.lang.reflect.InvocationTargetException; + +/** + * Utility methods to interact with com.google.protobuf.ByteString via reflection. + * + *

          This is a hack to avoid the open source project to depend on protobuf-lite/javalite, which is + * causing conflicts for users (see https://github.com/google/TestParameterInjector/issues/24). + */ +final class ByteStringReflection { + + private static final Optional> MAYBE_BYTE_STRING_CLASS = maybeGetByteStringClass(); + + /** Equivalent of {@code object instanceof ByteString} */ + static boolean isInstanceOfByteString(Object object) { + if (MAYBE_BYTE_STRING_CLASS.isPresent()) { + return MAYBE_BYTE_STRING_CLASS.get().isInstance(object); + } else { + return false; + } + } + + /** Eqvuivalent of {@code ((ByteString) byteString).toByteArray()} */ + static byte[] byteStringToByteArray(Object byteString) { + try { + return (byte[]) + MAYBE_BYTE_STRING_CLASS.get().getDeclaredMethod("toByteArray").invoke(byteString); + /* + * Do not merge the 3 catch blocks below. javac would infer a type of + * ReflectiveOperationException, which Animal Sniffer would reject. (Old versions of + * Android don't *seem* to mind, but there might be edge cases of which we're unaware.) + */ + } catch (IllegalAccessException e) { + throw new LinkageError("Accessing toByteArray()", e); + } catch (InvocationTargetException e) { + throw new LinkageError("Accessing toByteArray()", e); + } catch (NoSuchMethodException e) { + throw new LinkageError("Accessing toByteArray()", e); + } + } + + private static Optional> maybeGetByteStringClass() { + try { + return Optional.of(Class.forName("com.google.protobuf.ByteString")); + } catch (ClassNotFoundException unused) { + return Optional.absent(); + } + } + + private ByteStringReflection() {} // Inhibit instantiation +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 18be553..124e613 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -36,7 +36,6 @@ import com.google.common.collect.Lists; import com.google.common.collect.Range; import com.google.common.primitives.Primitives; import com.google.common.util.concurrent.UncheckedExecutionException; -import com.google.protobuf.ByteString; import com.google.testing.junit.testparameterinjector.TestInfo.TestInfoParameter; import java.io.Serializable; import java.lang.annotation.Annotation; @@ -147,8 +146,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } resultBuider.append("]"); return resultBuider.toString(); - } else if (value() instanceof ByteString) { - return Arrays.toString(((ByteString) value()).toByteArray()); + } else if (ByteStringReflection.isInstanceOfByteString(value())) { + return Arrays.toString(ByteStringReflection.byteStringToByteArray(value())); } else { return String.valueOf(value()); } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ByteStringReflection.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ByteStringReflection.java new file mode 100644 index 0000000..b801d17 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ByteStringReflection.java @@ -0,0 +1,67 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import com.google.common.base.Optional; +import java.lang.reflect.InvocationTargetException; + +/** + * Utility methods to interact with com.google.protobuf.ByteString via reflection. + * + *

          This is a hack to avoid the open source project to depend on protobuf-lite/javalite, which is + * causing conflicts for users (see https://github.com/google/TestParameterInjector/issues/24). + */ +final class ByteStringReflection { + + private static final Optional> MAYBE_BYTE_STRING_CLASS = maybeGetByteStringClass(); + + /** Equivalent of {@code object instanceof ByteString} */ + static boolean isInstanceOfByteString(Object object) { + if (MAYBE_BYTE_STRING_CLASS.isPresent()) { + return MAYBE_BYTE_STRING_CLASS.get().isInstance(object); + } else { + return false; + } + } + + /** Eqvuivalent of {@code ((ByteString) byteString).toByteArray()} */ + static byte[] byteStringToByteArray(Object byteString) { + try { + return (byte[]) + MAYBE_BYTE_STRING_CLASS.get().getDeclaredMethod("toByteArray").invoke(byteString); + /* + * Do not merge the 3 catch blocks below. javac would infer a type of + * ReflectiveOperationException, which Animal Sniffer would reject. (Old versions of + * Android don't *seem* to mind, but there might be edge cases of which we're unaware.) + */ + } catch (IllegalAccessException e) { + throw new LinkageError("Accessing toByteArray()", e); + } catch (InvocationTargetException e) { + throw new LinkageError("Accessing toByteArray()", e); + } catch (NoSuchMethodException e) { + throw new LinkageError("Accessing toByteArray()", e); + } + } + + private static Optional> maybeGetByteStringClass() { + try { + return Optional.of(Class.forName("com.google.protobuf.ByteString")); + } catch (ClassNotFoundException unused) { + return Optional.absent(); + } + } + + private ByteStringReflection() {} // Inhibit instantiation +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java index 08c79b3..f14d6f5 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java @@ -36,7 +36,6 @@ import com.google.common.collect.Lists; import com.google.common.collect.Range; import com.google.common.primitives.Primitives; import com.google.common.util.concurrent.UncheckedExecutionException; -import com.google.protobuf.ByteString; import com.google.testing.junit.testparameterinjector.junit5.TestInfo.TestInfoParameter; import java.io.Serializable; import java.lang.annotation.Annotation; @@ -147,8 +146,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } resultBuider.append("]"); return resultBuider.toString(); - } else if (value() instanceof ByteString) { - return Arrays.toString(((ByteString) value()).toByteArray()); + } else if (ByteStringReflection.isInstanceOfByteString(value())) { + return Arrays.toString(ByteStringReflection.byteStringToByteArray(value())); } else { return String.valueOf(value()); } -- cgit v1.2.3 From 46c575829ab9e7a37cf96da1972882b6564570eb Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Mon, 14 Nov 2022 09:52:09 +0000 Subject: Bugfix: Recover from LinkageErrors if they happen when getting the ByteString class. This is part of https://github.com/google/TestParameterInjector/issues/24 --- .../testing/junit/testparameterinjector/ByteStringReflection.java | 2 +- .../junit/testparameterinjector/junit5/ByteStringReflection.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ByteStringReflection.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ByteStringReflection.java index 56203a8..ba03726 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ByteStringReflection.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ByteStringReflection.java @@ -58,7 +58,7 @@ final class ByteStringReflection { private static Optional> maybeGetByteStringClass() { try { return Optional.of(Class.forName("com.google.protobuf.ByteString")); - } catch (ClassNotFoundException unused) { + } catch (ClassNotFoundException | LinkageError unused) { return Optional.absent(); } } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ByteStringReflection.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ByteStringReflection.java index b801d17..a75bd1d 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ByteStringReflection.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ByteStringReflection.java @@ -58,7 +58,7 @@ final class ByteStringReflection { private static Optional> maybeGetByteStringClass() { try { return Optional.of(Class.forName("com.google.protobuf.ByteString")); - } catch (ClassNotFoundException unused) { + } catch (ClassNotFoundException | LinkageError unused) { return Optional.absent(); } } -- cgit v1.2.3 From 0d48f9543f545f26f6d235ecefed3dba8b2665d7 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Mon, 14 Nov 2022 12:53:30 +0000 Subject: Replace dependency on protobuf.ByteString by reflection in ParameterValueParsing This fixes https://github.com/google/TestParameterInjector/issues/24 by removing the open source dependency on protobuf-javalite --- CHANGELOG.md | 5 +++ .../ByteStringReflection.java | 43 +++++++++++++++++++--- .../ParameterValueParsing.java | 11 +++--- .../junit5/ByteStringReflection.java | 43 +++++++++++++++++++--- .../junit5/ParameterValueParsing.java | 11 +++--- pom.xml | 11 +++--- 6 files changed, 97 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a5f9ae..a1d3ec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.10 + +- Removed dependency on `protobuf-javalite` (see + [issue #24](https://github.com/google/TestParameterInjector/issues/24)) + ## 1.9 - Bugfix: Support explicit ordering by the JUnit4 `@Rule`. For example: `@Rule(ordering=3)`. diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ByteStringReflection.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ByteStringReflection.java index ba03726..ca94a39 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ByteStringReflection.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ByteStringReflection.java @@ -15,6 +15,7 @@ package com.google.testing.junit.testparameterinjector; import com.google.common.base.Optional; +import com.google.common.collect.ImmutableMap; import java.lang.reflect.InvocationTargetException; /** @@ -25,7 +26,7 @@ import java.lang.reflect.InvocationTargetException; */ final class ByteStringReflection { - private static final Optional> MAYBE_BYTE_STRING_CLASS = maybeGetByteStringClass(); + static final Optional> MAYBE_BYTE_STRING_CLASS = maybeGetByteStringClass(); /** Equivalent of {@code object instanceof ByteString} */ static boolean isInstanceOfByteString(Object object) { @@ -38,20 +39,50 @@ final class ByteStringReflection { /** Eqvuivalent of {@code ((ByteString) byteString).toByteArray()} */ static byte[] byteStringToByteArray(Object byteString) { + return (byte[]) + invokeByteStringMethod("toByteArray", /* obj= */ byteString, /* args= */ ImmutableMap.of()); + } + + /** + * Eqvuivalent of {@code ByteString.copyFromUtf8(text)}. + * + *

          Encodes {@code text} into a sequence of UTF-8 bytes and returns the result as a {@code + * ByteString}. + */ + static Object copyFromUtf8(String text) { + return invokeByteStringMethod( + "copyFromUtf8", /* obj= */ null, /* args= */ ImmutableMap.of(String.class, text)); + } + + /** + * Eqvuivalent of {@code ByteString.copyFrom(bytes)}. + * + *

          Copies the given bytes into a {@code ByteString}. + */ + static Object copyFrom(byte[] bytes) { + return invokeByteStringMethod( + "copyFrom", /* obj= */ null, /* args= */ ImmutableMap.of(byte[].class, bytes)); + } + + @SuppressWarnings("UseMultiCatch") + private static Object invokeByteStringMethod( + String methodName, Object obj, ImmutableMap, ?> args) { try { - return (byte[]) - MAYBE_BYTE_STRING_CLASS.get().getDeclaredMethod("toByteArray").invoke(byteString); + return MAYBE_BYTE_STRING_CLASS + .get() + .getMethod(methodName, args.keySet().toArray(new Class[0])) + .invoke(obj, args.values().toArray()); /* * Do not merge the 3 catch blocks below. javac would infer a type of * ReflectiveOperationException, which Animal Sniffer would reject. (Old versions of * Android don't *seem* to mind, but there might be edge cases of which we're unaware.) */ } catch (IllegalAccessException e) { - throw new LinkageError("Accessing toByteArray()", e); + throw new LinkageError(String.format("Accessing %s()", methodName), e); } catch (InvocationTargetException e) { - throw new LinkageError("Accessing toByteArray()", e); + throw new LinkageError(String.format("Calling %s()", methodName), e); } catch (NoSuchMethodException e) { - throw new LinkageError("Accessing toByteArray()", e); + throw new LinkageError(String.format("Calling %s()", methodName), e); } } diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java index 670d17d..f1d9131 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java @@ -23,7 +23,6 @@ import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; import com.google.common.reflect.TypeToken; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import com.google.protobuf.ByteString; import java.lang.reflect.ParameterizedType; import java.nio.charset.Charset; import java.util.LinkedHashMap; @@ -115,10 +114,12 @@ final class ParameterValueParsing { // See https://developer.android.com/reference/java/nio/charset/StandardCharsets. .supportParsedType(String.class, s -> s.getBytes(Charset.forName("UTF-8"))); - yamlValueTransformer - .ifJavaType(ByteString.class) - .supportParsedType(String.class, ByteString::copyFromUtf8) - .supportParsedType(byte[].class, ByteString::copyFrom); + if (ByteStringReflection.MAYBE_BYTE_STRING_CLASS.isPresent()) { + yamlValueTransformer + .ifJavaType((Class) ByteStringReflection.MAYBE_BYTE_STRING_CLASS.get()) + .supportParsedType(String.class, ByteStringReflection::copyFromUtf8) + .supportParsedType(byte[].class, ByteStringReflection::copyFrom); + } // Added mainly for protocol buffer parsing yamlValueTransformer diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ByteStringReflection.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ByteStringReflection.java index a75bd1d..80cac0b 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ByteStringReflection.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ByteStringReflection.java @@ -15,6 +15,7 @@ package com.google.testing.junit.testparameterinjector.junit5; import com.google.common.base.Optional; +import com.google.common.collect.ImmutableMap; import java.lang.reflect.InvocationTargetException; /** @@ -25,7 +26,7 @@ import java.lang.reflect.InvocationTargetException; */ final class ByteStringReflection { - private static final Optional> MAYBE_BYTE_STRING_CLASS = maybeGetByteStringClass(); + static final Optional> MAYBE_BYTE_STRING_CLASS = maybeGetByteStringClass(); /** Equivalent of {@code object instanceof ByteString} */ static boolean isInstanceOfByteString(Object object) { @@ -38,20 +39,50 @@ final class ByteStringReflection { /** Eqvuivalent of {@code ((ByteString) byteString).toByteArray()} */ static byte[] byteStringToByteArray(Object byteString) { + return (byte[]) + invokeByteStringMethod("toByteArray", /* obj= */ byteString, /* args= */ ImmutableMap.of()); + } + + /** + * Eqvuivalent of {@code ByteString.copyFromUtf8(text)}. + * + *

          Encodes {@code text} into a sequence of UTF-8 bytes and returns the result as a {@code + * ByteString}. + */ + static Object copyFromUtf8(String text) { + return invokeByteStringMethod( + "copyFromUtf8", /* obj= */ null, /* args= */ ImmutableMap.of(String.class, text)); + } + + /** + * Eqvuivalent of {@code ByteString.copyFrom(bytes)}. + * + *

          Copies the given bytes into a {@code ByteString}. + */ + static Object copyFrom(byte[] bytes) { + return invokeByteStringMethod( + "copyFrom", /* obj= */ null, /* args= */ ImmutableMap.of(byte[].class, bytes)); + } + + @SuppressWarnings("UseMultiCatch") + private static Object invokeByteStringMethod( + String methodName, Object obj, ImmutableMap, ?> args) { try { - return (byte[]) - MAYBE_BYTE_STRING_CLASS.get().getDeclaredMethod("toByteArray").invoke(byteString); + return MAYBE_BYTE_STRING_CLASS + .get() + .getMethod(methodName, args.keySet().toArray(new Class[0])) + .invoke(obj, args.values().toArray()); /* * Do not merge the 3 catch blocks below. javac would infer a type of * ReflectiveOperationException, which Animal Sniffer would reject. (Old versions of * Android don't *seem* to mind, but there might be edge cases of which we're unaware.) */ } catch (IllegalAccessException e) { - throw new LinkageError("Accessing toByteArray()", e); + throw new LinkageError(String.format("Accessing %s()", methodName), e); } catch (InvocationTargetException e) { - throw new LinkageError("Accessing toByteArray()", e); + throw new LinkageError(String.format("Calling %s()", methodName), e); } catch (NoSuchMethodException e) { - throw new LinkageError("Accessing toByteArray()", e); + throw new LinkageError(String.format("Calling %s()", methodName), e); } } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java index 3511308..c330a91 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java @@ -23,7 +23,6 @@ import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; import com.google.common.reflect.TypeToken; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import com.google.protobuf.ByteString; import java.lang.reflect.ParameterizedType; import java.nio.charset.Charset; import java.util.LinkedHashMap; @@ -115,10 +114,12 @@ final class ParameterValueParsing { // See https://developer.android.com/reference/java/nio/charset/StandardCharsets. .supportParsedType(String.class, s -> s.getBytes(Charset.forName("UTF-8"))); - yamlValueTransformer - .ifJavaType(ByteString.class) - .supportParsedType(String.class, ByteString::copyFromUtf8) - .supportParsedType(byte[].class, ByteString::copyFrom); + if (ByteStringReflection.MAYBE_BYTE_STRING_CLASS.isPresent()) { + yamlValueTransformer + .ifJavaType((Class) ByteStringReflection.MAYBE_BYTE_STRING_CLASS.get()) + .supportParsedType(String.class, ByteStringReflection::copyFromUtf8) + .supportParsedType(byte[].class, ByteStringReflection::copyFrom); + } // Added mainly for protocol buffer parsing yamlValueTransformer diff --git a/pom.xml b/pom.xml index 8936586..b5c0d07 100644 --- a/pom.xml +++ b/pom.xml @@ -139,11 +139,6 @@ guava 30.1-jre - - com.google.protobuf - protobuf-javalite - 3.20.0 - org.yaml snakeyaml @@ -157,6 +152,12 @@ 1.1.2 test + + com.google.protobuf + protobuf-javalite + 3.20.0 + test + -- cgit v1.2.3 From 58d298f97e0a9fc3e1288ebbcc57d8b8b50458f9 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Mon, 14 Nov 2022 14:37:58 +0000 Subject: pom.xml: Add -Xlint:deprecation so that deprecation warnings are printed out in full --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index b5c0d07..f8aa0da 100644 --- a/pom.xml +++ b/pom.xml @@ -176,6 +176,7 @@ 1.8 1.8 true + -Xlint:deprecation com.google.auto.value -- cgit v1.2.3 From 32089c6e837f588648298fe0b8a6181f83ed5c43 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 15 Nov 2022 13:17:29 +0000 Subject: Replace deprecated method BlockJUnit4ClassRunner.withPotentialTimeout(). withPotentialTimeout() is deprecated and 'will be private soon' --- .../testparameterinjector/PluggableTestRunner.java | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java index 10cd52a..27d5bc5 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java @@ -28,10 +28,12 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.TimeUnit; import org.junit.Rule; import org.junit.Test; import org.junit.internal.runners.model.ReflectiveCallable; import org.junit.internal.runners.statements.Fail; +import org.junit.internal.runners.statements.FailOnTimeout; import org.junit.rules.MethodRule; import org.junit.rules.TestRule; import org.junit.runner.Description; @@ -209,13 +211,29 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { Statement statement = methodInvoker(method, testObject); statement = possiblyExpectingExceptions(method, testObject, statement); - statement = withPotentialTimeout(method, testObject, statement); + statement = withPotentialTimeoutInternal(method, testObject, statement); statement = withBefores(method, testObject, statement); statement = withAfters(method, testObject, statement); statement = withRules(method, testObject, statement); return statement; } + // Note: This does the same as BlockJUnit4ClassRunner.withPotentialTimeout(), which is deprecated + // and will soon be private. + private Statement withPotentialTimeoutInternal( + FrameworkMethod method, Object test, Statement next) { + Test testAnnotation = method.getAnnotation(Test.class); + if (testAnnotation == null) { + return next; + } else if (testAnnotation.timeout() <= 0) { + return next; + } else { + return FailOnTimeout.builder() + .withTimeout(testAnnotation.timeout(), TimeUnit.MILLISECONDS) + .build(next); + } + } + @Override protected final Statement methodInvoker(FrameworkMethod frameworkMethod, Object testObject) { TestInfo testInfo = ((OverriddenFrameworkMethod) frameworkMethod).getTestInfo(); -- cgit v1.2.3 From 0158f78c18c0ce62733adb1e4270c87adc2e5099 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 16 Nov 2022 09:20:59 +0000 Subject: Bump version to v1.10 in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 19d25c1..2a07e77 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ And add the following dependency to your `.pom` file: com.google.testparameterinjector test-parameter-injector - 1.9 + 1.10 test ``` @@ -97,7 +97,7 @@ And add the following dependency to your `.pom` file: com.google.testparameterinjector test-parameter-injector-junit5 - 1.9 + 1.10 test ``` -- cgit v1.2.3 From f5b7baf65a98f190a1d067a5bd3e1f75c8c5bd9a Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 16 Nov 2022 09:22:30 +0000 Subject: Aribtrary auto-formatting changes --- .../TestParameterAnnotationMethodProcessor.java | 6 +++--- .../junit5/TestParameterAnnotationMethodProcessor.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 124e613..8d48503 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -567,7 +567,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso Class valueMethodReturnType = getValueMethodReturnType( testParameterAnnotationType.annotationType(), - /* paramClass = */ Optional.of(parameterType)); + /* paramClass= */ Optional.of(parameterType)); if (!parameterType.isAssignableFrom(valueMethodReturnType)) { errors.add( new IllegalStateException( @@ -600,7 +600,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso testParameterAnnotationTypes) { if (parameterType.isAssignableFrom( getValueMethodReturnType( - testParameterAnnotationType, /* paramClass = */ Optional.absent()))) { + testParameterAnnotationType, /* paramClass= */ Optional.absent()))) { if (matchingTestParameterAnnotationFound) { errors.add( new IllegalStateException( @@ -1151,7 +1151,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso if (methodParameterType.isAssignableFrom( getValueMethodReturnType( testParameterValue.annotationTypeOrigin().annotationType(), - /* paramClass = */ Optional.absent()))) { + /* paramClass= */ Optional.absent()))) { return testParameterValue.value(); } } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java index f14d6f5..737ab74 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java @@ -567,7 +567,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso Class valueMethodReturnType = getValueMethodReturnType( testParameterAnnotationType.annotationType(), - /* paramClass = */ Optional.of(parameterType)); + /* paramClass= */ Optional.of(parameterType)); if (!parameterType.isAssignableFrom(valueMethodReturnType)) { errors.add( new IllegalStateException( @@ -600,7 +600,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso testParameterAnnotationTypes) { if (parameterType.isAssignableFrom( getValueMethodReturnType( - testParameterAnnotationType, /* paramClass = */ Optional.absent()))) { + testParameterAnnotationType, /* paramClass= */ Optional.absent()))) { if (matchingTestParameterAnnotationFound) { errors.add( new IllegalStateException( @@ -1151,7 +1151,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso if (methodParameterType.isAssignableFrom( getValueMethodReturnType( testParameterValue.annotationTypeOrigin().annotationType(), - /* paramClass = */ Optional.absent()))) { + /* paramClass= */ Optional.absent()))) { return testParameterValue.value(); } } -- cgit v1.2.3 From e4314f91238465874b46b26f2271dc59a43a708e Mon Sep 17 00:00:00 2001 From: Oliver Eikemeier Date: Thu, 23 Mar 2023 15:02:41 +0100 Subject: Replace deprecated call to org.yaml.snakeyaml.constructor.SafeConstructor Signed-off-by: Oliver Eikemeier --- .../testing/junit/testparameterinjector/ParameterValueParsing.java | 5 +++-- .../junit/testparameterinjector/junit5/ParameterValueParsing.java | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java index f1d9131..1c32781 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.annotation.Nullable; +import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; @@ -43,7 +44,7 @@ final class ParameterValueParsing { static boolean isValidYamlString(String yamlString) { try { - new Yaml(new SafeConstructor()).load(yamlString); + new Yaml(new SafeConstructor(new LoaderOptions())).load(yamlString); return true; } catch (RuntimeException e) { return false; @@ -55,7 +56,7 @@ final class ParameterValueParsing { } static Object parseYamlStringToObject(String yamlString) { - return new Yaml(new SafeConstructor()).load(yamlString); + return new Yaml(new SafeConstructor(new LoaderOptions())).load(yamlString); } @SuppressWarnings({"unchecked"}) diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java index c330a91..1f60c53 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.annotation.Nullable; +import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; @@ -43,7 +44,7 @@ final class ParameterValueParsing { static boolean isValidYamlString(String yamlString) { try { - new Yaml(new SafeConstructor()).load(yamlString); + new Yaml(new SafeConstructor(new LoaderOptions())).load(yamlString); return true; } catch (RuntimeException e) { return false; @@ -55,7 +56,7 @@ final class ParameterValueParsing { } static Object parseYamlStringToObject(String yamlString) { - return new Yaml(new SafeConstructor()).load(yamlString); + return new Yaml(new SafeConstructor(new LoaderOptions())).load(yamlString); } @SuppressWarnings({"unchecked"}) -- cgit v1.2.3 From 75931e3a3185d40226f1bd884351d339870e6040 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 24 Mar 2023 12:31:04 +0000 Subject: Remove Alex Jurkowski as developer --- pom.xml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pom.xml b/pom.xml index f8aa0da..29fc376 100644 --- a/pom.xml +++ b/pom.xml @@ -78,17 +78,6 @@ +0 - - ajurkowski - Alex Jurkowski - ajurkowski@google.com - Google Inc. - http://www.google.com/ - - developer - - -6 - -- cgit v1.2.3 From 8258a53e4d725066b5b24cf2ce23f7cd3e733759 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Mon, 27 Mar 2023 08:14:11 +0000 Subject: Bump version to v1.11 in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2a07e77..faae4b8 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ And add the following dependency to your `.pom` file: com.google.testparameterinjector test-parameter-injector - 1.10 + 1.11 test ``` @@ -97,7 +97,7 @@ And add the following dependency to your `.pom` file: com.google.testparameterinjector test-parameter-injector-junit5 - 1.10 + 1.11 test ``` -- cgit v1.2.3 From e0383ef1074c966292074c2249cb3d56ebcfc603 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 19 May 2023 12:14:40 +0000 Subject: Remove unused field @TestParameterAnnotation.name --- .../junit/testparameterinjector/TestParameterAnnotation.java | 7 ------- .../TestParameterAnnotationMethodProcessor.java | 5 +---- .../testparameterinjector/junit5/TestParameterAnnotation.java | 7 ------- .../junit5/TestParameterAnnotationMethodProcessor.java | 5 +---- 4 files changed, 2 insertions(+), 22 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java index 9a2cc46..deb4cd5 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java @@ -28,7 +28,6 @@ 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; /** @@ -143,12 +142,6 @@ import java.util.List; @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 validator() default DefaultValidator.class; diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 8d48503..010d549 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -46,7 +46,6 @@ 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; @@ -116,11 +115,9 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso */ String toTestNameString() { Class 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 @@ -130,7 +127,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso .trim() .replaceAll("\\s+", " "); } else { - return MessageFormat.format(namePattern, valueAsString()).trim().replaceAll("\\s+", " "); + return valueAsString().trim().replaceAll("\\s+", " "); } } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotation.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotation.java index 8d9051e..255e4f5 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotation.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotation.java @@ -28,7 +28,6 @@ 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; /** @@ -143,12 +142,6 @@ import java.util.List; @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 validator() default DefaultValidator.class; diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java index 737ab74..8ddbb1c 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java @@ -46,7 +46,6 @@ 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; @@ -116,11 +115,9 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso */ String toTestNameString() { Class 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 @@ -130,7 +127,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso .trim() .replaceAll("\\s+", " "); } else { - return MessageFormat.format(namePattern, valueAsString()).trim().replaceAll("\\s+", " "); + return valueAsString().trim().replaceAll("\\s+", " "); } } -- cgit v1.2.3 From 5581864fb4a475016aa02793b067db3cbbf50801 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 23 May 2023 08:20:53 +0000 Subject: Prefix ambiguous strings and null values with the parameter name --- .../TestParameterAnnotationMethodProcessor.java | 17 +++++++++++------ ...TestParameterAnnotationMethodProcessorTest.java | 8 ++++---- .../testparameterinjector/TestParameterTest.java | 6 +++--- .../TestParameterAnnotationMethodProcessor.java | 17 +++++++++++------ .../junit5/TestParameterInjectorJUnit5Test.java | 22 +++++++++++----------- 5 files changed, 40 insertions(+), 30 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 010d549..f05c174 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -21,6 +21,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.google.auto.value.AutoAnnotation; import com.google.auto.value.AutoValue; +import com.google.common.base.CharMatcher; import com.google.common.base.Optional; import com.google.common.base.Throwables; import com.google.common.cache.Cache; @@ -114,13 +115,17 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso * brackets). */ String toTestNameString() { - Class annotationType = annotationTypeOrigin().annotationType(); - if (paramName().isPresent() - && paramClass().isPresent() - && 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 + && (value() == null + || + // Primitives are often ambiguous + Primitives.unwrap(value().getClass()).isPrimitive() + // Ambiguous String cases + || value().equals("null") + || (value() instanceof CharSequence + && CharMatcher.anyOf("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + .matchesNoneOf((CharSequence) value())))) { + // 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(), valueAsString()) diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java index dd3f1c5..3fff85b 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java @@ -289,10 +289,10 @@ public class TestParameterAnnotationMethodProcessorTest { return ImmutableMap.builder() .put("test1[1.ABC]", "ABC") .put("test1[2.ABC]", "ABC") - .put("test2[123 (Integer)]", "123") - .put("test2[123 (String)]", "123") - .put("test2[null (String)]", "null") - .put("test2[null (null reference)]", "null") + .put("test2[testObject=123 (Integer)]", "123") + .put("test2[testObject=123 (String)]", "123") + .put("test2[testObject=null (String)]", "null") + .put("test2[testObject=null (null reference)]", "null") .build(); } } diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java index e099521..bc7123b 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java @@ -131,8 +131,8 @@ public class TestParameterTest { @Test public void stringTest( - @TestParameter(valuesProvider = TestStringProvider.class) String string) { - storeTestParametersForThisTest(string); + @TestParameter(valuesProvider = TestStringProvider.class) String stringParam) { + storeTestParametersForThisTest(stringParam); } @Test @@ -146,7 +146,7 @@ public class TestParameterTest { return ImmutableMap.builder() .put("stringTest[A]", "A") .put("stringTest[B]", "B") - .put("stringTest[null]", "null") + .put("stringTest[stringParam=null]", "null") .put("charMatcherTest[CharMatcher.any()]", "CharMatcher.any()") .put("charMatcherTest[CharMatcher.ascii()]", "CharMatcher.ascii()") .put("charMatcherTest[CharMatcher.whitespace()]", "CharMatcher.whitespace()") diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java index 8ddbb1c..eb3d658 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java @@ -21,6 +21,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.google.auto.value.AutoAnnotation; import com.google.auto.value.AutoValue; +import com.google.common.base.CharMatcher; import com.google.common.base.Optional; import com.google.common.base.Throwables; import com.google.common.cache.Cache; @@ -114,13 +115,17 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso * brackets). */ String toTestNameString() { - Class annotationType = annotationTypeOrigin().annotationType(); - if (paramName().isPresent() - && paramClass().isPresent() - && 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 + && (value() == null + || + // Primitives are often ambiguous + Primitives.unwrap(value().getClass()).isPrimitive() + // Ambiguous String cases + || value().equals("null") + || (value() instanceof CharSequence + && CharMatcher.anyOf("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + .matchesNoneOf((CharSequence) value())))) { + // 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(), valueAsString()) diff --git a/junit5/src/test/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorJUnit5Test.java b/junit5/src/test/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorJUnit5Test.java index 59ffb18..25b6316 100644 --- a/junit5/src/test/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorJUnit5Test.java +++ b/junit5/src/test/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorJUnit5Test.java @@ -138,8 +138,8 @@ class TestParameterInjectorJUnit5Test { .put("withoutParameters", "") .put("withParameters_success[{name: 1, number: 3.3}]", "1:3.3") .put("withParameters_success[{name: abc, number: 5}]", "abc:5.0") - .put("withParameter_success[2,bool=false]", "2:false") - .put("withParameter_success[2,bool=true]", "2:true") + .put("withParameter_success[name=2,bool=false]", "2:false") + .put("withParameter_success[name=2,bool=true]", "2:true") .put("withParameter_success[xyz,bool=false]", "xyz:false") .put("withParameter_success[xyz,bool=true]", "xyz:true") .build(); @@ -186,13 +186,13 @@ class TestParameterInjectorJUnit5Test { .put("withParameters_success[{name: abc},AAA,constr=true]", "true:AAA:abc") .put("withParameters_success[{name: abc},BBB,constr=false]", "false:BBB:abc") .put("withParameters_success[{name: abc},BBB,constr=true]", "true:BBB:abc") - .put("withParameter_success[AAA,constr=false,2]", "false:AAA:2") + .put("withParameter_success[AAA,constr=false,name=2]", "false:AAA:2") .put("withParameter_success[AAA,constr=false,xyz]", "false:AAA:xyz") - .put("withParameter_success[AAA,constr=true,2]", "true:AAA:2") + .put("withParameter_success[AAA,constr=true,name=2]", "true:AAA:2") .put("withParameter_success[AAA,constr=true,xyz]", "true:AAA:xyz") - .put("withParameter_success[BBB,constr=false,2]", "false:BBB:2") + .put("withParameter_success[BBB,constr=false,name=2]", "false:BBB:2") .put("withParameter_success[BBB,constr=false,xyz]", "false:BBB:xyz") - .put("withParameter_success[BBB,constr=true,2]", "true:BBB:2") + .put("withParameter_success[BBB,constr=true,name=2]", "true:BBB:2") .put("withParameter_success[BBB,constr=true,xyz]", "true:BBB:xyz") .put("withoutParameters[AAA,constr=false]", "false:AAA") .put("withoutParameters[AAA,constr=true]", "true:AAA") @@ -244,13 +244,13 @@ class TestParameterInjectorJUnit5Test { .put("withParameters_success[{constr: false},{name: 1},BBB]", "false:BBB:1") .put("withParameters_success[{constr: false},{name: abc},AAA]", "false:AAA:abc") .put("withParameters_success[{constr: false},{name: abc},BBB]", "false:BBB:abc") - .put("withParameter_success[{constr: true},AAA,2]", "true:AAA:2") + .put("withParameter_success[{constr: true},AAA,name=2]", "true:AAA:2") .put("withParameter_success[{constr: true},AAA,xyz]", "true:AAA:xyz") - .put("withParameter_success[{constr: true},BBB,2]", "true:BBB:2") + .put("withParameter_success[{constr: true},BBB,name=2]", "true:BBB:2") .put("withParameter_success[{constr: true},BBB,xyz]", "true:BBB:xyz") - .put("withParameter_success[{constr: false},AAA,2]", "false:AAA:2") + .put("withParameter_success[{constr: false},AAA,name=2]", "false:AAA:2") .put("withParameter_success[{constr: false},AAA,xyz]", "false:AAA:xyz") - .put("withParameter_success[{constr: false},BBB,2]", "false:BBB:2") + .put("withParameter_success[{constr: false},BBB,name=2]", "false:BBB:2") .put("withParameter_success[{constr: false},BBB,xyz]", "false:BBB:xyz") .put("withoutParameters[{constr: true},AAA]", "true:AAA") .put("withoutParameters[{constr: true},BBB]", "true:BBB") @@ -302,7 +302,7 @@ class TestParameterInjectorJUnit5Test { return ImmutableMap.builder() .put("stringTest[A]", "A") .put("stringTest[B]", "B") - .put("stringTest[null]", "null") + .put("stringTest[string=null]", "null") .put("charMatcherTest[CharMatcher.any()]", "CharMatcher.any()") .put("charMatcherTest[CharMatcher.ascii()]", "CharMatcher.ascii()") .put("charMatcherTest[CharMatcher.whitespace()]", "CharMatcher.whitespace()") -- cgit v1.2.3 From 3b217539ee1bbbab7c53713b0ce06fa7a9a6bf94 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 23 May 2023 08:31:45 +0000 Subject: Add changelog entries for the recent changes --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1d3ec3..77324fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 1.12 + +- Tweak to the test name generation: Show the parameter name if its value is potentially + ambiguous (e.g. null, "" or "123"). +- Made `TestParametersValues.name()` optional. If missing, a name will be generated. + +## 1.11 + +- Replaced deprecated call to org.yaml.snakeyaml.constructor.SafeConstructor + ## 1.10 - Removed dependency on `protobuf-javalite` (see -- cgit v1.2.3 From 7f9e335bd09d6b2d1ea64a770c5e9c6abb6dce84 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 23 May 2023 08:36:47 +0000 Subject: Make TestParametersValues.name() optional --- .../ParameterValueParsing.java | 44 ++++++++++++++++++++++ .../TestParameterAnnotationMethodProcessor.java | 42 +-------------------- .../testparameterinjector/TestParameters.java | 21 ++++++++++- .../ParameterValueParsingTest.java | 34 +++++++++++++++++ .../TestParametersMethodProcessorTest.java | 37 +++++++++++++++++- .../junit5/ParameterValueParsing.java | 44 ++++++++++++++++++++++ .../TestParameterAnnotationMethodProcessor.java | 42 +-------------------- .../junit5/TestParameters.java | 21 ++++++++++- 8 files changed, 197 insertions(+), 88 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java index 1c32781..09e75af 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java @@ -18,13 +18,17 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import com.google.common.base.CharMatcher; import com.google.common.base.Function; +import com.google.common.base.Optional; import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; import com.google.common.reflect.TypeToken; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.lang.reflect.Array; import java.lang.reflect.ParameterizedType; import java.nio.charset.Charset; +import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -221,5 +225,45 @@ final class ParameterValueParsing { } } + static String formatTestNameString(Optional parameterName, @Nullable Object value) { + String result = valueAsString(value); + if (parameterName.isPresent()) { + if (value == null + || + // Primitives are often ambiguous + Primitives.unwrap(value.getClass()).isPrimitive() + // Ambiguous String cases + || value.equals("null") + || (value instanceof CharSequence + && CharMatcher.anyOf("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + .matchesNoneOf((CharSequence) value))) { + // Prefix the parameter value with its field name. This is to avoid test names + // such as myMethod_success[true,false,2]. Instead, it'll be + // myMethod_success[dryRun=true,experimentFlag=false,retries=2]. + result = String.format("%s=%s", parameterName.get(), valueAsString(value)); + } + } + return result.trim().replaceAll("\\s+", " "); + } + + private static String valueAsString(Object value) { + if (value != null && value.getClass().isArray()) { + StringBuilder resultBuider = new StringBuilder(); + resultBuider.append("["); + for (int i = 0; i < Array.getLength(value); i++) { + if (i > 0) { + resultBuider.append(", "); + } + resultBuider.append(Array.get(value, i)); + } + resultBuider.append("]"); + return resultBuider.toString(); + } else if (ByteStringReflection.isInstanceOfByteString(value)) { + return Arrays.toString(ByteStringReflection.byteStringToByteArray(value)); + } else { + return String.valueOf(value); + } + } + private ParameterValueParsing() {} } diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index f05c174..4e49df9 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -21,7 +21,6 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.google.auto.value.AutoAnnotation; import com.google.auto.value.AutoValue; -import com.google.common.base.CharMatcher; import com.google.common.base.Optional; import com.google.common.base.Throwables; import com.google.common.cache.Cache; @@ -35,14 +34,12 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Range; -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.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -115,44 +112,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso * brackets). */ String toTestNameString() { - if (paramName().isPresent() - && (value() == null - || - // Primitives are often ambiguous - Primitives.unwrap(value().getClass()).isPrimitive() - // Ambiguous String cases - || value().equals("null") - || (value() instanceof CharSequence - && CharMatcher.anyOf("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - .matchesNoneOf((CharSequence) value())))) { - // 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(), valueAsString()) - .trim() - .replaceAll("\\s+", " "); - } else { - return valueAsString().trim().replaceAll("\\s+", " "); - } - } - - private String valueAsString() { - if (value() != null && value().getClass().isArray()) { - StringBuilder resultBuider = new StringBuilder(); - resultBuider.append("["); - for (int i = 0; i < Array.getLength(value()); i++) { - if (i > 0) { - resultBuider.append(", "); - } - resultBuider.append(Array.get(value(), i)); - } - resultBuider.append("]"); - return resultBuider.toString(); - } else if (ByteStringReflection.isInstanceOfByteString(value())) { - return Arrays.toString(ByteStringReflection.byteStringToByteArray(value())); - } else { - return String.valueOf(value()); - } + return ParameterValueParsing.formatTestNameString(paramName(), value()); } public static ImmutableList create( diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java index 9b1c5c9..684e770 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java @@ -14,14 +14,15 @@ 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.base.Optional; import com.google.common.collect.ImmutableList; +import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValuesProvider; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; @@ -207,6 +208,8 @@ public @interface TestParameters { /** * Sets a name for this set of parameters that will be used for describing this test. * + *

          Setting a name is optional. If unset, one will be generated from the parameter values. + * *

          Example: If a test method is called "personIsAdult" and this name is "teenager", the * name of the resulting test will be "personIsAdult[teenager]". */ @@ -233,7 +236,21 @@ public @interface TestParameters { } public TestParametersValues build() { - checkState(name != null, "This set of parameters needs a name (%s)", parametersMap); + if (name == null) { + // Name is not set. Auto-generate one based on the parameter name and values + StringBuilder nameBuilder = new StringBuilder(); + nameBuilder.append('{'); + for (String parameterName : parametersMap.keySet()) { + if (nameBuilder.length() > 1) { + nameBuilder.append(", "); + } + nameBuilder.append( + ParameterValueParsing.formatTestNameString( + Optional.of(parameterName), parametersMap.get(parameterName))); + } + nameBuilder.append('}'); + name = nameBuilder.toString(); + } return new AutoValue_TestParameters_TestParametersValues( name, unmodifiableMap(new LinkedHashMap<>(parametersMap))); } diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java index 7b98707..2a4e1aa 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java @@ -16,7 +16,10 @@ package com.google.testing.junit.testparameterinjector; import static com.google.common.truth.Truth.assertThat; +import com.google.common.base.CharMatcher; +import com.google.common.base.Optional; import com.google.protobuf.ByteString; +import javax.annotation.Nullable; import org.junit.Test; import org.junit.runner.RunWith; @@ -140,6 +143,37 @@ public class ParameterValueParsingTest { assertThat(result).isEqualTo(parseYamlValueToJavaTypeCases.expectedResult); } + enum FormatTestNameStringTestCases { + NULL_REFERENCE(/* value= */ null, /* expectedResult= */ "param=null"), + BOOLEAN(/* value= */ false, /* expectedResult= */ "param=false"), + INTEGER(/* value= */ 123, /* expectedResult= */ "param=123"), + REGULAR_STRING(/* value= */ "abc", /* expectedResult= */ "abc"), + EMPTY_STRING(/* value= */ "", /* expectedResult= */ "param="), + NULL_STRING(/* value= */ "null", /* expectedResult= */ "param=null"), + INTEGER_STRING(/* value= */ "123", /* expectedResult= */ "param=123"), + ARRAY(/* value= */ new byte[] {2, 3, 4}, /* expectedResult= */ "[2, 3, 4]"), + CHAR_MATCHER(/* value= */ CharMatcher.any(), /* expectedResult= */ "CharMatcher.any()"), + LAST(/* value= */ "123", /* expectedResult= */ "param=123"); + + @Nullable final Object value; + final String expectedResult; + + FormatTestNameStringTestCases(@Nullable Object value, String expectedResult) { + this.value = value; + this.expectedResult = expectedResult; + } + } + + @Test + public void formatTestNameString_success(@TestParameter FormatTestNameStringTestCases testCase) + throws Exception { + String result = + ParameterValueParsing.formatTestNameString( + /* parameterName= */ Optional.of("param"), /* value= */ testCase.value); + + assertThat(result).isEqualTo(testCase.expectedResult); + } + private enum TestEnum { AAA, BBB; diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java index a8f5cff..d5417e6 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java @@ -54,7 +54,7 @@ public class TestParametersMethodProcessorTest { public List provideValues() { return ImmutableList.of( TestParametersValues.builder().name("one").addParameter("testEnum", TestEnum.ONE).build(), - TestParametersValues.builder().name("two").addParameter("testEnum", TestEnum.TWO).build(), + TestParametersValues.builder().addParameter("testEnum", TestEnum.TWO).build(), TestParametersValues.builder().name("null-case").addParameter("testEnum", null).build()); } } @@ -241,12 +241,45 @@ public class TestParametersMethodProcessorTest { ImmutableMap expectedTestNameToStringifiedParameters() { return ImmutableMap.builder() .put("test[one]", "ONE") - .put("test[two]", "TWO") + .put("test[{TWO}]", "TWO") .put("test[null-case]", "null") .build(); } } + @RunAsTest + public static class MethodAnnotationWithProvider extends SuccessfulTestCaseBase { + + @TestParameters(valuesProvider = CustomProvider.class) + @Test + public void test(int testInt, TestEnum testEnum) { + storeTestParametersForThisTest(testInt, testEnum); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put("test[{testInt=5, ONE}]", "5:ONE") + .put("test[{testInt=10, TWO}]", "10:TWO") + .build(); + } + + private static final class CustomProvider implements TestParametersValuesProvider { + @Override + public List provideValues() { + return ImmutableList.of( + TestParametersValues.builder() + .addParameter("testInt", 5) + .addParameter("testEnum", TestEnum.ONE) + .build(), + TestParametersValues.builder() + .addParameter("testInt", 10) + .addParameter("testEnum", TestEnum.TWO) + .build()); + } + } + } + public abstract static class BaseClassWithMethodAnnotation extends SuccessfulTestCaseBase { @Test diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java index 1f60c53..9080bf5 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java @@ -18,13 +18,17 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import com.google.common.base.CharMatcher; import com.google.common.base.Function; +import com.google.common.base.Optional; import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; import com.google.common.reflect.TypeToken; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.lang.reflect.Array; import java.lang.reflect.ParameterizedType; import java.nio.charset.Charset; +import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -221,5 +225,45 @@ final class ParameterValueParsing { } } + static String formatTestNameString(Optional parameterName, @Nullable Object value) { + String result = valueAsString(value); + if (parameterName.isPresent()) { + if (value == null + || + // Primitives are often ambiguous + Primitives.unwrap(value.getClass()).isPrimitive() + // Ambiguous String cases + || value.equals("null") + || (value instanceof CharSequence + && CharMatcher.anyOf("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + .matchesNoneOf((CharSequence) value))) { + // Prefix the parameter value with its field name. This is to avoid test names + // such as myMethod_success[true,false,2]. Instead, it'll be + // myMethod_success[dryRun=true,experimentFlag=false,retries=2]. + result = String.format("%s=%s", parameterName.get(), valueAsString(value)); + } + } + return result.trim().replaceAll("\\s+", " "); + } + + private static String valueAsString(Object value) { + if (value != null && value.getClass().isArray()) { + StringBuilder resultBuider = new StringBuilder(); + resultBuider.append("["); + for (int i = 0; i < Array.getLength(value); i++) { + if (i > 0) { + resultBuider.append(", "); + } + resultBuider.append(Array.get(value, i)); + } + resultBuider.append("]"); + return resultBuider.toString(); + } else if (ByteStringReflection.isInstanceOfByteString(value)) { + return Arrays.toString(ByteStringReflection.byteStringToByteArray(value)); + } else { + return String.valueOf(value); + } + } + private ParameterValueParsing() {} } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java index eb3d658..ec1ac1a 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java @@ -21,7 +21,6 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.google.auto.value.AutoAnnotation; import com.google.auto.value.AutoValue; -import com.google.common.base.CharMatcher; import com.google.common.base.Optional; import com.google.common.base.Throwables; import com.google.common.cache.Cache; @@ -35,14 +34,12 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Range; -import com.google.common.primitives.Primitives; import com.google.common.util.concurrent.UncheckedExecutionException; import com.google.testing.junit.testparameterinjector.junit5.TestInfo.TestInfoParameter; import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -115,44 +112,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso * brackets). */ String toTestNameString() { - if (paramName().isPresent() - && (value() == null - || - // Primitives are often ambiguous - Primitives.unwrap(value().getClass()).isPrimitive() - // Ambiguous String cases - || value().equals("null") - || (value() instanceof CharSequence - && CharMatcher.anyOf("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - .matchesNoneOf((CharSequence) value())))) { - // 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(), valueAsString()) - .trim() - .replaceAll("\\s+", " "); - } else { - return valueAsString().trim().replaceAll("\\s+", " "); - } - } - - private String valueAsString() { - if (value() != null && value().getClass().isArray()) { - StringBuilder resultBuider = new StringBuilder(); - resultBuider.append("["); - for (int i = 0; i < Array.getLength(value()); i++) { - if (i > 0) { - resultBuider.append(", "); - } - resultBuider.append(Array.get(value(), i)); - } - resultBuider.append("]"); - return resultBuider.toString(); - } else if (ByteStringReflection.isInstanceOfByteString(value())) { - return Arrays.toString(ByteStringReflection.byteStringToByteArray(value())); - } else { - return String.valueOf(value()); - } + return ParameterValueParsing.formatTestNameString(paramName(), value()); } public static ImmutableList create( diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameters.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameters.java index 3a3c40c..07d0fff 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameters.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameters.java @@ -14,14 +14,15 @@ package com.google.testing.junit.testparameterinjector.junit5; -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.base.Optional; import com.google.common.collect.ImmutableList; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValuesProvider; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.Target; @@ -207,6 +208,8 @@ public @interface TestParameters { /** * Sets a name for this set of parameters that will be used for describing this test. * + *

          Setting a name is optional. If unset, one will be generated from the parameter values. + * *

          Example: If a test method is called "personIsAdult" and this name is "teenager", the * name of the resulting test will be "personIsAdult[teenager]". */ @@ -233,7 +236,21 @@ public @interface TestParameters { } public TestParametersValues build() { - checkState(name != null, "This set of parameters needs a name (%s)", parametersMap); + if (name == null) { + // Name is not set. Auto-generate one based on the parameter name and values + StringBuilder nameBuilder = new StringBuilder(); + nameBuilder.append('{'); + for (String parameterName : parametersMap.keySet()) { + if (nameBuilder.length() > 1) { + nameBuilder.append(", "); + } + nameBuilder.append( + ParameterValueParsing.formatTestNameString( + Optional.of(parameterName), parametersMap.get(parameterName))); + } + nameBuilder.append('}'); + name = nameBuilder.toString(); + } return new AutoValue_TestParameters_TestParametersValues( name, unmodifiableMap(new LinkedHashMap<>(parametersMap))); } -- cgit v1.2.3 From 3cefbc22c90a11cda3467172c4f1e3aafae8c1c1 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 23 May 2023 10:34:47 +0000 Subject: Bump version to v1.12 in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index faae4b8..321397a 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ And add the following dependency to your `.pom` file: com.google.testparameterinjector test-parameter-injector - 1.11 + 1.12 test ``` @@ -97,7 +97,7 @@ And add the following dependency to your `.pom` file: com.google.testparameterinjector test-parameter-injector-junit5 - 1.11 + 1.12 test ``` -- cgit v1.2.3 From fcfe10215559f257145cbf2537aafb041b3abae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrikas=20Arnas=20Smi=C4=8Dius?= Date: Wed, 2 Aug 2023 14:06:40 +0300 Subject: Bump org.yaml.snakeyaml 1.27 -> 2.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 29fc376..6fc8f65 100644 --- a/pom.xml +++ b/pom.xml @@ -131,7 +131,7 @@ org.yaml snakeyaml - 1.27 + 2.0 -- cgit v1.2.3 From 763fe059b7f39e7eb44f8c608cc4d47f262efb3b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:14:22 +0000 Subject: Bump com.google.guava:guava from 30.1-jre to 32.0.0-jre Bumps [com.google.guava:guava](https://github.com/google/guava) from 30.1-jre to 32.0.0-jre. - [Release notes](https://github.com/google/guava/releases) - [Commits](https://github.com/google/guava/commits) --- updated-dependencies: - dependency-name: com.google.guava:guava dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6fc8f65..bc4e067 100644 --- a/pom.xml +++ b/pom.xml @@ -126,7 +126,7 @@ com.google.guava guava - 30.1-jre + 32.0.0-jre org.yaml -- cgit v1.2.3 From 9221b469de62b1fd4859d7ed4c4ca3c33576eb06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:14:23 +0000 Subject: Bump com.google.protobuf:protobuf-javalite from 3.20.0 to 3.20.3 Bumps com.google.protobuf:protobuf-javalite from 3.20.0 to 3.20.3. --- updated-dependencies: - dependency-name: com.google.protobuf:protobuf-javalite dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6fc8f65..1bffb68 100644 --- a/pom.xml +++ b/pom.xml @@ -144,7 +144,7 @@ com.google.protobuf protobuf-javalite - 3.20.0 + 3.20.3 test -- cgit v1.2.3 From e42d9d98485f8a2a5c8c55460f27d61fd6f6ea36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 01:19:31 +0000 Subject: Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yaml | 2 +- .github/workflows/release.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index d4f17e4..3452265 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-java@v3 with: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c790b2b..034eff7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-java@v3 with: -- cgit v1.2.3 From 782fec0510bf19a3d88ca54f331c37259da19ebc Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 13 Oct 2023 11:03:11 +0000 Subject: Add support for BigInteger and UnsignedLong --- .../ParameterValueParsing.java | 22 ++++++++++++++++++ .../ParameterValueParsingTest.java | 26 ++++++++++++++++++++++ .../junit5/ParameterValueParsing.java | 22 ++++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java index 09e75af..f18db3d 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java @@ -23,10 +23,12 @@ import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; +import com.google.common.primitives.UnsignedLong; import com.google.common.reflect.TypeToken; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.lang.reflect.Array; import java.lang.reflect.ParameterizedType; +import java.math.BigInteger; import java.nio.charset.Charset; import java.util.Arrays; import java.util.LinkedHashMap; @@ -63,6 +65,11 @@ final class ParameterValueParsing { return new Yaml(new SafeConstructor(new LoaderOptions())).load(yamlString); } + private static UnsignedLong parseYamlSignedLongToUnsignedLong(long number) { + checkState(number >= 0, "%s should be greater than or equal to zero", number); + return UnsignedLong.fromLongBits(number); + } + @SuppressWarnings({"unchecked"}) static Object parseYamlObjectToJavaType(Object parsedYaml, TypeToken javaType) { // Pass along null so we don't have to worry about it below @@ -92,6 +99,21 @@ final class ParameterValueParsing { .supportParsedType(Long.class, self -> self) .supportParsedType(Integer.class, Integer::longValue); + yamlValueTransformer + .ifJavaType(UnsignedLong.class) + .supportParsedType(Long.class, self -> parseYamlSignedLongToUnsignedLong(self.longValue())) + .supportParsedType( + Integer.class, self -> parseYamlSignedLongToUnsignedLong(self.longValue())) + // UnsignedLong::valueOf(BigInteger) will validate that BigInteger is in the valid range and + // throws otherwise. + .supportParsedType(BigInteger.class, UnsignedLong::valueOf); + + yamlValueTransformer + .ifJavaType(BigInteger.class) + .supportParsedType(Long.class, self -> BigInteger.valueOf(self.longValue())) + .supportParsedType(Integer.class, self -> BigInteger.valueOf(self.longValue())) + .supportParsedType(BigInteger.class, self -> self); + yamlValueTransformer .ifJavaType(Float.class) .supportParsedType(Float.class, self -> self) diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java index 2a4e1aa..76cac1f 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java @@ -18,7 +18,9 @@ import static com.google.common.truth.Truth.assertThat; import com.google.common.base.CharMatcher; import com.google.common.base.Optional; +import com.google.common.primitives.UnsignedLong; import com.google.protobuf.ByteString; +import java.math.BigInteger; import javax.annotation.Nullable; import org.junit.Test; import org.junit.runner.RunWith; @@ -56,6 +58,30 @@ public class ParameterValueParsingTest { /* yamlString= */ "442147483648", /* javaClass= */ String.class, /* expectedResult= */ "442147483648"), + BIG_INTEGER_TO_BIGINTEGER( + /* yamlString= */ "1000000000000000000000000000", + /* javaClass= */ BigInteger.class, + /* expectedResult= */ new BigInteger("1000000000000000000000000000")), + BIG_INTEGER_TO_UNSIGNED_LONG( + /* yamlString= */ "18446744073709551615", // This is UnsignedLong.MAX_VALUE. + /* javaClass= */ UnsignedLong.class, + /* expectedResult= */ UnsignedLong.MAX_VALUE), + LONG_TO_UNSIGNED_LONG( + /* yamlString= */ "10000000000000", + /* javaClass= */ UnsignedLong.class, + /* expectedResult= */ UnsignedLong.fromLongBits(10000000000000L)), + LONG_TO_BIG_INTEGER( + /* yamlString= */ "10000000000000", + /* javaClass= */ BigInteger.class, + /* expectedResult= */ BigInteger.valueOf(10000000000000L)), + INTEGER_TO_BIG_INTEGER( + /* yamlString= */ "1000000", + /* javaClass= */ BigInteger.class, + /* expectedResult= */ BigInteger.valueOf(1000000)), + INTEGER_TO_UNSIGNED_LONG( + /* yamlString= */ "1000000", + /* javaClass= */ UnsignedLong.class, + /* expectedResult= */ UnsignedLong.fromLongBits(1000000)), DOUBLE_TO_STRING( /* yamlString= */ "1.23", /* javaClass= */ String.class, /* expectedResult= */ "1.23"), diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java index 9080bf5..3183594 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java @@ -23,10 +23,12 @@ import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; +import com.google.common.primitives.UnsignedLong; import com.google.common.reflect.TypeToken; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.lang.reflect.Array; import java.lang.reflect.ParameterizedType; +import java.math.BigInteger; import java.nio.charset.Charset; import java.util.Arrays; import java.util.LinkedHashMap; @@ -63,6 +65,11 @@ final class ParameterValueParsing { return new Yaml(new SafeConstructor(new LoaderOptions())).load(yamlString); } + private static UnsignedLong parseYamlSignedLongToUnsignedLong(long number) { + checkState(number >= 0, "%s should be greater than or equal to zero", number); + return UnsignedLong.fromLongBits(number); + } + @SuppressWarnings({"unchecked"}) static Object parseYamlObjectToJavaType(Object parsedYaml, TypeToken javaType) { // Pass along null so we don't have to worry about it below @@ -92,6 +99,21 @@ final class ParameterValueParsing { .supportParsedType(Long.class, self -> self) .supportParsedType(Integer.class, Integer::longValue); + yamlValueTransformer + .ifJavaType(UnsignedLong.class) + .supportParsedType(Long.class, self -> parseYamlSignedLongToUnsignedLong(self.longValue())) + .supportParsedType( + Integer.class, self -> parseYamlSignedLongToUnsignedLong(self.longValue())) + // UnsignedLong::valueOf(BigInteger) will validate that BigInteger is in the valid range and + // throws otherwise. + .supportParsedType(BigInteger.class, UnsignedLong::valueOf); + + yamlValueTransformer + .ifJavaType(BigInteger.class) + .supportParsedType(Long.class, self -> BigInteger.valueOf(self.longValue())) + .supportParsedType(Integer.class, self -> BigInteger.valueOf(self.longValue())) + .supportParsedType(BigInteger.class, self -> self); + yamlValueTransformer .ifJavaType(Float.class) .supportParsedType(Float.class, self -> self) -- cgit v1.2.3 From 4f10c56a798a1eb4667ed3205bd25b2dc088a9e2 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 13 Oct 2023 11:03:49 +0000 Subject: JUnit4: Fix for interrupted test cases causing random failures with thread reuse (porting [the earlier fix in JUnit4](https://github.com/junit-team/junit4/issues/1365)) --- CHANGELOG.md | 7 +++++++ .../testing/junit/testparameterinjector/PluggableTestRunner.java | 1 + .../TestParameterAnnotationMethodProcessor.java | 1 + .../junit5/TestParameterAnnotationMethodProcessor.java | 1 + 4 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77324fa..dde5e13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.13 + +- Add support for `BigInteger` and `UnsignedLong` +- JUnit4: Fix for interrupted test cases causing random failures with thread + reuse (porting [the earlier fix in + JUnit4](https://github.com/junit-team/junit4/issues/1365)) + ## 1.12 - Tweak to the test name generation: Show the parameter name if its value is potentially diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java index 27d5bc5..5f4c061 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java @@ -215,6 +215,7 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { statement = withBefores(method, testObject, statement); statement = withAfters(method, testObject, statement); statement = withRules(method, testObject, statement); + statement = withInterruptIsolation(statement); return statement; } diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 4e49df9..d2043c7 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -139,6 +139,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso .toList(); } } + /** * Returns a {@link TestParameterValues} for retrieving the {@link TestParameterAnnotation} * annotation values for a the {@code testInfo}. diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java index ec1ac1a..5f47dfb 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java @@ -139,6 +139,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso .toList(); } } + /** * Returns a {@link TestParameterValues} for retrieving the {@link TestParameterAnnotation} * annotation values for a the {@code testInfo}. -- cgit v1.2.3 From 497c0f27f1a0445f556e746b5e659e1477ca8834 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 13 Oct 2023 11:06:38 +0000 Subject: Allow setting a custom name in TestParameter value providers. --- .../ParameterValueParsing.java | 28 +++-- .../junit/testparameterinjector/TestParameter.java | 14 +++ .../TestParameterAnnotationMethodProcessor.java | 136 ++++++++++++--------- .../testparameterinjector/TestParameterValue.java | 43 +++++++ .../ParameterValueParsingTest.java | 3 +- .../testparameterinjector/TestParameterTest.java | 71 +++++++++-- .../junit5/ParameterValueParsing.java | 28 +++-- .../junit5/TestParameter.java | 14 +++ .../TestParameterAnnotationMethodProcessor.java | 136 ++++++++++++--------- .../junit5/TestParameterValue.java | 43 +++++++ .../junit5/TestParameterInjectorJUnit5Test.java | 5 +- 11 files changed, 381 insertions(+), 140 deletions(-) create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValue.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValue.java diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java index f18db3d..e09c1d9 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java @@ -248,21 +248,33 @@ final class ParameterValueParsing { } static String formatTestNameString(Optional parameterName, @Nullable Object value) { - String result = valueAsString(value); - if (parameterName.isPresent()) { - if (value == null + Object unwrappedValue; + Optional customName; + + if (value instanceof TestParameterValue) { + TestParameterValue tpValue = (TestParameterValue) value; + unwrappedValue = tpValue.getWrappedValue(); + customName = tpValue.getCustomName(); + } else { + unwrappedValue = value; + customName = Optional.absent(); + } + + String result = customName.or(() -> valueAsString(unwrappedValue)); + if (parameterName.isPresent() && !customName.isPresent()) { + if (unwrappedValue == null || // Primitives are often ambiguous - Primitives.unwrap(value.getClass()).isPrimitive() + Primitives.unwrap(unwrappedValue.getClass()).isPrimitive() // Ambiguous String cases - || value.equals("null") - || (value instanceof CharSequence + || unwrappedValue.equals("null") + || (unwrappedValue instanceof CharSequence && CharMatcher.anyOf("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - .matchesNoneOf((CharSequence) value))) { + .matchesNoneOf((CharSequence) unwrappedValue))) { // Prefix the parameter value with its field name. This is to avoid test names // such as myMethod_success[true,false,2]. Instead, it'll be // myMethod_success[dryRun=true,experimentFlag=false,retries=2]. - result = String.format("%s=%s", parameterName.get(), valueAsString(value)); + result = String.format("%s=%s", parameterName.get(), valueAsString(unwrappedValue)); } } return result.trim().replaceAll("\\s+", " "); diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java index 7f9d93e..ed03484 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java @@ -33,6 +33,7 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import javax.annotation.Nullable; /** * Test parameter annotation that defines the values that a single parameter can have. @@ -119,6 +120,19 @@ public @interface TestParameter { /** Interface for custom providers of test parameter values. */ interface TestParameterValuesProvider { List provideValues(); + + /** + * Wraps the given value in an object that allows you to give the parameter value a different + * name. The TestParameterInjector framework will recognize the returned {@link + * TestParameterValue} instances and unwrap them at injection time. + * + *

          Usage: {@code value(file.content).withName(file.name)}. + * + *

          Do not override this method. + */ + default TestParameterValue value(@Nullable Object wrappedValue) { + return TestParameterValue.wrap(wrappedValue); + } } /** Default {@link TestParameterValuesProvider} implementation that does nothing. */ diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index d2043c7..f8afe65 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -17,6 +17,7 @@ package com.google.testing.junit.testparameterinjector; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Verify.verify; +import static com.google.common.collect.Lists.newArrayList; import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.google.auto.value.AutoAnnotation; @@ -68,7 +69,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso * value()} method. */ @AutoValue - abstract static class TestParameterValue implements Serializable { + abstract static class TestParameterValueHolder implements Serializable { private static final long serialVersionUID = -6491624726743872379L; @@ -82,8 +83,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso * annotation's {@code value()} method (e.g. 'true' or 'false' in the case of a Boolean * parameter). */ - @Nullable - abstract Object value(); + abstract TestParameterValue wrappedValue(); /** The index of this value in {@link #specifiedValues()}. */ abstract int valueIndex(); @@ -107,17 +107,26 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso */ abstract Optional paramName(); + /** + * Returns {@link #wrappedValue()} without the {@link TestParameterValue} wrapper if it exists. + */ + @Nullable + Object unwrappedValue() { + return wrappedValue().getWrappedValue(); + } + /** * Returns a String that represents this value and is fit for use in a test name (between * brackets). */ String toTestNameString() { - return ParameterValueParsing.formatTestNameString(paramName(), value()); + return ParameterValueParsing.formatTestNameString(paramName(), wrappedValue()); } - public static ImmutableList create( + public static ImmutableList create( AnnotationWithMetadata annotationWithMetadata, Origin origin) { - List specifiedValues = getParametersAnnotationValues(annotationWithMetadata); + List specifiedValues = + getParametersAnnotationValues(annotationWithMetadata); checkState( !specifiedValues.isEmpty(), "The number of parameter values should not be 0" @@ -127,13 +136,15 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso Range.closedOpen(0, specifiedValues.size()), DiscreteDomain.integers())) .transform( valueIndex -> - (TestParameterValue) - new AutoValue_TestParameterAnnotationMethodProcessor_TestParameterValue( + (TestParameterValueHolder) + new AutoValue_TestParameterAnnotationMethodProcessor_TestParameterValueHolder( AnnotationTypeOrigin.create( annotationWithMetadata.annotation().annotationType(), origin), specifiedValues.get(valueIndex), valueIndex, - new ArrayList<>(specifiedValues), + newArrayList( + FluentIterable.from(specifiedValues) + .transform(TestParameterValue::getWrappedValue)), annotationWithMetadata.paramClass(), annotationWithMetadata.paramName())) .toList(); @@ -160,7 +171,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso .annotationTypeOrigin() .annotationType() .equals(annotationType)) - .transform(TestParameterValue::value) + .transform(TestParameterValueHolder::unwrappedValue) .first(); } } @@ -174,17 +185,24 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return getTestParameterValues(testInfo).getValue(annotationType); } - private static List getParametersAnnotationValues( + private static ImmutableList getParametersAnnotationValues( AnnotationWithMetadata annotationWithMetadata) { Annotation annotation = annotationWithMetadata.annotation(); TestParameterAnnotation testParameter = annotation.annotationType().getAnnotation(TestParameterAnnotation.class); Class valueProvider = testParameter.valueProvider(); try { - return valueProvider - .getConstructor() - .newInstance() - .provideValues(annotation, annotationWithMetadata.paramClass()); + return FluentIterable.from( + valueProvider + .getConstructor() + .newInstance() + .provideValues(annotation, annotationWithMetadata.paramClass())) + .transform( + value -> + (value instanceof TestParameterValue) + ? (TestParameterValue) value + : TestParameterValue.wrap(value)) + .toList(); } catch (ReflectiveOperationException e) { throw new RuntimeException( "Unexpected exception while invoking value provider " + valueProvider, e); @@ -270,7 +288,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso CacheBuilder.newBuilder() .maximumSize(1000) .build(CacheLoader.from(this::calculateAnnotationTypeOrigins)); - private final Cache>> parameterValuesCache = + private final Cache>> parameterValuesCache = CacheBuilder.newBuilder().maximumSize(1000).build(); private TestParameterAnnotationMethodProcessor(boolean onlyForFieldsAndParameters) { @@ -605,14 +623,14 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return Optional.absent(); } else { TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); - List testParameterValues = + List testParameterValues = getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()); Class[] parameterTypes = constructor.getParameterTypes(); Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); List parameterValues = new ArrayList<>(/* initialCapacity= */ parameterTypes.length); List> processedAnnotationTypes = new ArrayList<>(); - List parameterValuesForConstructor = + List parameterValuesForConstructor = filterByOrigin( testParameterValues, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER); for (int i = 0; i < parameterTypes.length; i++) { @@ -646,7 +664,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } else { TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); checkState(testIndexHolder != null); - List testParameterValues = + List testParameterValues = filterByOrigin( getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()), Origin.CLASS, @@ -695,7 +713,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso */ @Override public List calculateTestInfos(TestInfo originalTest) { - List> parameterValuesForMethod = + List> parameterValuesForMethod = getParameterValuesForMethod(originalTest.getMethod(), originalTest.getTestClass()); if (parameterValuesForMethod.equals(ImmutableList.of(ImmutableList.of()))) { @@ -707,7 +725,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso for (int parametersIndex = 0; parametersIndex < parameterValuesForMethod.size(); ++parametersIndex) { - List testParameterValues = parameterValuesForMethod.get(parametersIndex); + List testParameterValues = + parameterValuesForMethod.get(parametersIndex); testInfos.add( originalTest .withExtraParameters( @@ -715,7 +734,9 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso .transform( param -> TestInfoParameter.create( - param.toTestNameString(), param.value(), param.valueIndex())) + param.toTestNameString(), + param.unwrappedValue(), + param.valueIndex())) .toList()) .withExtraAnnotation( TestIndexHolderFactory.create( @@ -729,13 +750,13 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return testInfos.build(); } - private List> getParameterValuesForMethod( + private List> getParameterValuesForMethod( Method method, Class testClass) { try { return parameterValuesCache.get( method, () -> { - List> testParameterValuesList = + List> testParameterValuesList = getAnnotationValuesForUsedAnnotationTypes(method, testClass); return FluentIterable.from(Lists.cartesianProduct(testParameterValuesList)) @@ -758,7 +779,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } } - private List getParameterValuesForTest( + private List getParameterValuesForTest( TestIndexHolder testIndexHolder, Class testClass) { verify( testIndexHolder.testClassName().equals(testClass.getName()), @@ -775,7 +796,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso * Returns the list of annotation index for all annotations defined in a given test method and its * class. */ - private ImmutableList> getAnnotationValuesForUsedAnnotationTypes( + private ImmutableList> getAnnotationValuesForUsedAnnotationTypes( Method method, Class testClass) { ImmutableList annotationTypes = FluentIterable.from(getAnnotationTypeOrigins(testClass, Origin.CLASS)) @@ -871,7 +892,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso * method, and the one defined on the method takes precedence over the same annotation defined on * the class. */ - private ImmutableList> getAnnotationFromParametersOrTestOrClass( + private ImmutableList> getAnnotationFromParametersOrTestOrClass( AnnotationTypeOrigin annotationTypeOrigin, Method method, Class testClass) { Origin origin = annotationTypeOrigin.origin(); Class annotationType = annotationTypeOrigin.annotationType(); @@ -887,7 +908,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso Annotation annotation = getOnlyConstructor(testClass).getAnnotation(annotationType); if (annotation != null) { return ImmutableList.of( - TestParameterValue.create(AnnotationWithMetadata.withoutMetadata(annotation), origin)); + TestParameterValueHolder.create( + AnnotationWithMetadata.withoutMetadata(annotation), origin)); } } else if (origin == Origin.METHOD_PARAMETER) { @@ -899,7 +921,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } else if (origin == Origin.METHOD) { if (method.isAnnotationPresent(annotationType)) { return ImmutableList.of( - TestParameterValue.create( + TestParameterValueHolder.create( AnnotationWithMetadata.withoutMetadata(method.getAnnotation(annotationType)), origin)); } @@ -924,19 +946,21 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso Annotation annotation = testClass.getAnnotation(annotationType); if (annotation != null) { return ImmutableList.of( - TestParameterValue.create(AnnotationWithMetadata.withoutMetadata(annotation), origin)); + TestParameterValueHolder.create( + AnnotationWithMetadata.withoutMetadata(annotation), origin)); } } return ImmutableList.of(); } - private static ImmutableList> toTestParameterValueList( + private static ImmutableList> toTestParameterValueList( List annotationWithMetadatas, Origin origin) { return FluentIterable.from(annotationWithMetadatas) .transform( annotationWithMetadata -> - (List) - new ArrayList<>(TestParameterValue.create(annotationWithMetadata, origin))) + (List) + new ArrayList<>( + TestParameterValueHolder.create(annotationWithMetadata, origin))) .toList(); } @@ -1019,27 +1043,27 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); try { if (testIndexHolder != null) { - List testParameterValues = + List testParameterValues = getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()); // Do not include {@link Origin#METHOD_PARAMETER} nor {@link Origin#CONSTRUCTOR_PARAMETER} // annotations. - List testParameterValuesForFieldInjection = + List testParameterValuesForFieldInjection = filterByOrigin(testParameterValues, Origin.CLASS, Origin.FIELD, Origin.METHOD); // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class // in the example above. - List remainingTestParameterValuesForFieldInjection = + List remainingTestParameterValuesForFieldInjection = new ArrayList<>(testParameterValuesForFieldInjection); for (Field declaredField : FluentIterable.from(listWithParents(testInstance.getClass())) .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields())) .toList()) { - for (TestParameterValue testParameterValue : + for (TestParameterValueHolder testParameterValue : remainingTestParameterValuesForFieldInjection) { if (declaredField.isAnnotationPresent( testParameterValue.annotationTypeOrigin().annotationType())) { declaredField.setAccessible(true); - declaredField.set(testInstance, testParameterValue.value()); + declaredField.set(testInstance, testParameterValue.unwrappedValue()); remainingTestParameterValuesForFieldInjection.remove(testParameterValue); break; } @@ -1052,11 +1076,11 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } /** - * Returns an {@link TestParameterValue} list that contains only the values originating from one - * of the {@code origins}. + * Returns an {@link TestParameterValueHolder} list that contains only the values originating from + * one of the {@code origins}. */ - private static ImmutableList filterByOrigin( - List testParameterValues, Origin... origins) { + private static ImmutableList filterByOrigin( + List testParameterValues, Origin... origins) { Set originsToFilterBy = ImmutableSet.copyOf(origins); return FluentIterable.from(testParameterValues) .filter( @@ -1079,12 +1103,12 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso /** Returns a {@link TestParameterAnnotation}'s value for a method or constructor parameter. */ private Object getParameterValue( - List testParameterValues, + List testParameterValues, Class methodParameterType, Annotation[] parameterAnnotations, List> processedAnnotationTypes) { List> iteratedAnnotationTypes = new ArrayList<>(); - for (TestParameterValue testParameterValue : testParameterValues) { + for (TestParameterValueHolder testParameterValue : testParameterValues) { // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class // in the example above. for (Annotation parameterAnnotation : parameterAnnotations) { @@ -1101,21 +1125,21 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso if (Collections.frequency(processedAnnotationTypes, annotationType) == Collections.frequency(iteratedAnnotationTypes, annotationType)) { processedAnnotationTypes.add(annotationType); - return testParameterValue.value(); + return testParameterValue.unwrappedValue(); } iteratedAnnotationTypes.add(annotationType); } } } // If no annotation matches, use the method parameter type. - for (TestParameterValue testParameterValue : testParameterValues) { + for (TestParameterValueHolder testParameterValue : testParameterValues) { // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class // in the example above. if (methodParameterType.isAssignableFrom( getValueMethodReturnType( testParameterValue.annotationTypeOrigin().annotationType(), /* paramClass= */ Optional.absent()))) { - return testParameterValue.value(); + return testParameterValue.unwrappedValue(); } } throw new IllegalStateException( @@ -1158,10 +1182,11 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso /** * Returns whether the test should be skipped according to the {@code annotationType}'s {@link - * TestParameterValidator} and the current list of {@link TestParameterValue}. + * TestParameterValidator} and the current list of {@link TestParameterValueHolder}. */ private static boolean callShouldSkip( - Class annotationType, List testParameterValues) { + Class annotationType, + List testParameterValues) { TestParameterAnnotation annotation = annotationType.getAnnotation(TestParameterAnnotation.class); Class validator = annotation.validator(); @@ -1177,14 +1202,14 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso private static class ValidatorContext implements TestParameterValidator.Context { - private final List testParameterValues; + private final List testParameterValues; private final Set valueList; - public ValidatorContext(List testParameterValues) { + public ValidatorContext(List testParameterValues) { this.testParameterValues = testParameterValues; this.valueList = FluentIterable.from(testParameterValues) - .transform(TestParameterValue::value) + .transform(TestParameterValueHolder::unwrappedValue) .filter(Objects::nonNull) .toSet(); } @@ -1201,17 +1226,18 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso @Override public Optional getValue(Class testParameter) { - return getParameter(testParameter).transform(TestParameterValue::value); + return getParameter(testParameter).transform(TestParameterValueHolder::unwrappedValue); } @Override public List getSpecifiedValues(Class testParameter) { return getParameter(testParameter) - .transform(TestParameterValue::specifiedValues) + .transform(TestParameterValueHolder::specifiedValues) .or(ImmutableList.of()); } - private Optional getParameter(Class testParameter) { + private Optional getParameter( + Class testParameter) { return FluentIterable.from(testParameterValues) .firstMatch(value -> value.annotationTypeOrigin().annotationType().equals(testParameter)); } diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValue.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValue.java new file mode 100644 index 0000000..44d7951 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValue.java @@ -0,0 +1,43 @@ +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Optional; +import javax.annotation.Nullable; + +/** + * Wrapper class around a parameter value. Use this to give a value a name that is different from + * its {@code toString()} method. + */ +public class TestParameterValue { + private final @Nullable Object wrappedValue; + private final Optional customName; + + private TestParameterValue(@Nullable Object wrappedValue, Optional customName) { + this.wrappedValue = wrappedValue; + this.customName = checkNotNull(customName); + } + + /** Wraps the given value. */ + public static TestParameterValue wrap(@Nullable Object wrappedValue) { + return new TestParameterValue(wrappedValue, /* customName= */ Optional.absent()); + } + + /** + * Returns a new {@link TestParameterValue} instance that stores the given name. The + * TestParameterInjector framework will use this name instead of {@code wrappedValue.toString()} + * when generating the test name. + */ + public TestParameterValue withName(String name) { + return new TestParameterValue(wrappedValue, Optional.of(name)); + } + + @Nullable + Object getWrappedValue() { + return wrappedValue; + } + + Optional getCustomName() { + return customName; + } +} diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java index 76cac1f..a9336b7 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java @@ -178,8 +178,7 @@ public class ParameterValueParsingTest { NULL_STRING(/* value= */ "null", /* expectedResult= */ "param=null"), INTEGER_STRING(/* value= */ "123", /* expectedResult= */ "param=123"), ARRAY(/* value= */ new byte[] {2, 3, 4}, /* expectedResult= */ "[2, 3, 4]"), - CHAR_MATCHER(/* value= */ CharMatcher.any(), /* expectedResult= */ "CharMatcher.any()"), - LAST(/* value= */ "123", /* expectedResult= */ "param=123"); + CHAR_MATCHER(/* value= */ CharMatcher.any(), /* expectedResult= */ "CharMatcher.any()"); @Nullable final Object value; final String expectedResult; diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java index bc7123b..1d5b233 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java @@ -129,34 +129,85 @@ public class TestParameterTest { @RunAsTest public static class WithValuesProvider extends SuccessfulTestCaseBase { + private final int number1; + + @TestParameter(valuesProvider = TestNumberProvider.class) + private int number2; + + public WithValuesProvider( + @TestParameter(valuesProvider = TestNumberProvider.class) int number1) { + this.number1 = number1; + } + @Test public void stringTest( @TestParameter(valuesProvider = TestStringProvider.class) String stringParam) { - storeTestParametersForThisTest(stringParam); + storeTestParametersForThisTest(number1, number2, stringParam); } @Test public void charMatcherTest( @TestParameter(valuesProvider = CharMatcherProvider.class) CharMatcher charMatcher) { - storeTestParametersForThisTest(charMatcher); + storeTestParametersForThisTest(number1, number2, charMatcher); } @Override ImmutableMap expectedTestNameToStringifiedParameters() { return ImmutableMap.builder() - .put("stringTest[A]", "A") - .put("stringTest[B]", "B") - .put("stringTest[stringParam=null]", "null") - .put("charMatcherTest[CharMatcher.any()]", "CharMatcher.any()") - .put("charMatcherTest[CharMatcher.ascii()]", "CharMatcher.ascii()") - .put("charMatcherTest[CharMatcher.whitespace()]", "CharMatcher.whitespace()") + .put("stringTest[one,one,A]", "1:1:A") + .put("stringTest[one,one,B]", "1:1:B") + .put("stringTest[one,one,stringParam=null]", "1:1:null") + .put("stringTest[one,one,nothing]", "1:1:null") + .put("stringTest[one,one,wizard]", "1:1:harry") + .put("stringTest[one,number1=2,A]", "2:1:A") + .put("stringTest[one,number1=2,B]", "2:1:B") + .put("stringTest[one,number1=2,stringParam=null]", "2:1:null") + .put("stringTest[one,number1=2,nothing]", "2:1:null") + .put("stringTest[one,number1=2,wizard]", "2:1:harry") + .put("stringTest[number2=2,one,A]", "1:2:A") + .put("stringTest[number2=2,one,B]", "1:2:B") + .put("stringTest[number2=2,one,stringParam=null]", "1:2:null") + .put("stringTest[number2=2,one,nothing]", "1:2:null") + .put("stringTest[number2=2,one,wizard]", "1:2:harry") + .put("stringTest[number2=2,number1=2,A]", "2:2:A") + .put("stringTest[number2=2,number1=2,B]", "2:2:B") + .put("stringTest[number2=2,number1=2,stringParam=null]", "2:2:null") + .put("stringTest[number2=2,number1=2,nothing]", "2:2:null") + .put("stringTest[number2=2,number1=2,wizard]", "2:2:harry") + .put("charMatcherTest[one,one,CharMatcher.any()]", "1:1:CharMatcher.any()") + .put("charMatcherTest[one,one,CharMatcher.ascii()]", "1:1:CharMatcher.ascii()") + .put("charMatcherTest[one,one,CharMatcher.whitespace()]", "1:1:CharMatcher.whitespace()") + .put("charMatcherTest[one,number1=2,CharMatcher.any()]", "2:1:CharMatcher.any()") + .put("charMatcherTest[one,number1=2,CharMatcher.ascii()]", "2:1:CharMatcher.ascii()") + .put( + "charMatcherTest[one,number1=2,CharMatcher.whitespace()]", + "2:1:CharMatcher.whitespace()") + .put("charMatcherTest[number2=2,one,CharMatcher.any()]", "1:2:CharMatcher.any()") + .put("charMatcherTest[number2=2,one,CharMatcher.ascii()]", "1:2:CharMatcher.ascii()") + .put( + "charMatcherTest[number2=2,one,CharMatcher.whitespace()]", + "1:2:CharMatcher.whitespace()") + .put("charMatcherTest[number2=2,number1=2,CharMatcher.any()]", "2:2:CharMatcher.any()") + .put( + "charMatcherTest[number2=2,number1=2,CharMatcher.ascii()]", "2:2:CharMatcher.ascii()") + .put( + "charMatcherTest[number2=2,number1=2,CharMatcher.whitespace()]", + "2:2:CharMatcher.whitespace()") .build(); } + private static final class TestNumberProvider implements TestParameterValuesProvider { + @Override + public List provideValues() { + return newArrayList(value(1).withName("one"), 2); + } + } + private static final class TestStringProvider implements TestParameterValuesProvider { @Override - public List provideValues() { - return newArrayList("A", "B", null); + public List provideValues() { + return newArrayList( + "A", "B", null, value(null).withName("nothing"), value("harry").withName("wizard")); } } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java index 3183594..130c186 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java @@ -248,21 +248,33 @@ final class ParameterValueParsing { } static String formatTestNameString(Optional parameterName, @Nullable Object value) { - String result = valueAsString(value); - if (parameterName.isPresent()) { - if (value == null + Object unwrappedValue; + Optional customName; + + if (value instanceof TestParameterValue) { + TestParameterValue tpValue = (TestParameterValue) value; + unwrappedValue = tpValue.getWrappedValue(); + customName = tpValue.getCustomName(); + } else { + unwrappedValue = value; + customName = Optional.absent(); + } + + String result = customName.or(() -> valueAsString(unwrappedValue)); + if (parameterName.isPresent() && !customName.isPresent()) { + if (unwrappedValue == null || // Primitives are often ambiguous - Primitives.unwrap(value.getClass()).isPrimitive() + Primitives.unwrap(unwrappedValue.getClass()).isPrimitive() // Ambiguous String cases - || value.equals("null") - || (value instanceof CharSequence + || unwrappedValue.equals("null") + || (unwrappedValue instanceof CharSequence && CharMatcher.anyOf("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - .matchesNoneOf((CharSequence) value))) { + .matchesNoneOf((CharSequence) unwrappedValue))) { // Prefix the parameter value with its field name. This is to avoid test names // such as myMethod_success[true,false,2]. Instead, it'll be // myMethod_success[dryRun=true,experimentFlag=false,retries=2]. - result = String.format("%s=%s", parameterName.get(), valueAsString(value)); + result = String.format("%s=%s", parameterName.get(), valueAsString(unwrappedValue)); } } return result.trim().replaceAll("\\s+", " "); diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java index dca6325..eb8c240 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java @@ -33,6 +33,7 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import javax.annotation.Nullable; /** * Test parameter annotation that defines the values that a single parameter can have. @@ -119,6 +120,19 @@ public @interface TestParameter { /** Interface for custom providers of test parameter values. */ interface TestParameterValuesProvider { List provideValues(); + + /** + * Wraps the given value in an object that allows you to give the parameter value a different + * name. The TestParameterInjector framework will recognize the returned {@link + * TestParameterValue} instances and unwrap them at injection time. + * + *

          Usage: {@code value(file.content).withName(file.name)}. + * + *

          Do not override this method. + */ + default TestParameterValue value(@Nullable Object wrappedValue) { + return TestParameterValue.wrap(wrappedValue); + } } /** Default {@link TestParameterValuesProvider} implementation that does nothing. */ diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java index 5f47dfb..05a4226 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java @@ -17,6 +17,7 @@ package com.google.testing.junit.testparameterinjector.junit5; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Verify.verify; +import static com.google.common.collect.Lists.newArrayList; import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.google.auto.value.AutoAnnotation; @@ -68,7 +69,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso * value()} method. */ @AutoValue - abstract static class TestParameterValue implements Serializable { + abstract static class TestParameterValueHolder implements Serializable { private static final long serialVersionUID = -6491624726743872379L; @@ -82,8 +83,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso * annotation's {@code value()} method (e.g. 'true' or 'false' in the case of a Boolean * parameter). */ - @Nullable - abstract Object value(); + abstract TestParameterValue wrappedValue(); /** The index of this value in {@link #specifiedValues()}. */ abstract int valueIndex(); @@ -107,17 +107,26 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso */ abstract Optional paramName(); + /** + * Returns {@link #wrappedValue()} without the {@link TestParameterValue} wrapper if it exists. + */ + @Nullable + Object unwrappedValue() { + return wrappedValue().getWrappedValue(); + } + /** * Returns a String that represents this value and is fit for use in a test name (between * brackets). */ String toTestNameString() { - return ParameterValueParsing.formatTestNameString(paramName(), value()); + return ParameterValueParsing.formatTestNameString(paramName(), wrappedValue()); } - public static ImmutableList create( + public static ImmutableList create( AnnotationWithMetadata annotationWithMetadata, Origin origin) { - List specifiedValues = getParametersAnnotationValues(annotationWithMetadata); + List specifiedValues = + getParametersAnnotationValues(annotationWithMetadata); checkState( !specifiedValues.isEmpty(), "The number of parameter values should not be 0" @@ -127,13 +136,15 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso Range.closedOpen(0, specifiedValues.size()), DiscreteDomain.integers())) .transform( valueIndex -> - (TestParameterValue) - new AutoValue_TestParameterAnnotationMethodProcessor_TestParameterValue( + (TestParameterValueHolder) + new AutoValue_TestParameterAnnotationMethodProcessor_TestParameterValueHolder( AnnotationTypeOrigin.create( annotationWithMetadata.annotation().annotationType(), origin), specifiedValues.get(valueIndex), valueIndex, - new ArrayList<>(specifiedValues), + newArrayList( + FluentIterable.from(specifiedValues) + .transform(TestParameterValue::getWrappedValue)), annotationWithMetadata.paramClass(), annotationWithMetadata.paramName())) .toList(); @@ -160,7 +171,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso .annotationTypeOrigin() .annotationType() .equals(annotationType)) - .transform(TestParameterValue::value) + .transform(TestParameterValueHolder::unwrappedValue) .first(); } } @@ -174,17 +185,24 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return getTestParameterValues(testInfo).getValue(annotationType); } - private static List getParametersAnnotationValues( + private static ImmutableList getParametersAnnotationValues( AnnotationWithMetadata annotationWithMetadata) { Annotation annotation = annotationWithMetadata.annotation(); TestParameterAnnotation testParameter = annotation.annotationType().getAnnotation(TestParameterAnnotation.class); Class valueProvider = testParameter.valueProvider(); try { - return valueProvider - .getConstructor() - .newInstance() - .provideValues(annotation, annotationWithMetadata.paramClass()); + return FluentIterable.from( + valueProvider + .getConstructor() + .newInstance() + .provideValues(annotation, annotationWithMetadata.paramClass())) + .transform( + value -> + (value instanceof TestParameterValue) + ? (TestParameterValue) value + : TestParameterValue.wrap(value)) + .toList(); } catch (ReflectiveOperationException e) { throw new RuntimeException( "Unexpected exception while invoking value provider " + valueProvider, e); @@ -270,7 +288,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso CacheBuilder.newBuilder() .maximumSize(1000) .build(CacheLoader.from(this::calculateAnnotationTypeOrigins)); - private final Cache>> parameterValuesCache = + private final Cache>> parameterValuesCache = CacheBuilder.newBuilder().maximumSize(1000).build(); private TestParameterAnnotationMethodProcessor(boolean onlyForFieldsAndParameters) { @@ -605,14 +623,14 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return Optional.absent(); } else { TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); - List testParameterValues = + List testParameterValues = getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()); Class[] parameterTypes = constructor.getParameterTypes(); Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); List parameterValues = new ArrayList<>(/* initialCapacity= */ parameterTypes.length); List> processedAnnotationTypes = new ArrayList<>(); - List parameterValuesForConstructor = + List parameterValuesForConstructor = filterByOrigin( testParameterValues, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER); for (int i = 0; i < parameterTypes.length; i++) { @@ -646,7 +664,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } else { TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); checkState(testIndexHolder != null); - List testParameterValues = + List testParameterValues = filterByOrigin( getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()), Origin.CLASS, @@ -695,7 +713,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso */ @Override public List calculateTestInfos(TestInfo originalTest) { - List> parameterValuesForMethod = + List> parameterValuesForMethod = getParameterValuesForMethod(originalTest.getMethod(), originalTest.getTestClass()); if (parameterValuesForMethod.equals(ImmutableList.of(ImmutableList.of()))) { @@ -707,7 +725,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso for (int parametersIndex = 0; parametersIndex < parameterValuesForMethod.size(); ++parametersIndex) { - List testParameterValues = parameterValuesForMethod.get(parametersIndex); + List testParameterValues = + parameterValuesForMethod.get(parametersIndex); testInfos.add( originalTest .withExtraParameters( @@ -715,7 +734,9 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso .transform( param -> TestInfoParameter.create( - param.toTestNameString(), param.value(), param.valueIndex())) + param.toTestNameString(), + param.unwrappedValue(), + param.valueIndex())) .toList()) .withExtraAnnotation( TestIndexHolderFactory.create( @@ -729,13 +750,13 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return testInfos.build(); } - private List> getParameterValuesForMethod( + private List> getParameterValuesForMethod( Method method, Class testClass) { try { return parameterValuesCache.get( method, () -> { - List> testParameterValuesList = + List> testParameterValuesList = getAnnotationValuesForUsedAnnotationTypes(method, testClass); return FluentIterable.from(Lists.cartesianProduct(testParameterValuesList)) @@ -758,7 +779,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } } - private List getParameterValuesForTest( + private List getParameterValuesForTest( TestIndexHolder testIndexHolder, Class testClass) { verify( testIndexHolder.testClassName().equals(testClass.getName()), @@ -775,7 +796,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso * Returns the list of annotation index for all annotations defined in a given test method and its * class. */ - private ImmutableList> getAnnotationValuesForUsedAnnotationTypes( + private ImmutableList> getAnnotationValuesForUsedAnnotationTypes( Method method, Class testClass) { ImmutableList annotationTypes = FluentIterable.from(getAnnotationTypeOrigins(testClass, Origin.CLASS)) @@ -871,7 +892,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso * method, and the one defined on the method takes precedence over the same annotation defined on * the class. */ - private ImmutableList> getAnnotationFromParametersOrTestOrClass( + private ImmutableList> getAnnotationFromParametersOrTestOrClass( AnnotationTypeOrigin annotationTypeOrigin, Method method, Class testClass) { Origin origin = annotationTypeOrigin.origin(); Class annotationType = annotationTypeOrigin.annotationType(); @@ -887,7 +908,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso Annotation annotation = getOnlyConstructor(testClass).getAnnotation(annotationType); if (annotation != null) { return ImmutableList.of( - TestParameterValue.create(AnnotationWithMetadata.withoutMetadata(annotation), origin)); + TestParameterValueHolder.create( + AnnotationWithMetadata.withoutMetadata(annotation), origin)); } } else if (origin == Origin.METHOD_PARAMETER) { @@ -899,7 +921,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } else if (origin == Origin.METHOD) { if (method.isAnnotationPresent(annotationType)) { return ImmutableList.of( - TestParameterValue.create( + TestParameterValueHolder.create( AnnotationWithMetadata.withoutMetadata(method.getAnnotation(annotationType)), origin)); } @@ -924,19 +946,21 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso Annotation annotation = testClass.getAnnotation(annotationType); if (annotation != null) { return ImmutableList.of( - TestParameterValue.create(AnnotationWithMetadata.withoutMetadata(annotation), origin)); + TestParameterValueHolder.create( + AnnotationWithMetadata.withoutMetadata(annotation), origin)); } } return ImmutableList.of(); } - private static ImmutableList> toTestParameterValueList( + private static ImmutableList> toTestParameterValueList( List annotationWithMetadatas, Origin origin) { return FluentIterable.from(annotationWithMetadatas) .transform( annotationWithMetadata -> - (List) - new ArrayList<>(TestParameterValue.create(annotationWithMetadata, origin))) + (List) + new ArrayList<>( + TestParameterValueHolder.create(annotationWithMetadata, origin))) .toList(); } @@ -1019,27 +1043,27 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); try { if (testIndexHolder != null) { - List testParameterValues = + List testParameterValues = getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()); // Do not include {@link Origin#METHOD_PARAMETER} nor {@link Origin#CONSTRUCTOR_PARAMETER} // annotations. - List testParameterValuesForFieldInjection = + List testParameterValuesForFieldInjection = filterByOrigin(testParameterValues, Origin.CLASS, Origin.FIELD, Origin.METHOD); // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class // in the example above. - List remainingTestParameterValuesForFieldInjection = + List remainingTestParameterValuesForFieldInjection = new ArrayList<>(testParameterValuesForFieldInjection); for (Field declaredField : FluentIterable.from(listWithParents(testInstance.getClass())) .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields())) .toList()) { - for (TestParameterValue testParameterValue : + for (TestParameterValueHolder testParameterValue : remainingTestParameterValuesForFieldInjection) { if (declaredField.isAnnotationPresent( testParameterValue.annotationTypeOrigin().annotationType())) { declaredField.setAccessible(true); - declaredField.set(testInstance, testParameterValue.value()); + declaredField.set(testInstance, testParameterValue.unwrappedValue()); remainingTestParameterValuesForFieldInjection.remove(testParameterValue); break; } @@ -1052,11 +1076,11 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } /** - * Returns an {@link TestParameterValue} list that contains only the values originating from one - * of the {@code origins}. + * Returns an {@link TestParameterValueHolder} list that contains only the values originating from + * one of the {@code origins}. */ - private static ImmutableList filterByOrigin( - List testParameterValues, Origin... origins) { + private static ImmutableList filterByOrigin( + List testParameterValues, Origin... origins) { Set originsToFilterBy = ImmutableSet.copyOf(origins); return FluentIterable.from(testParameterValues) .filter( @@ -1079,12 +1103,12 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso /** Returns a {@link TestParameterAnnotation}'s value for a method or constructor parameter. */ private Object getParameterValue( - List testParameterValues, + List testParameterValues, Class methodParameterType, Annotation[] parameterAnnotations, List> processedAnnotationTypes) { List> iteratedAnnotationTypes = new ArrayList<>(); - for (TestParameterValue testParameterValue : testParameterValues) { + for (TestParameterValueHolder testParameterValue : testParameterValues) { // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class // in the example above. for (Annotation parameterAnnotation : parameterAnnotations) { @@ -1101,21 +1125,21 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso if (Collections.frequency(processedAnnotationTypes, annotationType) == Collections.frequency(iteratedAnnotationTypes, annotationType)) { processedAnnotationTypes.add(annotationType); - return testParameterValue.value(); + return testParameterValue.unwrappedValue(); } iteratedAnnotationTypes.add(annotationType); } } } // If no annotation matches, use the method parameter type. - for (TestParameterValue testParameterValue : testParameterValues) { + for (TestParameterValueHolder testParameterValue : testParameterValues) { // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class // in the example above. if (methodParameterType.isAssignableFrom( getValueMethodReturnType( testParameterValue.annotationTypeOrigin().annotationType(), /* paramClass= */ Optional.absent()))) { - return testParameterValue.value(); + return testParameterValue.unwrappedValue(); } } throw new IllegalStateException( @@ -1158,10 +1182,11 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso /** * Returns whether the test should be skipped according to the {@code annotationType}'s {@link - * TestParameterValidator} and the current list of {@link TestParameterValue}. + * TestParameterValidator} and the current list of {@link TestParameterValueHolder}. */ private static boolean callShouldSkip( - Class annotationType, List testParameterValues) { + Class annotationType, + List testParameterValues) { TestParameterAnnotation annotation = annotationType.getAnnotation(TestParameterAnnotation.class); Class validator = annotation.validator(); @@ -1177,14 +1202,14 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso private static class ValidatorContext implements TestParameterValidator.Context { - private final List testParameterValues; + private final List testParameterValues; private final Set valueList; - public ValidatorContext(List testParameterValues) { + public ValidatorContext(List testParameterValues) { this.testParameterValues = testParameterValues; this.valueList = FluentIterable.from(testParameterValues) - .transform(TestParameterValue::value) + .transform(TestParameterValueHolder::unwrappedValue) .filter(Objects::nonNull) .toSet(); } @@ -1201,17 +1226,18 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso @Override public Optional getValue(Class testParameter) { - return getParameter(testParameter).transform(TestParameterValue::value); + return getParameter(testParameter).transform(TestParameterValueHolder::unwrappedValue); } @Override public List getSpecifiedValues(Class testParameter) { return getParameter(testParameter) - .transform(TestParameterValue::specifiedValues) + .transform(TestParameterValueHolder::specifiedValues) .or(ImmutableList.of()); } - private Optional getParameter(Class testParameter) { + private Optional getParameter( + Class testParameter) { return FluentIterable.from(testParameterValues) .firstMatch(value -> value.annotationTypeOrigin().annotationType().equals(testParameter)); } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValue.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValue.java new file mode 100644 index 0000000..508ecf7 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValue.java @@ -0,0 +1,43 @@ +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Optional; +import javax.annotation.Nullable; + +/** + * Wrapper class around a parameter value. Use this to give a value a name that is different from + * its {@code toString()} method. + */ +public class TestParameterValue { + private final @Nullable Object wrappedValue; + private final Optional customName; + + private TestParameterValue(@Nullable Object wrappedValue, Optional customName) { + this.wrappedValue = wrappedValue; + this.customName = checkNotNull(customName); + } + + /** Wraps the given value. */ + public static TestParameterValue wrap(@Nullable Object wrappedValue) { + return new TestParameterValue(wrappedValue, /* customName= */ Optional.absent()); + } + + /** + * Returns a new {@link TestParameterValue} instance that stores the given name. The + * TestParameterInjector framework will use this name instead of {@code wrappedValue.toString()} + * when generating the test name. + */ + public TestParameterValue withName(String name) { + return new TestParameterValue(wrappedValue, Optional.of(name)); + } + + @Nullable + Object getWrappedValue() { + return wrappedValue; + } + + Optional getCustomName() { + return customName; + } +} diff --git a/junit5/src/test/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorJUnit5Test.java b/junit5/src/test/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorJUnit5Test.java index 25b6316..0ebf54b 100644 --- a/junit5/src/test/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorJUnit5Test.java +++ b/junit5/src/test/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorJUnit5Test.java @@ -303,6 +303,7 @@ class TestParameterInjectorJUnit5Test { .put("stringTest[A]", "A") .put("stringTest[B]", "B") .put("stringTest[string=null]", "null") + .put("stringTest[wizard]", "harry") .put("charMatcherTest[CharMatcher.any()]", "CharMatcher.any()") .put("charMatcherTest[CharMatcher.ascii()]", "CharMatcher.ascii()") .put("charMatcherTest[CharMatcher.whitespace()]", "CharMatcher.whitespace()") @@ -311,8 +312,8 @@ class TestParameterInjectorJUnit5Test { private static final class TestStringProvider implements TestParameterValuesProvider { @Override - public List provideValues() { - return newArrayList("A", "B", null); + public List provideValues() { + return newArrayList("A", "B", null, value("harry").withName("wizard")); } } -- cgit v1.2.3 From 963c4d8570a1991db39b90e21914cf367ffe4843 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 13 Oct 2023 11:45:07 +0000 Subject: Add missing license statement --- .../junit/testparameterinjector/TestParameterValue.java | 14 ++++++++++++++ .../testparameterinjector/junit5/TestParameterValue.java | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValue.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValue.java index 44d7951..a16f0cb 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValue.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValue.java @@ -1,3 +1,17 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + package com.google.testing.junit.testparameterinjector; import static com.google.common.base.Preconditions.checkNotNull; diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValue.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValue.java index 508ecf7..f748521 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValue.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValue.java @@ -1,3 +1,17 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + package com.google.testing.junit.testparameterinjector.junit5; import static com.google.common.base.Preconditions.checkNotNull; -- cgit v1.2.3 From 1085d37c9e3b7975b9e6ac66fd258da5d7f58760 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 13 Oct 2023 11:49:37 +0000 Subject: Remove unused method --- .../TestParameterAnnotationMethodProcessor.java | 7 ------- .../junit5/TestParameterAnnotationMethodProcessor.java | 7 ------- 2 files changed, 14 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index f8afe65..31ecf25 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -95,12 +95,6 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso @SuppressWarnings("AutoValueImmutableFields") // intentional to allow null values abstract List 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> 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. @@ -145,7 +139,6 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso newArrayList( FluentIterable.from(specifiedValues) .transform(TestParameterValue::getWrappedValue)), - annotationWithMetadata.paramClass(), annotationWithMetadata.paramName())) .toList(); } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java index 05a4226..834a83d 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java @@ -95,12 +95,6 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso @SuppressWarnings("AutoValueImmutableFields") // intentional to allow null values abstract List 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> 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. @@ -145,7 +139,6 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso newArrayList( FluentIterable.from(specifiedValues) .transform(TestParameterValue::getWrappedValue)), - annotationWithMetadata.paramClass(), annotationWithMetadata.paramName())) .toList(); } -- cgit v1.2.3 From 98c441cdcbcfb5799f6d19ac6eebfeb986b9da37 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 13 Oct 2023 11:50:34 +0000 Subject: Add an entry for custom value names to the CHANGELOG --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dde5e13..d1954f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ ## 1.13 +- Add support for setting a custom name for a `@TestParameter` value given via a provider: + +```java +private static final class FruitProvider implements TestParameterValuesProvider { + @Override + public List provideValues() { + return ImmutableList.of( + value(new Apple()).withName("apple"), + value(new Banana()).withName("banana")); + } +} +``` + - Add support for `BigInteger` and `UnsignedLong` - JUnit4: Fix for interrupted test cases causing random failures with thread reuse (porting [the earlier fix in -- cgit v1.2.3 From cf991ecc0ef2ad3265c92a58e8c944f1e8d114a5 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 13 Oct 2023 11:53:35 +0000 Subject: Add to the README that value providers can have custom value names --- README.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 321397a..b0bc84b 100644 --- a/README.md +++ b/README.md @@ -345,9 +345,26 @@ private static final class CharMatcherProvider implements TestParameterValuesPro } ``` -Note that `provideValues()` dynamically construct the returned list, e.g. by -reading a file. There are no restrictions on the object types returned, but note -that `toString()` will be used for the test names. +Notes: + +- The `provideValues()` method can dynamically construct the returned list, + e.g. by reading a file. +- There are no restrictions on the object types returned. +- The `provideValues()` method is called before `@BeforeClass`, so don't rely + on any static state initialized in there. +- The returned objects' `toString()` will be used for the test names. If you + want to customize the value names, you can do that as follows: + + ``` + private static final class FruitProvider implements TestParameterValuesProvider { + @Override + public List provideValues() { + return ImmutableList.of( + value(new Apple()).withName("apple"), + value(new Banana()).withName("banana")); + } + } + ``` ### Dynamic parameter generation for `@TestParameters` -- cgit v1.2.3 From 26744e06e2a05f6967fc4273b7eeada58347f7f9 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Fri, 13 Oct 2023 12:05:10 +0000 Subject: Bump version to v1.13 in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b0bc84b..775473f 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ And add the following dependency to your `.pom` file: com.google.testparameterinjector test-parameter-injector - 1.12 + 1.13 test ``` @@ -97,7 +97,7 @@ And add the following dependency to your `.pom` file: com.google.testparameterinjector test-parameter-injector-junit5 - 1.12 + 1.13 test ``` -- cgit v1.2.3 From 39f38f236b3472cfc993565022ef17e891b6a198 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 24 Oct 2023 14:24:34 +0000 Subject: Add support for Powermock by making getOnlyConstructor() more lenient (filter out non-public constructors). This is consistent with how JUnit's getOnlyConstructor() is implemented. Fixes https://github.com/google/TestParameterInjector/issues/40 --- .../testparameterinjector/PluggableTestRunner.java | 7 +++- .../TestParameterAnnotationMethodProcessor.java | 17 +++----- .../TestParameterInjectorUtils.java | 47 ++++++++++++++++++++++ .../TestParametersMethodProcessor.java | 14 ++----- ...TestParameterAnnotationMethodProcessorTest.java | 12 +++++- .../TestParametersMethodProcessorTest.java | 14 +++++-- .../TestParameterAnnotationMethodProcessor.java | 17 +++----- .../junit5/TestParameterInjectorUtils.java | 47 ++++++++++++++++++++++ .../junit5/TestParametersMethodProcessor.java | 14 ++----- 9 files changed, 136 insertions(+), 53 deletions(-) create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorUtils.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorUtils.java diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java index 5f4c061..b2a0ad8 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java @@ -316,7 +316,8 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { private Object createTestForMethod(FrameworkMethod method) throws Exception { TestInfo testInfo = ((OverriddenFrameworkMethod) method).getTestInfo(); - Constructor constructor = getTestClass().getOnlyConstructor(); + Constructor constructor = + TestParameterInjectorUtils.getOnlyConstructor(getTestClass().getJavaClass()); // Construct a test instance Object testInstance; @@ -343,7 +344,9 @@ abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { @Override protected final void validateZeroArgConstructor(List errorsReturned) { ExecutableValidationResult validationResult = - getTestMethodProcessors().validateConstructor(getTestClass().getOnlyConstructor()); + getTestMethodProcessors() + .validateConstructor( + TestParameterInjectorUtils.getOnlyConstructor(getTestClass().getJavaClass())); if (validationResult.wasValidated()) { errorsReturned.addAll(validationResult.validationErrors()); diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 31ecf25..4132f12 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -867,7 +867,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso case FIELD: // Fall through. case CLASS: return getAnnotationListWithType( - getOnlyConstructor(testClass).getAnnotations(), + TestParameterInjectorUtils.getOnlyConstructor(testClass) + .getAnnotations(), annotationTypeOrigin.annotationType()) .isEmpty(); default: @@ -890,7 +891,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso Origin origin = annotationTypeOrigin.origin(); Class annotationType = annotationTypeOrigin.annotationType(); if (origin == Origin.CONSTRUCTOR_PARAMETER) { - Constructor constructor = getOnlyConstructor(testClass); + Constructor constructor = TestParameterInjectorUtils.getOnlyConstructor(testClass); List annotations = getAnnotationWithMetadataListWithType(constructor, annotationType); @@ -898,7 +899,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return toTestParameterValueList(annotations, origin); } } else if (origin == Origin.CONSTRUCTOR) { - Annotation annotation = getOnlyConstructor(testClass).getAnnotation(annotationType); + Annotation annotation = + TestParameterInjectorUtils.getOnlyConstructor(testClass).getAnnotation(annotationType); if (annotation != null) { return ImmutableList.of( TestParameterValueHolder.create( @@ -1022,15 +1024,6 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso .toList(); } - private static Constructor getOnlyConstructor(Class testClass) { - Constructor[] constructors = testClass.getDeclaredConstructors(); - checkState( - constructors.length == 1, - "a single public constructor is required for class %s", - testClass); - return constructors[0]; - } - @Override public void postProcessTestInstance(Object testInstance, TestInfo testInfo) { TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorUtils.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorUtils.java new file mode 100644 index 0000000..215719a --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorUtils.java @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Iterables.getOnlyElement; + +import com.google.common.collect.ImmutableList; +import java.lang.reflect.Constructor; + +/** Shared utility methods. */ +class TestParameterInjectorUtils { + + /** + * Return the only public constructor of the given test class. If there is none, return the only + * constructor. + * + *

          Normally, there should be exactly one constructor (public or other), but some frameworks + * introduce an extra non-public constructor (see + * https://github.com/google/TestParameterInjector/issues/40). + */ + static Constructor getOnlyConstructor(Class testClass) { + ImmutableList> constructors = ImmutableList.copyOf(testClass.getConstructors()); + if (constructors.isEmpty()) { + // There are no public constructors. This is likely a JUnit5 test, so we should take the only + // non-public constructor instead. + constructors = ImmutableList.copyOf(testClass.getDeclaredConstructors()); + } + checkState( + constructors.size() == 1, "Expected exactly one constructor, but got %s", constructors); + return getOnlyElement(constructors); + } + + private TestParameterInjectorUtils() {} +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java index 1a8d022..7dffc29 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java @@ -16,7 +16,6 @@ package com.google.testing.junit.testparameterinjector; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Verify.verify; -import static com.google.common.collect.Iterables.getOnlyElement; import com.google.auto.value.AutoAnnotation; import com.google.common.base.Optional; @@ -90,7 +89,8 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { @Override public List calculateTestInfos(TestInfo originalTest) { boolean constructorIsParameterized = - hasRelevantAnnotation(getOnlyConstructor(originalTest.getTestClass())); + hasRelevantAnnotation( + TestParameterInjectorUtils.getOnlyConstructor(originalTest.getTestClass())); boolean methodIsParameterized = hasRelevantAnnotation(originalTest.getMethod()); if (!constructorIsParameterized && !methodIsParameterized) { @@ -148,7 +148,7 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { private ImmutableList> getConstructorParametersOrSingleAbsentElement(Class testClass) { - Constructor constructor = getOnlyConstructor(testClass); + Constructor constructor = TestParameterInjectorUtils.getOnlyConstructor(testClass); return hasRelevantAnnotation(constructor) ? FluentIterable.from(getConstructorParameters(constructor)) .transform(Optional::of) @@ -444,14 +444,6 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { .toArray(Object.class)); } - private static Constructor getOnlyConstructor(Class testClass) { - ImmutableList> constructors = - ImmutableList.copyOf(testClass.getDeclaredConstructors()); - checkState( - constructors.size() == 1, "Expected exactly one constructor, but got %s", constructors); - return getOnlyElement(constructors); - } - /** * 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. diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java index 3fff85b..df3a16b 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java @@ -675,6 +675,14 @@ public class TestParameterAnnotationMethodProcessorTest { void test(@TestParameter boolean b) {} } + @ClassTestResult(Result.FAILURE) + public static class ErrorPackagePrivateConstructor { + ErrorPackagePrivateConstructor() {} + + @Test + public void test1() {} + } + public enum EnumA { A1, A2 @@ -829,7 +837,7 @@ public class TestParameterAnnotationMethodProcessorTest { case SUCCESS_FOR_ALL_PLACEMENTS_ONLY: assertThrows( - RuntimeException.class, + Exception.class, () -> SharedTestUtilitiesJUnit4.runTestsAndGetFailures( newTestRunnerWithParameterizedSupport( @@ -838,7 +846,7 @@ public class TestParameterAnnotationMethodProcessorTest { case FAILURE: assertThrows( - RuntimeException.class, + Exception.class, () -> SharedTestUtilitiesJUnit4.runTestsAndGetFailures( newTestRunnerWithParameterizedSupport( diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java index d5417e6..5628330 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java @@ -494,6 +494,14 @@ public class TestParametersMethodProcessorTest { public void test1(TestEnum testEnum) {} } + @RunAsTest(failsWithMessage = "Test class should have exactly one public constructor") + public static class InvalidTestBecausePackagePrivateConstructor { + InvalidTestBecausePackagePrivateConstructor() {} + + @Test + public void test1() {} + } + @Parameters(name = "{0}") public static Collection parameters() { return Arrays.stream(TestParametersMethodProcessorTest.class.getClasses()) @@ -527,12 +535,12 @@ public class TestParametersMethodProcessorTest { public void test_failure() throws Exception { assume().that(maybeFailureMessage.isPresent()).isTrue(); - IllegalStateException exception = + Exception exception = assertThrows( - IllegalStateException.class, + Exception.class, () -> SharedTestUtilitiesJUnit4.runTestsAndGetFailures(newTestRunner())); - assertThat(exception).hasMessageThat().isEqualTo(maybeFailureMessage.get()); + assertThat(exception).hasMessageThat().contains(maybeFailureMessage.get()); } private PluggableTestRunner newTestRunner() throws Exception { diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java index 834a83d..5aefe21 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java @@ -867,7 +867,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso case FIELD: // Fall through. case CLASS: return getAnnotationListWithType( - getOnlyConstructor(testClass).getAnnotations(), + TestParameterInjectorUtils.getOnlyConstructor(testClass) + .getAnnotations(), annotationTypeOrigin.annotationType()) .isEmpty(); default: @@ -890,7 +891,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso Origin origin = annotationTypeOrigin.origin(); Class annotationType = annotationTypeOrigin.annotationType(); if (origin == Origin.CONSTRUCTOR_PARAMETER) { - Constructor constructor = getOnlyConstructor(testClass); + Constructor constructor = TestParameterInjectorUtils.getOnlyConstructor(testClass); List annotations = getAnnotationWithMetadataListWithType(constructor, annotationType); @@ -898,7 +899,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return toTestParameterValueList(annotations, origin); } } else if (origin == Origin.CONSTRUCTOR) { - Annotation annotation = getOnlyConstructor(testClass).getAnnotation(annotationType); + Annotation annotation = + TestParameterInjectorUtils.getOnlyConstructor(testClass).getAnnotation(annotationType); if (annotation != null) { return ImmutableList.of( TestParameterValueHolder.create( @@ -1022,15 +1024,6 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso .toList(); } - private static Constructor getOnlyConstructor(Class testClass) { - Constructor[] constructors = testClass.getDeclaredConstructors(); - checkState( - constructors.length == 1, - "a single public constructor is required for class %s", - testClass); - return constructors[0]; - } - @Override public void postProcessTestInstance(Object testInstance, TestInfo testInfo) { TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorUtils.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorUtils.java new file mode 100644 index 0000000..cd47ea1 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorUtils.java @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Iterables.getOnlyElement; + +import com.google.common.collect.ImmutableList; +import java.lang.reflect.Constructor; + +/** Shared utility methods. */ +class TestParameterInjectorUtils { + + /** + * Return the only public constructor of the given test class. If there is none, return the only + * constructor. + * + *

          Normally, there should be exactly one constructor (public or other), but some frameworks + * introduce an extra non-public constructor (see + * https://github.com/google/TestParameterInjector/issues/40). + */ + static Constructor getOnlyConstructor(Class testClass) { + ImmutableList> constructors = ImmutableList.copyOf(testClass.getConstructors()); + if (constructors.isEmpty()) { + // There are no public constructors. This is likely a JUnit5 test, so we should take the only + // non-public constructor instead. + constructors = ImmutableList.copyOf(testClass.getDeclaredConstructors()); + } + checkState( + constructors.size() == 1, "Expected exactly one constructor, but got %s", constructors); + return getOnlyElement(constructors); + } + + private TestParameterInjectorUtils() {} +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParametersMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParametersMethodProcessor.java index 3ada177..26a1e65 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParametersMethodProcessor.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParametersMethodProcessor.java @@ -16,7 +16,6 @@ package com.google.testing.junit.testparameterinjector.junit5; import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Verify.verify; -import static com.google.common.collect.Iterables.getOnlyElement; import com.google.auto.value.AutoAnnotation; import com.google.common.base.Optional; @@ -90,7 +89,8 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { @Override public List calculateTestInfos(TestInfo originalTest) { boolean constructorIsParameterized = - hasRelevantAnnotation(getOnlyConstructor(originalTest.getTestClass())); + hasRelevantAnnotation( + TestParameterInjectorUtils.getOnlyConstructor(originalTest.getTestClass())); boolean methodIsParameterized = hasRelevantAnnotation(originalTest.getMethod()); if (!constructorIsParameterized && !methodIsParameterized) { @@ -148,7 +148,7 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { private ImmutableList> getConstructorParametersOrSingleAbsentElement(Class testClass) { - Constructor constructor = getOnlyConstructor(testClass); + Constructor constructor = TestParameterInjectorUtils.getOnlyConstructor(testClass); return hasRelevantAnnotation(constructor) ? FluentIterable.from(getConstructorParameters(constructor)) .transform(Optional::of) @@ -444,14 +444,6 @@ final class TestParametersMethodProcessor implements TestMethodProcessor { .toArray(Object.class)); } - private static Constructor getOnlyConstructor(Class testClass) { - ImmutableList> constructors = - ImmutableList.copyOf(testClass.getDeclaredConstructors()); - checkState( - constructors.size() == 1, "Expected exactly one constructor, but got %s", constructors); - return getOnlyElement(constructors); - } - /** * 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. -- cgit v1.2.3 From cbcab685f0593ff890dc7f3917f338a8b2baffaa Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 24 Oct 2023 15:30:25 +0000 Subject: Update CHANGELOG with the latest bugfix --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1954f1..535d387 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.14 + +- Fixed multiple constructors error when this library is used with Powermock. + See https://github.com/google/TestParameterInjector/issues/40. + ## 1.13 - Add support for setting a custom name for a `@TestParameter` value given via a provider: -- cgit v1.2.3 From 5d67f8cb66481638bcd117227bd0103c32c84491 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 24 Oct 2023 15:30:27 +0000 Subject: Bump version to v1.14 in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 775473f..02a9357 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ And add the following dependency to your `.pom` file: com.google.testparameterinjector test-parameter-injector - 1.13 + 1.14 test ``` @@ -97,7 +97,7 @@ And add the following dependency to your `.pom` file: com.google.testparameterinjector test-parameter-injector-junit5 - 1.13 + 1.14 test ``` -- cgit v1.2.3 From d81fc96a567a6f82c7b9883e4adcd7df8842c286 Mon Sep 17 00:00:00 2001 From: Chris Shao Date: Sun, 26 Nov 2023 15:50:31 -0600 Subject: fix: sort the nondeterministic method arrays and match fields with name --- .../TestParameterAnnotationMethodProcessor.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 4132f12..a098932 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -1047,7 +1047,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso for (TestParameterValueHolder testParameterValue : remainingTestParameterValuesForFieldInjection) { if (declaredField.isAnnotationPresent( - testParameterValue.annotationTypeOrigin().annotationType())) { + testParameterValue.annotationTypeOrigin().annotationType()) && + declaredField.getName() == testParameterValue.paramName().get()) { declaredField.setAccessible(true); declaredField.set(testInstance, testParameterValue.unwrappedValue()); remainingTestParameterValuesForFieldInjection.remove(testParameterValue); @@ -1275,7 +1276,9 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso private ImmutableList getMethodsIncludingParents(Class clazz) { ImmutableList.Builder resultBuilder = ImmutableList.builder(); while (clazz != null) { - resultBuilder.add(clazz.getDeclaredMethods()); + Method[] declaredMethods = clazz.getDeclaredMethods(); + Arrays.sort(declaredMethods, Comparator.comparing(Method::getName)); + resultBuilder.add(declaredMethods); clazz = clazz.getSuperclass(); } return resultBuilder.build(); -- cgit v1.2.3 From 468611d306e5f4a55aec5971fee39197410316cd Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 6 Dec 2023 09:02:39 +0000 Subject: Rewrite and extension of https://github.com/google/TestParameterInjector/pull/41 to JUnit5 --- .../TestParameterAnnotationMethodProcessor.java | 29 ++++++++++++++-------- .../TestParameterAnnotationMethodProcessor.java | 22 ++++++++++++---- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index a098932..4a9b163 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -34,6 +34,7 @@ import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; import com.google.common.collect.Range; import com.google.common.util.concurrent.UncheckedExecutionException; import com.google.testing.junit.testparameterinjector.TestInfo.TestInfoParameter; @@ -734,7 +735,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso .withExtraAnnotation( TestIndexHolderFactory.create( /* methodIndex= */ strictIndexOf( - getMethodsIncludingParents(originalTest.getTestClass()), + getMethodsIncludingParentsSorted(originalTest.getTestClass()), originalTest.getMethod()), parametersIndex, originalTest.getTestClass().getName()))); @@ -780,7 +781,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso + " class that this runner is handling (%s)", testIndexHolder.testClassName(), testClass.getName()); - Method testMethod = getMethodsIncludingParents(testClass).get(testIndexHolder.methodIndex()); + Method testMethod = + getMethodsIncludingParentsSorted(testClass).get(testIndexHolder.methodIndex()); return getParameterValuesForMethod(testMethod, testClass) .get(testIndexHolder.parametersIndex()); } @@ -1047,8 +1049,12 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso for (TestParameterValueHolder testParameterValue : remainingTestParameterValuesForFieldInjection) { if (declaredField.isAnnotationPresent( - testParameterValue.annotationTypeOrigin().annotationType()) && - declaredField.getName() == testParameterValue.paramName().get()) { + testParameterValue.annotationTypeOrigin().annotationType())) { + if (testParameterValue.paramName().isPresent() + && !declaredField.getName().equals(testParameterValue.paramName().get())) { + // names don't match + continue; + } declaredField.setAccessible(true); declaredField.set(testInstance, testParameterValue.unwrappedValue()); remainingTestParameterValuesForFieldInjection.remove(testParameterValue); @@ -1140,7 +1146,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso @Retention(RUNTIME) @interface TestIndexHolder { - /** The index of the test method in {@code getMethodsIncludingParents(testClass)} */ + /** The index of the test method in {@code getMethodsIncludingParentsSorted(testClass)} */ int methodIndex(); /** @@ -1273,15 +1279,18 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return index; } - private ImmutableList getMethodsIncludingParents(Class clazz) { + private ImmutableList getMethodsIncludingParentsSorted(Class clazz) { ImmutableList.Builder resultBuilder = ImmutableList.builder(); while (clazz != null) { - Method[] declaredMethods = clazz.getDeclaredMethods(); - Arrays.sort(declaredMethods, Comparator.comparing(Method::getName)); - resultBuilder.add(declaredMethods); + resultBuilder.add(clazz.getDeclaredMethods()); clazz = clazz.getSuperclass(); } - return resultBuilder.build(); + // Because getDeclaredMethods()'s order is not specified, there is the theoretical possibility + // that the order of methods is unstable. To partly fix this, we sort the result based on method + // name. This is still not perfect because of method overloading, but that should be + // sufficiently rare for test names. + return ImmutableList.sortedCopyOf( + Ordering.natural().onResultOf(Method::getName), resultBuilder.build()); } private static ImmutableList> listWithParents(Class clazz) { diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java index 5aefe21..cb5ceec 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java @@ -34,6 +34,7 @@ import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; import com.google.common.collect.Range; import com.google.common.util.concurrent.UncheckedExecutionException; import com.google.testing.junit.testparameterinjector.junit5.TestInfo.TestInfoParameter; @@ -734,7 +735,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso .withExtraAnnotation( TestIndexHolderFactory.create( /* methodIndex= */ strictIndexOf( - getMethodsIncludingParents(originalTest.getTestClass()), + getMethodsIncludingParentsSorted(originalTest.getTestClass()), originalTest.getMethod()), parametersIndex, originalTest.getTestClass().getName()))); @@ -780,7 +781,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso + " class that this runner is handling (%s)", testIndexHolder.testClassName(), testClass.getName()); - Method testMethod = getMethodsIncludingParents(testClass).get(testIndexHolder.methodIndex()); + Method testMethod = + getMethodsIncludingParentsSorted(testClass).get(testIndexHolder.methodIndex()); return getParameterValuesForMethod(testMethod, testClass) .get(testIndexHolder.parametersIndex()); } @@ -1048,6 +1050,11 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso remainingTestParameterValuesForFieldInjection) { if (declaredField.isAnnotationPresent( testParameterValue.annotationTypeOrigin().annotationType())) { + if (testParameterValue.paramName().isPresent() + && !declaredField.getName().equals(testParameterValue.paramName().get())) { + // names don't match + continue; + } declaredField.setAccessible(true); declaredField.set(testInstance, testParameterValue.unwrappedValue()); remainingTestParameterValuesForFieldInjection.remove(testParameterValue); @@ -1139,7 +1146,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso @Retention(RUNTIME) @interface TestIndexHolder { - /** The index of the test method in {@code getMethodsIncludingParents(testClass)} */ + /** The index of the test method in {@code getMethodsIncludingParentsSorted(testClass)} */ int methodIndex(); /** @@ -1272,13 +1279,18 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return index; } - private ImmutableList getMethodsIncludingParents(Class clazz) { + private ImmutableList getMethodsIncludingParentsSorted(Class clazz) { ImmutableList.Builder resultBuilder = ImmutableList.builder(); while (clazz != null) { resultBuilder.add(clazz.getDeclaredMethods()); clazz = clazz.getSuperclass(); } - return resultBuilder.build(); + // Because getDeclaredMethods()'s order is not specified, there is the theoretical possibility + // that the order of methods is unstable. To partly fix this, we sort the result based on method + // name. This is still not perfect because of method overloading, but that should be + // sufficiently rare for test names. + return ImmutableList.sortedCopyOf( + Ordering.natural().onResultOf(Method::getName), resultBuilder.build()); } private static ImmutableList> listWithParents(Class clazz) { -- cgit v1.2.3 From 9dd42b625d73ebcc7dd751349fe70c44902f6f0f Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 16 Jan 2024 09:53:24 +0000 Subject: TestParameterAnnotationMethodProcessor.AnnotationWithMetadata: Stop supporting equals() and hashCode(). This is a preparation for adding a new field to this class, which itself is an implementation change for adding a context-aware value provider for @TestParameter. https://github.com/google/TestParameterInjector/issues/44 --- .../TestParameterAnnotationMethodProcessor.java | 12 ++++++++++++ .../junit5/TestParameterAnnotationMethodProcessor.java | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 4a9b163..79ba92b 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -274,6 +274,18 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( annotation, Optional.absent(), Optional.absent()); } + + // Prevent anyone relying on equals() and hashCode() so that it remains possible to add fields + // to this class without breaking existing code. + @Override + public final boolean equals(Object other) { + throw new UnsupportedOperationException("Equality is not supported"); + } + + @Override + public final int hashCode() { + throw new UnsupportedOperationException("hashCode() is not supported"); + } } private final boolean onlyForFieldsAndParameters; diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java index cb5ceec..23462ac 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java @@ -274,6 +274,18 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( annotation, Optional.absent(), Optional.absent()); } + + // Prevent anyone relying on equals() and hashCode() so that it remains possible to add fields + // to this class without breaking existing code. + @Override + public final boolean equals(Object other) { + throw new UnsupportedOperationException("Equality is not supported"); + } + + @Override + public final int hashCode() { + throw new UnsupportedOperationException("hashCode() is not supported"); + } } private final boolean onlyForFieldsAndParameters; -- cgit v1.2.3 From f82c1ff0f4c7999493401bbfedebf8ab1d1eceb3 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 16 Jan 2024 09:55:10 +0000 Subject: TestParameterValueProvider: Add internal-only context-aware version of provideValues(). This will be used by a context-aware value provider for @TestParameter in a follow-up change. https://github.com/google/TestParameterInjector/issues/44 --- .../TestParameterAnnotationMethodProcessor.java | 113 ++++++++++++++++----- .../TestParameterValueProvider.java | 48 ++++++++- .../TestParameterAnnotationMethodProcessor.java | 113 ++++++++++++++++----- .../junit5/TestParameterValueProvider.java | 48 ++++++++- 4 files changed, 272 insertions(+), 50 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index 79ba92b..d186e84 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -190,7 +190,11 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso valueProvider .getConstructor() .newInstance() - .provideValues(annotation, annotationWithMetadata.paramClass())) + .provideValues( + annotation, + annotationWithMetadata.otherAnnotations(), + annotationWithMetadata.paramClass(), + annotationWithMetadata.testClass())) .transform( value -> (value instanceof TestParameterValue) @@ -247,6 +251,15 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso */ abstract Annotation annotation(); + /** + * A list of all other annotations on the field or parameter that was annotated with {@code + * annotation}. + * + *

          In case the annotation is annotating a method, constructor or class, {@code + * parameterClass} is an empty list. + */ + abstract ImmutableList otherAnnotations(); + /** * 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. @@ -259,20 +272,48 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso */ abstract Optional paramName(); + /** The class that contains the test that is currently being run. */ + abstract Class testClass(); + public static AnnotationWithMetadata withMetadata( - Annotation annotation, Class paramClass, String paramName) { + Annotation annotation, + Annotation[] allAnnotations, + Class paramClass, + String paramName, + Class testClass) { return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( - annotation, Optional.of(paramClass), Optional.of(paramName)); + annotation, + /* otherAnnotations= */ FluentIterable.from(allAnnotations) + .filter(a -> !a.equals(annotation)) + .toList(), + Optional.of(paramClass), + Optional.of(paramName), + testClass); } - public static AnnotationWithMetadata withMetadata(Annotation annotation, Class paramClass) { + public static AnnotationWithMetadata withMetadata( + Annotation annotation, + Annotation[] allAnnotations, + Class paramClass, + Class testClass) { return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( - annotation, Optional.of(paramClass), Optional.absent()); + annotation, + /* otherAnnotations= */ FluentIterable.from(allAnnotations) + .filter(a -> !a.equals(annotation)) + .toList(), + Optional.of(paramClass), + Optional.absent(), + testClass); } - public static AnnotationWithMetadata withoutMetadata(Annotation annotation) { + public static AnnotationWithMetadata withoutMetadata( + Annotation annotation, Class testClass) { return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( - annotation, Optional.absent(), Optional.absent()); + annotation, + /* otherAnnotations= */ ImmutableList.of(), + /* paramClass= */ Optional.absent(), + /* paramName= */ Optional.absent(), + testClass); } // Prevent anyone relying on equals() and hashCode() so that it remains possible to add fields @@ -907,7 +948,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso if (origin == Origin.CONSTRUCTOR_PARAMETER) { Constructor constructor = TestParameterInjectorUtils.getOnlyConstructor(testClass); List annotations = - getAnnotationWithMetadataListWithType(constructor, annotationType); + getAnnotationWithMetadataListWithType(constructor, annotationType, testClass); if (!annotations.isEmpty()) { return toTestParameterValueList(annotations, origin); @@ -918,12 +959,12 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso if (annotation != null) { return ImmutableList.of( TestParameterValueHolder.create( - AnnotationWithMetadata.withoutMetadata(annotation), origin)); + AnnotationWithMetadata.withoutMetadata(annotation, testClass), origin)); } } else if (origin == Origin.METHOD_PARAMETER) { List annotations = - getAnnotationWithMetadataListWithType(method, annotationType); + getAnnotationWithMetadataListWithType(method, annotationType, testClass); if (!annotations.isEmpty()) { return toTestParameterValueList(annotations, origin); } @@ -931,7 +972,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso if (method.isAnnotationPresent(annotationType)) { return ImmutableList.of( TestParameterValueHolder.create( - AnnotationWithMetadata.withoutMetadata(method.getAnnotation(annotationType)), + AnnotationWithMetadata.withoutMetadata( + method.getAnnotation(annotationType), testClass), origin)); } } else if (origin == Origin.FIELD) { @@ -946,7 +988,11 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso .transform( annotation -> AnnotationWithMetadata.withMetadata( - annotation, field.getType(), field.getName()))) + annotation, + field.getAnnotations(), + field.getType(), + field.getName(), + testClass))) .toList()); if (!annotations.isEmpty()) { return toTestParameterValueList(annotations, origin); @@ -956,7 +1002,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso if (annotation != null) { return ImmutableList.of( TestParameterValueHolder.create( - AnnotationWithMetadata.withoutMetadata(annotation), origin)); + AnnotationWithMetadata.withoutMetadata(annotation, testClass), origin)); } } return ImmutableList.of(); @@ -974,22 +1020,30 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } private static ImmutableList getAnnotationWithMetadataListWithType( - Method callable, Class annotationType) { + Method callable, Class annotationType, Class testClass) { try { - return getAnnotationWithMetadataListWithType(callable.getParameters(), annotationType); + return getAnnotationWithMetadataListWithType( + callable.getParameters(), annotationType, testClass); } catch (NoSuchMethodError ignored) { return getAnnotationWithMetadataListWithType( - callable.getParameterTypes(), callable.getParameterAnnotations(), annotationType); + callable.getParameterTypes(), + callable.getParameterAnnotations(), + annotationType, + testClass); } } private static ImmutableList getAnnotationWithMetadataListWithType( - Constructor callable, Class annotationType) { + Constructor callable, Class annotationType, Class testClass) { try { - return getAnnotationWithMetadataListWithType(callable.getParameters(), annotationType); + return getAnnotationWithMetadataListWithType( + callable.getParameters(), annotationType, testClass); } catch (NoSuchMethodError ignored) { return getAnnotationWithMetadataListWithType( - callable.getParameterTypes(), callable.getParameterAnnotations(), annotationType); + callable.getParameterTypes(), + callable.getParameterAnnotations(), + annotationType, + testClass); } } @@ -998,7 +1052,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso // which are optional anyway). @SuppressWarnings("AndroidJdkLibsChecker") private static ImmutableList getAnnotationWithMetadataListWithType( - Parameter[] parameters, Class annotationType) { + Parameter[] parameters, Class annotationType, Class testClass) { return FluentIterable.from(parameters) .transform( parameter -> { @@ -1007,8 +1061,16 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso ? null : parameter.isNamePresent() ? AnnotationWithMetadata.withMetadata( - annotation, parameter.getType(), parameter.getName()) - : AnnotationWithMetadata.withMetadata(annotation, parameter.getType()); + annotation, + /* allAnnotations= */ parameter.getAnnotations(), + parameter.getType(), + parameter.getName(), + testClass) + : AnnotationWithMetadata.withMetadata( + annotation, + /* allAnnotations= */ parameter.getAnnotations(), + parameter.getType(), + testClass); }) .filter(Objects::nonNull) .toList(); @@ -1017,14 +1079,17 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso private static ImmutableList getAnnotationWithMetadataListWithType( Class[] parameterTypes, Annotation[][] annotations, - Class annotationType) { + Class annotationType, + Class testClass) { checkArgument(parameterTypes.length == annotations.length); ImmutableList.Builder 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])); + resultBuilder.add( + AnnotationWithMetadata.withMetadata( + annotation, /* allAnnotations= */ annotations[i], parameterTypes[i], testClass)); } } } diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java index dbb334c..08cc173 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java @@ -15,6 +15,7 @@ package com.google.testing.junit.testparameterinjector; import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; import java.lang.annotation.Annotation; import java.util.List; @@ -34,7 +35,52 @@ interface TestParameterValueProvider { * annotation is annotating a method, constructor or class, {@code parameterClass} is an empty * optional. */ - List provideValues(Annotation annotation, Optional> parameterClass); + default List provideValues(Annotation annotation, Optional> parameterClass) { + throw new UnsupportedOperationException( + "If this is called by TestParameterInjector, it means that neither of the" + + " provideValues()-type methods have been implemented"); + } + + /** + * Extension of {@link #provideValues(Annotation, Optional>)} with extra context. + * + * @param annotation The annotation instance that was encountered in the test class. The + * definition of this annotation is itself annotated with the {@link TestParameterAnnotation} + * annotation. + * @param otherAnnotations A list of all other annotations on the field or parameter that was + * annotated with {@code annotation}. + *

          For example, if the test code is as follows: + *

          +   *       @Test
          +   *       public void myTest_success(
          +   *           @CustomAnnotation(123) @TestParameter(valuesProvider=MyProvider.class) Foo foo) {
          +   *         ...
          +   *       }
          +   *     
          + * then this list will contain a single element: @CustomAnnotation(123). + *

          In case the annotation is annotating a method, constructor or class, {@code + * parameterClass} is an empty list. + * @param parameterClass The class of the parameter or field that is being annotated. In case the + * annotation is annotating a method, constructor or class, {@code parameterClass} is an empty + * optional. + * @param testClass The class that contains the test that is currently being run. + *

          Having this can be useful when sharing providers between tests that have the same base + * class. In those cases, an abstract method can be called as follows: + *

          +   *       ((MyBaseClass) context.testClass().newInstance()).myAbstractMethod()
          +   *     
          + * + * @deprecated Don't use this method outside of the testparameterinjector codebase, as it is prone + * to being changed. + */ + @Deprecated + default List provideValues( + Annotation annotation, + ImmutableList otherAnnotations, + Optional> parameterClass, + Class testClass) { + return provideValues(annotation, parameterClass); + } /** * Returns the class of the list elements returned by {@link #provideValues(Annotation, diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java index 23462ac..41d9a7b 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java @@ -190,7 +190,11 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso valueProvider .getConstructor() .newInstance() - .provideValues(annotation, annotationWithMetadata.paramClass())) + .provideValues( + annotation, + annotationWithMetadata.otherAnnotations(), + annotationWithMetadata.paramClass(), + annotationWithMetadata.testClass())) .transform( value -> (value instanceof TestParameterValue) @@ -247,6 +251,15 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso */ abstract Annotation annotation(); + /** + * A list of all other annotations on the field or parameter that was annotated with {@code + * annotation}. + * + *

          In case the annotation is annotating a method, constructor or class, {@code + * parameterClass} is an empty list. + */ + abstract ImmutableList otherAnnotations(); + /** * 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. @@ -259,20 +272,48 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso */ abstract Optional paramName(); + /** The class that contains the test that is currently being run. */ + abstract Class testClass(); + public static AnnotationWithMetadata withMetadata( - Annotation annotation, Class paramClass, String paramName) { + Annotation annotation, + Annotation[] allAnnotations, + Class paramClass, + String paramName, + Class testClass) { return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( - annotation, Optional.of(paramClass), Optional.of(paramName)); + annotation, + /* otherAnnotations= */ FluentIterable.from(allAnnotations) + .filter(a -> !a.equals(annotation)) + .toList(), + Optional.of(paramClass), + Optional.of(paramName), + testClass); } - public static AnnotationWithMetadata withMetadata(Annotation annotation, Class paramClass) { + public static AnnotationWithMetadata withMetadata( + Annotation annotation, + Annotation[] allAnnotations, + Class paramClass, + Class testClass) { return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( - annotation, Optional.of(paramClass), Optional.absent()); + annotation, + /* otherAnnotations= */ FluentIterable.from(allAnnotations) + .filter(a -> !a.equals(annotation)) + .toList(), + Optional.of(paramClass), + Optional.absent(), + testClass); } - public static AnnotationWithMetadata withoutMetadata(Annotation annotation) { + public static AnnotationWithMetadata withoutMetadata( + Annotation annotation, Class testClass) { return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( - annotation, Optional.absent(), Optional.absent()); + annotation, + /* otherAnnotations= */ ImmutableList.of(), + /* paramClass= */ Optional.absent(), + /* paramName= */ Optional.absent(), + testClass); } // Prevent anyone relying on equals() and hashCode() so that it remains possible to add fields @@ -907,7 +948,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso if (origin == Origin.CONSTRUCTOR_PARAMETER) { Constructor constructor = TestParameterInjectorUtils.getOnlyConstructor(testClass); List annotations = - getAnnotationWithMetadataListWithType(constructor, annotationType); + getAnnotationWithMetadataListWithType(constructor, annotationType, testClass); if (!annotations.isEmpty()) { return toTestParameterValueList(annotations, origin); @@ -918,12 +959,12 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso if (annotation != null) { return ImmutableList.of( TestParameterValueHolder.create( - AnnotationWithMetadata.withoutMetadata(annotation), origin)); + AnnotationWithMetadata.withoutMetadata(annotation, testClass), origin)); } } else if (origin == Origin.METHOD_PARAMETER) { List annotations = - getAnnotationWithMetadataListWithType(method, annotationType); + getAnnotationWithMetadataListWithType(method, annotationType, testClass); if (!annotations.isEmpty()) { return toTestParameterValueList(annotations, origin); } @@ -931,7 +972,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso if (method.isAnnotationPresent(annotationType)) { return ImmutableList.of( TestParameterValueHolder.create( - AnnotationWithMetadata.withoutMetadata(method.getAnnotation(annotationType)), + AnnotationWithMetadata.withoutMetadata( + method.getAnnotation(annotationType), testClass), origin)); } } else if (origin == Origin.FIELD) { @@ -946,7 +988,11 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso .transform( annotation -> AnnotationWithMetadata.withMetadata( - annotation, field.getType(), field.getName()))) + annotation, + field.getAnnotations(), + field.getType(), + field.getName(), + testClass))) .toList()); if (!annotations.isEmpty()) { return toTestParameterValueList(annotations, origin); @@ -956,7 +1002,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso if (annotation != null) { return ImmutableList.of( TestParameterValueHolder.create( - AnnotationWithMetadata.withoutMetadata(annotation), origin)); + AnnotationWithMetadata.withoutMetadata(annotation, testClass), origin)); } } return ImmutableList.of(); @@ -974,22 +1020,30 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso } private static ImmutableList getAnnotationWithMetadataListWithType( - Method callable, Class annotationType) { + Method callable, Class annotationType, Class testClass) { try { - return getAnnotationWithMetadataListWithType(callable.getParameters(), annotationType); + return getAnnotationWithMetadataListWithType( + callable.getParameters(), annotationType, testClass); } catch (NoSuchMethodError ignored) { return getAnnotationWithMetadataListWithType( - callable.getParameterTypes(), callable.getParameterAnnotations(), annotationType); + callable.getParameterTypes(), + callable.getParameterAnnotations(), + annotationType, + testClass); } } private static ImmutableList getAnnotationWithMetadataListWithType( - Constructor callable, Class annotationType) { + Constructor callable, Class annotationType, Class testClass) { try { - return getAnnotationWithMetadataListWithType(callable.getParameters(), annotationType); + return getAnnotationWithMetadataListWithType( + callable.getParameters(), annotationType, testClass); } catch (NoSuchMethodError ignored) { return getAnnotationWithMetadataListWithType( - callable.getParameterTypes(), callable.getParameterAnnotations(), annotationType); + callable.getParameterTypes(), + callable.getParameterAnnotations(), + annotationType, + testClass); } } @@ -998,7 +1052,7 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso // which are optional anyway). @SuppressWarnings("AndroidJdkLibsChecker") private static ImmutableList getAnnotationWithMetadataListWithType( - Parameter[] parameters, Class annotationType) { + Parameter[] parameters, Class annotationType, Class testClass) { return FluentIterable.from(parameters) .transform( parameter -> { @@ -1007,8 +1061,16 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso ? null : parameter.isNamePresent() ? AnnotationWithMetadata.withMetadata( - annotation, parameter.getType(), parameter.getName()) - : AnnotationWithMetadata.withMetadata(annotation, parameter.getType()); + annotation, + /* allAnnotations= */ parameter.getAnnotations(), + parameter.getType(), + parameter.getName(), + testClass) + : AnnotationWithMetadata.withMetadata( + annotation, + /* allAnnotations= */ parameter.getAnnotations(), + parameter.getType(), + testClass); }) .filter(Objects::nonNull) .toList(); @@ -1017,14 +1079,17 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso private static ImmutableList getAnnotationWithMetadataListWithType( Class[] parameterTypes, Annotation[][] annotations, - Class annotationType) { + Class annotationType, + Class testClass) { checkArgument(parameterTypes.length == annotations.length); ImmutableList.Builder 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])); + resultBuilder.add( + AnnotationWithMetadata.withMetadata( + annotation, /* allAnnotations= */ annotations[i], parameterTypes[i], testClass)); } } } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java index 3c42eec..a3ad576 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java @@ -15,6 +15,7 @@ package com.google.testing.junit.testparameterinjector.junit5; import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; import java.lang.annotation.Annotation; import java.util.List; @@ -34,7 +35,52 @@ interface TestParameterValueProvider { * annotation is annotating a method, constructor or class, {@code parameterClass} is an empty * optional. */ - List provideValues(Annotation annotation, Optional> parameterClass); + default List provideValues(Annotation annotation, Optional> parameterClass) { + throw new UnsupportedOperationException( + "If this is called by TestParameterInjector, it means that neither of the" + + " provideValues()-type methods have been implemented"); + } + + /** + * Extension of {@link #provideValues(Annotation, Optional>)} with extra context. + * + * @param annotation The annotation instance that was encountered in the test class. The + * definition of this annotation is itself annotated with the {@link TestParameterAnnotation} + * annotation. + * @param otherAnnotations A list of all other annotations on the field or parameter that was + * annotated with {@code annotation}. + *

          For example, if the test code is as follows: + *

          +   *       @Test
          +   *       public void myTest_success(
          +   *           @CustomAnnotation(123) @TestParameter(valuesProvider=MyProvider.class) Foo foo) {
          +   *         ...
          +   *       }
          +   *     
          + * then this list will contain a single element: @CustomAnnotation(123). + *

          In case the annotation is annotating a method, constructor or class, {@code + * parameterClass} is an empty list. + * @param parameterClass The class of the parameter or field that is being annotated. In case the + * annotation is annotating a method, constructor or class, {@code parameterClass} is an empty + * optional. + * @param testClass The class that contains the test that is currently being run. + *

          Having this can be useful when sharing providers between tests that have the same base + * class. In those cases, an abstract method can be called as follows: + *

          +   *       ((MyBaseClass) context.testClass().newInstance()).myAbstractMethod()
          +   *     
          + * + * @deprecated Don't use this method outside of the testparameterinjector codebase, as it is prone + * to being changed. + */ + @Deprecated + default List provideValues( + Annotation annotation, + ImmutableList otherAnnotations, + Optional> parameterClass, + Class testClass) { + return provideValues(annotation, parameterClass); + } /** * Returns the class of the list elements returned by {@link #provideValues(Annotation, -- cgit v1.2.3 From a73e3f4771eb8abdb2d4b74038d03def87640a9d Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 16 Jan 2024 09:55:49 +0000 Subject: Implement context aware TestParameterValuesProvider. Fixes https://github.com/google/TestParameterInjector/issues/44 --- .../junit/testparameterinjector/TestParameter.java | 22 ++++- .../TestParameterValuesProvider.java | 107 +++++++++++++++++++++ .../testparameterinjector/TestParameterTest.java | 84 +++++++++++++++- .../junit5/TestParameter.java | 22 ++++- .../junit5/TestParameterValuesProvider.java | 107 +++++++++++++++++++++ 5 files changed, 330 insertions(+), 12 deletions(-) create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java index ed03484..b722c14 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java @@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; import com.google.testing.junit.testparameterinjector.TestParameter.InternalImplementationOfThisParameter; +import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider.Context; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.annotation.Target; @@ -147,7 +148,10 @@ public @interface TestParameter { final class InternalImplementationOfThisParameter implements TestParameterValueProvider { @Override public List provideValues( - Annotation uncastAnnotation, Optional> maybeParameterClass) { + Annotation uncastAnnotation, + ImmutableList otherAnnotations, + Optional> maybeParameterClass, + Class testClass) { TestParameter annotation = (TestParameter) uncastAnnotation; Class parameterClass = getValueType(annotation.annotationType(), maybeParameterClass); @@ -165,7 +169,8 @@ public @interface TestParameter { .transform(v -> parseStringValue(v, parameterClass)) .toArray(Object.class)); } else if (valuesProviderIsSet) { - return getValuesFromProvider(annotation.valuesProvider()); + return getValuesFromProvider( + annotation.valuesProvider(), Context.create(otherAnnotations, testClass)); } else { if (Enum.class.isAssignableFrom(parameterClass)) { return Arrays.asList((Object[]) parameterClass.asSubclass(Enum.class).getEnumConstants()); @@ -206,12 +211,21 @@ public @interface TestParameter { } private static List getValuesFromProvider( - Class valuesProvider) { + Class valuesProvider, Context context) { try { Constructor constructor = valuesProvider.getDeclaredConstructor(); constructor.setAccessible(true); - return new ArrayList<>(constructor.newInstance().provideValues()); + TestParameterValuesProvider instance = constructor.newInstance(); + if (instance + instanceof com.google.testing.junit.testparameterinjector.TestParameterValuesProvider) { + return new ArrayList<>( + ((com.google.testing.junit.testparameterinjector.TestParameterValuesProvider) + instance) + .provideValues(context)); + } else { + return new ArrayList<>(instance.provideValues()); + } } catch (NoSuchMethodException e) { if (!Modifier.isStatic(valuesProvider.getModifiers()) && valuesProvider.isMemberClass()) { throw new IllegalStateException( diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java new file mode 100644 index 0000000..dad0627 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java @@ -0,0 +1,107 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Joiner; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import java.lang.annotation.Annotation; +import java.util.List; +import javax.annotation.Nullable; + +/** + * Abstract class for custom providers of @TestParameter values. + * + *

          This is a replacement for {@link TestParameter.TestParameterValuesProvider}, which will soon + * be deprecated. The difference with the former interface is that this class provides a {@code + * Context} instance when invoking {@link #provideValues}. + */ +public abstract class TestParameterValuesProvider + implements TestParameter.TestParameterValuesProvider { + + abstract List provideValues(Context context); + + @Override + public final List provideValues() { + throw new UnsupportedOperationException( + "The TestParameterInjector framework should never call this method, and instead call" + + " #provideValues(Context)"); + } + + /** + * Wraps the given value in an object that allows you to give the parameter value a different + * name. The TestParameterInjector framework will recognize the returned {@link + * TestParameterValue} instances and unwrap them at injection time. + * + *

          Usage: {@code value(file.content).withName(file.name)}. + */ + @Override + public final TestParameterValue value(@Nullable Object wrappedValue) { + // Overriding this method as final because it is not supposed to be overwritten + return TestParameter.TestParameterValuesProvider.super.value(wrappedValue); + } + + /** + * An immutable value class that contains extra information about the context of the parameter for + * which values are being provided. + */ + @AutoValue + public abstract static class Context { + /** + * A list of all other annotations on the field or parameter that was annotated + * with @TestParameter. + * + *

          For example, if the test code is as follows: + * + *

          {@code
          +     * @Test
          +     * public void myTest_success(
          +     *     @CustomAnnotation(123) @TestParameter(valuesProvider=MyProvider.class) Foo foo) {
          +     *   ...
          +     * }
          +     * }
          + * + * then this list will contain a single element: @CustomAnnotation(123). + */ + public abstract ImmutableList otherAnnotations(); + + /** + * The class that contains the test that is currently being run. + * + *

          Having this can be useful when sharing providers between tests that have the same base + * class. In those cases, an abstract method can be called as follows: + * + *

          +     *   ((MyBaseClass) context.testClass().newInstance()).myAbstractMethod()
          +     * 
          + */ + public abstract Class testClass(); + + static Context create(ImmutableList otherAnnotations, Class testClass) { + return new AutoValue_TestParameterValuesProvider_Context(otherAnnotations, testClass); + } + + @Override + public final String toString() { + return String.format( + "Context(otherAnnotations=[%s],testClass=%s)", + FluentIterable.from(otherAnnotations()).join(Joiner.on(',')), + testClass().getSimpleName()); + } + + Context() {} // Prevent implementations outside of this package + } +} diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java index 1d5b233..1e3f776 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java @@ -16,16 +16,20 @@ 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 com.google.common.base.CharMatcher; +import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableMap; import com.google.testing.junit.testparameterinjector.SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase; -import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider; +import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider.Context; +import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.util.Arrays; import java.util.Collection; import java.util.List; +import javax.inject.Qualifier; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -196,14 +200,16 @@ public class TestParameterTest { .build(); } - private static final class TestNumberProvider implements TestParameterValuesProvider { + private static final class TestNumberProvider + implements TestParameter.TestParameterValuesProvider { @Override public List provideValues() { return newArrayList(value(1).withName("one"), 2); } } - private static final class TestStringProvider implements TestParameterValuesProvider { + private static final class TestStringProvider + implements TestParameter.TestParameterValuesProvider { @Override public List provideValues() { return newArrayList( @@ -211,7 +217,8 @@ public class TestParameterTest { } } - private static final class CharMatcherProvider implements TestParameterValuesProvider { + private static final class CharMatcherProvider + implements TestParameter.TestParameterValuesProvider { @Override public List provideValues() { return newArrayList(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace()); @@ -219,6 +226,75 @@ public class TestParameterTest { } } + @RunAsTest + public static class WithContextAwareValuesProvider extends SuccessfulTestCaseBase { + + @CustomFieldAnnotation + @TestParameter(valuesProvider = InjectContextProvider.class) + private Context contextFromField; + + private final Context contextFromConstructor; + + public WithContextAwareValuesProvider( + @TestParameter(valuesProvider = InjectContextProvider.class) Context context) { + this.contextFromConstructor = context; + } + + @Test + public void contextTest( + @CustomParameterAnnotation1 + @CustomParameterAnnotation2 + @TestParameter(valuesProvider = InjectContextProvider.class) + Context contextFromParameter) { + assertThat(contextFromField.testClass()).isEqualTo(WithContextAwareValuesProvider.class); + assertThat(contextFromConstructor.testClass()) + .isEqualTo(WithContextAwareValuesProvider.class); + assertThat(contextFromParameter.testClass()).isEqualTo(WithContextAwareValuesProvider.class); + + assertThat( + FluentIterable.from(contextFromField.otherAnnotations()) + .transform(Annotation::annotationType) + .toList()) + .containsExactly(CustomFieldAnnotation.class); + assertThat(contextFromConstructor.otherAnnotations()).isEmpty(); + assertThat( + FluentIterable.from(contextFromParameter.otherAnnotations()) + .transform(Annotation::annotationType) + .toList()) + .containsExactly(CustomParameterAnnotation1.class, CustomParameterAnnotation2.class); + + storeTestParametersForThisTest(contextFromParameter); + } + + @Override + ImmutableMap expectedTestNameToStringifiedParameters() { + return ImmutableMap.builder() + .put( + "contextTest[1.Context(otherAnnotations=[@com.google.testing.junit.tes...,1.Context(otherAnnotations=[],testClass=WithContextAwareV...,1.Context(otherAnnotations=[@com.google.testing.junit.tes...]", + "Context(otherAnnotations=[@com.google.testing.junit.testparameterinjector.TestParameterTest.WithContextAwareValuesProvider.CustomParameterAnnotation1(),@com.google.testing.junit.testparameterinjector.TestParameterTest.WithContextAwareValuesProvider.CustomParameterAnnotation2()],testClass=WithContextAwareValuesProvider)") + .build(); + } + + private static final class InjectContextProvider extends TestParameterValuesProvider { + @Override + public List provideValues(Context context) { + return newArrayList(context); + } + } + + @Qualifier + @Retention(RUNTIME) + @interface CustomFieldAnnotation {} + + @Qualifier + @Retention(RUNTIME) + @interface CustomParameterAnnotation1 {} + + @Qualifier + @Retention(RUNTIME) + @interface CustomParameterAnnotation2 {} + } + @Parameters(name = "{0}") public static Collection parameters() { return Arrays.stream(TestParameterTest.class.getClasses()) diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java index eb8c240..7b182e2 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java @@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; import com.google.testing.junit.testparameterinjector.junit5.TestParameter.InternalImplementationOfThisParameter; +import com.google.testing.junit.testparameterinjector.junit5.TestParameterValuesProvider.Context; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.annotation.Target; @@ -147,7 +148,10 @@ public @interface TestParameter { final class InternalImplementationOfThisParameter implements TestParameterValueProvider { @Override public List provideValues( - Annotation uncastAnnotation, Optional> maybeParameterClass) { + Annotation uncastAnnotation, + ImmutableList otherAnnotations, + Optional> maybeParameterClass, + Class testClass) { TestParameter annotation = (TestParameter) uncastAnnotation; Class parameterClass = getValueType(annotation.annotationType(), maybeParameterClass); @@ -165,7 +169,8 @@ public @interface TestParameter { .transform(v -> parseStringValue(v, parameterClass)) .toArray(Object.class)); } else if (valuesProviderIsSet) { - return getValuesFromProvider(annotation.valuesProvider()); + return getValuesFromProvider( + annotation.valuesProvider(), Context.create(otherAnnotations, testClass)); } else { if (Enum.class.isAssignableFrom(parameterClass)) { return Arrays.asList((Object[]) parameterClass.asSubclass(Enum.class).getEnumConstants()); @@ -206,12 +211,21 @@ public @interface TestParameter { } private static List getValuesFromProvider( - Class valuesProvider) { + Class valuesProvider, Context context) { try { Constructor constructor = valuesProvider.getDeclaredConstructor(); constructor.setAccessible(true); - return new ArrayList<>(constructor.newInstance().provideValues()); + TestParameterValuesProvider instance = constructor.newInstance(); + if (instance + instanceof com.google.testing.junit.testparameterinjector.junit5.TestParameterValuesProvider) { + return new ArrayList<>( + ((com.google.testing.junit.testparameterinjector.junit5.TestParameterValuesProvider) + instance) + .provideValues(context)); + } else { + return new ArrayList<>(instance.provideValues()); + } } catch (NoSuchMethodException e) { if (!Modifier.isStatic(valuesProvider.getModifiers()) && valuesProvider.isMemberClass()) { throw new IllegalStateException( diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java new file mode 100644 index 0000000..c4642d3 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java @@ -0,0 +1,107 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Joiner; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import java.lang.annotation.Annotation; +import java.util.List; +import javax.annotation.Nullable; + +/** + * Abstract class for custom providers of @TestParameter values. + * + *

          This is a replacement for {@link TestParameter.TestParameterValuesProvider}, which will soon + * be deprecated. The difference with the former interface is that this class provides a {@code + * Context} instance when invoking {@link #provideValues}. + */ +public abstract class TestParameterValuesProvider + implements TestParameter.TestParameterValuesProvider { + + abstract List provideValues(Context context); + + @Override + public final List provideValues() { + throw new UnsupportedOperationException( + "The TestParameterInjector framework should never call this method, and instead call" + + " #provideValues(Context)"); + } + + /** + * Wraps the given value in an object that allows you to give the parameter value a different + * name. The TestParameterInjector framework will recognize the returned {@link + * TestParameterValue} instances and unwrap them at injection time. + * + *

          Usage: {@code value(file.content).withName(file.name)}. + */ + @Override + public final TestParameterValue value(@Nullable Object wrappedValue) { + // Overriding this method as final because it is not supposed to be overwritten + return TestParameter.TestParameterValuesProvider.super.value(wrappedValue); + } + + /** + * An immutable value class that contains extra information about the context of the parameter for + * which values are being provided. + */ + @AutoValue + public abstract static class Context { + /** + * A list of all other annotations on the field or parameter that was annotated + * with @TestParameter. + * + *

          For example, if the test code is as follows: + * + *

          {@code
          +     * @Test
          +     * public void myTest_success(
          +     *     @CustomAnnotation(123) @TestParameter(valuesProvider=MyProvider.class) Foo foo) {
          +     *   ...
          +     * }
          +     * }
          + * + * then this list will contain a single element: @CustomAnnotation(123). + */ + public abstract ImmutableList otherAnnotations(); + + /** + * The class that contains the test that is currently being run. + * + *

          Having this can be useful when sharing providers between tests that have the same base + * class. In those cases, an abstract method can be called as follows: + * + *

          +     *   ((MyBaseClass) context.testClass().newInstance()).myAbstractMethod()
          +     * 
          + */ + public abstract Class testClass(); + + static Context create(ImmutableList otherAnnotations, Class testClass) { + return new AutoValue_TestParameterValuesProvider_Context(otherAnnotations, testClass); + } + + @Override + public final String toString() { + return String.format( + "Context(otherAnnotations=[%s],testClass=%s)", + FluentIterable.from(otherAnnotations()).join(Joiner.on(',')), + testClass().getSimpleName()); + } + + Context() {} // Prevent implementations outside of this package + } +} -- cgit v1.2.3 From b90cb7320ed7dd9b6797e89cacac59ea19d414bf Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 16 Jan 2024 13:34:23 +0000 Subject: Unit tests: Avoid unnecessary dependency on javax.inject https://github.com/google/TestParameterInjector/issues/44 --- .../google/testing/junit/testparameterinjector/TestParameterTest.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java index 1e3f776..ba4b2f7 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java @@ -29,7 +29,6 @@ import java.lang.annotation.Retention; import java.util.Arrays; import java.util.Collection; import java.util.List; -import javax.inject.Qualifier; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -282,15 +281,12 @@ public class TestParameterTest { } } - @Qualifier @Retention(RUNTIME) @interface CustomFieldAnnotation {} - @Qualifier @Retention(RUNTIME) @interface CustomParameterAnnotation1 {} - @Qualifier @Retention(RUNTIME) @interface CustomParameterAnnotation2 {} } -- cgit v1.2.3 From 23b3b22fce920618c5eda062fa36889850098163 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 16 Jan 2024 13:54:28 +0000 Subject: Mark test as Google internal because it relies on class.toString() https://github.com/google/TestParameterInjector/issues/44 --- .../testparameterinjector/TestParameterTest.java | 70 ---------------------- 1 file changed, 70 deletions(-) diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java index ba4b2f7..61534e6 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java @@ -16,15 +16,11 @@ 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 com.google.common.base.CharMatcher; -import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableMap; import com.google.testing.junit.testparameterinjector.SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase; -import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider.Context; -import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.util.Arrays; import java.util.Collection; @@ -225,72 +221,6 @@ public class TestParameterTest { } } - @RunAsTest - public static class WithContextAwareValuesProvider extends SuccessfulTestCaseBase { - - @CustomFieldAnnotation - @TestParameter(valuesProvider = InjectContextProvider.class) - private Context contextFromField; - - private final Context contextFromConstructor; - - public WithContextAwareValuesProvider( - @TestParameter(valuesProvider = InjectContextProvider.class) Context context) { - this.contextFromConstructor = context; - } - - @Test - public void contextTest( - @CustomParameterAnnotation1 - @CustomParameterAnnotation2 - @TestParameter(valuesProvider = InjectContextProvider.class) - Context contextFromParameter) { - assertThat(contextFromField.testClass()).isEqualTo(WithContextAwareValuesProvider.class); - assertThat(contextFromConstructor.testClass()) - .isEqualTo(WithContextAwareValuesProvider.class); - assertThat(contextFromParameter.testClass()).isEqualTo(WithContextAwareValuesProvider.class); - - assertThat( - FluentIterable.from(contextFromField.otherAnnotations()) - .transform(Annotation::annotationType) - .toList()) - .containsExactly(CustomFieldAnnotation.class); - assertThat(contextFromConstructor.otherAnnotations()).isEmpty(); - assertThat( - FluentIterable.from(contextFromParameter.otherAnnotations()) - .transform(Annotation::annotationType) - .toList()) - .containsExactly(CustomParameterAnnotation1.class, CustomParameterAnnotation2.class); - - storeTestParametersForThisTest(contextFromParameter); - } - - @Override - ImmutableMap expectedTestNameToStringifiedParameters() { - return ImmutableMap.builder() - .put( - "contextTest[1.Context(otherAnnotations=[@com.google.testing.junit.tes...,1.Context(otherAnnotations=[],testClass=WithContextAwareV...,1.Context(otherAnnotations=[@com.google.testing.junit.tes...]", - "Context(otherAnnotations=[@com.google.testing.junit.testparameterinjector.TestParameterTest.WithContextAwareValuesProvider.CustomParameterAnnotation1(),@com.google.testing.junit.testparameterinjector.TestParameterTest.WithContextAwareValuesProvider.CustomParameterAnnotation2()],testClass=WithContextAwareValuesProvider)") - .build(); - } - - private static final class InjectContextProvider extends TestParameterValuesProvider { - @Override - public List provideValues(Context context) { - return newArrayList(context); - } - } - - @Retention(RUNTIME) - @interface CustomFieldAnnotation {} - - @Retention(RUNTIME) - @interface CustomParameterAnnotation1 {} - - @Retention(RUNTIME) - @interface CustomParameterAnnotation2 {} - } - @Parameters(name = "{0}") public static Collection parameters() { return Arrays.stream(TestParameterTest.class.getClasses()) -- cgit v1.2.3 From c46648271d3e27e6311e708ee8278186d4e7d361 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 16 Jan 2024 13:55:12 +0000 Subject: Make TestParameterValuesProvider.provideValues() protected so it can actually be used by clients. https://github.com/google/TestParameterInjector/issues/44 --- .../junit/testparameterinjector/TestParameterValuesProvider.java | 2 +- .../junit/testparameterinjector/junit5/TestParameterValuesProvider.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java index dad0627..08369ef 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java @@ -32,7 +32,7 @@ import javax.annotation.Nullable; public abstract class TestParameterValuesProvider implements TestParameter.TestParameterValuesProvider { - abstract List provideValues(Context context); + protected abstract List provideValues(Context context); @Override public final List provideValues() { diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java index c4642d3..36a50a1 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java @@ -32,7 +32,7 @@ import javax.annotation.Nullable; public abstract class TestParameterValuesProvider implements TestParameter.TestParameterValuesProvider { - abstract List provideValues(Context context); + protected abstract List provideValues(Context context); @Override public final List provideValues() { -- cgit v1.2.3 From 4d6e752965b84ca66df9c0bbf56928b6e91ba8e2 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Tue, 16 Jan 2024 21:20:38 +0000 Subject: TestParameterValuesProvider.Context: Fix javadoc compilation error by escaping @ characters https://github.com/google/TestParameterInjector/issues/44 --- .../testparameterinjector/TestParameterValuesProvider.java | 14 +++++++------- .../junit5/TestParameterValuesProvider.java | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java index 08369ef..a0078a0 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java @@ -66,13 +66,13 @@ public abstract class TestParameterValuesProvider * *

          For example, if the test code is as follows: * - *

          {@code
          -     * @Test
          -     * public void myTest_success(
          -     *     @CustomAnnotation(123) @TestParameter(valuesProvider=MyProvider.class) Foo foo) {
          -     *   ...
          -     * }
          -     * }
          + *
          +     *   {@literal @}Test
          +     *   public void myTest_success(
          +     *       {@literal @}CustomAnnotation(123) {@literal @}TestParameter(valuesProvider=MyProvider.class) Foo foo) {
          +     *     ...
          +     *   }
          +     * 
          * * then this list will contain a single element: @CustomAnnotation(123). */ diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java index 36a50a1..46810cf 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java @@ -66,13 +66,13 @@ public abstract class TestParameterValuesProvider * *

          For example, if the test code is as follows: * - *

          {@code
          -     * @Test
          -     * public void myTest_success(
          -     *     @CustomAnnotation(123) @TestParameter(valuesProvider=MyProvider.class) Foo foo) {
          -     *   ...
          -     * }
          -     * }
          + *
          +     *   {@literal @}Test
          +     *   public void myTest_success(
          +     *       {@literal @}CustomAnnotation(123) {@literal @}TestParameter(valuesProvider=MyProvider.class) Foo foo) {
          +     *     ...
          +     *   }
          +     * 
          * * then this list will contain a single element: @CustomAnnotation(123). */ -- cgit v1.2.3 From 8a3e8127e0acd1fe2a71e6727e3797227b70bfde Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 17 Jan 2024 10:10:47 +0000 Subject: TestParameterValuesProvider.Context: Replace otherAnnotations() by a utility method for getting an annotation of a specific type https://github.com/google/TestParameterInjector/issues/44 --- .../TestParameterValuesProvider.java | 41 +++++++++++++++++++++- .../junit5/TestParameterValuesProvider.java | 41 +++++++++++++++++++++- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java index a0078a0..e4ad748 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java @@ -14,6 +14,9 @@ package com.google.testing.junit.testparameterinjector; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.Iterables.getOnlyElement; + import com.google.auto.value.AutoValue; import com.google.common.base.Joiner; import com.google.common.collect.FluentIterable; @@ -76,7 +79,7 @@ public abstract class TestParameterValuesProvider * * then this list will contain a single element: @CustomAnnotation(123). */ - public abstract ImmutableList otherAnnotations(); + abstract ImmutableList otherAnnotations(); /** * The class that contains the test that is currently being run. @@ -94,6 +97,42 @@ public abstract class TestParameterValuesProvider return new AutoValue_TestParameterValuesProvider_Context(otherAnnotations, testClass); } + /** + * Returns the only annotation with the given type on the field or parameter that was annotated + * with @TestParameter. + * + *

          For example, if the test code is as follows: + * + *

          +     *   {@literal @}Test
          +     *   public void myTest_success(
          +     *       {@literal @}CustomAnnotation(123) {@literal @}TestParameter(valuesProvider=MyProvider.class) Foo foo) {
          +     *     ...
          +     *   }
          +     * 
          + * + * then {@code context.getOtherAnnotation(CustomAnnotation.class).value()} will equal 123. + * + * @throws NoSuchElementException if this there is no annotation with the given type + * @throws IllegalArgumentException if there are multiple annotations with the given type + * @throws IllegalArgumentException if the argument it TestParameter.class because it is already + * handled by the TestParameterInjector framework. + */ + @SuppressWarnings("unchecked") // Safe because of the filter operation + public final A getOtherAnnotation(Class annotationType) { + checkArgument( + !TestParameter.class.equals(annotationType), + "Getting the @TestParameter annotating the field or parameter is not allowed because" + + " it is already handled by the TestParameterInjector framework."); + return (A) + getOnlyElement( + FluentIterable.from(otherAnnotations()) + .filter(annotation -> annotation.annotationType().equals(annotationType)) + .toList()); + } + + // TODO: b/317524353 - Add support for repeated annotations + @Override public final String toString() { return String.format( diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java index 46810cf..4e0ee50 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java @@ -14,6 +14,9 @@ package com.google.testing.junit.testparameterinjector.junit5; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.Iterables.getOnlyElement; + import com.google.auto.value.AutoValue; import com.google.common.base.Joiner; import com.google.common.collect.FluentIterable; @@ -76,7 +79,7 @@ public abstract class TestParameterValuesProvider * * then this list will contain a single element: @CustomAnnotation(123). */ - public abstract ImmutableList otherAnnotations(); + abstract ImmutableList otherAnnotations(); /** * The class that contains the test that is currently being run. @@ -94,6 +97,42 @@ public abstract class TestParameterValuesProvider return new AutoValue_TestParameterValuesProvider_Context(otherAnnotations, testClass); } + /** + * Returns the only annotation with the given type on the field or parameter that was annotated + * with @TestParameter. + * + *

          For example, if the test code is as follows: + * + *

          +     *   {@literal @}Test
          +     *   public void myTest_success(
          +     *       {@literal @}CustomAnnotation(123) {@literal @}TestParameter(valuesProvider=MyProvider.class) Foo foo) {
          +     *     ...
          +     *   }
          +     * 
          + * + * then {@code context.getOtherAnnotation(CustomAnnotation.class).value()} will equal 123. + * + * @throws NoSuchElementException if this there is no annotation with the given type + * @throws IllegalArgumentException if there are multiple annotations with the given type + * @throws IllegalArgumentException if the argument it TestParameter.class because it is already + * handled by the TestParameterInjector framework. + */ + @SuppressWarnings("unchecked") // Safe because of the filter operation + public final
          A getOtherAnnotation(Class annotationType) { + checkArgument( + !TestParameter.class.equals(annotationType), + "Getting the @TestParameter annotating the field or parameter is not allowed because" + + " it is already handled by the TestParameterInjector framework."); + return (A) + getOnlyElement( + FluentIterable.from(otherAnnotations()) + .filter(annotation -> annotation.annotationType().equals(annotationType)) + .toList()); + } + + // TODO: b/317524353 - Add support for repeated annotations + @Override public final String toString() { return String.format( -- cgit v1.2.3 From e312198862b27a3ca25772b230dae716e33dbca0 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 17 Jan 2024 10:11:49 +0000 Subject: TestParameterValuesProvider.Context: Convert from AutoValue to regular class. Reason: To avoid clients from relying on equals/hashCode being consistent with the contents. This allows future expansion. https://github.com/google/TestParameterInjector/issues/44 --- .../junit/testparameterinjector/TestParameter.java | 2 +- .../TestParameterValuesProvider.java | 69 ++++++++++------------ .../junit5/TestParameter.java | 2 +- .../junit5/TestParameterValuesProvider.java | 69 ++++++++++------------ 4 files changed, 66 insertions(+), 76 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java index b722c14..2fa0375 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java @@ -170,7 +170,7 @@ public @interface TestParameter { .toArray(Object.class)); } else if (valuesProviderIsSet) { return getValuesFromProvider( - annotation.valuesProvider(), Context.create(otherAnnotations, testClass)); + annotation.valuesProvider(), new Context(otherAnnotations, testClass)); } else { if (Enum.class.isAssignableFrom(parameterClass)) { return Arrays.asList((Object[]) parameterClass.asSubclass(Enum.class).getEnumConstants()); diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java index e4ad748..96dd2ca 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java @@ -17,7 +17,7 @@ package com.google.testing.junit.testparameterinjector; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.Iterables.getOnlyElement; -import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; @@ -61,40 +61,14 @@ public abstract class TestParameterValuesProvider * An immutable value class that contains extra information about the context of the parameter for * which values are being provided. */ - @AutoValue - public abstract static class Context { - /** - * A list of all other annotations on the field or parameter that was annotated - * with @TestParameter. - * - *

          For example, if the test code is as follows: - * - *

          -     *   {@literal @}Test
          -     *   public void myTest_success(
          -     *       {@literal @}CustomAnnotation(123) {@literal @}TestParameter(valuesProvider=MyProvider.class) Foo foo) {
          -     *     ...
          -     *   }
          -     * 
          - * - * then this list will contain a single element: @CustomAnnotation(123). - */ - abstract ImmutableList otherAnnotations(); + public static final class Context { - /** - * The class that contains the test that is currently being run. - * - *

          Having this can be useful when sharing providers between tests that have the same base - * class. In those cases, an abstract method can be called as follows: - * - *

          -     *   ((MyBaseClass) context.testClass().newInstance()).myAbstractMethod()
          -     * 
          - */ - public abstract Class testClass(); + private final ImmutableList otherAnnotations; + private final Class testClass; - static Context create(ImmutableList otherAnnotations, Class testClass) { - return new AutoValue_TestParameterValuesProvider_Context(otherAnnotations, testClass); + Context(ImmutableList otherAnnotations, Class testClass) { + this.otherAnnotations = otherAnnotations; + this.testClass = testClass; } /** @@ -119,7 +93,7 @@ public abstract class TestParameterValuesProvider * handled by the TestParameterInjector framework. */ @SuppressWarnings("unchecked") // Safe because of the filter operation - public final
          A getOtherAnnotation(Class annotationType) { + public A getOtherAnnotation(Class annotationType) { checkArgument( !TestParameter.class.equals(annotationType), "Getting the @TestParameter annotating the field or parameter is not allowed because" @@ -133,14 +107,35 @@ public abstract class TestParameterValuesProvider // TODO: b/317524353 - Add support for repeated annotations + /** + * The class that contains the test that is currently being run. + * + *

          Having this can be useful when sharing providers between tests that have the same base + * class. In those cases, an abstract method can be called as follows: + * + *

          +     *   ((MyBaseClass) context.testClass().newInstance()).myAbstractMethod()
          +     * 
          + */ + public Class testClass() { + return testClass; + } + + /** + * A list of all other annotations on the field or parameter that was annotated + * with @TestParameter. + */ + @VisibleForTesting + ImmutableList otherAnnotations() { + return otherAnnotations; + } + @Override - public final String toString() { + public String toString() { return String.format( "Context(otherAnnotations=[%s],testClass=%s)", FluentIterable.from(otherAnnotations()).join(Joiner.on(',')), testClass().getSimpleName()); } - - Context() {} // Prevent implementations outside of this package } } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java index 7b182e2..f4e59aa 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java @@ -170,7 +170,7 @@ public @interface TestParameter { .toArray(Object.class)); } else if (valuesProviderIsSet) { return getValuesFromProvider( - annotation.valuesProvider(), Context.create(otherAnnotations, testClass)); + annotation.valuesProvider(), new Context(otherAnnotations, testClass)); } else { if (Enum.class.isAssignableFrom(parameterClass)) { return Arrays.asList((Object[]) parameterClass.asSubclass(Enum.class).getEnumConstants()); diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java index 4e0ee50..c68a705 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java @@ -17,7 +17,7 @@ package com.google.testing.junit.testparameterinjector.junit5; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.Iterables.getOnlyElement; -import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; @@ -61,40 +61,14 @@ public abstract class TestParameterValuesProvider * An immutable value class that contains extra information about the context of the parameter for * which values are being provided. */ - @AutoValue - public abstract static class Context { - /** - * A list of all other annotations on the field or parameter that was annotated - * with @TestParameter. - * - *

          For example, if the test code is as follows: - * - *

          -     *   {@literal @}Test
          -     *   public void myTest_success(
          -     *       {@literal @}CustomAnnotation(123) {@literal @}TestParameter(valuesProvider=MyProvider.class) Foo foo) {
          -     *     ...
          -     *   }
          -     * 
          - * - * then this list will contain a single element: @CustomAnnotation(123). - */ - abstract ImmutableList otherAnnotations(); + public static final class Context { - /** - * The class that contains the test that is currently being run. - * - *

          Having this can be useful when sharing providers between tests that have the same base - * class. In those cases, an abstract method can be called as follows: - * - *

          -     *   ((MyBaseClass) context.testClass().newInstance()).myAbstractMethod()
          -     * 
          - */ - public abstract Class testClass(); + private final ImmutableList otherAnnotations; + private final Class testClass; - static Context create(ImmutableList otherAnnotations, Class testClass) { - return new AutoValue_TestParameterValuesProvider_Context(otherAnnotations, testClass); + Context(ImmutableList otherAnnotations, Class testClass) { + this.otherAnnotations = otherAnnotations; + this.testClass = testClass; } /** @@ -119,7 +93,7 @@ public abstract class TestParameterValuesProvider * handled by the TestParameterInjector framework. */ @SuppressWarnings("unchecked") // Safe because of the filter operation - public final
          A getOtherAnnotation(Class annotationType) { + public A getOtherAnnotation(Class annotationType) { checkArgument( !TestParameter.class.equals(annotationType), "Getting the @TestParameter annotating the field or parameter is not allowed because" @@ -133,14 +107,35 @@ public abstract class TestParameterValuesProvider // TODO: b/317524353 - Add support for repeated annotations + /** + * The class that contains the test that is currently being run. + * + *

          Having this can be useful when sharing providers between tests that have the same base + * class. In those cases, an abstract method can be called as follows: + * + *

          +     *   ((MyBaseClass) context.testClass().newInstance()).myAbstractMethod()
          +     * 
          + */ + public Class testClass() { + return testClass; + } + + /** + * A list of all other annotations on the field or parameter that was annotated + * with @TestParameter. + */ + @VisibleForTesting + ImmutableList otherAnnotations() { + return otherAnnotations; + } + @Override - public final String toString() { + public String toString() { return String.format( "Context(otherAnnotations=[%s],testClass=%s)", FluentIterable.from(otherAnnotations()).join(Joiner.on(',')), testClass().getSimpleName()); } - - Context() {} // Prevent implementations outside of this package } } -- cgit v1.2.3 From 25cf9ffd9ca74a49da627401f859e3f7c0c788d2 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 17 Jan 2024 10:12:54 +0000 Subject: Update CHANGELOG with the new context aware values provider https://github.com/google/TestParameterInjector/issues/44 --- CHANGELOG.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 535d387..da49481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,39 @@ +## 1.15 + +- Add context aware version of [`TestParameterValuesProvider`]( + https://google.github.io/TestParameterInjector/docs/latest/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.html). + It is the same as the old [`TestParameter.TestParameterValuesProvider`]( + https://google.github.io/TestParameterInjector/docs/latest/com/google/testing/junit/testparameterinjector/TestParameter.TestParameterValuesProvider.html), + except that `provideValues()` was changed to `provideValues(Context)` where + [`Context`]( + https://google.github.io/TestParameterInjector/docs/latest/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.Context.html) + contains the test class and the other annotations. This allows for more generic + providers that take into account custom annotations with extra data, or the + implementation of abstract methods on a base test class. + + Example usage: + +```java +import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider; + +private static final class MyProvider extends TestParameterValuesProvider { + @Override + public List provideValues(Context context) throws Exception { + var testInstance = context.testClass().getDeclaredConstructor().newInstance(); + var fooList = ((MyBaseTestClass) testInstance).getFooList(); + // ... + + // OR + + var fooList = context.getOtherAnnotation(MyCustomAnnotation.class).fooList(); + // ... + } +} +``` + +- Fixed some theoretical non-determinism that could arise from Java reflection + methods + ## 1.14 - Fixed multiple constructors error when this library is used with Powermock. -- cgit v1.2.3 From e83226da9478f6efe2b15e12614c0a8605ad0d68 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 17 Jan 2024 10:14:53 +0000 Subject: Update README with the new context aware values provider https://github.com/google/TestParameterInjector/issues/44 --- README.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 02a9357..233b7e3 100644 --- a/README.md +++ b/README.md @@ -331,15 +331,17 @@ Instead of providing a list of parsable strings, you can implement your own `TestParameterValuesProvider` as follows: ```java +import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider; + @Test public void matchesAllOf_throwsOnNull( @TestParameter(valuesProvider = CharMatcherProvider.class) CharMatcher charMatcher) { assertThrows(NullPointerException.class, () -> charMatcher.matchesAllOf(null)); } -private static final class CharMatcherProvider implements TestParameterValuesProvider { +private static final class CharMatcherProvider extends TestParameterValuesProvider { @Override - public List provideValues() { + public List provideValues(Context context) { return ImmutableList.of(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace()); } } @@ -356,9 +358,9 @@ Notes: want to customize the value names, you can do that as follows: ``` - private static final class FruitProvider implements TestParameterValuesProvider { + private static final class FruitProvider extends TestParameterValuesProvider { @Override - public List provideValues() { + public List provideValues(Context context) { return ImmutableList.of( value(new Apple()).withName("apple"), value(new Banana()).withName("banana")); @@ -366,6 +368,11 @@ Notes: } ``` +- The given `Context` contains the test class and other annotations on the + `@TestParameter`-annotated parameter/field. This allows more generic + providers that take into account custom annotations with extra data, or the + implementation of abstract methods on a base test class. + ### Dynamic parameter generation for `@TestParameters` Instead of providing a YAML mapping of parameters, you can implement your own -- cgit v1.2.3 From f58ebf8455f393751f6aa981ca5a12cae0125c95 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Wed, 17 Jan 2024 10:15:37 +0000 Subject: Update @TestParameter javadoc with the new context aware values provider https://github.com/google/TestParameterInjector/issues/44 --- .../google/testing/junit/testparameterinjector/TestParameter.java | 6 ++++-- .../testing/junit/testparameterinjector/junit5/TestParameter.java | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java index 2fa0375..6272e44 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java @@ -100,6 +100,8 @@ public @interface TestParameter { *

          Example * *

          +   * import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider;
          +   *
              * {@literal @}Test
              * public void matchesAllOf_throwsOnNull(
              *     {@literal @}TestParameter(valuesProvider = CharMatcherProvider.class)
          @@ -107,9 +109,9 @@ public @interface TestParameter {
              *   assertThrows(NullPointerException.class, () -> charMatcher.matchesAllOf(null));
              * }
              *
          -   * private static final class CharMatcherProvider implements TestParameterValuesProvider {
          +   * private static final class CharMatcherProvider extends TestParameterValuesProvider {
              *   {@literal @}Override
          -   *   public {@literal List} provideValues() {
          +   *   public {@literal List} provideValues(Context context) {
              *     return ImmutableList.of(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace());
              *   }
              * }
          diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java
          index f4e59aa..06a1cd5 100644
          --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java
          +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java
          @@ -100,6 +100,8 @@ public @interface TestParameter {
              * 

          Example * *

          +   * import com.google.testing.junit.testparameterinjector.junit5.TestParameterValuesProvider;
          +   *
              * {@literal @}Test
              * public void matchesAllOf_throwsOnNull(
              *     {@literal @}TestParameter(valuesProvider = CharMatcherProvider.class)
          @@ -107,9 +109,9 @@ public @interface TestParameter {
              *   assertThrows(NullPointerException.class, () -> charMatcher.matchesAllOf(null));
              * }
              *
          -   * private static final class CharMatcherProvider implements TestParameterValuesProvider {
          +   * private static final class CharMatcherProvider extends TestParameterValuesProvider {
              *   {@literal @}Override
          -   *   public {@literal List} provideValues() {
          +   *   public {@literal List} provideValues(Context context) {
              *     return ImmutableList.of(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace());
              *   }
              * }
          -- 
          cgit v1.2.3
          
          
          From 616afa4d030631062a0f3c8854b44204f64a1d13 Mon Sep 17 00:00:00 2001
          From: Jens Nyman 
          Date: Wed, 17 Jan 2024 10:24:11 +0000
          Subject: Fix missing javadoc reference
          
          https://github.com/google/TestParameterInjector/issues/44
          ---
           .../testing/junit/testparameterinjector/TestParameterValuesProvider.java | 1 +
           .../junit/testparameterinjector/junit5/TestParameterValuesProvider.java  | 1 +
           2 files changed, 2 insertions(+)
          
          diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java
          index 96dd2ca..4e62128 100644
          --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java
          +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java
          @@ -17,6 +17,7 @@ package com.google.testing.junit.testparameterinjector;
           import static com.google.common.base.Preconditions.checkArgument;
           import static com.google.common.collect.Iterables.getOnlyElement;
           
          +import java.util.NoSuchElementException;
           import com.google.common.annotations.VisibleForTesting;
           import com.google.common.base.Joiner;
           import com.google.common.collect.FluentIterable;
          diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java
          index c68a705..ac52755 100644
          --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java
          +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java
          @@ -17,6 +17,7 @@ package com.google.testing.junit.testparameterinjector.junit5;
           import static com.google.common.base.Preconditions.checkArgument;
           import static com.google.common.collect.Iterables.getOnlyElement;
           
          +import java.util.NoSuchElementException;
           import com.google.common.annotations.VisibleForTesting;
           import com.google.common.base.Joiner;
           import com.google.common.collect.FluentIterable;
          -- 
          cgit v1.2.3
          
          
          From 68c2a772720dbe8e926ab6b663566cb434769b98 Mon Sep 17 00:00:00 2001
          From: Jens Nyman 
          Date: Wed, 17 Jan 2024 10:25:48 +0000
          Subject: TestParameterValuesProvider: Allow provideValues(Context) to throw
           any exception.
          
          This avoids the need for a try-catch when calling testClass.getDeclaredConstructor().newInstance()
          
          https://github.com/google/TestParameterInjector/issues/44
          ---
           .../google/testing/junit/testparameterinjector/TestParameter.java  | 7 +++++++
           .../junit/testparameterinjector/TestParameterValuesProvider.java   | 4 ++--
           .../testing/junit/testparameterinjector/junit5/TestParameter.java  | 7 +++++++
           .../testparameterinjector/junit5/TestParameterValuesProvider.java  | 4 ++--
           4 files changed, 18 insertions(+), 4 deletions(-)
          
          diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
          index 6272e44..992c259 100644
          --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
          +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
          @@ -244,6 +244,13 @@ public @interface TestParameter {
                   }
                 } catch (ReflectiveOperationException e) {
                   throw new IllegalStateException(e);
          +      } catch (Exception e) {
          +        // Catch any unchecked exception that may come from `provideValues(Context)`
          +        if (e instanceof RuntimeException) {
          +          throw (RuntimeException) e;
          +        } else {
          +          throw new IllegalStateException(e);
          +        }
                 }
               }
             }
          diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java
          index 4e62128..9fa8dc2 100644
          --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java
          +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java
          @@ -17,13 +17,13 @@ package com.google.testing.junit.testparameterinjector;
           import static com.google.common.base.Preconditions.checkArgument;
           import static com.google.common.collect.Iterables.getOnlyElement;
           
          -import java.util.NoSuchElementException;
           import com.google.common.annotations.VisibleForTesting;
           import com.google.common.base.Joiner;
           import com.google.common.collect.FluentIterable;
           import com.google.common.collect.ImmutableList;
           import java.lang.annotation.Annotation;
           import java.util.List;
          +import java.util.NoSuchElementException;
           import javax.annotation.Nullable;
           
           /**
          @@ -36,7 +36,7 @@ import javax.annotation.Nullable;
           public abstract class TestParameterValuesProvider
               implements TestParameter.TestParameterValuesProvider {
           
          -  protected abstract List provideValues(Context context);
          +  protected abstract List provideValues(Context context) throws Exception;
           
             @Override
             public final List provideValues() {
          diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java
          index 06a1cd5..40bd569 100644
          --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java
          +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java
          @@ -244,6 +244,13 @@ public @interface TestParameter {
                   }
                 } catch (ReflectiveOperationException e) {
                   throw new IllegalStateException(e);
          +      } catch (Exception e) {
          +        // Catch any unchecked exception that may come from `provideValues(Context)`
          +        if (e instanceof RuntimeException) {
          +          throw (RuntimeException) e;
          +        } else {
          +          throw new IllegalStateException(e);
          +        }
                 }
               }
             }
          diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java
          index ac52755..2232d19 100644
          --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java
          +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java
          @@ -17,13 +17,13 @@ package com.google.testing.junit.testparameterinjector.junit5;
           import static com.google.common.base.Preconditions.checkArgument;
           import static com.google.common.collect.Iterables.getOnlyElement;
           
          -import java.util.NoSuchElementException;
           import com.google.common.annotations.VisibleForTesting;
           import com.google.common.base.Joiner;
           import com.google.common.collect.FluentIterable;
           import com.google.common.collect.ImmutableList;
           import java.lang.annotation.Annotation;
           import java.util.List;
          +import java.util.NoSuchElementException;
           import javax.annotation.Nullable;
           
           /**
          @@ -36,7 +36,7 @@ import javax.annotation.Nullable;
           public abstract class TestParameterValuesProvider
               implements TestParameter.TestParameterValuesProvider {
           
          -  protected abstract List provideValues(Context context);
          +  protected abstract List provideValues(Context context) throws Exception;
           
             @Override
             public final List provideValues() {
          -- 
          cgit v1.2.3
          
          
          From ca9d8d3d580919b1f4ed92cff3b1a726c8bec3d2 Mon Sep 17 00:00:00 2001
          From: Jens Nyman 
          Date: Thu, 18 Jan 2024 10:50:25 +0000
          Subject: Bump version to v1.15 in README.md
          
          ---
           README.md | 4 ++--
           1 file changed, 2 insertions(+), 2 deletions(-)
          
          diff --git a/README.md b/README.md
          index 233b7e3..d0178a6 100644
          --- a/README.md
          +++ b/README.md
          @@ -54,7 +54,7 @@ And add the following dependency to your `.pom` file:
           
             com.google.testparameterinjector
             test-parameter-injector
          -  1.14
          +  1.15
             test
           
           ```
          @@ -97,7 +97,7 @@ And add the following dependency to your `.pom` file:
           
             com.google.testparameterinjector
             test-parameter-injector-junit5
          -  1.14
          +  1.15
             test
           
           ```
          -- 
          cgit v1.2.3
          
          
          From e0e1058330f7b559502438b7bc08d0316c5c8908 Mon Sep 17 00:00:00 2001
          From: Jens Nyman 
          Date: Mon, 19 Feb 2024 13:38:30 +0000
          Subject: Mark the old TestParameterValuesProvider type as deprecated
          
          ---
           .../testing/junit/testparameterinjector/TestParameter.java       | 9 ++++++++-
           .../junit/testparameterinjector/TestParameterValuesProvider.java | 5 +++++
           .../junit/testparameterinjector/junit5/TestParameter.java        | 9 ++++++++-
           .../junit5/TestParameterValuesProvider.java                      | 5 +++++
           4 files changed, 26 insertions(+), 2 deletions(-)
          
          diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
          index 992c259..240a57b 100644
          --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
          +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
          @@ -120,7 +120,14 @@ public @interface TestParameter {
             Class valuesProvider() default
                 DefaultTestParameterValuesProvider.class;
           
          -  /** Interface for custom providers of test parameter values. */
          +  /**
          +   * Interface for custom providers of test parameter values.
          +   *
          +   * @deprecated Use {@link
          +   *     com.google.testing.junit.testparameterinjector.TestParameterValuesProvider} instead. The
          +   *     replacement implements this same interface, but with an additional Context parameter.
          +   */
          +  @Deprecated
             interface TestParameterValuesProvider {
               List provideValues();
           
          diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java
          index 9fa8dc2..078026a 100644
          --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java
          +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java
          @@ -38,7 +38,12 @@ public abstract class TestParameterValuesProvider
           
             protected abstract List provideValues(Context context) throws Exception;
           
          +  /**
          +   * @deprecated This method should never be called as it will simply throw an {@link
          +   *     UnsupportedOperationException}.
          +   */
             @Override
          +  @Deprecated
             public final List provideValues() {
               throw new UnsupportedOperationException(
                   "The TestParameterInjector framework should never call this method, and instead call"
          diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java
          index 40bd569..e294f6d 100644
          --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java
          +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java
          @@ -120,7 +120,14 @@ public @interface TestParameter {
             Class valuesProvider() default
                 DefaultTestParameterValuesProvider.class;
           
          -  /** Interface for custom providers of test parameter values. */
          +  /**
          +   * Interface for custom providers of test parameter values.
          +   *
          +   * @deprecated Use {@link
          +   *     com.google.testing.junit.testparameterinjector.junit5.TestParameterValuesProvider} instead. The
          +   *     replacement implements this same interface, but with an additional Context parameter.
          +   */
          +  @Deprecated
             interface TestParameterValuesProvider {
               List provideValues();
           
          diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java
          index 2232d19..2cf9da6 100644
          --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java
          +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java
          @@ -38,7 +38,12 @@ public abstract class TestParameterValuesProvider
           
             protected abstract List provideValues(Context context) throws Exception;
           
          +  /**
          +   * @deprecated This method should never be called as it will simply throw an {@link
          +   *     UnsupportedOperationException}.
          +   */
             @Override
          +  @Deprecated
             public final List provideValues() {
               throw new UnsupportedOperationException(
                   "The TestParameterInjector framework should never call this method, and instead call"
          -- 
          cgit v1.2.3
          
          
          From 2825e00e97a6554eafbfb4d9737a2d535e7eb267 Mon Sep 17 00:00:00 2001
          From: Jens Nyman 
          Date: Wed, 21 Feb 2024 21:25:57 +0000
          Subject: Refactorings for Google-internal change
          
          ---
           .../junit/testparameterinjector/TestParameter.java       |  3 +--
           .../TestParameterValuesProvider.java                     |  2 +-
           .../TestParameterAnnotationMethodProcessorTest.java      |  9 ++++-----
           .../TestParameterInjectorKotlinTest.kt                   |  9 ++++++---
           .../junit/testparameterinjector/TestParameterTest.java   | 16 +++++++---------
           .../testparameterinjector/junit5/TestParameter.java      |  3 +--
           .../junit5/TestParameterValuesProvider.java              |  2 +-
           7 files changed, 21 insertions(+), 23 deletions(-)
          
          diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
          index 240a57b..8e78e7c 100644
          --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
          +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java
          @@ -34,7 +34,6 @@ import java.lang.reflect.Modifier;
           import java.util.ArrayList;
           import java.util.Arrays;
           import java.util.List;
          -import javax.annotation.Nullable;
           
           /**
            * Test parameter annotation that defines the values that a single parameter can have.
          @@ -140,7 +139,7 @@ public @interface TestParameter {
                *
                * 

          Do not override this method. */ - default TestParameterValue value(@Nullable Object wrappedValue) { + default TestParameterValue value(@javax.annotation.Nullable Object wrappedValue) { return TestParameterValue.wrap(wrappedValue); } } diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java index 078026a..129d514 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java @@ -60,7 +60,7 @@ public abstract class TestParameterValuesProvider @Override public final TestParameterValue value(@Nullable Object wrappedValue) { // Overriding this method as final because it is not supposed to be overwritten - return TestParameter.TestParameterValuesProvider.super.value(wrappedValue); + return TestParameterValue.wrap(wrappedValue); } /** diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java index df3a16b..458b623 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java @@ -22,7 +22,6 @@ import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.testing.junit.testparameterinjector.SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase; -import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.util.Arrays; @@ -272,9 +271,9 @@ public class TestParameterAnnotationMethodProcessorTest { storeTestParametersForThisTest(testString); } - private static final class Test2Provider implements TestParameterValuesProvider { + private static final class Test2Provider extends TestParameterValuesProvider { @Override - public List provideValues() { + public List provideValues(TestParameterValuesProvider.Context context) { return newArrayList(123, "123", "null", null); } } @@ -660,9 +659,9 @@ public class TestParameterAnnotationMethodProcessorTest { public void test(@TestParameter(valuesProvider = NonStaticProvider.class) int i) {} @SuppressWarnings("ClassCanBeStatic") - class NonStaticProvider implements TestParameterValuesProvider { + class NonStaticProvider extends TestParameterValuesProvider { @Override - public List provideValues() { + public List provideValues(TestParameterValuesProvider.Context context) { return ImmutableList.of(); } } diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt index e015533..10ce60e 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt @@ -17,7 +17,6 @@ package com.google.testing.junit.testparameterinjector import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap import com.google.testing.junit.testparameterinjector.SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase -import com.google.testing.junit.testparameterinjector.TestParameter.TestParameterValuesProvider import java.util.Arrays import org.junit.Test import org.junit.runner.RunWith @@ -148,8 +147,8 @@ class TestParameterInjectorKotlinTest { .buildOrThrow() } - private class DoubleValueClassProvider : TestParameterValuesProvider { - override fun provideValues(): List { + private class DoubleValueClassProvider : TestParameterValuesProvider() { + override fun provideValues(context: Context): List { return ImmutableList.of(DoubleValueClass(1.0), DoubleValueClass(2.5)) } } @@ -262,6 +261,7 @@ class TestParameterInjectorKotlinTest { .collect(ImmutableList.toImmutableList()) } } + annotation class RunAsTest enum class Color { @@ -269,7 +269,10 @@ class TestParameterInjectorKotlinTest { BLUE, GREEN } + @JvmInline value class ColorValueClass(val onlyValue: Color) + @JvmInline value class StringValueClass(val onlyValue: String) + @JvmInline value class DoubleValueClass(val onlyValue: Double) } diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java index 61534e6..d5d3c96 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java @@ -21,6 +21,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.google.common.base.CharMatcher; import com.google.common.collect.ImmutableMap; import com.google.testing.junit.testparameterinjector.SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase; +import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider.Context; import java.lang.annotation.Retention; import java.util.Arrays; import java.util.Collection; @@ -195,27 +196,24 @@ public class TestParameterTest { .build(); } - private static final class TestNumberProvider - implements TestParameter.TestParameterValuesProvider { + private static final class TestNumberProvider extends TestParameterValuesProvider { @Override - public List provideValues() { + public List provideValues(Context context) { return newArrayList(value(1).withName("one"), 2); } } - private static final class TestStringProvider - implements TestParameter.TestParameterValuesProvider { + private static final class TestStringProvider extends TestParameterValuesProvider { @Override - public List provideValues() { + public List provideValues(Context context) { return newArrayList( "A", "B", null, value(null).withName("nothing"), value("harry").withName("wizard")); } } - private static final class CharMatcherProvider - implements TestParameter.TestParameterValuesProvider { + private static final class CharMatcherProvider extends TestParameterValuesProvider { @Override - public List provideValues() { + public List provideValues(Context context) { return newArrayList(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace()); } } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java index e294f6d..9798c2f 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java @@ -34,7 +34,6 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import javax.annotation.Nullable; /** * Test parameter annotation that defines the values that a single parameter can have. @@ -140,7 +139,7 @@ public @interface TestParameter { * *

          Do not override this method. */ - default TestParameterValue value(@Nullable Object wrappedValue) { + default TestParameterValue value(@javax.annotation.Nullable Object wrappedValue) { return TestParameterValue.wrap(wrappedValue); } } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java index 2cf9da6..51169b8 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java @@ -60,7 +60,7 @@ public abstract class TestParameterValuesProvider @Override public final TestParameterValue value(@Nullable Object wrappedValue) { // Overriding this method as final because it is not supposed to be overwritten - return TestParameter.TestParameterValuesProvider.super.value(wrappedValue); + return TestParameterValue.wrap(wrappedValue); } /** -- cgit v1.2.3 From 9c1183e5d495f833f7352400255236a5b8a042b9 Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Thu, 22 Feb 2024 16:54:28 +0000 Subject: Update changelog with deprecation of TestParameter.TestParameterValuesProvider --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index da49481..5b80fa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.16 + +- Deprecated [`TestParameter.TestParameterValuesProvider`]( + https://google.github.io/TestParameterInjector/docs/latest/com/google/testing/junit/testparameterinjector/TestParameter.TestParameterValuesProvider.html) + in favor of its newer version [`TestParameterValuesProvider`]( + https://google.github.io/TestParameterInjector/docs/latest/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.html). + ## 1.15 - Add context aware version of [`TestParameterValuesProvider`]( -- cgit v1.2.3 From 42fab13ba4f87b8556f9aca032f849e6c13b070e Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Thu, 22 Feb 2024 20:14:03 +0000 Subject: Refactor: Create a common GenericParameterContext type that can serve as basis for the context classes of @TestParameter and @TestParameters, as wel as being used ineternally --- .../GenericParameterContext.java | 76 ++++++++++++++++++++++ .../junit/testparameterinjector/TestParameter.java | 9 +-- .../TestParameterAnnotationMethodProcessor.java | 34 ++++------ .../TestParameterValueProvider.java | 6 +- .../TestParameterValuesProvider.java | 34 +++------- .../testparameterinjector/TestParameterTest.java | 8 +++ .../junit5/GenericParameterContext.java | 76 ++++++++++++++++++++++ .../junit5/TestParameter.java | 9 +-- .../TestParameterAnnotationMethodProcessor.java | 34 ++++------ .../junit5/TestParameterValueProvider.java | 6 +- .../junit5/TestParameterValuesProvider.java | 34 +++------- 11 files changed, 208 insertions(+), 118 deletions(-) create mode 100644 junit4/src/main/java/com/google/testing/junit/testparameterinjector/GenericParameterContext.java create mode 100644 junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/GenericParameterContext.java diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/GenericParameterContext.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/GenericParameterContext.java new file mode 100644 index 0000000..f2a8c73 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/GenericParameterContext.java @@ -0,0 +1,76 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.collect.Iterables.getOnlyElement; + +import com.google.common.base.Joiner; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Ordering; +import java.lang.annotation.Annotation; +import java.util.NoSuchElementException; + +/** A value class that contains extra information about the context of a field or parameter. */ +final class GenericParameterContext { + + private final ImmutableList annotationsOnParameter; + private final Class testClass; + + GenericParameterContext(ImmutableList annotationsOnParameter, Class testClass) { + this.annotationsOnParameter = annotationsOnParameter; + this.testClass = testClass; + } + + /** + * Returns the only annotation with the given type on the field or parameter. + * + * @throws NoSuchElementException if this there is no annotation with the given type + * @throws IllegalArgumentException if there are multiple annotations with the given type + */ + @SuppressWarnings("unchecked") // Safe because of the filter operation + A getAnnotation(Class annotationType) { + return (A) + getOnlyElement( + FluentIterable.from(annotationsOnParameter) + .filter(annotation -> annotation.annotationType().equals(annotationType)) + .toList()); + } + + // TODO: b/317524353 - Add support for repeated annotations + + /** The class that contains the test that is currently being run. */ + Class testClass() { + return testClass; + } + + /** A list of all annotations on the field or parameter. */ + ImmutableList annotationsOnParameter() { + return annotationsOnParameter; + } + + @Override + public String toString() { + return String.format( + "context(annotationsOnParameter=[%s],testClass=%s)", + FluentIterable.from( + ImmutableList.sortedCopyOf( + Ordering.natural().onResultOf(Annotation::toString), annotationsOnParameter)) + .transform( + annotation -> String.format("@%s", annotation.annotationType().getSimpleName())) + .join(Joiner.on(',')), + testClass().getSimpleName()); + } +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java index 8e78e7c..d193ec6 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java @@ -21,7 +21,6 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.google.common.base.Optional; import com.google.common.collect.FluentIterable; -import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; import com.google.testing.junit.testparameterinjector.TestParameter.InternalImplementationOfThisParameter; @@ -148,7 +147,7 @@ public @interface TestParameter { class DefaultTestParameterValuesProvider implements TestParameterValuesProvider { @Override public List provideValues() { - return ImmutableList.of(); + return com.google.common.collect.ImmutableList.of(); } } @@ -157,9 +156,8 @@ public @interface TestParameter { @Override public List provideValues( Annotation uncastAnnotation, - ImmutableList otherAnnotations, Optional> maybeParameterClass, - Class testClass) { + GenericParameterContext context) { TestParameter annotation = (TestParameter) uncastAnnotation; Class parameterClass = getValueType(annotation.annotationType(), maybeParameterClass); @@ -177,8 +175,7 @@ public @interface TestParameter { .transform(v -> parseStringValue(v, parameterClass)) .toArray(Object.class)); } else if (valuesProviderIsSet) { - return getValuesFromProvider( - annotation.valuesProvider(), new Context(otherAnnotations, testClass)); + return getValuesFromProvider(annotation.valuesProvider(), new Context(context)); } else { if (Enum.class.isAssignableFrom(parameterClass)) { return Arrays.asList((Object[]) parameterClass.asSubclass(Enum.class).getEnumConstants()); diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index d186e84..a4bcb74 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -192,9 +192,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso .newInstance() .provideValues( annotation, - annotationWithMetadata.otherAnnotations(), annotationWithMetadata.paramClass(), - annotationWithMetadata.testClass())) + annotationWithMetadata.context())) .transform( value -> (value instanceof TestParameterValue) @@ -251,15 +250,6 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso */ abstract Annotation annotation(); - /** - * A list of all other annotations on the field or parameter that was annotated with {@code - * annotation}. - * - *

          In case the annotation is annotating a method, constructor or class, {@code - * parameterClass} is an empty list. - */ - abstract ImmutableList otherAnnotations(); - /** * 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. @@ -272,8 +262,13 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso */ abstract Optional paramName(); - /** The class that contains the test that is currently being run. */ - abstract Class testClass(); + /** + * A value class that contains extra information about the context of this parameter. + * + *

          In case the annotation is annotating a method, constructor or class (deprecated + * functionality), the annotations in the context will be empty. + */ + abstract GenericParameterContext context(); public static AnnotationWithMetadata withMetadata( Annotation annotation, @@ -283,12 +278,9 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso Class testClass) { return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( annotation, - /* otherAnnotations= */ FluentIterable.from(allAnnotations) - .filter(a -> !a.equals(annotation)) - .toList(), Optional.of(paramClass), Optional.of(paramName), - testClass); + new GenericParameterContext(ImmutableList.copyOf(allAnnotations), testClass)); } public static AnnotationWithMetadata withMetadata( @@ -298,22 +290,18 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso Class testClass) { return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( annotation, - /* otherAnnotations= */ FluentIterable.from(allAnnotations) - .filter(a -> !a.equals(annotation)) - .toList(), Optional.of(paramClass), Optional.absent(), - testClass); + new GenericParameterContext(ImmutableList.copyOf(allAnnotations), testClass)); } public static AnnotationWithMetadata withoutMetadata( Annotation annotation, Class testClass) { return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( annotation, - /* otherAnnotations= */ ImmutableList.of(), /* paramClass= */ Optional.absent(), /* paramName= */ Optional.absent(), - testClass); + new GenericParameterContext(/* annotationsOnParameter= */ ImmutableList.of(), testClass)); } // Prevent anyone relying on equals() and hashCode() so that it remains possible to add fields diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java index 08cc173..38c3356 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java @@ -15,7 +15,6 @@ package com.google.testing.junit.testparameterinjector; import com.google.common.base.Optional; -import com.google.common.collect.ImmutableList; import java.lang.annotation.Annotation; import java.util.List; @@ -75,10 +74,7 @@ interface TestParameterValueProvider { */ @Deprecated default List provideValues( - Annotation annotation, - ImmutableList otherAnnotations, - Optional> parameterClass, - Class testClass) { + Annotation annotation, Optional> parameterClass, GenericParameterContext context) { return provideValues(annotation, parameterClass); } diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java index 129d514..a8fd19d 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java @@ -15,11 +15,8 @@ package com.google.testing.junit.testparameterinjector; import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.collect.Iterables.getOnlyElement; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Joiner; -import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import java.lang.annotation.Annotation; import java.util.List; @@ -69,12 +66,10 @@ public abstract class TestParameterValuesProvider */ public static final class Context { - private final ImmutableList otherAnnotations; - private final Class testClass; + private final GenericParameterContext delegate; - Context(ImmutableList otherAnnotations, Class testClass) { - this.otherAnnotations = otherAnnotations; - this.testClass = testClass; + Context(GenericParameterContext delegate) { + this.delegate = delegate; } /** @@ -98,17 +93,12 @@ public abstract class TestParameterValuesProvider * @throws IllegalArgumentException if the argument it TestParameter.class because it is already * handled by the TestParameterInjector framework. */ - @SuppressWarnings("unchecked") // Safe because of the filter operation public A getOtherAnnotation(Class annotationType) { checkArgument( !TestParameter.class.equals(annotationType), "Getting the @TestParameter annotating the field or parameter is not allowed because" + " it is already handled by the TestParameterInjector framework."); - return (A) - getOnlyElement( - FluentIterable.from(otherAnnotations()) - .filter(annotation -> annotation.annotationType().equals(annotationType)) - .toList()); + return delegate.getAnnotation(annotationType); } // TODO: b/317524353 - Add support for repeated annotations @@ -124,24 +114,18 @@ public abstract class TestParameterValuesProvider * */ public Class testClass() { - return testClass; + return delegate.testClass(); } - /** - * A list of all other annotations on the field or parameter that was annotated - * with @TestParameter. - */ + /** A list of all annotations on the field or parameter. */ @VisibleForTesting - ImmutableList otherAnnotations() { - return otherAnnotations; + ImmutableList annotationsOnParameter() { + return delegate.annotationsOnParameter(); } @Override public String toString() { - return String.format( - "Context(otherAnnotations=[%s],testClass=%s)", - FluentIterable.from(otherAnnotations()).join(Joiner.on(',')), - testClass().getSimpleName()); + return delegate.toString(); } } } diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java index d5d3c96..7c915ea 100644 --- a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java @@ -19,9 +19,12 @@ import static com.google.common.collect.Lists.newArrayList; import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.google.common.base.CharMatcher; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.testing.junit.testparameterinjector.SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase; import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider.Context; +import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.util.Arrays; import java.util.Collection; @@ -243,4 +246,9 @@ public class TestParameterTest { } }); } + + private static ImmutableList> annotationTypes( + Iterable annotations) { + return FluentIterable.from(annotations).transform(Annotation::annotationType).toList(); + } } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/GenericParameterContext.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/GenericParameterContext.java new file mode 100644 index 0000000..15ac68b --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/GenericParameterContext.java @@ -0,0 +1,76 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.collect.Iterables.getOnlyElement; + +import com.google.common.base.Joiner; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Ordering; +import java.lang.annotation.Annotation; +import java.util.NoSuchElementException; + +/** A value class that contains extra information about the context of a field or parameter. */ +final class GenericParameterContext { + + private final ImmutableList annotationsOnParameter; + private final Class testClass; + + GenericParameterContext(ImmutableList annotationsOnParameter, Class testClass) { + this.annotationsOnParameter = annotationsOnParameter; + this.testClass = testClass; + } + + /** + * Returns the only annotation with the given type on the field or parameter. + * + * @throws NoSuchElementException if this there is no annotation with the given type + * @throws IllegalArgumentException if there are multiple annotations with the given type + */ + @SuppressWarnings("unchecked") // Safe because of the filter operation + A getAnnotation(Class annotationType) { + return (A) + getOnlyElement( + FluentIterable.from(annotationsOnParameter) + .filter(annotation -> annotation.annotationType().equals(annotationType)) + .toList()); + } + + // TODO: b/317524353 - Add support for repeated annotations + + /** The class that contains the test that is currently being run. */ + Class testClass() { + return testClass; + } + + /** A list of all annotations on the field or parameter. */ + ImmutableList annotationsOnParameter() { + return annotationsOnParameter; + } + + @Override + public String toString() { + return String.format( + "context(annotationsOnParameter=[%s],testClass=%s)", + FluentIterable.from( + ImmutableList.sortedCopyOf( + Ordering.natural().onResultOf(Annotation::toString), annotationsOnParameter)) + .transform( + annotation -> String.format("@%s", annotation.annotationType().getSimpleName())) + .join(Joiner.on(',')), + testClass().getSimpleName()); + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java index 9798c2f..c26c8ed 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java @@ -21,7 +21,6 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.google.common.base.Optional; import com.google.common.collect.FluentIterable; -import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; import com.google.testing.junit.testparameterinjector.junit5.TestParameter.InternalImplementationOfThisParameter; @@ -148,7 +147,7 @@ public @interface TestParameter { class DefaultTestParameterValuesProvider implements TestParameterValuesProvider { @Override public List provideValues() { - return ImmutableList.of(); + return com.google.common.collect.ImmutableList.of(); } } @@ -157,9 +156,8 @@ public @interface TestParameter { @Override public List provideValues( Annotation uncastAnnotation, - ImmutableList otherAnnotations, Optional> maybeParameterClass, - Class testClass) { + GenericParameterContext context) { TestParameter annotation = (TestParameter) uncastAnnotation; Class parameterClass = getValueType(annotation.annotationType(), maybeParameterClass); @@ -177,8 +175,7 @@ public @interface TestParameter { .transform(v -> parseStringValue(v, parameterClass)) .toArray(Object.class)); } else if (valuesProviderIsSet) { - return getValuesFromProvider( - annotation.valuesProvider(), new Context(otherAnnotations, testClass)); + return getValuesFromProvider(annotation.valuesProvider(), new Context(context)); } else { if (Enum.class.isAssignableFrom(parameterClass)) { return Arrays.asList((Object[]) parameterClass.asSubclass(Enum.class).getEnumConstants()); diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java index 41d9a7b..3024ffb 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java @@ -192,9 +192,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso .newInstance() .provideValues( annotation, - annotationWithMetadata.otherAnnotations(), annotationWithMetadata.paramClass(), - annotationWithMetadata.testClass())) + annotationWithMetadata.context())) .transform( value -> (value instanceof TestParameterValue) @@ -251,15 +250,6 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso */ abstract Annotation annotation(); - /** - * A list of all other annotations on the field or parameter that was annotated with {@code - * annotation}. - * - *

          In case the annotation is annotating a method, constructor or class, {@code - * parameterClass} is an empty list. - */ - abstract ImmutableList otherAnnotations(); - /** * 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. @@ -272,8 +262,13 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso */ abstract Optional paramName(); - /** The class that contains the test that is currently being run. */ - abstract Class testClass(); + /** + * A value class that contains extra information about the context of this parameter. + * + *

          In case the annotation is annotating a method, constructor or class (deprecated + * functionality), the annotations in the context will be empty. + */ + abstract GenericParameterContext context(); public static AnnotationWithMetadata withMetadata( Annotation annotation, @@ -283,12 +278,9 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso Class testClass) { return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( annotation, - /* otherAnnotations= */ FluentIterable.from(allAnnotations) - .filter(a -> !a.equals(annotation)) - .toList(), Optional.of(paramClass), Optional.of(paramName), - testClass); + new GenericParameterContext(ImmutableList.copyOf(allAnnotations), testClass)); } public static AnnotationWithMetadata withMetadata( @@ -298,22 +290,18 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso Class testClass) { return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( annotation, - /* otherAnnotations= */ FluentIterable.from(allAnnotations) - .filter(a -> !a.equals(annotation)) - .toList(), Optional.of(paramClass), Optional.absent(), - testClass); + new GenericParameterContext(ImmutableList.copyOf(allAnnotations), testClass)); } public static AnnotationWithMetadata withoutMetadata( Annotation annotation, Class testClass) { return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( annotation, - /* otherAnnotations= */ ImmutableList.of(), /* paramClass= */ Optional.absent(), /* paramName= */ Optional.absent(), - testClass); + new GenericParameterContext(/* annotationsOnParameter= */ ImmutableList.of(), testClass)); } // Prevent anyone relying on equals() and hashCode() so that it remains possible to add fields diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java index a3ad576..9cc9f88 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java @@ -15,7 +15,6 @@ package com.google.testing.junit.testparameterinjector.junit5; import com.google.common.base.Optional; -import com.google.common.collect.ImmutableList; import java.lang.annotation.Annotation; import java.util.List; @@ -75,10 +74,7 @@ interface TestParameterValueProvider { */ @Deprecated default List provideValues( - Annotation annotation, - ImmutableList otherAnnotations, - Optional> parameterClass, - Class testClass) { + Annotation annotation, Optional> parameterClass, GenericParameterContext context) { return provideValues(annotation, parameterClass); } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java index 51169b8..603f24d 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java @@ -15,11 +15,8 @@ package com.google.testing.junit.testparameterinjector.junit5; import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.collect.Iterables.getOnlyElement; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Joiner; -import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import java.lang.annotation.Annotation; import java.util.List; @@ -69,12 +66,10 @@ public abstract class TestParameterValuesProvider */ public static final class Context { - private final ImmutableList otherAnnotations; - private final Class testClass; + private final GenericParameterContext delegate; - Context(ImmutableList otherAnnotations, Class testClass) { - this.otherAnnotations = otherAnnotations; - this.testClass = testClass; + Context(GenericParameterContext delegate) { + this.delegate = delegate; } /** @@ -98,17 +93,12 @@ public abstract class TestParameterValuesProvider * @throws IllegalArgumentException if the argument it TestParameter.class because it is already * handled by the TestParameterInjector framework. */ - @SuppressWarnings("unchecked") // Safe because of the filter operation public A getOtherAnnotation(Class annotationType) { checkArgument( !TestParameter.class.equals(annotationType), "Getting the @TestParameter annotating the field or parameter is not allowed because" + " it is already handled by the TestParameterInjector framework."); - return (A) - getOnlyElement( - FluentIterable.from(otherAnnotations()) - .filter(annotation -> annotation.annotationType().equals(annotationType)) - .toList()); + return delegate.getAnnotation(annotationType); } // TODO: b/317524353 - Add support for repeated annotations @@ -124,24 +114,18 @@ public abstract class TestParameterValuesProvider * */ public Class testClass() { - return testClass; + return delegate.testClass(); } - /** - * A list of all other annotations on the field or parameter that was annotated - * with @TestParameter. - */ + /** A list of all annotations on the field or parameter. */ @VisibleForTesting - ImmutableList otherAnnotations() { - return otherAnnotations; + ImmutableList annotationsOnParameter() { + return delegate.annotationsOnParameter(); } @Override public String toString() { - return String.format( - "Context(otherAnnotations=[%s],testClass=%s)", - FluentIterable.from(otherAnnotations()).join(Joiner.on(',')), - testClass().getSimpleName()); + return delegate.toString(); } } } -- cgit v1.2.3 From 12066d29df68922d8c4a1a0c2c6128abc487340f Mon Sep 17 00:00:00 2001 From: Jens Nyman Date: Mon, 18 Mar 2024 13:27:20 +0000 Subject: Context: Support repeated annotations --- .../GenericParameterContext.java | 119 ++++++++++++++++++++- .../TestParameterAnnotationMethodProcessor.java | 49 ++++----- .../TestParameterValuesProvider.java | 33 +++++- .../junit5/GenericParameterContext.java | 119 ++++++++++++++++++++- .../TestParameterAnnotationMethodProcessor.java | 49 ++++----- .../junit5/TestParameterValuesProvider.java | 33 +++++- 6 files changed, 344 insertions(+), 58 deletions(-) diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/GenericParameterContext.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/GenericParameterContext.java index f2a8c73..5586d7b 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/GenericParameterContext.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/GenericParameterContext.java @@ -16,24 +16,86 @@ package com.google.testing.junit.testparameterinjector; import static com.google.common.collect.Iterables.getOnlyElement; +import com.google.common.base.Function; import com.google.common.base.Joiner; +import com.google.common.base.Optional; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.Ordering; import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.Proxy; import java.util.NoSuchElementException; /** A value class that contains extra information about the context of a field or parameter. */ final class GenericParameterContext { private final ImmutableList annotationsOnParameter; + + /** Same contract as #getAnnotations */ + private final Function, ImmutableList> + getAnnotationsFunction; + private final Class testClass; - GenericParameterContext(ImmutableList annotationsOnParameter, Class testClass) { + private GenericParameterContext( + ImmutableList annotationsOnParameter, + Function, ImmutableList> + getAnnotationsFunction, + Class testClass) { this.annotationsOnParameter = annotationsOnParameter; + this.getAnnotationsFunction = getAnnotationsFunction; this.testClass = testClass; } + // Field.getAnnotationsByType() is not available on old Android SDKs. There is a fallback in that + // case in this method. + @SuppressWarnings("AndroidJdkLibsChecker") + static GenericParameterContext create(Field field, Class testClass) { + return new GenericParameterContext( + ImmutableList.copyOf(field.getAnnotations()), + /* getAnnotationsFunction= */ annotationType -> { + try { + return ImmutableList.copyOf(field.getAnnotationsByType(annotationType)); + } catch (NoSuchMethodError ignored) { + return getAnnotationsFallback( + ImmutableList.copyOf(field.getAnnotations()), annotationType); + } + }, + testClass); + } + + // Parameter is not available on old Android SDKs, and isn't desugared. That's why this method + // should only be called with a fallback. + @SuppressWarnings("AndroidJdkLibsChecker") + static GenericParameterContext create(Parameter parameter, Class testClass) { + return new GenericParameterContext( + ImmutableList.copyOf(parameter.getAnnotations()), + /* getAnnotationsFunction= */ annotationType -> + ImmutableList.copyOf(parameter.getAnnotationsByType(annotationType)), + testClass); + } + + static GenericParameterContext createWithRepeatableAnnotationsFallback( + Annotation[] annotationsOnParameter, Class testClass) { + return new GenericParameterContext( + ImmutableList.copyOf(annotationsOnParameter), + /* getAnnotationsFunction= */ annotationType -> + getAnnotationsFallback(ImmutableList.copyOf(annotationsOnParameter), annotationType), + testClass); + } + + static GenericParameterContext createWithoutParameterAnnotations(Class testClass) { + return new GenericParameterContext( + /* annotationsOnParameter= */ ImmutableList.of(), + /* getAnnotationsFunction= */ annotationType -> + getAnnotationsFallback(ImmutableList.of(), annotationType), + testClass); + } + /** * Returns the only annotation with the given type on the field or parameter. * @@ -49,7 +111,15 @@ final class GenericParameterContext { .toList()); } - // TODO: b/317524353 - Add support for repeated annotations + /** + * Returns the annotations with the given type on the field or parameter. + * + *

          Returns an empty list if this there is no annotation with the given type. + */ + @SuppressWarnings("unchecked") // Safe because of the getAnnotationsFunction contract + ImmutableList getAnnotations(Class annotationType) { + return (ImmutableList) getAnnotationsFunction.apply(annotationType); + } /** The class that contains the test that is currently being run. */ Class testClass() { @@ -73,4 +143,49 @@ final class GenericParameterContext { .join(Joiner.on(',')), testClass().getSimpleName()); } + + private static ImmutableList getAnnotationsFallback( + ImmutableList annotationsOnParameter, + Class annotationType) { + ImmutableList candidates = + FluentIterable.from(annotationsOnParameter) + .filter(annotation -> annotation.annotationType().equals(annotationType)) + .toList(); + if (candidates.isEmpty() && getContainerType(annotationType).isPresent()) { + ImmutableList containerAnnotations = + getAnnotationsFallback(annotationsOnParameter, getContainerType(annotationType).get()); + if (containerAnnotations.size() == 1) { + Annotation containerAnnotation = getOnlyElement(containerAnnotations); + try { + Method annotationValueMethod = + containerAnnotation.annotationType().getDeclaredMethod("value"); + annotationValueMethod.setAccessible(true); + return ImmutableList.copyOf( + (Annotation[]) + Proxy.getInvocationHandler(containerAnnotation) + .invoke(containerAnnotation, annotationValueMethod, null)); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + return ImmutableList.of(); + } else { + return candidates; + } + } + + private static Optional> getContainerType( + Class annotationType) { + try { + Repeatable repeatable = annotationType.getAnnotation(Repeatable.class); + if (repeatable == null) { + return Optional.absent(); + } else { + return Optional.of(repeatable.value()); + } + } catch (NoClassDefFoundError ignored) { + // If @Repeatable does not exist, then there is no container type by definition + return Optional.absent(); + } + } } diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java index a4bcb74..16b206a 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -272,36 +272,26 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso public static AnnotationWithMetadata withMetadata( Annotation annotation, - Annotation[] allAnnotations, Class paramClass, String paramName, - Class testClass) { + GenericParameterContext context) { return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( - annotation, - Optional.of(paramClass), - Optional.of(paramName), - new GenericParameterContext(ImmutableList.copyOf(allAnnotations), testClass)); + annotation, Optional.of(paramClass), Optional.of(paramName), context); } public static AnnotationWithMetadata withMetadata( - Annotation annotation, - Annotation[] allAnnotations, - Class paramClass, - Class testClass) { + Annotation annotation, Class paramClass, GenericParameterContext context) { return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( - annotation, - Optional.of(paramClass), - Optional.absent(), - new GenericParameterContext(ImmutableList.copyOf(allAnnotations), testClass)); + annotation, Optional.of(paramClass), Optional.absent(), context); } public static AnnotationWithMetadata withoutMetadata( - Annotation annotation, Class testClass) { + Annotation annotation, GenericParameterContext context) { return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( annotation, /* paramClass= */ Optional.absent(), /* paramName= */ Optional.absent(), - new GenericParameterContext(/* annotationsOnParameter= */ ImmutableList.of(), testClass)); + context); } // Prevent anyone relying on equals() and hashCode() so that it remains possible to add fields @@ -947,7 +937,10 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso if (annotation != null) { return ImmutableList.of( TestParameterValueHolder.create( - AnnotationWithMetadata.withoutMetadata(annotation, testClass), origin)); + AnnotationWithMetadata.withoutMetadata( + annotation, + GenericParameterContext.createWithoutParameterAnnotations(testClass)), + origin)); } } else if (origin == Origin.METHOD_PARAMETER) { @@ -961,7 +954,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return ImmutableList.of( TestParameterValueHolder.create( AnnotationWithMetadata.withoutMetadata( - method.getAnnotation(annotationType), testClass), + method.getAnnotation(annotationType), + GenericParameterContext.createWithoutParameterAnnotations(testClass)), origin)); } } else if (origin == Origin.FIELD) { @@ -977,10 +971,9 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso annotation -> AnnotationWithMetadata.withMetadata( annotation, - field.getAnnotations(), field.getType(), field.getName(), - testClass))) + GenericParameterContext.create(field, testClass)))) .toList()); if (!annotations.isEmpty()) { return toTestParameterValueList(annotations, origin); @@ -990,7 +983,10 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso if (annotation != null) { return ImmutableList.of( TestParameterValueHolder.create( - AnnotationWithMetadata.withoutMetadata(annotation, testClass), origin)); + AnnotationWithMetadata.withoutMetadata( + annotation, + GenericParameterContext.createWithoutParameterAnnotations(testClass)), + origin)); } } return ImmutableList.of(); @@ -1050,15 +1046,13 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso : parameter.isNamePresent() ? AnnotationWithMetadata.withMetadata( annotation, - /* allAnnotations= */ parameter.getAnnotations(), parameter.getType(), parameter.getName(), - testClass) + GenericParameterContext.create(parameter, testClass)) : AnnotationWithMetadata.withMetadata( annotation, - /* allAnnotations= */ parameter.getAnnotations(), parameter.getType(), - testClass); + GenericParameterContext.create(parameter, testClass)); }) .filter(Objects::nonNull) .toList(); @@ -1077,7 +1071,10 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso if (annotation.annotationType().equals(annotationType)) { resultBuilder.add( AnnotationWithMetadata.withMetadata( - annotation, /* allAnnotations= */ annotations[i], parameterTypes[i], testClass)); + annotation, + parameterTypes[i], + GenericParameterContext.createWithRepeatableAnnotationsFallback( + annotations[i], testClass))); } } } diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java index a8fd19d..ccdb18b 100644 --- a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java @@ -101,7 +101,38 @@ public abstract class TestParameterValuesProvider return delegate.getAnnotation(annotationType); } - // TODO: b/317524353 - Add support for repeated annotations + /** + * Returns the only annotation with the given type on the field or parameter that was annotated + * with @TestParameter. + * + *

          For example, if the test code is as follows: + * + *

          +     *   {@literal @}Test
          +     *   public void myTest_success(
          +     *       {@literal @}CustomAnnotation(123)
          +     *       {@literal @}CustomAnnotation(456)
          +     *       {@literal @}TestParameter(valuesProvider=MyProvider.class)
          +     *       Foo foo) {
          +     *     ...
          +     *   }
          +     * 
          + * + * then {@code context.getOtherAnnotations(CustomAnnotation.class)} will return the annotation + * with 123 and 456. + * + *

          Returns an empty list if this there is no annotation with the given type. + * + * @throws IllegalArgumentException if the argument it TestParameter.class because it is already + * handled by the TestParameterInjector framework. + */ + public ImmutableList getOtherAnnotations(Class annotationType) { + checkArgument( + !TestParameter.class.equals(annotationType), + "Getting the @TestParameter annotating the field or parameter is not allowed because" + + " it is already handled by the TestParameterInjector framework."); + return delegate.getAnnotations(annotationType); + } /** * The class that contains the test that is currently being run. diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/GenericParameterContext.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/GenericParameterContext.java index 15ac68b..02e5367 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/GenericParameterContext.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/GenericParameterContext.java @@ -16,24 +16,86 @@ package com.google.testing.junit.testparameterinjector.junit5; import static com.google.common.collect.Iterables.getOnlyElement; +import com.google.common.base.Function; import com.google.common.base.Joiner; +import com.google.common.base.Optional; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.Ordering; import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.Proxy; import java.util.NoSuchElementException; /** A value class that contains extra information about the context of a field or parameter. */ final class GenericParameterContext { private final ImmutableList annotationsOnParameter; + + /** Same contract as #getAnnotations */ + private final Function, ImmutableList> + getAnnotationsFunction; + private final Class testClass; - GenericParameterContext(ImmutableList annotationsOnParameter, Class testClass) { + private GenericParameterContext( + ImmutableList annotationsOnParameter, + Function, ImmutableList> + getAnnotationsFunction, + Class testClass) { this.annotationsOnParameter = annotationsOnParameter; + this.getAnnotationsFunction = getAnnotationsFunction; this.testClass = testClass; } + // Field.getAnnotationsByType() is not available on old Android SDKs. There is a fallback in that + // case in this method. + @SuppressWarnings("AndroidJdkLibsChecker") + static GenericParameterContext create(Field field, Class testClass) { + return new GenericParameterContext( + ImmutableList.copyOf(field.getAnnotations()), + /* getAnnotationsFunction= */ annotationType -> { + try { + return ImmutableList.copyOf(field.getAnnotationsByType(annotationType)); + } catch (NoSuchMethodError ignored) { + return getAnnotationsFallback( + ImmutableList.copyOf(field.getAnnotations()), annotationType); + } + }, + testClass); + } + + // Parameter is not available on old Android SDKs, and isn't desugared. That's why this method + // should only be called with a fallback. + @SuppressWarnings("AndroidJdkLibsChecker") + static GenericParameterContext create(Parameter parameter, Class testClass) { + return new GenericParameterContext( + ImmutableList.copyOf(parameter.getAnnotations()), + /* getAnnotationsFunction= */ annotationType -> + ImmutableList.copyOf(parameter.getAnnotationsByType(annotationType)), + testClass); + } + + static GenericParameterContext createWithRepeatableAnnotationsFallback( + Annotation[] annotationsOnParameter, Class testClass) { + return new GenericParameterContext( + ImmutableList.copyOf(annotationsOnParameter), + /* getAnnotationsFunction= */ annotationType -> + getAnnotationsFallback(ImmutableList.copyOf(annotationsOnParameter), annotationType), + testClass); + } + + static GenericParameterContext createWithoutParameterAnnotations(Class testClass) { + return new GenericParameterContext( + /* annotationsOnParameter= */ ImmutableList.of(), + /* getAnnotationsFunction= */ annotationType -> + getAnnotationsFallback(ImmutableList.of(), annotationType), + testClass); + } + /** * Returns the only annotation with the given type on the field or parameter. * @@ -49,7 +111,15 @@ final class GenericParameterContext { .toList()); } - // TODO: b/317524353 - Add support for repeated annotations + /** + * Returns the annotations with the given type on the field or parameter. + * + *

          Returns an empty list if this there is no annotation with the given type. + */ + @SuppressWarnings("unchecked") // Safe because of the getAnnotationsFunction contract + ImmutableList getAnnotations(Class annotationType) { + return (ImmutableList) getAnnotationsFunction.apply(annotationType); + } /** The class that contains the test that is currently being run. */ Class testClass() { @@ -73,4 +143,49 @@ final class GenericParameterContext { .join(Joiner.on(',')), testClass().getSimpleName()); } + + private static ImmutableList getAnnotationsFallback( + ImmutableList annotationsOnParameter, + Class annotationType) { + ImmutableList candidates = + FluentIterable.from(annotationsOnParameter) + .filter(annotation -> annotation.annotationType().equals(annotationType)) + .toList(); + if (candidates.isEmpty() && getContainerType(annotationType).isPresent()) { + ImmutableList containerAnnotations = + getAnnotationsFallback(annotationsOnParameter, getContainerType(annotationType).get()); + if (containerAnnotations.size() == 1) { + Annotation containerAnnotation = getOnlyElement(containerAnnotations); + try { + Method annotationValueMethod = + containerAnnotation.annotationType().getDeclaredMethod("value"); + annotationValueMethod.setAccessible(true); + return ImmutableList.copyOf( + (Annotation[]) + Proxy.getInvocationHandler(containerAnnotation) + .invoke(containerAnnotation, annotationValueMethod, null)); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + return ImmutableList.of(); + } else { + return candidates; + } + } + + private static Optional> getContainerType( + Class annotationType) { + try { + Repeatable repeatable = annotationType.getAnnotation(Repeatable.class); + if (repeatable == null) { + return Optional.absent(); + } else { + return Optional.of(repeatable.value()); + } + } catch (NoClassDefFoundError ignored) { + // If @Repeatable does not exist, then there is no container type by definition + return Optional.absent(); + } + } } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java index 3024ffb..7fd2336 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java @@ -272,36 +272,26 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso public static AnnotationWithMetadata withMetadata( Annotation annotation, - Annotation[] allAnnotations, Class paramClass, String paramName, - Class testClass) { + GenericParameterContext context) { return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( - annotation, - Optional.of(paramClass), - Optional.of(paramName), - new GenericParameterContext(ImmutableList.copyOf(allAnnotations), testClass)); + annotation, Optional.of(paramClass), Optional.of(paramName), context); } public static AnnotationWithMetadata withMetadata( - Annotation annotation, - Annotation[] allAnnotations, - Class paramClass, - Class testClass) { + Annotation annotation, Class paramClass, GenericParameterContext context) { return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( - annotation, - Optional.of(paramClass), - Optional.absent(), - new GenericParameterContext(ImmutableList.copyOf(allAnnotations), testClass)); + annotation, Optional.of(paramClass), Optional.absent(), context); } public static AnnotationWithMetadata withoutMetadata( - Annotation annotation, Class testClass) { + Annotation annotation, GenericParameterContext context) { return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( annotation, /* paramClass= */ Optional.absent(), /* paramName= */ Optional.absent(), - new GenericParameterContext(/* annotationsOnParameter= */ ImmutableList.of(), testClass)); + context); } // Prevent anyone relying on equals() and hashCode() so that it remains possible to add fields @@ -947,7 +937,10 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso if (annotation != null) { return ImmutableList.of( TestParameterValueHolder.create( - AnnotationWithMetadata.withoutMetadata(annotation, testClass), origin)); + AnnotationWithMetadata.withoutMetadata( + annotation, + GenericParameterContext.createWithoutParameterAnnotations(testClass)), + origin)); } } else if (origin == Origin.METHOD_PARAMETER) { @@ -961,7 +954,8 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso return ImmutableList.of( TestParameterValueHolder.create( AnnotationWithMetadata.withoutMetadata( - method.getAnnotation(annotationType), testClass), + method.getAnnotation(annotationType), + GenericParameterContext.createWithoutParameterAnnotations(testClass)), origin)); } } else if (origin == Origin.FIELD) { @@ -977,10 +971,9 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso annotation -> AnnotationWithMetadata.withMetadata( annotation, - field.getAnnotations(), field.getType(), field.getName(), - testClass))) + GenericParameterContext.create(field, testClass)))) .toList()); if (!annotations.isEmpty()) { return toTestParameterValueList(annotations, origin); @@ -990,7 +983,10 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso if (annotation != null) { return ImmutableList.of( TestParameterValueHolder.create( - AnnotationWithMetadata.withoutMetadata(annotation, testClass), origin)); + AnnotationWithMetadata.withoutMetadata( + annotation, + GenericParameterContext.createWithoutParameterAnnotations(testClass)), + origin)); } } return ImmutableList.of(); @@ -1050,15 +1046,13 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso : parameter.isNamePresent() ? AnnotationWithMetadata.withMetadata( annotation, - /* allAnnotations= */ parameter.getAnnotations(), parameter.getType(), parameter.getName(), - testClass) + GenericParameterContext.create(parameter, testClass)) : AnnotationWithMetadata.withMetadata( annotation, - /* allAnnotations= */ parameter.getAnnotations(), parameter.getType(), - testClass); + GenericParameterContext.create(parameter, testClass)); }) .filter(Objects::nonNull) .toList(); @@ -1077,7 +1071,10 @@ final class TestParameterAnnotationMethodProcessor implements TestMethodProcesso if (annotation.annotationType().equals(annotationType)) { resultBuilder.add( AnnotationWithMetadata.withMetadata( - annotation, /* allAnnotations= */ annotations[i], parameterTypes[i], testClass)); + annotation, + parameterTypes[i], + GenericParameterContext.createWithRepeatableAnnotationsFallback( + annotations[i], testClass))); } } } diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java index 603f24d..29f945f 100644 --- a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java @@ -101,7 +101,38 @@ public abstract class TestParameterValuesProvider return delegate.getAnnotation(annotationType); } - // TODO: b/317524353 - Add support for repeated annotations + /** + * Returns the only annotation with the given type on the field or parameter that was annotated + * with @TestParameter. + * + *

          For example, if the test code is as follows: + * + *

          +     *   {@literal @}Test
          +     *   public void myTest_success(
          +     *       {@literal @}CustomAnnotation(123)
          +     *       {@literal @}CustomAnnotation(456)
          +     *       {@literal @}TestParameter(valuesProvider=MyProvider.class)
          +     *       Foo foo) {
          +     *     ...
          +     *   }
          +     * 
          + * + * then {@code context.getOtherAnnotations(CustomAnnotation.class)} will return the annotation + * with 123 and 456. + * + *

          Returns an empty list if this there is no annotation with the given type. + * + * @throws IllegalArgumentException if the argument it TestParameter.class because it is already + * handled by the TestParameterInjector framework. + */ + public ImmutableList getOtherAnnotations(Class annotationType) { + checkArgument( + !TestParameter.class.equals(annotationType), + "Getting the @TestParameter annotating the field or parameter is not allowed because" + + " it is already handled by the TestParameterInjector framework."); + return delegate.getAnnotations(annotationType); + } /** * The class that contains the test that is currently being run. -- cgit v1.2.3