diff options
author | David Duarte <licorne@google.com> | 2024-04-05 18:19:46 +0000 |
---|---|---|
committer | David Duarte <licorne@google.com> | 2024-04-05 18:24:08 +0000 |
commit | 273cae1c01676b8ec59a0271aeeee5304b645ae6 (patch) | |
tree | ea6745de613747a454d5dbc59882551899915abd | |
parent | 4a1b074a53356ed6f81836343058859652f3ca4f (diff) | |
parent | 12066d29df68922d8c4a1a0c2c6128abc487340f (diff) | |
download | TestParameterInjector-273cae1c01676b8ec59a0271aeeee5304b645ae6.tar.gz |
Merge remote-tracking branch 'aosp/upstream-main' into main
Bug: 333088631
Test: m TestParameterInjector
Change-Id: Ie6334c3fc67bcf8213b450942c5bcab1e71feac4
61 files changed, 13307 insertions, 38 deletions
diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..f262838 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,21 @@ +# Copyright 2021 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +version: 2 + +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..3452265 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,48 @@ +# Copyright 2021 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +name: build + +on: + pull_request: {} + push: + branches: + - '**' + tags-ignore: + - '**' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 11 + cache: maven + + - run: mvn --update-snapshots -B verify javadoc:javadoc + + - name: Deploy docs to website + if: ${{ github.ref == 'refs/heads/main' }} + uses: JamesIves/github-pages-deploy-action@releases/v3 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: site + FOLDER: junit4/target/site/apidocs + TARGET_FOLDER: docs/latest/ + CLEAN: true diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..034eff7 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,43 @@ +# Copyright 2021 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +name: release + +on: + push: + tags: + - '**' + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 11 + + - run: mvn javadoc:javadoc + + - name: Deploy docs to website + uses: JamesIves/github-pages-deploy-action@releases/v3 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: site + FOLDER: junit4/target/site/apidocs + TARGET_FOLDER: docs/1.x/ + CLEAN: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/CHANGELOG.md b/CHANGELOG.md index ebe26a6..5b80fa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,138 @@ +## 1.16 + +- Deprecated [`TestParameter.TestParameterValuesProvider`]( + https://google.github.io/TestParameterInjector/docs/latest/com/google/testing/junit/testparameterinjector/TestParameter.TestParameterValuesProvider.html) + in favor of its newer version [`TestParameterValuesProvider`]( + https://google.github.io/TestParameterInjector/docs/latest/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.html). + +## 1.15 + +- Add context aware version of [`TestParameterValuesProvider`]( + https://google.github.io/TestParameterInjector/docs/latest/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.html). + It is the same as the old [`TestParameter.TestParameterValuesProvider`]( + https://google.github.io/TestParameterInjector/docs/latest/com/google/testing/junit/testparameterinjector/TestParameter.TestParameterValuesProvider.html), + except that `provideValues()` was changed to `provideValues(Context)` where + [`Context`]( + https://google.github.io/TestParameterInjector/docs/latest/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.Context.html) + contains the test class and the other annotations. This allows for more generic + providers that take into account custom annotations with extra data, or the + implementation of abstract methods on a base test class. + + Example usage: + +```java +import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider; + +private static final class MyProvider extends TestParameterValuesProvider { + @Override + public List<?> provideValues(Context context) throws Exception { + var testInstance = context.testClass().getDeclaredConstructor().newInstance(); + var fooList = ((MyBaseTestClass) testInstance).getFooList(); + // ... + + // OR + + var fooList = context.getOtherAnnotation(MyCustomAnnotation.class).fooList(); + // ... + } +} +``` + +- Fixed some theoretical non-determinism that could arise from Java reflection + methods + +## 1.14 + +- Fixed multiple constructors error when this library is used with Powermock. + See https://github.com/google/TestParameterInjector/issues/40. + +## 1.13 + +- Add support for setting a custom name for a `@TestParameter` value given via a provider: + +```java +private static final class FruitProvider implements TestParameterValuesProvider { + @Override + public List<?> provideValues() { + return ImmutableList.of( + value(new Apple()).withName("apple"), + value(new Banana()).withName("banana")); + } +} +``` + +- Add support for `BigInteger` and `UnsignedLong` +- JUnit4: Fix for interrupted test cases causing random failures with thread + reuse (porting [the earlier fix in + JUnit4](https://github.com/junit-team/junit4/issues/1365)) + +## 1.12 + +- Tweak to the test name generation: Show the parameter name if its value is potentially + ambiguous (e.g. null, "" or "123"). +- Made `TestParametersValues.name()` optional. If missing, a name will be generated. + +## 1.11 + +- Replaced deprecated call to org.yaml.snakeyaml.constructor.SafeConstructor + +## 1.10 + +- Removed dependency on `protobuf-javalite` (see + [issue #24](https://github.com/google/TestParameterInjector/issues/24)) + +## 1.9 + +- Bugfix: Support explicit ordering by the JUnit4 `@Rule`. For example: `@Rule(ordering=3)`. +- Potential test name change: Test names are no longer dependent on the locale of the machine + running it (e.g. doubles with integer values are always formatted with a trailing `.0`) + +## 1.8 + +- Add support for JUnit5 (Jupiter) + +## 1.7 + +- Remove `TestParameterInjector` support for `org.junit.runners.Parameterized`, + which was undocumented and thus unlikely to be used. + +## 1.6 + +- Bugfixes +- Better documentation + +## 1.5 + +- `@TestParameters` can now also be used as a repeated annotation: + +```java +// Newly added and recommended for new code +@Test +@TestParameters("{age: 17, expectIsAdult: false}") +@TestParameters("{age: 22, expectIsAdult: true}") +public void withRepeatedAnnotation(int age, boolean expectIsAdult){...} + +// The old way of using @TestParameters is still supported +@Test +@TestParameters({ + "{age: 17, expectIsAdult: false}", + "{age: 22, expectIsAdult: true}", +}) +public void withSingleAnnotation(int age, boolean expectIsAdult){...} +``` + +- `@TestParameters` supports setting a custom test name: + +```java +@Test +@TestParameters(customName = "teenager", value = "{age: 17, expectIsAdult: false}") +@TestParameters(customName = "young adult", value = "{age: 22, expectIsAdult: true}") +public void personIsAdult(int age, boolean expectIsAdult){...} +``` + +- Test names with very long parameter strings are abbreviated differentily: In + some cases, more characters are allowed. + ## 1.4 - Bugfix: Run test methods declared in a base class (instead of throwing an @@ -11,7 +11,7 @@ third_party { type: GIT value: "https://github.com/google/TestParameterInjector" } - version: "e65d6bebdba9df211b258fae996fe34b6eadb787" - last_upgrade_date { year: 2021 month: 7 day: 26 } + version: "12066d29df68922d8c4a1a0c2c6128abc487340f" + last_upgrade_date { year: 2024 month: 4 day: 5 } license_type: NOTICE } @@ -5,7 +5,7 @@ TestParameterInjector ## Introduction -`TestParameterInjector` is a JUnit4 test runner that runs its test methods for +`TestParameterInjector` is a JUnit4 and JUnit5 test runner that runs its test methods for different combinations of field/parameter values. Parameterized tests are a great way to avoid code duplication between tests and @@ -23,6 +23,8 @@ frameworks used at Google. ## Getting started +### JUnit4 + To start using `TestParameterInjector` right away, copy the following snippet: ```java @@ -52,7 +54,8 @@ And add the following dependency to your `.pom` file: <dependency> <groupId>com.google.testparameterinjector</groupId> <artifactId>test-parameter-injector</artifactId> - <version>1.4</version> + <version>1.15</version> + <scope>test</scope> </dependency> ``` @@ -60,9 +63,57 @@ or see [this maven.org page](https://search.maven.org/artifact/com.google.testparameterinjector/test-parameter-injector) for instructions for other build tools. +### JUnit5 (Jupiter) +<details> +<summary>Click to expand</summary> + +To start using `TestParameterInjector` right away, copy the following snippet: + +```java +import com.google.testing.junit.testparameterinjector.junit5.TestParameterInjectorTest; +import com.google.testing.junit.testparameterinjector.junit5.TestParameter; + +class MyTest { + + @TestParameter boolean isDryRun; + + @TestParameterInjectorTest + void test1(@TestParameter boolean enableFlag) { + // ... + } + + @TestParameterInjectorTest + void test2(@TestParameter MyEnum myEnum) { + // ... + } + + enum MyEnum { VALUE_A, VALUE_B, VALUE_C } +} +``` + +And add the following dependency to your `.pom` file: + +```xml +<dependency> + <groupId>com.google.testparameterinjector</groupId> + <artifactId>test-parameter-injector-junit5</artifactId> + <version>1.15</version> + <scope>test</scope> +</dependency> +``` + +or see [this maven.org +page](https://search.maven.org/artifact/com.google.testparameterinjector/test-parameter-injector-junit5) +for instructions for other build tools. + +</details> ## Basics +**Note about JUnit4 vs JUnit5:**<br /> +The code below assumes you're using JUnit4. For JUnit5 users, simply remove the +`@RunWith` annotation and replace `@Test` by `@TestParameterInjectorTest`. + ### `@TestParameter` for testing all combinations #### Parameterizing a single test method @@ -108,6 +159,10 @@ public class MyTest { In this example, both `test1` and `test2` will be run twice (once for each parameter value). +The test runner will set these fields before calling any methods, so it is safe +to use such `@TestParameter`-annotated fields for setting up other test values +and behavior in `@Before` methods. + #### Supported types The following examples show most of the supported types. See the `@TestParameter` javadoc for more details. @@ -207,44 +262,116 @@ mappings: ```java @Test -@TestParameters({ - "{age: 17, expectIsAdult: false}", - "{age: 22, expectIsAdult: true}", -}) +@TestParameters("{age: 17, expectIsAdult: false}") +@TestParameters("{age: 22, expectIsAdult: true}") public void personIsAdult(int age, boolean expectIsAdult) { ... } ``` +which would generate the following tests: + +``` +MyTest#personIsAdult[{age: 17, expectIsAdult: false}] +MyTest#personIsAdult[{age: 22, expectIsAdult: true}] +``` + The string format supports the same types as `@TestParameter` (e.g. enums). See the `@TestParameters` javadoc for more info. `@TestParameters` works in the same way on the constructor, in which case all tests will be run for the given parameter sets. +> Tip: Consider setting a custom name if the YAML string is large: +> +> ```java +> @Test +> @TestParameters(customName = "teenager", value = "{age: 17, expectIsAdult: false}") +> @TestParameters(customName = "young adult", value = "{age: 22, expectIsAdult: true}") +> public void personIsAdult(int age, boolean expectIsAdult) { ... } +> ``` +> +> This will generate the following test names: +> +> ``` +> MyTest#personIsAdult[teenager] +> MyTest#personIsAdult[young adult] +> ``` + +### Filtering unwanted parameters + +Sometimes, you want to exclude a parameter or a combination of parameters. We +recommend doing this via JUnit assumptions which is also supported by +[Truth](https://truth.dev/): + +```java +import static com.google.common.truth.TruthJUnit.assume; + +@Test +public void myTest(@TestParameter Fruit fruit) { + assume().that(fruit).isNotEqualTo(Fruit.BANANA); + + // At this point, the test will only run for APPLE and CHERRY. + // The BANANA case will silently be ignored. +} + +enum Fruit { APPLE, BANANA, CHERRY } +``` + +Note that the above works regardless of what parameterization framework you +choose. + ## Advanced usage +**Note about JUnit4 vs JUnit5:**<br /> +The code below assumes you're using JUnit4. For JUnit5 users, simply remove the +`@RunWith` annotation and replace `@Test` by `@TestParameterInjectorTest`. + ### Dynamic parameter generation for `@TestParameter` Instead of providing a list of parsable strings, you can implement your own `TestParameterValuesProvider` as follows: ```java +import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider; + @Test public void matchesAllOf_throwsOnNull( @TestParameter(valuesProvider = CharMatcherProvider.class) CharMatcher charMatcher) { assertThrows(NullPointerException.class, () -> charMatcher.matchesAllOf(null)); } -private static final class CharMatcherProvider implements TestParameterValuesProvider { +private static final class CharMatcherProvider extends TestParameterValuesProvider { @Override - public List<CharMatcher> provideValues() { + public List<CharMatcher> provideValues(Context context) { return ImmutableList.of(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace()); } } ``` -Note that `provideValues()` dynamically construct the returned list, e.g. by -reading a file. There are no restrictions on the object types returned, but note -that `toString()` will be used for the test names. +Notes: + +- The `provideValues()` method can dynamically construct the returned list, + e.g. by reading a file. +- There are no restrictions on the object types returned. +- The `provideValues()` method is called before `@BeforeClass`, so don't rely + on any static state initialized in there. +- The returned objects' `toString()` will be used for the test names. If you + want to customize the value names, you can do that as follows: + + ``` + private static final class FruitProvider extends TestParameterValuesProvider { + @Override + public List<?> provideValues(Context context) { + return ImmutableList.of( + value(new Apple()).withName("apple"), + value(new Banana()).withName("banana")); + } + } + ``` + +- The given `Context` contains the test class and other annotations on the + `@TestParameter`-annotated parameter/field. This allows more generic + providers that take into account custom annotations with extra data, or the + implementation of abstract methods on a base test class. ### Dynamic parameter generation for `@TestParameters` diff --git a/junit4/pom.xml b/junit4/pom.xml new file mode 100644 index 0000000..789f648 --- /dev/null +++ b/junit4/pom.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Copyright 2021 Google Inc. + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>com.google.testparameterinjector</groupId> + <artifactId>test-parameter-injector-parent</artifactId> + <version>HEAD-SNAPSHOT</version> + </parent> + + <artifactId>test-parameter-injector</artifactId> + + <name>TestParameterInjector for JUnit4</name> + + <dependencies> + <!-- Compile-time dependencies --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.13.2</version> + </dependency> + </dependencies> +</project> diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java new file mode 100644 index 0000000..6c23efa --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/BaseTestParameterValidator.java @@ -0,0 +1,90 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static java.lang.Math.min; + +import com.google.common.collect.FluentIterable; +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Default base class for {@link TestParameterValidator}, simplifying how validators can exclude + * variable independent test parameters annotations. + */ +abstract class BaseTestParameterValidator implements TestParameterValidator { + + @Override + public boolean shouldSkip(Context context) { + for (List<Class<? extends Annotation>> parameters : getIndependentParameters(context)) { + checkArgument(!parameters.isEmpty()); + // For independent test parameters, the only allowed tests will be those that use the same + // Nth specified parameter, except for parameter values that have less specified values than + // others. + + // For example, if parameter A has values a1 and a2, parameter B has values b1 and b2, and + // parameter C has values c1, c2 and c3, given that A, B and C are independent, the only + // tests that will not be skipped will be {(a1, b1, c1), (a2, b2, c2), (a2, b2, c3)}, + // instead of 12 tests that would constitute their cartesian product. + + // First, find the largest specified value count (parameter C in the example above), + // so that we can easily determine which parameter value should be used for validating the + // other parameters (e.g. should this test be for (a1, b1, c1), (a2, b2, c2), or + // (a2, b2, c3). The test parameter 'C' will be the 'leadingParameter'. + + Class<? extends Annotation> leadingParameter = + FluentIterable.from(parameters) + .toSortedList( + (o1, o2) -> + Integer.compare( + context.getSpecifiedValues(o1).size(), + context.getSpecifiedValues(o2).size())) + .reverse() + .get(0); + + // Second, determine which index is the current value in the specified value list of + // the leading parameter. In the example above, the index of the current value 'c2' of the + // leading parameter 'C' would be '1', given the specified values (c1, c2, c3). + int leadingParameterValueIndex = + getValueIndex(context, leadingParameter, context.getValue(leadingParameter).get()); + checkState(leadingParameterValueIndex >= 0); + // Each independent test parameter should be the same index, or the last available index. + // For example, if the parameter is A, and the leading parameter (C) index is 2, the A's index + // should be 1, since a2 is the only available value. + for (Class<? extends Annotation> parameter : parameters) { + List<Object> specifiedValues = context.getSpecifiedValues(parameter); + int valueIndex = specifiedValues.indexOf(context.getValue(parameter).get()); + int requiredValueIndex = min(leadingParameterValueIndex, specifiedValues.size() - 1); + if (valueIndex != requiredValueIndex) { + return true; + } + } + } + return false; + } + + private int getValueIndex(Context context, Class<? extends Annotation> annotation, Object value) { + return context.getSpecifiedValues(annotation).indexOf(value); + } + + /** + * Returns a list of TestParameterAnnotation annotated annotation types that are mutually + * independent, and therefore the combinations of their values do not need to be tested. + */ + protected abstract List<List<Class<? extends Annotation>>> getIndependentParameters( + Context context); +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ByteStringReflection.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ByteStringReflection.java new file mode 100644 index 0000000..ca94a39 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ByteStringReflection.java @@ -0,0 +1,98 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableMap; +import java.lang.reflect.InvocationTargetException; + +/** + * Utility methods to interact with com.google.protobuf.ByteString via reflection. + * + * <p>This is a hack to avoid the open source project to depend on protobuf-lite/javalite, which is + * causing conflicts for users (see https://github.com/google/TestParameterInjector/issues/24). + */ +final class ByteStringReflection { + + static final Optional<Class<?>> MAYBE_BYTE_STRING_CLASS = maybeGetByteStringClass(); + + /** Equivalent of {@code object instanceof ByteString} */ + static boolean isInstanceOfByteString(Object object) { + if (MAYBE_BYTE_STRING_CLASS.isPresent()) { + return MAYBE_BYTE_STRING_CLASS.get().isInstance(object); + } else { + return false; + } + } + + /** Eqvuivalent of {@code ((ByteString) byteString).toByteArray()} */ + static byte[] byteStringToByteArray(Object byteString) { + return (byte[]) + invokeByteStringMethod("toByteArray", /* obj= */ byteString, /* args= */ ImmutableMap.of()); + } + + /** + * Eqvuivalent of {@code ByteString.copyFromUtf8(text)}. + * + * <p>Encodes {@code text} into a sequence of UTF-8 bytes and returns the result as a {@code + * ByteString}. + */ + static Object copyFromUtf8(String text) { + return invokeByteStringMethod( + "copyFromUtf8", /* obj= */ null, /* args= */ ImmutableMap.of(String.class, text)); + } + + /** + * Eqvuivalent of {@code ByteString.copyFrom(bytes)}. + * + * <p>Copies the given bytes into a {@code ByteString}. + */ + static Object copyFrom(byte[] bytes) { + return invokeByteStringMethod( + "copyFrom", /* obj= */ null, /* args= */ ImmutableMap.of(byte[].class, bytes)); + } + + @SuppressWarnings("UseMultiCatch") + private static Object invokeByteStringMethod( + String methodName, Object obj, ImmutableMap<Class<?>, ?> args) { + try { + return MAYBE_BYTE_STRING_CLASS + .get() + .getMethod(methodName, args.keySet().toArray(new Class<?>[0])) + .invoke(obj, args.values().toArray()); + /* + * Do not merge the 3 catch blocks below. javac would infer a type of + * ReflectiveOperationException, which Animal Sniffer would reject. (Old versions of + * Android don't *seem* to mind, but there might be edge cases of which we're unaware.) + */ + } catch (IllegalAccessException e) { + throw new LinkageError(String.format("Accessing %s()", methodName), e); + } catch (InvocationTargetException e) { + throw new LinkageError(String.format("Calling %s()", methodName), e); + } catch (NoSuchMethodException e) { + throw new LinkageError(String.format("Calling %s()", methodName), e); + } + } + + private static Optional<Class<?>> maybeGetByteStringClass() { + try { + return Optional.of(Class.forName("com.google.protobuf.ByteString")); + } catch (ClassNotFoundException | LinkageError unused) { + return Optional.absent(); + } + } + + private ByteStringReflection() {} // Inhibit instantiation +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java new file mode 100644 index 0000000..47b445b --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ExecutableValidationResult.java @@ -0,0 +1,72 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.Iterables.getOnlyElement; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import java.util.Collection; + +/** + * Value class that captures the result of a validating a single constructor or test method. + * + * <p>If the validation is not validated by any processor, it will be validated using the default + * validator. If a processor validates a constructor/test method, the remaining processors will + * *not* be called. + */ +@AutoValue +abstract class ExecutableValidationResult { + + /** Returns true if the properties of the given constructor/test method were validated. */ + public abstract boolean wasValidated(); + + /** Returns the validation errors, if any. */ + public abstract ImmutableList<Throwable> validationErrors(); + + static ExecutableValidationResult notValidated() { + return of(/* wasValidated= */ false, /* validationErrors= */ ImmutableList.of()); + } + + static ExecutableValidationResult validated(Collection<Throwable> errors) { + return of(/* wasValidated= */ true, /* validationErrors= */ errors); + } + + static ExecutableValidationResult validated(Throwable error) { + return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of(error)); + } + + static ExecutableValidationResult valid() { + return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of()); + } + + private static ExecutableValidationResult of( + boolean wasValidated, Collection<Throwable> validationErrors) { + checkArgument(wasValidated || validationErrors.isEmpty()); + return new AutoValue_ExecutableValidationResult( + wasValidated, ImmutableList.copyOf(validationErrors)); + } + + void assertValid() { + if (wasValidated() && !validationErrors().isEmpty()) { + if (validationErrors().size() == 1) { + throw new AssertionError(getOnlyElement(validationErrors())); + } else { + throw new AssertionError(String.format("Found validation errors: %s", validationErrors())); + } + } + } +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/GenericParameterContext.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/GenericParameterContext.java new file mode 100644 index 0000000..5586d7b --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/GenericParameterContext.java @@ -0,0 +1,191 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.collect.Iterables.getOnlyElement; + +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.base.Optional; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Ordering; +import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.Proxy; +import java.util.NoSuchElementException; + +/** A value class that contains extra information about the context of a field or parameter. */ +final class GenericParameterContext { + + private final ImmutableList<Annotation> annotationsOnParameter; + + /** Same contract as #getAnnotations */ + private final Function<Class<? extends Annotation>, ImmutableList<? extends Annotation>> + getAnnotationsFunction; + + private final Class<?> testClass; + + private GenericParameterContext( + ImmutableList<Annotation> annotationsOnParameter, + Function<Class<? extends Annotation>, ImmutableList<? extends Annotation>> + getAnnotationsFunction, + Class<?> testClass) { + this.annotationsOnParameter = annotationsOnParameter; + this.getAnnotationsFunction = getAnnotationsFunction; + this.testClass = testClass; + } + + // Field.getAnnotationsByType() is not available on old Android SDKs. There is a fallback in that + // case in this method. + @SuppressWarnings("AndroidJdkLibsChecker") + static GenericParameterContext create(Field field, Class<?> testClass) { + return new GenericParameterContext( + ImmutableList.copyOf(field.getAnnotations()), + /* getAnnotationsFunction= */ annotationType -> { + try { + return ImmutableList.copyOf(field.getAnnotationsByType(annotationType)); + } catch (NoSuchMethodError ignored) { + return getAnnotationsFallback( + ImmutableList.copyOf(field.getAnnotations()), annotationType); + } + }, + testClass); + } + + // Parameter is not available on old Android SDKs, and isn't desugared. That's why this method + // should only be called with a fallback. + @SuppressWarnings("AndroidJdkLibsChecker") + static GenericParameterContext create(Parameter parameter, Class<?> testClass) { + return new GenericParameterContext( + ImmutableList.copyOf(parameter.getAnnotations()), + /* getAnnotationsFunction= */ annotationType -> + ImmutableList.copyOf(parameter.getAnnotationsByType(annotationType)), + testClass); + } + + static GenericParameterContext createWithRepeatableAnnotationsFallback( + Annotation[] annotationsOnParameter, Class<?> testClass) { + return new GenericParameterContext( + ImmutableList.copyOf(annotationsOnParameter), + /* getAnnotationsFunction= */ annotationType -> + getAnnotationsFallback(ImmutableList.copyOf(annotationsOnParameter), annotationType), + testClass); + } + + static GenericParameterContext createWithoutParameterAnnotations(Class<?> testClass) { + return new GenericParameterContext( + /* annotationsOnParameter= */ ImmutableList.of(), + /* getAnnotationsFunction= */ annotationType -> + getAnnotationsFallback(ImmutableList.of(), annotationType), + testClass); + } + + /** + * Returns the only annotation with the given type on the field or parameter. + * + * @throws NoSuchElementException if this there is no annotation with the given type + * @throws IllegalArgumentException if there are multiple annotations with the given type + */ + @SuppressWarnings("unchecked") // Safe because of the filter operation + <A extends Annotation> A getAnnotation(Class<A> annotationType) { + return (A) + getOnlyElement( + FluentIterable.from(annotationsOnParameter) + .filter(annotation -> annotation.annotationType().equals(annotationType)) + .toList()); + } + + /** + * Returns the annotations with the given type on the field or parameter. + * + * <p>Returns an empty list if this there is no annotation with the given type. + */ + @SuppressWarnings("unchecked") // Safe because of the getAnnotationsFunction contract + <A extends Annotation> ImmutableList<A> getAnnotations(Class<A> annotationType) { + return (ImmutableList<A>) getAnnotationsFunction.apply(annotationType); + } + + /** The class that contains the test that is currently being run. */ + Class<?> testClass() { + return testClass; + } + + /** A list of all annotations on the field or parameter. */ + ImmutableList<Annotation> annotationsOnParameter() { + return annotationsOnParameter; + } + + @Override + public String toString() { + return String.format( + "context(annotationsOnParameter=[%s],testClass=%s)", + FluentIterable.from( + ImmutableList.sortedCopyOf( + Ordering.natural().onResultOf(Annotation::toString), annotationsOnParameter)) + .transform( + annotation -> String.format("@%s", annotation.annotationType().getSimpleName())) + .join(Joiner.on(',')), + testClass().getSimpleName()); + } + + private static ImmutableList<Annotation> getAnnotationsFallback( + ImmutableList<Annotation> annotationsOnParameter, + Class<? extends Annotation> annotationType) { + ImmutableList<Annotation> candidates = + FluentIterable.from(annotationsOnParameter) + .filter(annotation -> annotation.annotationType().equals(annotationType)) + .toList(); + if (candidates.isEmpty() && getContainerType(annotationType).isPresent()) { + ImmutableList<Annotation> containerAnnotations = + getAnnotationsFallback(annotationsOnParameter, getContainerType(annotationType).get()); + if (containerAnnotations.size() == 1) { + Annotation containerAnnotation = getOnlyElement(containerAnnotations); + try { + Method annotationValueMethod = + containerAnnotation.annotationType().getDeclaredMethod("value"); + annotationValueMethod.setAccessible(true); + return ImmutableList.copyOf( + (Annotation[]) + Proxy.getInvocationHandler(containerAnnotation) + .invoke(containerAnnotation, annotationValueMethod, null)); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + return ImmutableList.of(); + } else { + return candidates; + } + } + + private static Optional<Class<? extends Annotation>> getContainerType( + Class<? extends Annotation> annotationType) { + try { + Repeatable repeatable = annotationType.getAnnotation(Repeatable.class); + if (repeatable == null) { + return Optional.absent(); + } else { + return Optional.of(repeatable.value()); + } + } catch (NoClassDefFoundError ignored) { + // If @Repeatable does not exist, then there is no container type by definition + return Optional.absent(); + } + } +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java new file mode 100644 index 0000000..e09c1d9 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/ParameterValueParsing.java @@ -0,0 +1,303 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.collect.Lists; +import com.google.common.primitives.Primitives; +import com.google.common.primitives.UnsignedLong; +import com.google.common.reflect.TypeToken; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.lang.reflect.Array; +import java.lang.reflect.ParameterizedType; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import javax.annotation.Nullable; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; + +/** A helper class for parsing parameter values from strings. */ +final class ParameterValueParsing { + + @SuppressWarnings("unchecked") + static <E extends Enum<E>> Enum<?> parseEnum(String str, Class<?> enumType) { + return Enum.valueOf((Class<E>) enumType, str); + } + + static boolean isValidYamlString(String yamlString) { + try { + new Yaml(new SafeConstructor(new LoaderOptions())).load(yamlString); + return true; + } catch (RuntimeException e) { + return false; + } + } + + static Object parseYamlStringToJavaType(String yamlString, Class<?> javaType) { + return parseYamlObjectToJavaType(parseYamlStringToObject(yamlString), TypeToken.of(javaType)); + } + + static Object parseYamlStringToObject(String yamlString) { + return new Yaml(new SafeConstructor(new LoaderOptions())).load(yamlString); + } + + private static UnsignedLong parseYamlSignedLongToUnsignedLong(long number) { + checkState(number >= 0, "%s should be greater than or equal to zero", number); + return UnsignedLong.fromLongBits(number); + } + + @SuppressWarnings({"unchecked"}) + static Object parseYamlObjectToJavaType(Object parsedYaml, TypeToken<?> javaType) { + // Pass along null so we don't have to worry about it below + if (parsedYaml == null) { + return null; + } + + YamlValueTransformer yamlValueTransformer = + new YamlValueTransformer(parsedYaml, javaType.getRawType()); + + yamlValueTransformer + .ifJavaType(String.class) + .supportParsedType(String.class, self -> self) + // Also support other primitives because it's easy to accidentally write e.g. a number when + // a string was intended in YAML + .supportParsedType(Boolean.class, Object::toString) + .supportParsedType(Integer.class, Object::toString) + .supportParsedType(Long.class, Object::toString) + .supportParsedType(Double.class, Object::toString); + + yamlValueTransformer.ifJavaType(Boolean.class).supportParsedType(Boolean.class, self -> self); + + yamlValueTransformer.ifJavaType(Integer.class).supportParsedType(Integer.class, self -> self); + + yamlValueTransformer + .ifJavaType(Long.class) + .supportParsedType(Long.class, self -> self) + .supportParsedType(Integer.class, Integer::longValue); + + yamlValueTransformer + .ifJavaType(UnsignedLong.class) + .supportParsedType(Long.class, self -> parseYamlSignedLongToUnsignedLong(self.longValue())) + .supportParsedType( + Integer.class, self -> parseYamlSignedLongToUnsignedLong(self.longValue())) + // UnsignedLong::valueOf(BigInteger) will validate that BigInteger is in the valid range and + // throws otherwise. + .supportParsedType(BigInteger.class, UnsignedLong::valueOf); + + yamlValueTransformer + .ifJavaType(BigInteger.class) + .supportParsedType(Long.class, self -> BigInteger.valueOf(self.longValue())) + .supportParsedType(Integer.class, self -> BigInteger.valueOf(self.longValue())) + .supportParsedType(BigInteger.class, self -> self); + + yamlValueTransformer + .ifJavaType(Float.class) + .supportParsedType(Float.class, self -> self) + .supportParsedType(Double.class, Double::floatValue) + .supportParsedType(Integer.class, Integer::floatValue) + .supportParsedType(String.class, Float::valueOf); + + yamlValueTransformer + .ifJavaType(Double.class) + .supportParsedType(Double.class, self -> self) + .supportParsedType(Integer.class, Integer::doubleValue) + .supportParsedType(Long.class, Long::doubleValue) + .supportParsedType(String.class, Double::valueOf); + + yamlValueTransformer + .ifJavaType(Enum.class) + .supportParsedType( + String.class, str -> ParameterValueParsing.parseEnum(str, javaType.getRawType())); + + yamlValueTransformer + .ifJavaType(byte[].class) + .supportParsedType(byte[].class, self -> self) + // Uses String based charset because StandardCharsets was not introduced until later + // versions of Android + // See https://developer.android.com/reference/java/nio/charset/StandardCharsets. + .supportParsedType(String.class, s -> s.getBytes(Charset.forName("UTF-8"))); + + if (ByteStringReflection.MAYBE_BYTE_STRING_CLASS.isPresent()) { + yamlValueTransformer + .ifJavaType((Class<Object>) ByteStringReflection.MAYBE_BYTE_STRING_CLASS.get()) + .supportParsedType(String.class, ByteStringReflection::copyFromUtf8) + .supportParsedType(byte[].class, ByteStringReflection::copyFrom); + } + + // Added mainly for protocol buffer parsing + yamlValueTransformer + .ifJavaType(List.class) + .supportParsedType( + List.class, + list -> + Lists.transform( + list, + e -> + parseYamlObjectToJavaType( + e, getGenericParameterType(javaType, /* parameterIndex= */ 0)))); + yamlValueTransformer + .ifJavaType(Map.class) + .supportParsedType(Map.class, map -> parseYamlMapToJavaMap(map, javaType)); + + return yamlValueTransformer.transformedJavaValue(); + } + + private static Map<?, ?> parseYamlMapToJavaMap(Map<?, ?> map, TypeToken<?> javaType) { + Map<Object, Object> returnedMap = new LinkedHashMap<>(); + for (Entry<?, ?> entry : map.entrySet()) { + returnedMap.put( + parseYamlObjectToJavaType( + entry.getKey(), getGenericParameterType(javaType, /* parameterIndex= */ 0)), + parseYamlObjectToJavaType( + entry.getValue(), getGenericParameterType(javaType, /* parameterIndex= */ 1))); + } + return returnedMap; + } + + private static TypeToken<?> getGenericParameterType(TypeToken<?> typeToken, int parameterIndex) { + checkArgument( + typeToken.getType() instanceof ParameterizedType, + "Could not parse the generic parameter of type %s", + typeToken); + + ParameterizedType parameterizedType = (ParameterizedType) typeToken.getType(); + return TypeToken.of(parameterizedType.getActualTypeArguments()[parameterIndex]); + } + + private static final class YamlValueTransformer { + private final Object parsedYaml; + private final Class<?> javaType; + @Nullable private Object transformedJavaValue; + + YamlValueTransformer(Object parsedYaml, Class<?> javaType) { + this.parsedYaml = parsedYaml; + this.javaType = javaType; + } + + <JavaT> SupportedJavaType<JavaT> ifJavaType(Class<JavaT> supportedJavaType) { + return new SupportedJavaType<>(supportedJavaType); + } + + Object transformedJavaValue() { + checkArgument( + transformedJavaValue != null, + "Could not map YAML value %s (class = %s) to java class %s", + parsedYaml, + parsedYaml.getClass(), + javaType); + return transformedJavaValue; + } + + final class SupportedJavaType<JavaT> { + + private final Class<JavaT> supportedJavaType; + + private SupportedJavaType(Class<JavaT> supportedJavaType) { + this.supportedJavaType = supportedJavaType; + } + + @SuppressWarnings("unchecked") + @CanIgnoreReturnValue + <ParsedYamlT> SupportedJavaType<JavaT> supportParsedType( + Class<ParsedYamlT> parsedYamlType, Function<ParsedYamlT, JavaT> transformation) { + if (Primitives.wrap(supportedJavaType).isAssignableFrom(Primitives.wrap(javaType))) { + if (Primitives.wrap(parsedYamlType).isInstance(parsedYaml)) { + checkState( + transformedJavaValue == null, + "This case is already handled. This is a bug in" + + " testparameterinjector.TestParametersMethodProcessor."); + try { + transformedJavaValue = checkNotNull(transformation.apply((ParsedYamlT) parsedYaml)); + } catch (Exception e) { + throw new IllegalArgumentException( + String.format( + "Could not map YAML value %s (class = %s) to java class %s", + parsedYaml, parsedYaml.getClass(), javaType), + e); + } + } + } + + return this; + } + } + } + + static String formatTestNameString(Optional<String> parameterName, @Nullable Object value) { + Object unwrappedValue; + Optional<String> customName; + + if (value instanceof TestParameterValue) { + TestParameterValue tpValue = (TestParameterValue) value; + unwrappedValue = tpValue.getWrappedValue(); + customName = tpValue.getCustomName(); + } else { + unwrappedValue = value; + customName = Optional.absent(); + } + + String result = customName.or(() -> valueAsString(unwrappedValue)); + if (parameterName.isPresent() && !customName.isPresent()) { + if (unwrappedValue == null + || + // Primitives are often ambiguous + Primitives.unwrap(unwrappedValue.getClass()).isPrimitive() + // Ambiguous String cases + || unwrappedValue.equals("null") + || (unwrappedValue instanceof CharSequence + && CharMatcher.anyOf("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + .matchesNoneOf((CharSequence) unwrappedValue))) { + // Prefix the parameter value with its field name. This is to avoid test names + // such as myMethod_success[true,false,2]. Instead, it'll be + // myMethod_success[dryRun=true,experimentFlag=false,retries=2]. + result = String.format("%s=%s", parameterName.get(), valueAsString(unwrappedValue)); + } + } + return result.trim().replaceAll("\\s+", " "); + } + + private static String valueAsString(Object value) { + if (value != null && value.getClass().isArray()) { + StringBuilder resultBuider = new StringBuilder(); + resultBuider.append("["); + for (int i = 0; i < Array.getLength(value); i++) { + if (i > 0) { + resultBuider.append(", "); + } + resultBuider.append(Array.get(value, i)); + } + resultBuider.append("]"); + return resultBuider.toString(); + } else if (ByteStringReflection.isInstanceOfByteString(value)) { + return Arrays.toString(ByteStringReflection.byteStringToByteArray(value)); + } else { + return String.valueOf(value); + } + } + + private ParameterValueParsing() {} +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java new file mode 100644 index 0000000..b2a0ad8 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/PluggableTestRunner.java @@ -0,0 +1,441 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import com.google.common.base.Joiner; +import com.google.common.base.Throwables; +import com.google.common.collect.ComparisonChain; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Lists; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.Rule; +import org.junit.Test; +import org.junit.internal.runners.model.ReflectiveCallable; +import org.junit.internal.runners.statements.Fail; +import org.junit.internal.runners.statements.FailOnTimeout; +import org.junit.rules.MethodRule; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.BlockJUnit4ClassRunner; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.InitializationError; +import org.junit.runners.model.MemberValueConsumer; +import org.junit.runners.model.Statement; +import org.junit.runners.model.TestClass; + +/** + * Class to substitute JUnit4 runner in JUnit4 tests, adding additional functionality. + * + * <p>See {@link TestParameterInjector} for an example implementation. + */ +abstract class PluggableTestRunner extends BlockJUnit4ClassRunner { + + /** + * A {@link ThreadLocal} is used to handle cases where multiple tests are executing in the same + * java process in different threads. + * + * <p>A null value indicates that the TestInfo hasn't been set yet, which would typically happen + * if the test hasn't yet started, or the {@link PluggableTestRunner} is not the test runner. + */ + private static final ThreadLocal<TestInfo> currentTestInfo = new ThreadLocal<>(); + + private TestMethodProcessorList testMethodProcessors; + + protected PluggableTestRunner(Class<?> klass) throws InitializationError { + super(klass); + } + + /** Returns the TestMethodProcessorList to use. This is meant to be overridden by subclasses. */ + protected abstract TestMethodProcessorList createTestMethodProcessorList(); + + /** + * This method is run to perform optional additional operations on the test instance, right after + * it was created. + */ + protected void finalizeCreatedTestInstance(Object testInstance) { + // Do nothing by default + } + + /** + * If true, all test methods (across different TestMethodProcessors) will be sorted in a + * deterministic way. + * + * <p>Deterministic means that the order will not change, even when tests are added/removed or + * between releases. + * + * @deprecated Override {@link #sortTestMethods} with preferred sorting strategy. + */ + @Deprecated + protected boolean shouldSortTestMethodsDeterministically() { + return false; // Don't sort methods by default + } + + /** + * Sort test methods (across different TestMethodProcessors). + * + * <p>This should be deterministic. The order should not change, even when tests are added/removed + * or between releases. + */ + protected ImmutableList<FrameworkMethod> sortTestMethods(ImmutableList<FrameworkMethod> methods) { + if (!shouldSortTestMethodsDeterministically()) { + return methods; + } + return FluentIterable.from(methods) + .toSortedList( + (o1, o2) -> + ComparisonChain.start() + .compare(o1.getName().hashCode(), o2.getName().hashCode()) + .compare(o1.getName(), o2.getName()) + .result()); + } + + /** + * Returns classes used as annotations to indicate test methods. + * + * <p>Defaults to {@link Test}. + */ + protected ImmutableList<Class<? extends Annotation>> getSupportedTestAnnotations() { + return ImmutableList.of(Test.class); + } + + /** + * {@link TestRule}s that will be executed before the ones defined in the test class. This is + * meant to be overridden by subclasses. + */ + protected List<TestRule> getExtraTestRules() { + return ImmutableList.of(); + } + + @Override + protected final ImmutableList<FrameworkMethod> computeTestMethods() { + return sortTestMethods( + FluentIterable.from(getSupportedTestAnnotations()) + .transformAndConcat(annotation -> getTestClass().getAnnotatedMethods(annotation)) + .transformAndConcat(this::processMethod) + .toList()); + } + + /** Implementation of a JUnit FrameworkMethod where the name and annotation list is overridden. */ + private static class OverriddenFrameworkMethod extends FrameworkMethod { + + private final TestInfo testInfo; + + public OverriddenFrameworkMethod(Method method, TestInfo testInfo) { + super(method); + this.testInfo = testInfo; + } + + public TestInfo getTestInfo() { + return testInfo; + } + + @Override + public String getName() { + return testInfo.getName(); + } + + @Override + public Annotation[] getAnnotations() { + List<Annotation> annotations = testInfo.getAnnotations(); + return annotations.toArray(new Annotation[0]); + } + + @Override + public <T extends Annotation> T getAnnotation(final Class<T> annotationClass) { + return testInfo.getAnnotation(annotationClass); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof PluggableTestRunner.OverriddenFrameworkMethod)) { + return false; + } + + OverriddenFrameworkMethod other = (OverriddenFrameworkMethod) obj; + return super.equals(other) && other.testInfo.equals(testInfo); + } + + @Override + public int hashCode() { + return super.hashCode() * 37 + testInfo.hashCode(); + } + } + + private ImmutableList<FrameworkMethod> processMethod(FrameworkMethod initialMethod) { + return FluentIterable.from( + getTestMethodProcessors() + .calculateTestInfos(initialMethod.getMethod(), getTestClass().getJavaClass())) + .transform( + testInfo -> + (FrameworkMethod) new OverriddenFrameworkMethod(testInfo.getMethod(), testInfo)) + .toList(); + } + + // Note: This is a copy of the parent implementation, except that instead of calling + // #createTest(), this method calls #createTestForMethod(method). + @Override + protected final Statement methodBlock(final FrameworkMethod method) { + Object testObject; + try { + testObject = + new ReflectiveCallable() { + @Override + protected Object runReflectiveCall() throws Throwable { + return createTestForMethod(method); + } + }.run(); + } catch (Throwable e) { + return new Fail(e); + } + + Statement statement = methodInvoker(method, testObject); + statement = possiblyExpectingExceptions(method, testObject, statement); + statement = withPotentialTimeoutInternal(method, testObject, statement); + statement = withBefores(method, testObject, statement); + statement = withAfters(method, testObject, statement); + statement = withRules(method, testObject, statement); + statement = withInterruptIsolation(statement); + return statement; + } + + // Note: This does the same as BlockJUnit4ClassRunner.withPotentialTimeout(), which is deprecated + // and will soon be private. + private Statement withPotentialTimeoutInternal( + FrameworkMethod method, Object test, Statement next) { + Test testAnnotation = method.getAnnotation(Test.class); + if (testAnnotation == null) { + return next; + } else if (testAnnotation.timeout() <= 0) { + return next; + } else { + return FailOnTimeout.builder() + .withTimeout(testAnnotation.timeout(), TimeUnit.MILLISECONDS) + .build(next); + } + } + + @Override + protected final Statement methodInvoker(FrameworkMethod frameworkMethod, Object testObject) { + TestInfo testInfo = ((OverriddenFrameworkMethod) frameworkMethod).getTestInfo(); + + if (testInfo.getMethod().getParameterTypes().length == 0) { + return super.methodInvoker(frameworkMethod, testObject); + } else { + List<Object> parameters = getTestMethodProcessors().getTestMethodParameters(testInfo); + return new Statement() { + @Override + public void evaluate() throws Throwable { + frameworkMethod.invokeExplosively(testObject, parameters.toArray()); + } + }; + } + } + + /** Modifies the statement with each {@link MethodRule} and {@link TestRule} */ + private Statement withRules(FrameworkMethod method, Object target, Statement statement) { + Description testDescription = describeChild(method); + TestClass testClass = getTestClass(); + + LinkedListMultimap<Integer, Object> orderToRulesMultimap = LinkedListMultimap.create(); + MemberValueConsumer<Object> collector = + (frameworkMember, rule) -> { + Rule ruleAnnotation = frameworkMember.getAnnotation(Rule.class); + int order = ruleAnnotation == null ? Rule.DEFAULT_ORDER : ruleAnnotation.order(); + if (orderToRulesMultimap.containsValue(rule) + && rule instanceof MethodRule + && rule instanceof TestRule) { + // This rule was already added because it is both a MethodRule and a TestRule. + // For legacy reasons, we need to put the new rule at the end of the list. + orderToRulesMultimap.remove(order, rule); + } + orderToRulesMultimap.put(order, rule); + }; + + testClass.collectAnnotatedMethodValues(target, Rule.class, MethodRule.class, collector::accept); + testClass.collectAnnotatedFieldValues(target, Rule.class, MethodRule.class, collector::accept); + testClass.collectAnnotatedMethodValues(target, Rule.class, TestRule.class, collector::accept); + testClass.collectAnnotatedFieldValues(target, Rule.class, TestRule.class, collector::accept); + + ArrayList<Integer> keys = new ArrayList<>(orderToRulesMultimap.keySet()); + Collections.sort(keys); + ImmutableList<Object> orderedRules = + FluentIterable.from(keys) + .transformAndConcat( + // Execute the rules in the reverse order of when the fields occurred. This may look + // counter-intuitive, but that is what the default JUnit4 runner does, and there is + // no reason to deviate from that here. + key -> Lists.reverse(orderToRulesMultimap.get(key))) + .toList(); + + // Note: The perceived order* is the reverse of the order in which the code below applies the + // rules to the statements because each subsequent rule wraps the previous statement. + // + // [*] The rule implementation can add its logic both before or after the base statement, so the + // order depends on the rule implementation. If all rules put their logic before the base + // statement, the order matches that of `orderedRules`. + + for (Object rule : Lists.reverse(orderedRules)) { + if (rule instanceof TestRule) { + statement = ((TestRule) rule).apply(statement, testDescription); + } else if (rule instanceof MethodRule) { + statement = ((MethodRule) rule).apply(statement, method, target); + } else { + throw new AssertionError(rule); + } + } + + // Apply extra rules + for (TestRule testRule : getExtraTestRules()) { + statement = testRule.apply(statement, testDescription); + } + statement = new ContextMethodRule().apply(statement, method, target); + + return statement; + } + + private Object createTestForMethod(FrameworkMethod method) throws Exception { + TestInfo testInfo = ((OverriddenFrameworkMethod) method).getTestInfo(); + Constructor<?> constructor = + TestParameterInjectorUtils.getOnlyConstructor(getTestClass().getJavaClass()); + + // Construct a test instance + Object testInstance; + if (constructor.getParameterTypes().length == 0) { + testInstance = createTest(); + } else { + List<Object> constructorParameters = + getTestMethodProcessors().getConstructorParameters(constructor, testInfo); + try { + testInstance = constructor.newInstance(constructorParameters.toArray()); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + // Run all post processors on the newly created instance + getTestMethodProcessors().postProcessTestInstance(testInstance, testInfo); + + finalizeCreatedTestInstance(testInstance); + + return testInstance; + } + + @Override + protected final void validateZeroArgConstructor(List<Throwable> errorsReturned) { + ExecutableValidationResult validationResult = + getTestMethodProcessors() + .validateConstructor( + TestParameterInjectorUtils.getOnlyConstructor(getTestClass().getJavaClass())); + + if (validationResult.wasValidated()) { + errorsReturned.addAll(validationResult.validationErrors()); + } else { + super.validateZeroArgConstructor(errorsReturned); + } + } + + @Override + protected final void validateTestMethods(List<Throwable> errorsReturned) { + List<FrameworkMethod> testMethods = + FluentIterable.from(getSupportedTestAnnotations()) + .transformAndConcat(annotation -> getTestClass().getAnnotatedMethods(annotation)) + .toList(); + for (FrameworkMethod testMethod : testMethods) { + ExecutableValidationResult validationResult = + getTestMethodProcessors() + .validateTestMethod(testMethod.getMethod(), getTestClass().getJavaClass()); + + if (Modifier.isStatic(testMethod.getMethod().getModifiers())) { + errorsReturned.add( + new Exception(String.format("Method %s() should not be static", testMethod.getName()))); + } + if (!Modifier.isPublic(testMethod.getMethod().getModifiers())) { + errorsReturned.add( + new Exception(String.format("Method %s() should be public", testMethod.getName()))); + } + + if (validationResult.wasValidated()) { + errorsReturned.addAll(validationResult.validationErrors()); + } else { + testMethod.validatePublicVoidNoArg(/* isStatic= */ false, errorsReturned); + } + } + } + + // Fix for ParentRunner bug: + // Overriding this method because a superclass (ParentRunner) is calling this in its constructor + // and then throwing an InitializationError that doesn't have any of the causes in the exception + // message. + @Override + protected final void collectInitializationErrors(List<Throwable> errors) { + super.collectInitializationErrors(errors); + if (!errors.isEmpty()) { + throw new RuntimeException( + String.format( + "Found %s issues while initializing the test runner:\n\n - %s\n\n\n", + errors.size(), + FluentIterable.from(errors) + .transform(Throwables::getStackTraceAsString) + .join(Joiner.on("\n\n\n - ")))); + } + } + + // Override this test as final because it is not (always) invoked + @Override + protected final Object createTest() throws Exception { + return super.createTest(); + } + + // Override this test as final because it is not (always) invoked + @Override + protected final void validatePublicVoidNoArgMethods( + Class<? extends Annotation> annotation, boolean isStatic, List<Throwable> errors) { + super.validatePublicVoidNoArgMethods(annotation, isStatic, errors); + } + + private synchronized TestMethodProcessorList getTestMethodProcessors() { + if (testMethodProcessors == null) { + testMethodProcessors = createTestMethodProcessorList(); + } + return testMethodProcessors; + } + + /** {@link MethodRule} that sets up the Context for each test. */ + private static class ContextMethodRule implements MethodRule { + @Override + public Statement apply(Statement statement, FrameworkMethod method, Object o) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + currentTestInfo.set(((OverriddenFrameworkMethod) method).getTestInfo()); + try { + statement.evaluate(); + } finally { + currentTestInfo.set(null); + } + } + }; + } + } +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java new file mode 100644 index 0000000..965d41a --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestInfo.java @@ -0,0 +1,325 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Joiner; +import com.google.common.collect.ContiguousSet; +import com.google.common.collect.DiscreteDomain; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; +import com.google.common.collect.Range; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import javax.annotation.Nullable; + +/** A POJO containing information about a test (name and anotations). */ +@AutoValue +abstract class TestInfo { + + /** + * The maximum amount of characters that {@link #getName()} can have. + * + * <p>See b/168325767 for the reason behind this. tl;dr the name is put into a Unix file with max + * 255 characters. The surrounding constant characters take up 31 characters. The max is reduced + * by an additional 24 characters to account for future changes. + */ + static final int MAX_TEST_NAME_LENGTH = 200; + + public abstract Method getMethod(); + + /** + * The test class that is being run. + * + * <p>Note that this is not always the same as the class that declares {@link #getMethod()} + * because test methods can be inherited. + */ + public abstract Class<?> getTestClass(); + + public final String getName() { + if (getParameters().isEmpty()) { + return getMethod().getName(); + } else { + return String.format( + "%s[%s]", + getMethod().getName(), + FluentIterable.from(getParameters()) + .transform(TestInfoParameter::getValueInTestName) + .join(Joiner.on(","))); + } + } + + abstract ImmutableList<TestInfoParameter> getParameters(); + + public abstract ImmutableList<Annotation> getAnnotations(); + + @Nullable + public final <T extends Annotation> T getAnnotation(Class<T> annotationClass) { + for (Annotation annotation : getAnnotations()) { + if (annotationClass.isInstance(annotation)) { + return annotationClass.cast(annotation); + } + } + return null; + } + + final TestInfo withExtraParameters(List<TestInfoParameter> parameters) { + return new AutoValue_TestInfo( + getMethod(), + getTestClass(), + ImmutableList.<TestInfoParameter>builder() + .addAll(this.getParameters()) + .addAll(parameters) + .build(), + getAnnotations()); + } + + final TestInfo withExtraAnnotation(Annotation annotation) { + ImmutableList<Annotation> newAnnotations = + ImmutableList.<Annotation>builder().addAll(this.getAnnotations()).add(annotation).build(); + return new AutoValue_TestInfo(getMethod(), getTestClass(), getParameters(), newAnnotations); + } + + /** + * Returns a new TestInfo instance with updated parameter names. + * + * @param parameterWithIndexToNewName A function of the parameter and its index in the {@link + * #getParameters()} list to the new name. + */ + private TestInfo withUpdatedParameterNames( + Java8BiFunction<TestInfoParameter, Integer, String> parameterWithIndexToNewName) { + return new AutoValue_TestInfo( + getMethod(), + getTestClass(), + FluentIterable.from( + ContiguousSet.create( + Range.closedOpen(0, getParameters().size()), DiscreteDomain.integers())) + .transform( + parameterIndex -> { + TestInfoParameter parameter = getParameters().get(parameterIndex); + return parameter.withValueInTestName( + parameterWithIndexToNewName.apply(parameter, parameterIndex)); + }) + .toList(), + getAnnotations()); + } + + public static TestInfo legacyCreate( + Method method, Class<?> testClass, String name, List<Annotation> annotations) { + return new AutoValue_TestInfo( + method, testClass, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations)); + } + + static TestInfo createWithoutParameters( + Method method, Class<?> testClass, List<Annotation> annotations) { + return new AutoValue_TestInfo( + method, testClass, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations)); + } + + static ImmutableList<TestInfo> shortenNamesIfNecessary(List<TestInfo> testInfos) { + if (FluentIterable.from(testInfos) + .anyMatch(info -> info.getName().length() > MAX_TEST_NAME_LENGTH)) { + int numberOfParameters = testInfos.get(0).getParameters().size(); + + if (numberOfParameters == 0) { + return ImmutableList.copyOf(testInfos); + } else { + Set<Integer> parameterIndicesThatNeedUpdate = + FluentIterable.from( + ContiguousSet.create( + Range.closedOpen(0, numberOfParameters), DiscreteDomain.integers())) + .filter( + parameterIndex -> + FluentIterable.from(testInfos) + .anyMatch( + info -> + info.getParameters() + .get(parameterIndex) + .getValueInTestName() + .length() + > getMaxCharactersPerParameter(info, numberOfParameters))) + .toSet(); + + return FluentIterable.from(testInfos) + .transform( + info -> + info.withUpdatedParameterNames( + (parameter, parameterIndex) -> + parameterIndicesThatNeedUpdate.contains(parameterIndex) + ? getShortenedName( + parameter, + getMaxCharactersPerParameter(info, numberOfParameters)) + : info.getParameters().get(parameterIndex).getValueInTestName())) + .toList(); + } + } else { + return ImmutableList.copyOf(testInfos); + } + } + + private static int getMaxCharactersPerParameter(TestInfo testInfo, int numberOfParameters) { + int maxLengthOfAllParameters = + // Subtract 2 characters for square brackets + MAX_TEST_NAME_LENGTH - testInfo.getMethod().getName().length() - 2; + + // Subtract 4 characters to leave place for joining commas and the parameter index. + return maxLengthOfAllParameters / numberOfParameters - 4; + } + + static ImmutableList<TestInfo> deduplicateTestNames(List<TestInfo> testInfos) { + long uniqueTestNameCount = + FluentIterable.from(testInfos).transform(TestInfo::getName).toSet().size(); + if (testInfos.size() == uniqueTestNameCount) { + // Return early if there are no duplicates + return ImmutableList.copyOf(testInfos); + } else { + return deduplicateWithNumberPrefixes(maybeAddTypesIfDuplicate(testInfos)); + } + } + + private static String getShortenedName( + TestInfoParameter parameter, int maxCharactersPerParameter) { + if (maxCharactersPerParameter < 4) { + // Not enough characters for "..." suffix + return String.valueOf(parameter.getIndexInValueSource() + 1); + } else { + String shortenedName = + parameter.getValueInTestName().length() > maxCharactersPerParameter + ? parameter.getValueInTestName().substring(0, maxCharactersPerParameter - 3) + "..." + : parameter.getValueInTestName(); + return String.format("%s.%s", parameter.getIndexInValueSource() + 1, shortenedName); + } + } + + private static ImmutableList<TestInfo> maybeAddTypesIfDuplicate(List<TestInfo> testInfos) { + Multimap<String, TestInfo> testNameToInfo = + MultimapBuilder.linkedHashKeys().arrayListValues().build(); + for (TestInfo testInfo : testInfos) { + testNameToInfo.put(testInfo.getName(), testInfo); + } + + return FluentIterable.from(testNameToInfo.keySet()) + .transformAndConcat( + testName -> { + Collection<TestInfo> matchedInfos = testNameToInfo.get(testName); + if (matchedInfos.size() == 1) { + // There was only one method with this name, so no deduplication is necessary + return matchedInfos; + } else { + // Found tests with duplicate test names + int numParameters = matchedInfos.iterator().next().getParameters().size(); + Set<Integer> indicesThatShouldGetSuffix = + // Find parameter indices for which a suffix would allow the reader to + // differentiate + FluentIterable.from( + ContiguousSet.create( + Range.closedOpen(0, numParameters), DiscreteDomain.integers())) + .filter( + parameterIndex -> + FluentIterable.from(matchedInfos) + .transform( + info -> + getTypeSuffix( + info.getParameters() + .get(parameterIndex) + .getValue())) + .toSet() + .size() + > 1) + .toSet(); + + return FluentIterable.from(matchedInfos) + .transform( + testInfo -> + testInfo.withUpdatedParameterNames( + (parameter, parameterIndex) -> + indicesThatShouldGetSuffix.contains(parameterIndex) + ? parameter.getValueInTestName() + + getTypeSuffix(parameter.getValue()) + : parameter.getValueInTestName())); + } + }) + .toList(); + } + + private static String getTypeSuffix(@Nullable Object value) { + if (value == null) { + return " (null reference)"; + } else { + return String.format(" (%s)", value.getClass().getSimpleName()); + } + } + + private static ImmutableList<TestInfo> deduplicateWithNumberPrefixes( + ImmutableList<TestInfo> testInfos) { + long uniqueTestNameCount = + FluentIterable.from(testInfos).transform(TestInfo::getName).toSet().size(); + if (testInfos.size() == uniqueTestNameCount) { + return ImmutableList.copyOf(testInfos); + } else { + // There are still duplicates, even after adding type suffixes. As a last resort: add a + // counter to all parameters to guarantee that each case is unique. + return FluentIterable.from(testInfos) + .transform( + testInfo -> + testInfo.withUpdatedParameterNames( + (parameter, parameterIndex) -> + String.format( + "%s.%s", + parameter.getIndexInValueSource() + 1, + parameter.getValueInTestName()))) + .toList(); + } + } + + @AutoValue + abstract static class TestInfoParameter { + + abstract String getValueInTestName(); + + @Nullable + abstract Object getValue(); + + /** + * The index of this parameter value in the list of all values provided by the provider that + * returned this value. + */ + abstract int getIndexInValueSource(); + + final TestInfoParameter withValueInTestName(String newValueInTestName) { + return create(newValueInTestName, getValue(), getIndexInValueSource()); + } + + static TestInfoParameter create( + String valueInTestName, @Nullable Object value, int indexInValueSource) { + checkArgument(indexInValueSource >= 0); + return new AutoValue_TestInfo_TestInfoParameter( + checkNotNull(valueInTestName), value, indexInValueSource); + } + } + + /** Copy of Java8's java.util.BiFunction which is not available in older versions of the JDK */ + interface Java8BiFunction<I, J, K> { + K apply(I a, J b); + } +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java new file mode 100644 index 0000000..60a01bc --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessor.java @@ -0,0 +1,66 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import com.google.common.base.Optional; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; + +/** + * Interface to change the list of methods used in a test. + * + * <p>Note: Implementations of this interface are expected to be immutable, i.e. they no longer + * change after construction. + */ +interface TestMethodProcessor { + + /** Allows to transform the test information (name and annotations). */ + List<TestInfo> calculateTestInfos(TestInfo originalTest); + + /** + * If this processor can handle the given constructor, returns the parameters with which it should + * be invoked. + * + * <p>This method is never called for a parameterless constructor. + */ + Optional<List<Object>> maybeGetConstructorParameters( + Constructor<?> constructor, TestInfo testInfo); + + /** + * If this processor can handle the given test, returns the parameters with which {@code + * testInfo.getMethod()} should be invoked. + * + * <p>This method is never called for a parameterless {@code testInfo.getMethod()}. + */ + Optional<List<Object>> maybeGetTestMethodParameters(TestInfo testInfo); + + /** + * Optionally process the test instance right after construction to ready it for the given test + * instance. + */ + void postProcessTestInstance(Object testInstance, TestInfo testInfo); + + /** Optionally validates the given constructor. */ + ExecutableValidationResult validateConstructor(Constructor<?> constructor); + + /** + * Optionally validates the given method. + * + * <p>Note that the given method is not necessarily declared in the given class because test + * methods can be inherited. + */ + ExecutableValidationResult validateTestMethod(Method testMethod, Class<?> testClass); +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java new file mode 100644 index 0000000..2caf531 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestMethodProcessorList.java @@ -0,0 +1,146 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import com.google.common.base.Optional; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +/** + * Combined version of all {@link TestMethodProcessor} implementations that this package supports. + */ +final class TestMethodProcessorList { + + private final ImmutableList<TestMethodProcessor> testMethodProcessors; + + private TestMethodProcessorList(ImmutableList<TestMethodProcessor> testMethodProcessors) { + this.testMethodProcessors = testMethodProcessors; + } + + /** + * Returns a TestMethodProcessorList that supports all features that this package supports, except + * the following legacy features: + * + * <ul> + * <li>No support for {@link org.junit.runners.Parameterized} + * <li>No support for class and method-level parameters, except for @TestParameters + * </ul> + */ + public static TestMethodProcessorList createNewParameterizedProcessors() { + return new TestMethodProcessorList( + ImmutableList.of( + new TestParametersMethodProcessor(), + TestParameterAnnotationMethodProcessor.onlyForFieldsAndParameters())); + } + + static TestMethodProcessorList empty() { + return new TestMethodProcessorList(ImmutableList.of()); + } + + /** + * Calculates the TestInfo instances for the given test method. Each TestInfo corresponds to a + * single test. + * + * <p>The returned list always contains at least one element. If there is no parameterization, + * this would be the TestInfo for running the test method without parameters. + */ + public List<TestInfo> calculateTestInfos(Method testMethod, Class<?> testClass) { + List<TestInfo> testInfos = + ImmutableList.of( + TestInfo.createWithoutParameters( + testMethod, testClass, ImmutableList.copyOf(testMethod.getAnnotations()))); + + for (final TestMethodProcessor testMethodProcessor : testMethodProcessors) { + List<TestInfo> list = new ArrayList<>(); + for (TestInfo lastTestInfo : testInfos) { + list.addAll(testMethodProcessor.calculateTestInfos(lastTestInfo)); + } + testInfos = list; + } + + testInfos = TestInfo.deduplicateTestNames(TestInfo.shortenNamesIfNecessary(testInfos)); + + return testInfos; + } + + /** + * Returns the parameters with which it should be invoked. + * + * <p>This method is never called for a parameterless constructor. + */ + public List<Object> getConstructorParameters(Constructor<?> constructor, TestInfo testInfo) { + return FluentIterable.from(testMethodProcessors) + .transform(processor -> processor.maybeGetConstructorParameters(constructor, testInfo)) + .filter(Optional::isPresent) + .transform(Optional::get) + .first() + .or( + () -> { + throw new IllegalStateException( + String.format( + "Could not generate parameter values for %s. Did you forget an annotation?", + constructor)); + }); + } + + /** + * Returns the parameters with which {@code testInfo.getMethod()} should be invoked. + * + * <p>This method is never called for a parameterless {@code testInfo.getMethod()}. + */ + public List<Object> getTestMethodParameters(TestInfo testInfo) { + return FluentIterable.from(testMethodProcessors) + .transform(processor -> processor.maybeGetTestMethodParameters(testInfo)) + .filter(Optional::isPresent) + .transform(Optional::get) + .first() + .or( + () -> { + throw new IllegalStateException( + String.format( + "Could not generate parameter values for %s. Did you forget an annotation?", + testInfo.getMethod())); + }); + } + + /** + * Optionally process the test instance right after construction to ready it for the given test. + */ + public void postProcessTestInstance(Object testInstance, TestInfo testInfo) { + for (TestMethodProcessor testMethodProcessor : testMethodProcessors) { + testMethodProcessor.postProcessTestInstance(testInstance, testInfo); + } + } + + /** Optionally validates the given constructor. */ + public ExecutableValidationResult validateConstructor(Constructor<?> constructor) { + return FluentIterable.from(testMethodProcessors) + .transform(processor -> processor.validateConstructor(constructor)) + .firstMatch(ExecutableValidationResult::wasValidated) + .or(ExecutableValidationResult.notValidated()); + } + + /** Optionally validates the given method. */ + public ExecutableValidationResult validateTestMethod(Method testMethod, Class<?> testClass) { + return FluentIterable.from(testMethodProcessors) + .transform(processor -> processor.validateTestMethod(testMethod, testClass)) + .firstMatch(ExecutableValidationResult::wasValidated) + .or(ExecutableValidationResult.notValidated()); + } +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java new file mode 100644 index 0000000..d193ec6 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameter.java @@ -0,0 +1,260 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.base.Preconditions.checkState; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.base.Optional; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.Lists; +import com.google.common.primitives.Primitives; +import com.google.testing.junit.testparameterinjector.TestParameter.InternalImplementationOfThisParameter; +import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider.Context; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Test parameter annotation that defines the values that a single parameter can have. + * + * <p>For enums and booleans, the values can be automatically derived as all possible values: + * + * <pre> + * {@literal @}Test + * public void test1(@TestParameter MyEnum myEnum, @TestParameter boolean myBoolean) { + * // ... will run for [(A,false), (A,true), (B,false), (B,true), (C,false), (C,true)] + * } + * + * enum MyEnum { A, B, C } + * </pre> + * + * <p>The values can be explicitly defined as a parsed string: + * + * <pre> + * public void test1( + * {@literal @}TestParameter({"{name: Hermione, age: 18}", "{name: Dumbledore, age: 115}"}) + * UpdateCharacterRequest request, + * {@literal @}TestParameter({"1", "4"}) int bookNumber) { + * // ... will run for [(Hermione,1), (Hermione,4), (Dumbledore,1), (Dumbledore,4)] + * } + * </pre> + * + * <p>For more flexibility, see {{@link #valuesProvider()}}. If you don't want to test all possible + * combinations but instead want to specify sets of parameters explicitly, use @{@link + * TestParameters}. + */ +@Retention(RUNTIME) +@Target({FIELD, PARAMETER}) +@TestParameterAnnotation(valueProvider = InternalImplementationOfThisParameter.class) +public @interface TestParameter { + + /** + * Array of stringified values for the annotated type. + * + * <p>Types that are supported: + * + * <ul> + * <li>String: No parsing happens + * <li>boolean: Specified as YAML boolean + * <li>long and int: Specified as YAML integer + * <li>float and double: Specified as YAML floating point or integer + * <li>Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()} + * <li>Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML bytes + * (example: "!!binary 'ZGF0YQ=='") + * </ul> + * + * <p>For dynamic sets of parameters or parameter types that are not supported here, use {@link + * #valuesProvider()} and leave this field empty. + * + * <p>For examples, see {@link TestParameter}. + */ + String[] value() default {}; + + /** + * Sets a provider that will return a list of parameter values. + * + * <p>If this field is set, {@link #value()} must be empty and vice versa. + * + * <p><b>Example</b> + * + * <pre> + * import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider; + * + * {@literal @}Test + * public void matchesAllOf_throwsOnNull( + * {@literal @}TestParameter(valuesProvider = CharMatcherProvider.class) + * CharMatcher charMatcher) { + * assertThrows(NullPointerException.class, () -> charMatcher.matchesAllOf(null)); + * } + * + * private static final class CharMatcherProvider extends TestParameterValuesProvider { + * {@literal @}Override + * public {@literal List<CharMatcher>} provideValues(Context context) { + * return ImmutableList.of(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace()); + * } + * } + * </pre> + */ + Class<? extends TestParameterValuesProvider> valuesProvider() default + DefaultTestParameterValuesProvider.class; + + /** + * Interface for custom providers of test parameter values. + * + * @deprecated Use {@link + * com.google.testing.junit.testparameterinjector.TestParameterValuesProvider} instead. The + * replacement implements this same interface, but with an additional Context parameter. + */ + @Deprecated + interface TestParameterValuesProvider { + List<?> provideValues(); + + /** + * Wraps the given value in an object that allows you to give the parameter value a different + * name. The TestParameterInjector framework will recognize the returned {@link + * TestParameterValue} instances and unwrap them at injection time. + * + * <p>Usage: {@code value(file.content).withName(file.name)}. + * + * <p>Do not override this method. + */ + default TestParameterValue value(@javax.annotation.Nullable Object wrappedValue) { + return TestParameterValue.wrap(wrappedValue); + } + } + + /** Default {@link TestParameterValuesProvider} implementation that does nothing. */ + class DefaultTestParameterValuesProvider implements TestParameterValuesProvider { + @Override + public List<Object> provideValues() { + return com.google.common.collect.ImmutableList.of(); + } + } + + /** Implementation of this parameter annotation. */ + final class InternalImplementationOfThisParameter implements TestParameterValueProvider { + @Override + public List<Object> provideValues( + Annotation uncastAnnotation, + Optional<Class<?>> maybeParameterClass, + GenericParameterContext context) { + TestParameter annotation = (TestParameter) uncastAnnotation; + Class<?> parameterClass = getValueType(annotation.annotationType(), maybeParameterClass); + + boolean valueIsSet = annotation.value().length > 0; + boolean valuesProviderIsSet = + !annotation.valuesProvider().equals(DefaultTestParameterValuesProvider.class); + checkState( + !(valueIsSet && valuesProviderIsSet), + "It is not allowed to specify both value and valuesProvider on annotation %s", + annotation); + + if (valueIsSet) { + return Lists.newArrayList( + FluentIterable.from(annotation.value()) + .transform(v -> parseStringValue(v, parameterClass)) + .toArray(Object.class)); + } else if (valuesProviderIsSet) { + return getValuesFromProvider(annotation.valuesProvider(), new Context(context)); + } else { + if (Enum.class.isAssignableFrom(parameterClass)) { + return Arrays.asList((Object[]) parameterClass.asSubclass(Enum.class).getEnumConstants()); + } else if (Primitives.wrap(parameterClass).equals(Boolean.class)) { + return Arrays.asList(false, true); + } else { + throw new IllegalStateException( + String.format( + "A @TestParameter without values can only be placed at an enum or a boolean, but" + + " was placed by a %s", + parameterClass)); + } + } + } + + @Override + public Class<?> getValueType( + Class<? extends Annotation> annotationType, Optional<Class<?>> parameterClass) { + if (parameterClass.isPresent()) { + return parameterClass.get(); + } + throw new AssertionError( + String.format( + "An empty parameter class should not be possible since" + + " @TestParameter can only target FIELD or PARAMETER, both" + + " of which are supported for annotation %s.", + annotationType)); + } + + private static Object parseStringValue(String value, Class<?> parameterClass) { + if (parameterClass.equals(String.class)) { + return value.equals("null") ? null : value; + } else if (Enum.class.isAssignableFrom(parameterClass)) { + return value.equals("null") ? null : ParameterValueParsing.parseEnum(value, parameterClass); + } else { + return ParameterValueParsing.parseYamlStringToJavaType(value, parameterClass); + } + } + + private static List<Object> getValuesFromProvider( + Class<? extends TestParameterValuesProvider> valuesProvider, Context context) { + try { + Constructor<? extends TestParameterValuesProvider> constructor = + valuesProvider.getDeclaredConstructor(); + constructor.setAccessible(true); + TestParameterValuesProvider instance = constructor.newInstance(); + if (instance + instanceof com.google.testing.junit.testparameterinjector.TestParameterValuesProvider) { + return new ArrayList<>( + ((com.google.testing.junit.testparameterinjector.TestParameterValuesProvider) + instance) + .provideValues(context)); + } else { + return new ArrayList<>(instance.provideValues()); + } + } catch (NoSuchMethodException e) { + if (!Modifier.isStatic(valuesProvider.getModifiers()) && valuesProvider.isMemberClass()) { + throw new IllegalStateException( + String.format( + "Could not find a no-arg constructor for %s, probably because it is a not-static" + + " inner class. You can fix this by making %s static.", + valuesProvider.getSimpleName(), valuesProvider.getSimpleName()), + e); + } else { + throw new IllegalStateException( + String.format( + "Could not find a no-arg constructor for %s.", valuesProvider.getSimpleName()), + e); + } + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } catch (Exception e) { + // Catch any unchecked exception that may come from `provideValues(Context)` + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } else { + throw new IllegalStateException(e); + } + } + } + } +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java new file mode 100644 index 0000000..deb4cd5 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotation.java @@ -0,0 +1,244 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Verify.verify; +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Primitives; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; + +/** + * Annotation to define a test annotation used to have parameterized methods, in either a + * parameterized or non parameterized test. + * + * <p>Parameterized tests enabled by defining a annotation (see {@link TestParameter} as an example) + * for the type of the parameter, defining a member variable annotated with this annotation, and + * specifying the parameter with the same annotation for each test, or for the whole class, for + * example: + * + * <pre>{@code + * @RunWith(TestParameterInjector.class) + * public class ColorTest { + * @Retention(RUNTIME) + * @Target({TYPE, METHOD, FIELD}) + * @TestParameterAnnotation + * public @interface ColorParameter { + * Color[] value() default {}; + * } + * + * @ColorParameter({BLUE, WHITE, RED}) private Color color; + * + * @Test + * public void test() { + * assertThat(paint(color)).isSuccessful(); + * } + * } + * }</pre> + * + * <p>An alternative is to use a method parameter for injection: + * + * <pre>{@code + * @RunWith(TestParameterInjector.class) + * public class ColorTest { + * @Retention(RUNTIME) + * @Target({TYPE, METHOD, FIELD}) + * @TestParameterAnnotation + * public @interface ColorParameter { + * Color[] value() default {}; + * } + * + * @Test + * @ColorParameter({BLUE, WHITE, RED}) + * public void test(Color color) { + * assertThat(paint(color)).isSuccessful(); + * } + * } + * }</pre> + * + * <p>Yet another alternative is to use a method parameter for injection, but with the annotation + * specified on the parameter itself, which helps when multiple arguments share the + * same @TestParameterAnnotation annotation. + * + * <pre>{@code + * @RunWith(TestParameterInjector.class) + * public class ColorTest { + * @Retention(RUNTIME) + * @Target({TYPE, METHOD, FIELD}) + * @TestParameterAnnotation + * public @interface ColorParameter { + * Color[] value() default {}; + * } + * + * @Test + * public void test(@ColorParameter({BLUE, WHITE}) Color color1, + * @ColorParameter({WHITE, RED}) Color color2) { + * assertThat(paint(color1. color2)).isSuccessful(); + * } + * } + * }</pre> + * + * <p>Class constructors can also be annotated with @TestParameterAnnotation annotations, as shown + * below: + * + * <pre>{@code + * @RunWith(TestParameterInjector.class) + * public class ColorTest { + * @Retention(RUNTIME) + * @Target({TYPE, METHOD, FIELD}) + * public @TestParameterAnnotation + * public @interface ColorParameter { + * Color[] value() default {}; + * } + * + * public ColorTest(@ColorParameter({BLUE, WHITE}) Color color) { + * ... + * } + * + * @Test + * public void test() {...} + * } + * }</pre> + * + * <p>Each field that needs to be injected from a parameter requires its dedicated distinct + * annotation. + * + * <p>If the same annotation is defined both on the class and method, the method parameter values + * take precedence. + * + * <p>If the same annotation is defined both on the class and constructor, the constructor parameter + * values take precedence. + * + * <p>Annotations cannot be duplicated between the constructor or constructor parameters and a + * method or method parameter. + * + * <p>Since the parameter values must be specified in an annotation return value, they are + * restricted to the annotation method return type set (primitive, Class, Enum, String, etc...). If + * parameters have to be dynamically generated, the conventional Parameterized mechanism with {@code + * Parameters} has to be used instead. + */ +@Retention(RUNTIME) +@Target({ANNOTATION_TYPE}) +@interface TestParameterAnnotation { + + /** Specifies a validator for the parameter to determine whether test should be skipped. */ + Class<? extends TestParameterValidator> validator() default DefaultValidator.class; + + /** Specifies a value provider for the parameter to provide the values to test. */ + Class<? extends TestParameterValueProvider> valueProvider() default DefaultValueProvider.class; + + /** Default {@link TestParameterValidator} implementation which skips no test. */ + class DefaultValidator implements TestParameterValidator { + + @Override + public boolean shouldSkip(Context context) { + return false; + } + } + + /** + * Default {@link TestParameterValueProvider} implementation that gets its values from the + * annotation's `value` method. + */ + class DefaultValueProvider implements TestParameterValueProvider { + + @Override + public List<Object> provideValues(Annotation annotation, Optional<Class<?>> parameterClass) { + Object parameters = getParametersAnnotationValues(annotation, annotation.annotationType()); + checkState( + parameters.getClass().isArray(), + "The return value of the value method should be an array"); + + int parameterCount = Array.getLength(parameters); + ImmutableList.Builder<Object> resultBuilder = ImmutableList.builder(); + for (int i = 0; i < parameterCount; i++) { + Object value = Array.get(parameters, i); + if (parameterClass.isPresent()) { + verify( + Primitives.wrap(parameterClass.get()).isInstance(value), + "Found %s annotation next to a parameter of type %s which doesn't match" + + " (annotation = %s)", + annotation.annotationType().getSimpleName(), + parameterClass.get().getSimpleName(), + annotation); + } + resultBuilder.add(value); + } + return resultBuilder.build(); + } + + @Override + public Class<?> getValueType( + Class<? extends Annotation> annotationType, Optional<Class<?>> parameterClass) { + try { + Method valueMethod = annotationType.getMethod("value"); + return valueMethod.getReturnType().getComponentType(); + } catch (NoSuchMethodException e) { + throw new RuntimeException( + "The @TestParameterAnnotation annotation should have a single value() method.", e); + } + } + + /** + * Returns the parameters of the test parameter, by calling the {@code value} method on the + * annotation. + */ + private static Object getParametersAnnotationValues( + Annotation annotation, Class<? extends Annotation> annotationType) { + Method valueMethod; + try { + valueMethod = annotationType.getMethod("value"); + } catch (NoSuchMethodException e) { + throw new RuntimeException( + "The @TestParameterAnnotation annotation should have a single value() method.", e); + } + Object parameters; + try { + parameters = valueMethod.invoke(annotation); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof IllegalAccessError) { + // There seems to be a bug or at least something weird with the JVM that causes + // IllegalAccessError to be thrown because the return value is not visible when it is a + // non-public nested type. See + // http://mail.openjdk.java.net/pipermail/core-libs-dev/2014-January/024180.html for more + // info. + throw new RuntimeException( + String.format( + "Could not access %s.value(). This is probably because %s is not visible to the" + + " annotation proxy. To fix this, make %s public.", + annotationType.getSimpleName(), + valueMethod.getReturnType().getSimpleName(), + valueMethod.getReturnType().getSimpleName())); + // Note: Not chaining the exception to reduce the clutter for the reader + } else { + throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e); + } + } catch (Exception e) { + throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e); + } + return parameters; + } + } +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java new file mode 100644 index 0000000..16b206a --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessor.java @@ -0,0 +1,1369 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Verify.verify; +import static com.google.common.collect.Lists.newArrayList; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.auto.value.AutoAnnotation; +import com.google.auto.value.AutoValue; +import com.google.common.base.Optional; +import com.google.common.base.Throwables; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ContiguousSet; +import com.google.common.collect.DiscreteDomain; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; +import com.google.common.collect.Range; +import com.google.common.util.concurrent.UncheckedExecutionException; +import com.google.testing.junit.testparameterinjector.TestInfo.TestInfoParameter; +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import javax.annotation.Nullable; + +/** + * {@code TestMethodProcessor} implementation for supporting parameterized tests annotated with + * {@link TestParameterAnnotation}. + * + * @see TestParameterAnnotation + */ +final class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { + + /** + * Class to hold an annotation type and origin and one of the values as returned by the {@code + * value()} method. + */ + @AutoValue + abstract static class TestParameterValueHolder implements Serializable { + + private static final long serialVersionUID = -6491624726743872379L; + + /** + * Annotation type and origin of the annotation annotated with {@link TestParameterAnnotation}. + */ + abstract AnnotationTypeOrigin annotationTypeOrigin(); + + /** + * The value used for the test as returned by the @TestParameterAnnotation annotated + * annotation's {@code value()} method (e.g. 'true' or 'false' in the case of a Boolean + * parameter). + */ + abstract TestParameterValue wrappedValue(); + + /** The index of this value in {@link #specifiedValues()}. */ + abstract int valueIndex(); + + /** + * The list of values specified by the @TestParameterAnnotation annotated annotation's {@code + * value()} method (e.g. {true, false} in the case of a boolean parameter). + */ + @SuppressWarnings("AutoValueImmutableFields") // intentional to allow null values + abstract List<Object> specifiedValues(); + + /** + * The name of the parameter or field that is being annotated. In case the annotation is + * annotating a method, constructor or class, {@code paramName} is an absent optional. + */ + abstract Optional<String> paramName(); + + /** + * Returns {@link #wrappedValue()} without the {@link TestParameterValue} wrapper if it exists. + */ + @Nullable + Object unwrappedValue() { + return wrappedValue().getWrappedValue(); + } + + /** + * Returns a String that represents this value and is fit for use in a test name (between + * brackets). + */ + String toTestNameString() { + return ParameterValueParsing.formatTestNameString(paramName(), wrappedValue()); + } + + public static ImmutableList<TestParameterValueHolder> create( + AnnotationWithMetadata annotationWithMetadata, Origin origin) { + List<TestParameterValue> specifiedValues = + getParametersAnnotationValues(annotationWithMetadata); + checkState( + !specifiedValues.isEmpty(), + "The number of parameter values should not be 0" + + ", otherwise the parameter would cause the test to be skipped."); + return FluentIterable.from( + ContiguousSet.create( + Range.closedOpen(0, specifiedValues.size()), DiscreteDomain.integers())) + .transform( + valueIndex -> + (TestParameterValueHolder) + new AutoValue_TestParameterAnnotationMethodProcessor_TestParameterValueHolder( + AnnotationTypeOrigin.create( + annotationWithMetadata.annotation().annotationType(), origin), + specifiedValues.get(valueIndex), + valueIndex, + newArrayList( + FluentIterable.from(specifiedValues) + .transform(TestParameterValue::getWrappedValue)), + annotationWithMetadata.paramName())) + .toList(); + } + } + + /** + * Returns a {@link TestParameterValues} for retrieving the {@link TestParameterAnnotation} + * annotation values for a the {@code testInfo}. + */ + public static TestParameterValues getTestParameterValues(TestInfo testInfo) { + TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); + if (testIndexHolder == null) { + return annotationType -> Optional.absent(); + } else { + return annotationType -> + FluentIterable.from( + new TestParameterAnnotationMethodProcessor( + /* onlyForFieldsAndParameters= */ false) + .getParameterValuesForTest(testIndexHolder, testInfo.getTestClass())) + .filter( + testParameterValue -> + testParameterValue + .annotationTypeOrigin() + .annotationType() + .equals(annotationType)) + .transform(TestParameterValueHolder::unwrappedValue) + .first(); + } + } + + /** + * Returns a {@link TestParameterAnnotation} value for the current test as specified by {@code + * testInfo}, or {@link Optional#absent()} if the {@code annotationType} is not found. + */ + public static Optional<Object> getTestParameterValue( + TestInfo testInfo, Class<? extends Annotation> annotationType) { + return getTestParameterValues(testInfo).getValue(annotationType); + } + + private static ImmutableList<TestParameterValue> getParametersAnnotationValues( + AnnotationWithMetadata annotationWithMetadata) { + Annotation annotation = annotationWithMetadata.annotation(); + TestParameterAnnotation testParameter = + annotation.annotationType().getAnnotation(TestParameterAnnotation.class); + Class<? extends TestParameterValueProvider> valueProvider = testParameter.valueProvider(); + try { + return FluentIterable.from( + valueProvider + .getConstructor() + .newInstance() + .provideValues( + annotation, + annotationWithMetadata.paramClass(), + annotationWithMetadata.context())) + .transform( + value -> + (value instanceof TestParameterValue) + ? (TestParameterValue) value + : TestParameterValue.wrap(value)) + .toList(); + } catch (ReflectiveOperationException e) { + throw new RuntimeException( + "Unexpected exception while invoking value provider " + valueProvider, e); + } + } + + /** The origin of an annotation type. */ + enum Origin { + CLASS, + FIELD, + METHOD, + METHOD_PARAMETER, + CONSTRUCTOR, + CONSTRUCTOR_PARAMETER, + } + + /** Class to hold an annotation type and the element where it was declared. */ + @AutoValue + abstract static class AnnotationTypeOrigin implements Serializable { + + private static final long serialVersionUID = 4909750539931241385L; + + /** Annotation type of the @TestParameterAnnotation annotated annotation. */ + abstract Class<? extends Annotation> annotationType(); + + /** Where the annotation was declared. */ + abstract Origin origin(); + + public static AnnotationTypeOrigin create( + Class<? extends Annotation> annotationType, Origin origin) { + return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationTypeOrigin( + annotationType, origin); + } + + @Override + public final String toString() { + return annotationType().getSimpleName() + ":" + origin(); + } + } + + /** Class to hold an annotation type and metadata about the annotated parameter. */ + @AutoValue + abstract static class AnnotationWithMetadata implements Serializable { + + /** + * The annotation whose interface is itself annotated by the @TestParameterAnnotation + * annotation. + */ + abstract Annotation annotation(); + + /** + * The class of the parameter or field that is being annotated. In case the annotation is + * annotating a method, constructor or class, {@code paramClass} is an absent optional. + */ + abstract Optional<Class<?>> paramClass(); + + /** + * The name of the parameter or field that is being annotated. In case the annotation is + * annotating a method, constructor or class, {@code paramName} is an absent optional. + */ + abstract Optional<String> paramName(); + + /** + * A value class that contains extra information about the context of this parameter. + * + * <p>In case the annotation is annotating a method, constructor or class (deprecated + * functionality), the annotations in the context will be empty. + */ + abstract GenericParameterContext context(); + + public static AnnotationWithMetadata withMetadata( + Annotation annotation, + Class<?> paramClass, + String paramName, + GenericParameterContext context) { + return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( + annotation, Optional.of(paramClass), Optional.of(paramName), context); + } + + public static AnnotationWithMetadata withMetadata( + Annotation annotation, Class<?> paramClass, GenericParameterContext context) { + return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( + annotation, Optional.of(paramClass), Optional.absent(), context); + } + + public static AnnotationWithMetadata withoutMetadata( + Annotation annotation, GenericParameterContext context) { + return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( + annotation, + /* paramClass= */ Optional.absent(), + /* paramName= */ Optional.absent(), + context); + } + + // Prevent anyone relying on equals() and hashCode() so that it remains possible to add fields + // to this class without breaking existing code. + @Override + public final boolean equals(Object other) { + throw new UnsupportedOperationException("Equality is not supported"); + } + + @Override + public final int hashCode() { + throw new UnsupportedOperationException("hashCode() is not supported"); + } + } + + private final boolean onlyForFieldsAndParameters; + private final LoadingCache<Class<?>, ImmutableList<AnnotationTypeOrigin>> + annotationTypeOriginsCache = + CacheBuilder.newBuilder() + .maximumSize(1000) + .build(CacheLoader.from(this::calculateAnnotationTypeOrigins)); + private final Cache<Method, List<List<TestParameterValueHolder>>> parameterValuesCache = + CacheBuilder.newBuilder().maximumSize(1000).build(); + + private TestParameterAnnotationMethodProcessor(boolean onlyForFieldsAndParameters) { + this.onlyForFieldsAndParameters = onlyForFieldsAndParameters; + } + + /** + * Constructs a new {@link TestMethodProcessor} that handles {@link + * TestParameterAnnotation}-annotated annotations that are placed anywhere: + * + * <ul> + * <li>At a method / constructor parameter + * <li>At a field + * <li>At a method / constructor on the class + * <li>At the test class + * </ul> + */ + static TestMethodProcessor forAllAnnotationPlacements() { + return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ false); + } + + /** + * Constructs a new {@link TestMethodProcessor} that handles {@link + * TestParameterAnnotation}-annotated annotations that are placed at fields or parameters. + * + * <p>Note that this excludes class and method-level annotations, as is the default (using the + * constructor). + */ + static TestMethodProcessor onlyForFieldsAndParameters() { + return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ true); + } + + private ImmutableList<AnnotationTypeOrigin> calculateAnnotationTypeOrigins(Class<?> testClass) { + // Collect all annotations used in declared fields and methods that have themselves a + // @TestParameterAnnotation annotation. + List<AnnotationTypeOrigin> fieldAnnotations = + extractTestParameterAnnotations( + FluentIterable.from(listWithParents(testClass)) + .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields())) + .transformAndConcat(field -> Arrays.asList(field.getAnnotations())) + .toList(), + Origin.FIELD); + List<AnnotationTypeOrigin> methodAnnotations = + extractTestParameterAnnotations( + FluentIterable.from(testClass.getMethods()) + .transformAndConcat(method -> Arrays.asList(method.getAnnotations())) + .toList(), + Origin.METHOD); + List<AnnotationTypeOrigin> parameterAnnotations = + extractTestParameterAnnotations( + FluentIterable.from(listWithParents(testClass)) + .transformAndConcat(c -> Arrays.asList(c.getDeclaredMethods())) + .transformAndConcat(method -> Arrays.asList(method.getParameterAnnotations())) + .transformAndConcat(Arrays::asList) + .toList(), + Origin.METHOD_PARAMETER); + List<AnnotationTypeOrigin> classAnnotations = + extractTestParameterAnnotations(Arrays.asList(testClass.getAnnotations()), Origin.CLASS); + List<AnnotationTypeOrigin> constructorAnnotations = + extractTestParameterAnnotations( + FluentIterable.from(testClass.getDeclaredConstructors()) + .transformAndConcat(constructor -> Arrays.asList(constructor.getAnnotations())) + .toList(), + Origin.CONSTRUCTOR); + List<AnnotationTypeOrigin> constructorParameterAnnotations = + extractTestParameterAnnotations( + FluentIterable.from(testClass.getDeclaredConstructors()) + .transformAndConcat( + constructor -> + FluentIterable.from(Arrays.asList(constructor.getParameterAnnotations())) + .transformAndConcat(Arrays::asList)) + .toList(), + Origin.CONSTRUCTOR_PARAMETER); + + checkDuplicatedClassAndFieldAnnotations( + constructorAnnotations, classAnnotations, fieldAnnotations); + + checkDuplicatedFieldsAnnotations(methodAnnotations, fieldAnnotations); + + checkState( + FluentIterable.from(constructorAnnotations).toSet().size() == constructorAnnotations.size(), + "Annotations should not be duplicated on the constructor."); + + checkState( + FluentIterable.from(classAnnotations).toSet().size() == classAnnotations.size(), + "Annotations should not be duplicated on the class."); + + if (onlyForFieldsAndParameters) { + checkState( + methodAnnotations.isEmpty(), + "This test runner (constructed by the testparameterinjector package) was configured" + + " to disallow method-level annotations that could be field/parameter" + + " annotations, but found %s", + methodAnnotations); + checkState( + classAnnotations.isEmpty(), + "This test runner (constructed by the testparameterinjector package) was configured" + + " to disallow class-level annotations that could be field/parameter annotations," + + " but found %s", + classAnnotations); + checkState( + constructorAnnotations.isEmpty(), + "This test runner (constructed by the testparameterinjector package) was configured" + + " to disallow constructor-level annotations that could be field/parameter" + + " annotations, but found %s", + constructorAnnotations); + } + + // The order matters, since it will determine which annotation processor is + // called first. + return FluentIterable.from(classAnnotations) + .append(fieldAnnotations) + .append(constructorAnnotations) + .append(constructorParameterAnnotations) + .append(methodAnnotations) + .append(parameterAnnotations) + .toSet() + .asList(); + } + + private ImmutableList<AnnotationTypeOrigin> getAnnotationTypeOrigins( + Class<?> testClass, Origin firstOrigin, Origin... otherOrigins) { + Set<Origin> originsToFilterBy = + ImmutableSet.<Origin>builder().add(firstOrigin).add(otherOrigins).build(); + try { + return FluentIterable.from(annotationTypeOriginsCache.getUnchecked(testClass)) + .filter(annotationTypeOrigin -> originsToFilterBy.contains(annotationTypeOrigin.origin())) + .toList(); + } catch (UncheckedExecutionException e) { + Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); + throw e; + } + } + + private void checkDuplicatedFieldsAnnotations( + List<AnnotationTypeOrigin> methodAnnotations, List<AnnotationTypeOrigin> fieldAnnotations) { + // If an annotation is duplicated on two fields, then it becomes specific, and cannot be + // overridden by a method. + if (FluentIterable.from(fieldAnnotations).toSet().size() != fieldAnnotations.size()) { + List<Class<? extends Annotation>> methodOrFieldAnnotations = + new ArrayList<>( + FluentIterable.from(methodAnnotations) + .append(new HashSet<>(fieldAnnotations)) + .transform(AnnotationTypeOrigin::annotationType) + .toList()); + + checkState( + FluentIterable.from(methodOrFieldAnnotations).toSet().size() + == methodOrFieldAnnotations.size(), + "Annotations should not be duplicated on a method and field" + + " if they are present on multiple fields"); + } + } + + private void checkDuplicatedClassAndFieldAnnotations( + List<AnnotationTypeOrigin> constructorAnnotations, + List<AnnotationTypeOrigin> classAnnotations, + List<AnnotationTypeOrigin> fieldAnnotations) { + ImmutableSet<? extends Class<? extends Annotation>> classAnnotationTypes = + FluentIterable.from(classAnnotations) + .transform(AnnotationTypeOrigin::annotationType) + .toSet(); + + ImmutableSet<? extends Class<? extends Annotation>> uniqueFieldAnnotations = + FluentIterable.from(fieldAnnotations) + .transform(AnnotationTypeOrigin::annotationType) + .toSet(); + ImmutableSet<? extends Class<? extends Annotation>> uniqueConstructorAnnotations = + FluentIterable.from(constructorAnnotations) + .transform(AnnotationTypeOrigin::annotationType) + .toSet(); + + checkState( + Collections.disjoint(classAnnotationTypes, uniqueFieldAnnotations), + "Annotations should not be duplicated on a class and field"); + + checkState( + Collections.disjoint(classAnnotationTypes, uniqueConstructorAnnotations), + "Annotations should not be duplicated on a class and constructor"); + + checkState( + Collections.disjoint(uniqueConstructorAnnotations, uniqueFieldAnnotations), + "Annotations should not be duplicated on a field and constructor"); + } + + private List<AnnotationTypeOrigin> extractTestParameterAnnotations( + List<Annotation> annotations, Origin origin) { + return new ArrayList<>( + FluentIterable.from(annotations) + .transform(Annotation::annotationType) + .filter( + annotationType -> annotationType.isAnnotationPresent(TestParameterAnnotation.class)) + .transform(annotationType -> AnnotationTypeOrigin.create(annotationType, origin)) + .toList()); + } + + @Override + public ExecutableValidationResult validateConstructor(Constructor<?> constructor) { + Class<?>[] parameterTypes = constructor.getParameterTypes(); + if (parameterTypes.length == 0) { + return ExecutableValidationResult.notValidated(); + } + // The constructor has parameters, they must be injected by a TestParameterAnnotation + // annotation. + Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); + Class<?> testClass = constructor.getDeclaringClass(); + return ExecutableValidationResult.validated( + validateMethodOrConstructorParameters( + removeOverrides( + getAnnotationTypeOrigins( + testClass, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER), + testClass), + testClass, + constructor, + parameterTypes, + parameterAnnotations)); + } + + @Override + public ExecutableValidationResult validateTestMethod(Method testMethod, Class<?> testClass) { + Class<?>[] methodParameterTypes = testMethod.getParameterTypes(); + if (methodParameterTypes.length == 0) { + return ExecutableValidationResult.notValidated(); + } else { + // The method has parameters, they must be injected by a TestParameterAnnotation annotation. + return ExecutableValidationResult.validated( + validateMethodOrConstructorParameters( + getAnnotationTypeOrigins( + testClass, Origin.CLASS, Origin.METHOD, Origin.METHOD_PARAMETER), + testClass, + testMethod, + methodParameterTypes, + testMethod.getParameterAnnotations())); + } + } + + private List<Throwable> validateMethodOrConstructorParameters( + List<AnnotationTypeOrigin> annotationTypeOrigins, + Class<?> testClass, + AnnotatedElement methodOrConstructor, + Class<?>[] parameterTypes, + Annotation[][] parametersAnnotations) { + List<Throwable> errors = new ArrayList<>(); + + for (int parameterIndex = 0; parameterIndex < parameterTypes.length; parameterIndex++) { + Class<?> parameterType = parameterTypes[parameterIndex]; + Annotation[] parameterAnnotations = parametersAnnotations[parameterIndex]; + boolean matchingTestParameterAnnotationFound = false; + // First, handle the case where the method parameter specifies the test parameter explicitly, + // e.g. {@code public void test(@ColorParameter({...}) Color c)}. + for (AnnotationTypeOrigin testParameterAnnotationType : annotationTypeOrigins) { + for (Annotation parameterAnnotation : parameterAnnotations) { + if (parameterAnnotation + .annotationType() + .equals(testParameterAnnotationType.annotationType())) { + // Verify that the type is assignable with the return type of the 'value' method. + Class<?> valueMethodReturnType = + getValueMethodReturnType( + testParameterAnnotationType.annotationType(), + /* paramClass= */ Optional.of(parameterType)); + if (!parameterType.isAssignableFrom(valueMethodReturnType)) { + errors.add( + new IllegalStateException( + String.format( + "Parameter of type %s annotated with %s does not match" + + " expected type %s in method/constructor %s", + parameterType.getName(), + testParameterAnnotationType.annotationType().getName(), + valueMethodReturnType.getName(), + methodOrConstructor))); + } else { + matchingTestParameterAnnotationFound = true; + } + } + } + } + // Second, handle the case where the method parameter does not specify the test parameter, + // and instead relies on the type matching, e.g. {@code public void test(Color c)}. + if (!matchingTestParameterAnnotationFound) { + ImmutableList<? extends Class<? extends Annotation>> testParameterAnnotationTypes = + getTestParameterAnnotations( + // Do not include METHOD_PARAMETER or CONSTRUCTOR_PARAMETER since they have already + // been evaluated. + filterAnnotationTypeOriginsByOrigin( + annotationTypeOrigins, Origin.CLASS, Origin.CONSTRUCTOR, Origin.METHOD), + testClass, + methodOrConstructor); + // If no annotation is present, simply compare the type. + for (Class<? extends Annotation> testParameterAnnotationType : + testParameterAnnotationTypes) { + if (parameterType.isAssignableFrom( + getValueMethodReturnType( + testParameterAnnotationType, /* paramClass= */ Optional.absent()))) { + if (matchingTestParameterAnnotationFound) { + errors.add( + new IllegalStateException( + String.format( + "Ambiguous method/constructor parameter type, matching multiple" + + " annotations for parameter of type %s in method %s", + parameterType.getName(), methodOrConstructor))); + } + matchingTestParameterAnnotationFound = true; + } + } + } + if (!matchingTestParameterAnnotationFound) { + errors.add( + new IllegalStateException( + String.format( + "No matching test parameter annotation found" + + " for parameter of type %s in method/constructor %s", + parameterType.getName(), methodOrConstructor))); + } + } + return errors; + } + + @Override + public Optional<List<Object>> maybeGetConstructorParameters( + Constructor<?> constructor, TestInfo testInfo) { + if (testInfo.getAnnotation(TestIndexHolder.class) == null + // Explicitly skip @TestParameters annotated methods to ensure compatibility. + // + // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported) + // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;). + // Without the @TestParameters check below, this class would try to find parameters for + // these methods. When there are no method parameters, this is a no-op, but when the method + // is annotated with @TestParameters, this throws an exception (because there are method + // parameters that this processor has no values for - they are provided by the + // @TestParameters processor). + || constructor.isAnnotationPresent(TestParameters.class)) { + return Optional.absent(); + } else { + TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); + List<TestParameterValueHolder> testParameterValues = + getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()); + + Class<?>[] parameterTypes = constructor.getParameterTypes(); + Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); + List<Object> parameterValues = new ArrayList<>(/* initialCapacity= */ parameterTypes.length); + List<Class<? extends Annotation>> processedAnnotationTypes = new ArrayList<>(); + List<TestParameterValueHolder> parameterValuesForConstructor = + filterByOrigin( + testParameterValues, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER); + for (int i = 0; i < parameterTypes.length; i++) { + // Initialize each parameter value from the corresponding TestParameterAnnotation value. + parameterValues.add( + getParameterValue( + parameterValuesForConstructor, + parameterTypes[i], + parameterAnnotations[i], + processedAnnotationTypes)); + } + return Optional.of(parameterValues); + } + } + + @Override + public Optional<List<Object>> maybeGetTestMethodParameters(TestInfo testInfo) { + Method testMethod = testInfo.getMethod(); + if (testInfo.getAnnotation(TestIndexHolder.class) == null + // Explicitly skip @TestParameters annotated methods to ensure compatibility. + // + // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported) + // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;). + // Without the @TestParameters check below, this class would try to find parameters for + // these methods. When there are no method parameters, this is a no-op, but when the method + // is annotated with @TestParameters, this throws an exception (because there are method + // parameters that this processor has no values for - they are provided by the + // @TestParameters processor). + || testMethod.isAnnotationPresent(TestParameters.class)) { + return Optional.absent(); + } else { + TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); + checkState(testIndexHolder != null); + List<TestParameterValueHolder> testParameterValues = + filterByOrigin( + getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()), + Origin.CLASS, + Origin.METHOD, + Origin.METHOD_PARAMETER); + + Class<?>[] parameterTypes = testMethod.getParameterTypes(); + Annotation[][] parametersAnnotations = testMethod.getParameterAnnotations(); + ArrayList<Object> parameterValues = + new ArrayList<>(/* initialCapacity= */ parameterTypes.length); + + List<Class<? extends Annotation>> processedAnnotationTypes = new ArrayList<>(); + for (int i = 0; i < parameterTypes.length; i++) { + parameterValues.add( + getParameterValue( + testParameterValues, + parameterTypes[i], + parametersAnnotations[i], + processedAnnotationTypes)); + } + + return Optional.of(parameterValues); + } + } + + /** + * Returns the {@link TestInfo}, one for each result of the cartesian product of each test + * parameter values. + * + * <p>For example, given the annotation {@code @ColorParameter({BLUE, WHITE, RED})} on a method, + * it method will return the TestParameterValues: "(@ColorParameter, BLUE), (@ColorParameter, + * WHITE), (@ColorParameter, RED)}). + * + * <p>For multiple annotations (say, {@code @TestParameter("foo", "bar")} and + * {@code @ColorParameter({BLUE, WHITE})}), it will generate the following result: + * + * <ul> + * <li>("foo", BLUE) + * <li>("foo", WHITE) + * <li>("bar", BLUE) + * <li>("bar", WHITE) + * <li> + * </ul> + * + * corresponding to the cartesian product of both annotations. + */ + @Override + public List<TestInfo> calculateTestInfos(TestInfo originalTest) { + List<List<TestParameterValueHolder>> parameterValuesForMethod = + getParameterValuesForMethod(originalTest.getMethod(), originalTest.getTestClass()); + + if (parameterValuesForMethod.equals(ImmutableList.of(ImmutableList.of()))) { + // This test is not parameterized + return ImmutableList.of(originalTest); + } + + ImmutableList.Builder<TestInfo> testInfos = ImmutableList.builder(); + for (int parametersIndex = 0; + parametersIndex < parameterValuesForMethod.size(); + ++parametersIndex) { + List<TestParameterValueHolder> testParameterValues = + parameterValuesForMethod.get(parametersIndex); + testInfos.add( + originalTest + .withExtraParameters( + FluentIterable.from(testParameterValues) + .transform( + param -> + TestInfoParameter.create( + param.toTestNameString(), + param.unwrappedValue(), + param.valueIndex())) + .toList()) + .withExtraAnnotation( + TestIndexHolderFactory.create( + /* methodIndex= */ strictIndexOf( + getMethodsIncludingParentsSorted(originalTest.getTestClass()), + originalTest.getMethod()), + parametersIndex, + originalTest.getTestClass().getName()))); + } + + return testInfos.build(); + } + + private List<List<TestParameterValueHolder>> getParameterValuesForMethod( + Method method, Class<?> testClass) { + try { + return parameterValuesCache.get( + method, + () -> { + List<List<TestParameterValueHolder>> testParameterValuesList = + getAnnotationValuesForUsedAnnotationTypes(method, testClass); + + return FluentIterable.from(Lists.cartesianProduct(testParameterValuesList)) + .filter( + // Skip tests based on the annotations' {@link Validator#shouldSkip} return + // value. + testParameterValues -> + FluentIterable.from(testParameterValues) + .filter( + testParameterValue -> + callShouldSkip( + testParameterValue.annotationTypeOrigin().annotationType(), + testParameterValues)) + .isEmpty()) + .toList(); + }); + } catch (ExecutionException | UncheckedExecutionException e) { + Throwables.throwIfUnchecked(e.getCause()); + throw new RuntimeException(e); + } + } + + private List<TestParameterValueHolder> getParameterValuesForTest( + TestIndexHolder testIndexHolder, Class<?> testClass) { + verify( + testIndexHolder.testClassName().equals(testClass.getName()), + "The class for which the given annotation was created (%s) is not the same as the test" + + " class that this runner is handling (%s)", + testIndexHolder.testClassName(), + testClass.getName()); + Method testMethod = + getMethodsIncludingParentsSorted(testClass).get(testIndexHolder.methodIndex()); + return getParameterValuesForMethod(testMethod, testClass) + .get(testIndexHolder.parametersIndex()); + } + + /** + * Returns the list of annotation index for all annotations defined in a given test method and its + * class. + */ + private ImmutableList<List<TestParameterValueHolder>> getAnnotationValuesForUsedAnnotationTypes( + Method method, Class<?> testClass) { + ImmutableList<AnnotationTypeOrigin> annotationTypes = + FluentIterable.from(getAnnotationTypeOrigins(testClass, Origin.CLASS)) + .append(getAnnotationTypeOrigins(testClass, Origin.FIELD)) + .append(getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR)) + .append(getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR_PARAMETER)) + .append(getAnnotationTypeOrigins(testClass, Origin.METHOD)) + .append( + ImmutableList.sortedCopyOf( + annotationComparator(method.getParameterAnnotations()), + getAnnotationTypeOrigins(testClass, Origin.METHOD_PARAMETER))) + .toList(); + + return FluentIterable.from(removeOverrides(annotationTypes, testClass, method)) + .transform( + annotationTypeOrigin -> + getAnnotationFromParametersOrTestOrClass(annotationTypeOrigin, method, testClass)) + .filter(l -> !l.isEmpty()) + .transformAndConcat(i -> i) + .toList(); + } + + private Comparator<AnnotationTypeOrigin> annotationComparator( + Annotation[][] parameterAnnotations) { + ImmutableList<String> annotationOrdering = + FluentIterable.from(parameterAnnotations) + .transformAndConcat(Arrays::asList) + .transform(Annotation::annotationType) + .transform(Class::getName) + .toList(); + return (annotationTypeOrigin, t1) -> + Integer.compare( + annotationOrdering.indexOf(annotationTypeOrigin.annotationType().getName()), + annotationOrdering.indexOf(t1.annotationType().getName())); + } + + /** + * Returns a list of {@link AnnotationTypeOrigin} where the overridden annotation are removed for + * the current {@code originalTest} and {@code testClass}. + * + * <p>Specifically, annotation defined on CLASS and FIELD elements will be removed if they are + * also defined on the method, method parameter, constructor, or constructor parameters. + */ + private List<AnnotationTypeOrigin> removeOverrides( + List<AnnotationTypeOrigin> annotationTypeOrigins, Class<?> testClass, Method method) { + return removeOverrides( + new ArrayList<>( + FluentIterable.from(annotationTypeOrigins) + .filter( + annotationTypeOrigin -> { + switch (annotationTypeOrigin.origin()) { + case FIELD: // Fall through. + case CLASS: + return getAnnotationListWithType( + method.getAnnotations(), annotationTypeOrigin.annotationType()) + .isEmpty(); + default: + return true; + } + }) + .toList()), + testClass); + } + + /** + * @see #removeOverrides(List, Class) + */ + private List<AnnotationTypeOrigin> removeOverrides( + List<AnnotationTypeOrigin> annotationTypeOrigins, Class<?> testClass) { + return new ArrayList<>( + FluentIterable.from(annotationTypeOrigins) + .filter( + annotationTypeOrigin -> { + switch (annotationTypeOrigin.origin()) { + case FIELD: // Fall through. + case CLASS: + return getAnnotationListWithType( + TestParameterInjectorUtils.getOnlyConstructor(testClass) + .getAnnotations(), + annotationTypeOrigin.annotationType()) + .isEmpty(); + default: + return true; + } + }) + .toList()); + } + + /** + * Returns the given annotations defined either on the method parameters, method or the test + * class. + * + * <p>The annotation from the parameters takes precedence over the same annotation defined on the + * method, and the one defined on the method takes precedence over the same annotation defined on + * the class. + */ + private ImmutableList<List<TestParameterValueHolder>> getAnnotationFromParametersOrTestOrClass( + AnnotationTypeOrigin annotationTypeOrigin, Method method, Class<?> testClass) { + Origin origin = annotationTypeOrigin.origin(); + Class<? extends Annotation> annotationType = annotationTypeOrigin.annotationType(); + if (origin == Origin.CONSTRUCTOR_PARAMETER) { + Constructor<?> constructor = TestParameterInjectorUtils.getOnlyConstructor(testClass); + List<AnnotationWithMetadata> annotations = + getAnnotationWithMetadataListWithType(constructor, annotationType, testClass); + + if (!annotations.isEmpty()) { + return toTestParameterValueList(annotations, origin); + } + } else if (origin == Origin.CONSTRUCTOR) { + Annotation annotation = + TestParameterInjectorUtils.getOnlyConstructor(testClass).getAnnotation(annotationType); + if (annotation != null) { + return ImmutableList.of( + TestParameterValueHolder.create( + AnnotationWithMetadata.withoutMetadata( + annotation, + GenericParameterContext.createWithoutParameterAnnotations(testClass)), + origin)); + } + + } else if (origin == Origin.METHOD_PARAMETER) { + List<AnnotationWithMetadata> annotations = + getAnnotationWithMetadataListWithType(method, annotationType, testClass); + if (!annotations.isEmpty()) { + return toTestParameterValueList(annotations, origin); + } + } else if (origin == Origin.METHOD) { + if (method.isAnnotationPresent(annotationType)) { + return ImmutableList.of( + TestParameterValueHolder.create( + AnnotationWithMetadata.withoutMetadata( + method.getAnnotation(annotationType), + GenericParameterContext.createWithoutParameterAnnotations(testClass)), + origin)); + } + } else if (origin == Origin.FIELD) { + List<AnnotationWithMetadata> annotations = + new ArrayList<>( + FluentIterable.from(listWithParents(testClass)) + .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields())) + .transformAndConcat( + field -> + FluentIterable.from( + getAnnotationListWithType(field.getAnnotations(), annotationType)) + .transform( + annotation -> + AnnotationWithMetadata.withMetadata( + annotation, + field.getType(), + field.getName(), + GenericParameterContext.create(field, testClass)))) + .toList()); + if (!annotations.isEmpty()) { + return toTestParameterValueList(annotations, origin); + } + } else if (origin == Origin.CLASS) { + Annotation annotation = testClass.getAnnotation(annotationType); + if (annotation != null) { + return ImmutableList.of( + TestParameterValueHolder.create( + AnnotationWithMetadata.withoutMetadata( + annotation, + GenericParameterContext.createWithoutParameterAnnotations(testClass)), + origin)); + } + } + return ImmutableList.of(); + } + + private static ImmutableList<List<TestParameterValueHolder>> toTestParameterValueList( + List<AnnotationWithMetadata> annotationWithMetadatas, Origin origin) { + return FluentIterable.from(annotationWithMetadatas) + .transform( + annotationWithMetadata -> + (List<TestParameterValueHolder>) + new ArrayList<>( + TestParameterValueHolder.create(annotationWithMetadata, origin))) + .toList(); + } + + private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType( + Method callable, Class<? extends Annotation> annotationType, Class<?> testClass) { + try { + return getAnnotationWithMetadataListWithType( + callable.getParameters(), annotationType, testClass); + } catch (NoSuchMethodError ignored) { + return getAnnotationWithMetadataListWithType( + callable.getParameterTypes(), + callable.getParameterAnnotations(), + annotationType, + testClass); + } + } + + private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType( + Constructor<?> callable, Class<? extends Annotation> annotationType, Class<?> testClass) { + try { + return getAnnotationWithMetadataListWithType( + callable.getParameters(), annotationType, testClass); + } catch (NoSuchMethodError ignored) { + return getAnnotationWithMetadataListWithType( + callable.getParameterTypes(), + callable.getParameterAnnotations(), + annotationType, + testClass); + } + } + + // Parameter is not available on old Android SDKs, and isn't desugared. That's why this method + // has a fallback that takes the parameter types and annotations (without the parameter names, + // which are optional anyway). + @SuppressWarnings("AndroidJdkLibsChecker") + private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType( + Parameter[] parameters, Class<? extends Annotation> annotationType, Class<?> testClass) { + return FluentIterable.from(parameters) + .transform( + parameter -> { + Annotation annotation = parameter.getAnnotation(annotationType); + return annotation == null + ? null + : parameter.isNamePresent() + ? AnnotationWithMetadata.withMetadata( + annotation, + parameter.getType(), + parameter.getName(), + GenericParameterContext.create(parameter, testClass)) + : AnnotationWithMetadata.withMetadata( + annotation, + parameter.getType(), + GenericParameterContext.create(parameter, testClass)); + }) + .filter(Objects::nonNull) + .toList(); + } + + private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType( + Class<?>[] parameterTypes, + Annotation[][] annotations, + Class<? extends Annotation> annotationType, + Class<?> testClass) { + checkArgument(parameterTypes.length == annotations.length); + + ImmutableList.Builder<AnnotationWithMetadata> resultBuilder = ImmutableList.builder(); + for (int i = 0; i < annotations.length; i++) { + for (Annotation annotation : annotations[i]) { + if (annotation.annotationType().equals(annotationType)) { + resultBuilder.add( + AnnotationWithMetadata.withMetadata( + annotation, + parameterTypes[i], + GenericParameterContext.createWithRepeatableAnnotationsFallback( + annotations[i], testClass))); + } + } + } + return resultBuilder.build(); + } + + private ImmutableList<Annotation> getAnnotationListWithType( + Annotation[] annotations, Class<? extends Annotation> annotationType) { + return FluentIterable.from(annotations) + .filter(annotation -> annotation.annotationType().equals(annotationType)) + .toList(); + } + + @Override + public void postProcessTestInstance(Object testInstance, TestInfo testInfo) { + TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); + try { + if (testIndexHolder != null) { + List<TestParameterValueHolder> testParameterValues = + getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()); + + // Do not include {@link Origin#METHOD_PARAMETER} nor {@link Origin#CONSTRUCTOR_PARAMETER} + // annotations. + List<TestParameterValueHolder> testParameterValuesForFieldInjection = + filterByOrigin(testParameterValues, Origin.CLASS, Origin.FIELD, Origin.METHOD); + // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class + // in the example above. + List<TestParameterValueHolder> remainingTestParameterValuesForFieldInjection = + new ArrayList<>(testParameterValuesForFieldInjection); + for (Field declaredField : + FluentIterable.from(listWithParents(testInstance.getClass())) + .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields())) + .toList()) { + for (TestParameterValueHolder testParameterValue : + remainingTestParameterValuesForFieldInjection) { + if (declaredField.isAnnotationPresent( + testParameterValue.annotationTypeOrigin().annotationType())) { + if (testParameterValue.paramName().isPresent() + && !declaredField.getName().equals(testParameterValue.paramName().get())) { + // names don't match + continue; + } + declaredField.setAccessible(true); + declaredField.set(testInstance, testParameterValue.unwrappedValue()); + remainingTestParameterValuesForFieldInjection.remove(testParameterValue); + break; + } + } + } + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns an {@link TestParameterValueHolder} list that contains only the values originating from + * one of the {@code origins}. + */ + private static ImmutableList<TestParameterValueHolder> filterByOrigin( + List<TestParameterValueHolder> testParameterValues, Origin... origins) { + Set<Origin> originsToFilterBy = ImmutableSet.copyOf(origins); + return FluentIterable.from(testParameterValues) + .filter( + testParameterValue -> + originsToFilterBy.contains(testParameterValue.annotationTypeOrigin().origin())) + .toList(); + } + + /** + * Returns an {@link AnnotationTypeOrigin} list that contains only the values originating from one + * of the {@code origins}. + */ + private static ImmutableList<AnnotationTypeOrigin> filterAnnotationTypeOriginsByOrigin( + List<AnnotationTypeOrigin> annotationTypeOrigins, Origin... origins) { + List<Origin> originList = Arrays.asList(origins); + return FluentIterable.from(annotationTypeOrigins) + .filter(annotationTypeOrigin -> originList.contains(annotationTypeOrigin.origin())) + .toList(); + } + + /** Returns a {@link TestParameterAnnotation}'s value for a method or constructor parameter. */ + private Object getParameterValue( + List<TestParameterValueHolder> testParameterValues, + Class<?> methodParameterType, + Annotation[] parameterAnnotations, + List<Class<? extends Annotation>> processedAnnotationTypes) { + List<Class<? extends Annotation>> iteratedAnnotationTypes = new ArrayList<>(); + for (TestParameterValueHolder testParameterValue : testParameterValues) { + // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class + // in the example above. + for (Annotation parameterAnnotation : parameterAnnotations) { + Class<? extends Annotation> annotationType = + testParameterValue.annotationTypeOrigin().annotationType(); + if (parameterAnnotation.annotationType().equals(annotationType)) { + // If multiple annotations exist, ensure that the proper one is selected. + // For instance, for: + // <code> + // test(@FooParameter(1,2) Foo foo, @FooParameter(3,4) Foo bar) {} + // </code> + // Verifies that the correct @FooParameter annotation value will be assigned to the + // corresponding variable. + if (Collections.frequency(processedAnnotationTypes, annotationType) + == Collections.frequency(iteratedAnnotationTypes, annotationType)) { + processedAnnotationTypes.add(annotationType); + return testParameterValue.unwrappedValue(); + } + iteratedAnnotationTypes.add(annotationType); + } + } + } + // If no annotation matches, use the method parameter type. + for (TestParameterValueHolder testParameterValue : testParameterValues) { + // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class + // in the example above. + if (methodParameterType.isAssignableFrom( + getValueMethodReturnType( + testParameterValue.annotationTypeOrigin().annotationType(), + /* paramClass= */ Optional.absent()))) { + return testParameterValue.unwrappedValue(); + } + } + throw new IllegalStateException( + "The method parameter should have matched a TestParameterAnnotation"); + } + + /** + * This mechanism is a workaround to be able to store the annotation values in the annotation list + * of the {@link TestInfo}, since we cannot carry other information through the test runner. + */ + @Retention(RUNTIME) + @interface TestIndexHolder { + + /** The index of the test method in {@code getMethodsIncludingParentsSorted(testClass)} */ + int methodIndex(); + + /** + * The index of the set of parameters to run the test method with in the list produced by {@link + * #getParameterValuesForMethod}. + */ + int parametersIndex(); + + /** + * The full name of the test class. Only used for verifying that assumptions about the above + * indices are valid. + */ + String testClassName(); + } + + /** Factory for {@link TestIndexHolder}. */ + static class TestIndexHolderFactory { + @AutoAnnotation + static TestIndexHolder create(int methodIndex, int parametersIndex, String testClassName) { + return new AutoAnnotation_TestParameterAnnotationMethodProcessor_TestIndexHolderFactory_create( + methodIndex, parametersIndex, testClassName); + } + + private TestIndexHolderFactory() {} + } + + /** + * Returns whether the test should be skipped according to the {@code annotationType}'s {@link + * TestParameterValidator} and the current list of {@link TestParameterValueHolder}. + */ + private static boolean callShouldSkip( + Class<? extends Annotation> annotationType, + List<TestParameterValueHolder> testParameterValues) { + TestParameterAnnotation annotation = + annotationType.getAnnotation(TestParameterAnnotation.class); + Class<? extends TestParameterValidator> validator = annotation.validator(); + try { + return validator + .getConstructor() + .newInstance() + .shouldSkip(new ValidatorContext(testParameterValues)); + } catch (Exception e) { + throw new RuntimeException("Unexpected exception while invoking validator " + validator, e); + } + } + + private static class ValidatorContext implements TestParameterValidator.Context { + + private final List<TestParameterValueHolder> testParameterValues; + private final Set<Object> valueList; + + public ValidatorContext(List<TestParameterValueHolder> testParameterValues) { + this.testParameterValues = testParameterValues; + this.valueList = + FluentIterable.from(testParameterValues) + .transform(TestParameterValueHolder::unwrappedValue) + .filter(Objects::nonNull) + .toSet(); + } + + @Override + public boolean has(Class<? extends Annotation> testParameter, Object value) { + return getValue(testParameter).transform(value::equals).or(false); + } + + @Override + public <T extends Enum<T>, U extends Enum<U>> boolean has(T value1, U value2) { + return valueList.contains(value1) && valueList.contains(value2); + } + + @Override + public Optional<Object> getValue(Class<? extends Annotation> testParameter) { + return getParameter(testParameter).transform(TestParameterValueHolder::unwrappedValue); + } + + @Override + public List<Object> getSpecifiedValues(Class<? extends Annotation> testParameter) { + return getParameter(testParameter) + .transform(TestParameterValueHolder::specifiedValues) + .or(ImmutableList.of()); + } + + private Optional<TestParameterValueHolder> getParameter( + Class<? extends Annotation> testParameter) { + return FluentIterable.from(testParameterValues) + .firstMatch(value -> value.annotationTypeOrigin().annotationType().equals(testParameter)); + } + } + + /** + * Returns the class of the list elements returned by {@code provideValues()}. + * + * @param annotationType The type of the annotation that was encountered in the test class. The + * definition of this annotation is itself annotated with the {@link TestParameterAnnotation} + * annotation. + * @param paramClass The class of the parameter or field that is being annotated. In case the + * annotation is annotating a method, constructor or class, {@code paramClass} is an absent + * optional. + */ + private static Class<?> getValueMethodReturnType( + Class<? extends Annotation> annotationType, Optional<Class<?>> paramClass) { + TestParameterAnnotation testParameter = + annotationType.getAnnotation(TestParameterAnnotation.class); + Class<? extends TestParameterValueProvider> valueProvider = testParameter.valueProvider(); + try { + return valueProvider.getConstructor().newInstance().getValueType(annotationType, paramClass); + } catch (Exception e) { + throw new RuntimeException( + "Unexpected exception while invoking value provider " + valueProvider, e); + } + } + + /** Returns the TestParameterAnnotation annotation types defined for a method or constructor. */ + private ImmutableList<? extends Class<? extends Annotation>> getTestParameterAnnotations( + List<AnnotationTypeOrigin> annotationTypeOrigins, + final Class<?> testClass, + AnnotatedElement methodOrConstructor) { + return FluentIterable.from(annotationTypeOrigins) + .transform(AnnotationTypeOrigin::annotationType) + .filter( + annotationType -> + testClass.isAnnotationPresent(annotationType) + || methodOrConstructor.isAnnotationPresent(annotationType)) + .toList(); + } + + private <T> int strictIndexOf(List<T> haystack, T needle) { + int index = haystack.indexOf(needle); + checkArgument(index >= 0, "Could not find '%s' in %s", needle, haystack); + return index; + } + + private ImmutableList<Method> getMethodsIncludingParentsSorted(Class<?> clazz) { + ImmutableList.Builder<Method> resultBuilder = ImmutableList.builder(); + while (clazz != null) { + resultBuilder.add(clazz.getDeclaredMethods()); + clazz = clazz.getSuperclass(); + } + // Because getDeclaredMethods()'s order is not specified, there is the theoretical possibility + // that the order of methods is unstable. To partly fix this, we sort the result based on method + // name. This is still not perfect because of method overloading, but that should be + // sufficiently rare for test names. + return ImmutableList.sortedCopyOf( + Ordering.natural().onResultOf(Method::getName), resultBuilder.build()); + } + + private static ImmutableList<Class<?>> listWithParents(Class<?> clazz) { + ImmutableList.Builder<Class<?>> resultBuilder = ImmutableList.builder(); + + Class<?> currentClass = clazz; + while (currentClass != null) { + resultBuilder.add(currentClass); + currentClass = currentClass.getSuperclass(); + } + + return resultBuilder.build(); + } +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java new file mode 100644 index 0000000..8b23e53 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjector.java @@ -0,0 +1,33 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import org.junit.runners.model.InitializationError; + +/** + * A JUnit4 test runner which knows how to instantiate and run test classes where each test case may + * be parameterized with its own unique set of test parameters. + */ +public final class TestParameterInjector extends PluggableTestRunner { + + public TestParameterInjector(Class<?> testClass) throws InitializationError { + super(testClass); + } + + @Override + protected TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.createNewParameterizedProcessors(); + } +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorUtils.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorUtils.java new file mode 100644 index 0000000..215719a --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorUtils.java @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Iterables.getOnlyElement; + +import com.google.common.collect.ImmutableList; +import java.lang.reflect.Constructor; + +/** Shared utility methods. */ +class TestParameterInjectorUtils { + + /** + * Return the only public constructor of the given test class. If there is none, return the only + * constructor. + * + * <p>Normally, there should be exactly one constructor (public or other), but some frameworks + * introduce an extra non-public constructor (see + * https://github.com/google/TestParameterInjector/issues/40). + */ + static Constructor<?> getOnlyConstructor(Class<?> testClass) { + ImmutableList<Constructor<?>> constructors = ImmutableList.copyOf(testClass.getConstructors()); + if (constructors.isEmpty()) { + // There are no public constructors. This is likely a JUnit5 test, so we should take the only + // non-public constructor instead. + constructors = ImmutableList.copyOf(testClass.getDeclaredConstructors()); + } + checkState( + constructors.size() == 1, "Expected exactly one constructor, but got %s", constructors); + return getOnlyElement(constructors); + } + + private TestParameterInjectorUtils() {} +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java new file mode 100644 index 0000000..3733833 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValidator.java @@ -0,0 +1,68 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import com.google.common.base.Optional; +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Validator interface which allows {@link TestParameterAnnotation} annotations to validate the set + * of annotation values for a given test instance, and to selectively skip the test. + */ +interface TestParameterValidator { + + /** + * This interface allows to access information on the current testwhen implementing {@link + * TestParameterValidator}. + */ + interface Context { + + /** Returns whether the current test has the {@link TestParameterAnnotation} value(s). */ + boolean has(Class<? extends Annotation> testParameter, Object value); + + /** + * Returns whether the current test has the two {@link TestParameterAnnotation} values, granted + * that the value is an enum, and each enum corresponds to a unique annotation. + */ + <T extends Enum<T>, U extends Enum<U>> boolean has(T value1, U value2); + + /** + * Returns all the current test value for a given {@link TestParameterAnnotation} annotated + * annotation. + */ + Optional<Object> getValue(Class<? extends Annotation> testParameter); + + /** + * Returns all the values specified for a given {@link TestParameterAnnotation} annotated + * annotation in the test. + * + * <p>For example, if the test annotates '@Foo(a,b,c)', getSpecifiedValues(Foo.class) will + * return [a,b,c]. + */ + List<Object> getSpecifiedValues(Class<? extends Annotation> testParameter); + } + + /** + * Returns whether the test should be skipped based on the annotations' values. + * + * <p>The {@code testParameterValues} list contains all {@link TestParameterAnnotation} + * annotations, including those specified at the class, field, method, method parameter, + * constructor, and constructor parameter for a given test. + * + * <p>This method is not invoked in the context of a running test statement. + */ + boolean shouldSkip(Context context); +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValue.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValue.java new file mode 100644 index 0000000..a16f0cb --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValue.java @@ -0,0 +1,57 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Optional; +import javax.annotation.Nullable; + +/** + * Wrapper class around a parameter value. Use this to give a value a name that is different from + * its {@code toString()} method. + */ +public class TestParameterValue { + private final @Nullable Object wrappedValue; + private final Optional<String> customName; + + private TestParameterValue(@Nullable Object wrappedValue, Optional<String> customName) { + this.wrappedValue = wrappedValue; + this.customName = checkNotNull(customName); + } + + /** Wraps the given value. */ + public static TestParameterValue wrap(@Nullable Object wrappedValue) { + return new TestParameterValue(wrappedValue, /* customName= */ Optional.absent()); + } + + /** + * Returns a new {@link TestParameterValue} instance that stores the given name. The + * TestParameterInjector framework will use this name instead of {@code wrappedValue.toString()} + * when generating the test name. + */ + public TestParameterValue withName(String name) { + return new TestParameterValue(wrappedValue, Optional.of(name)); + } + + @Nullable + Object getWrappedValue() { + return wrappedValue; + } + + Optional<String> getCustomName() { + return customName; + } +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java new file mode 100644 index 0000000..38c3356 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValueProvider.java @@ -0,0 +1,94 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import com.google.common.base.Optional; +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Interface which allows {@link TestParameterAnnotation} annotations to provide the values to test + * in a dynamic way. + */ +interface TestParameterValueProvider { + + /** + * Returns the parameter values for which the test should run. + * + * @param annotation The annotation instance that was encountered in the test class. The + * definition of this annotation is itself annotated with the {@link TestParameterAnnotation} + * annotation. + * @param parameterClass The class of the parameter or field that is being annotated. In case the + * annotation is annotating a method, constructor or class, {@code parameterClass} is an empty + * optional. + */ + default List<Object> provideValues(Annotation annotation, Optional<Class<?>> parameterClass) { + throw new UnsupportedOperationException( + "If this is called by TestParameterInjector, it means that neither of the" + + " provideValues()-type methods have been implemented"); + } + + /** + * Extension of {@link #provideValues(Annotation, Optional<Class<?>>)} with extra context. + * + * @param annotation The annotation instance that was encountered in the test class. The + * definition of this annotation is itself annotated with the {@link TestParameterAnnotation} + * annotation. + * @param otherAnnotations A list of all other annotations on the field or parameter that was + * annotated with {@code annotation}. + * <p>For example, if the test code is as follows: + * <pre> + * @Test + * public void myTest_success( + * @CustomAnnotation(123) @TestParameter(valuesProvider=MyProvider.class) Foo foo) { + * ... + * } + * </pre> + * then this list will contain a single element: @CustomAnnotation(123). + * <p>In case the annotation is annotating a method, constructor or class, {@code + * parameterClass} is an empty list. + * @param parameterClass The class of the parameter or field that is being annotated. In case the + * annotation is annotating a method, constructor or class, {@code parameterClass} is an empty + * optional. + * @param testClass The class that contains the test that is currently being run. + * <p>Having this can be useful when sharing providers between tests that have the same base + * class. In those cases, an abstract method can be called as follows: + * <pre> + * ((MyBaseClass) context.testClass().newInstance()).myAbstractMethod() + * </pre> + * + * @deprecated Don't use this method outside of the testparameterinjector codebase, as it is prone + * to being changed. + */ + @Deprecated + default List<Object> provideValues( + Annotation annotation, Optional<Class<?>> parameterClass, GenericParameterContext context) { + return provideValues(annotation, parameterClass); + } + + /** + * Returns the class of the list elements returned by {@link #provideValues(Annotation, + * Optional)}. + * + * @param annotationType The type of the annotation that was encountered in the test class. The + * definition of this annotation is itself annotated with the {@link TestParameterAnnotation} + * annotation. + * @param parameterClass The class of the parameter or field that is being annotated. In case the + * annotation is annotating a method, constructor or class, {@code parameterClass} is an empty + * optional. + */ + Class<?> getValueType( + Class<? extends Annotation> annotationType, Optional<Class<?>> parameterClass); +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java new file mode 100644 index 0000000..5207ec6 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValues.java @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import com.google.common.base.Optional; +import java.lang.annotation.Annotation; + +/** Interface to retrieve the {@link TestParameterAnnotation} values for a test. */ +interface TestParameterValues { + /** + * Returns a {@link TestParameterAnnotation} value for the current test as specified by {@code + * testInfo}, or {@link Optional#absent()} if the {@code annotationType} is not found. + */ + Optional<Object> getValue(Class<? extends Annotation> annotationType); +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java new file mode 100644 index 0000000..ccdb18b --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameterValuesProvider.java @@ -0,0 +1,162 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.NoSuchElementException; +import javax.annotation.Nullable; + +/** + * Abstract class for custom providers of @TestParameter values. + * + * <p>This is a replacement for {@link TestParameter.TestParameterValuesProvider}, which will soon + * be deprecated. The difference with the former interface is that this class provides a {@code + * Context} instance when invoking {@link #provideValues}. + */ +public abstract class TestParameterValuesProvider + implements TestParameter.TestParameterValuesProvider { + + protected abstract List<?> provideValues(Context context) throws Exception; + + /** + * @deprecated This method should never be called as it will simply throw an {@link + * UnsupportedOperationException}. + */ + @Override + @Deprecated + public final List<?> provideValues() { + throw new UnsupportedOperationException( + "The TestParameterInjector framework should never call this method, and instead call" + + " #provideValues(Context)"); + } + + /** + * Wraps the given value in an object that allows you to give the parameter value a different + * name. The TestParameterInjector framework will recognize the returned {@link + * TestParameterValue} instances and unwrap them at injection time. + * + * <p>Usage: {@code value(file.content).withName(file.name)}. + */ + @Override + public final TestParameterValue value(@Nullable Object wrappedValue) { + // Overriding this method as final because it is not supposed to be overwritten + return TestParameterValue.wrap(wrappedValue); + } + + /** + * An immutable value class that contains extra information about the context of the parameter for + * which values are being provided. + */ + public static final class Context { + + private final GenericParameterContext delegate; + + Context(GenericParameterContext delegate) { + this.delegate = delegate; + } + + /** + * Returns the only annotation with the given type on the field or parameter that was annotated + * with @TestParameter. + * + * <p>For example, if the test code is as follows: + * + * <pre> + * {@literal @}Test + * public void myTest_success( + * {@literal @}CustomAnnotation(123) {@literal @}TestParameter(valuesProvider=MyProvider.class) Foo foo) { + * ... + * } + * </pre> + * + * then {@code context.getOtherAnnotation(CustomAnnotation.class).value()} will equal 123. + * + * @throws NoSuchElementException if this there is no annotation with the given type + * @throws IllegalArgumentException if there are multiple annotations with the given type + * @throws IllegalArgumentException if the argument it TestParameter.class because it is already + * handled by the TestParameterInjector framework. + */ + public <A extends Annotation> A getOtherAnnotation(Class<A> annotationType) { + checkArgument( + !TestParameter.class.equals(annotationType), + "Getting the @TestParameter annotating the field or parameter is not allowed because" + + " it is already handled by the TestParameterInjector framework."); + return delegate.getAnnotation(annotationType); + } + + /** + * Returns the only annotation with the given type on the field or parameter that was annotated + * with @TestParameter. + * + * <p>For example, if the test code is as follows: + * + * <pre> + * {@literal @}Test + * public void myTest_success( + * {@literal @}CustomAnnotation(123) + * {@literal @}CustomAnnotation(456) + * {@literal @}TestParameter(valuesProvider=MyProvider.class) + * Foo foo) { + * ... + * } + * </pre> + * + * then {@code context.getOtherAnnotations(CustomAnnotation.class)} will return the annotation + * with 123 and 456. + * + * <p>Returns an empty list if this there is no annotation with the given type. + * + * @throws IllegalArgumentException if the argument it TestParameter.class because it is already + * handled by the TestParameterInjector framework. + */ + public <A extends Annotation> ImmutableList<A> getOtherAnnotations(Class<A> annotationType) { + checkArgument( + !TestParameter.class.equals(annotationType), + "Getting the @TestParameter annotating the field or parameter is not allowed because" + + " it is already handled by the TestParameterInjector framework."); + return delegate.getAnnotations(annotationType); + } + + /** + * The class that contains the test that is currently being run. + * + * <p>Having this can be useful when sharing providers between tests that have the same base + * class. In those cases, an abstract method can be called as follows: + * + * <pre> + * ((MyBaseClass) context.testClass().newInstance()).myAbstractMethod() + * </pre> + */ + public Class<?> testClass() { + return delegate.testClass(); + } + + /** A list of all annotations on the field or parameter. */ + @VisibleForTesting + ImmutableList<Annotation> annotationsOnParameter() { + return delegate.annotationsOnParameter(); + } + + @Override + public String toString() { + return delegate.toString(); + } + } +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java new file mode 100644 index 0000000..684e770 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParameters.java @@ -0,0 +1,276 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.util.Collections.unmodifiableMap; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValuesProvider; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * Annotation that can be placed (repeatedly) on @Test-methods or a test constructor to indicate the + * sets of parameters that it should be invoked with. + * + * <p>For @Test-methods, the method will be invoked for every set of parameters that is specified. + * For constructors, all the tests in the test class will be invoked on a class instance that was + * constructed by each set of parameters. + * + * <p>Note: If this annotation is used in a test class, the other methods in that class can still + * use other types of parameterization, such as {@linkplain TestParameter @TestParameter}. + * + * <p>See {@link #value()} for simple examples. + * + * <p>Warning: This annotation can only be used if the compiled java code contains the parameter + * names. This is typically done by passing the {@code -parameters} option to the Java compiler, + * which requires using Java 8 or higher and may not be available on Android. + */ +@Retention(RUNTIME) +@Target({CONSTRUCTOR, METHOD}) +@Repeatable(TestParameters.RepeatedTestParameters.class) +public @interface TestParameters { + + /** + * Specifies one or more stringified sets of parameters in YAML format. Each set corresponds to a + * single invocation of a test method. + * + * <p>Each element in this array is a full parameter set, formatted as a YAML mapping. The mapping + * keys must match the parameter names and the mapping values will be converted to the parameter + * type if possible. See yaml.org for the YAML syntax and the section below on the supported + * parameter types. + * + * <p>There are two distinct ways of using this annotation: repeated vs single: + * + * <p><b>Recommended usage: Separate annotation per parameter set</b> + * + * <p>This approach uses multiple @TestParameters annotations, one for each set of parameters, for + * example: + * + * <pre> + * {@literal @}Test + * {@literal @}TestParameters("{age: 17, expectIsAdult: false}") + * {@literal @}TestParameters("{age: 22, expectIsAdult: true}") + * public void personIsAdult(int age, boolean expectIsAdult) { ... } + * + * {@literal @}Test + * {@literal @}TestParameters("{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}") + * {@literal @}TestParameters("{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}") + * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... } + * </pre> + * + * <p><b>Old discouraged usage: Single annotation with all parameter sets</b> + * + * <p>This approach uses a single @TestParameter annotation for all parameter sets, for example: + * + * <pre> + * {@literal @}Test + * {@literal @}TestParameters({ + * "{age: 17, expectIsAdult: false}", + * "{age: 22, expectIsAdult: true}", + * }) + * public void personIsAdult(int age, boolean expectIsAdult) { ... } + * + * {@literal @}Test + * {@literal @}TestParameters({ + * "{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}", + * "{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}", + * }) + * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... } + * </pre> + * + * <p><b>Supported parameter types</b> + * + * <ul> + * <li>YAML primitives: + * <ul> + * <li>String: Specified as YAML string + * <li>boolean: Specified as YAML boolean + * <li>long and int: Specified as YAML integer + * <li>float and double: Specified as YAML floating point or integer + * </ul> + * <li> + * <li>Parsed types: + * <ul> + * <li>Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()} + * <li>Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML + * bytes (example: "!!binary 'ZGF0YQ=='") + * </ul> + * <li> + * </ul> + * + * <p>For dynamic sets of parameters or parameter types that are not supported here, use {@link + * #valuesProvider()} and leave this field empty. + */ + String[] value() default {}; + + /** + * Overrides the name of the parameter set that is used in the test name. + * + * <p>This can only be set if {@link #value()} has exactly one element. If not set, the YAML + * string in {@link #value()} is used in the test name. + * + * <p>For example: If this name is set to "young adult", then the test name might be + * "personIsAdult[young adult]" where the default might have been "personIsAdult[{age: 17, + * expectIsAdult: false}]". + */ + String customName() default ""; + + /** + * Sets a provider that will return a list of parameter sets. Each element in the returned list + * corresponds to a single invocation of a test method. + * + * <p>If this field is set, {@link #value()} must be empty and vice versa. + * + * <p><b>Example</b> + * + * <pre> + * {@literal @}Test + * {@literal @}TestParameters(valuesProvider = IsAdultValueProvider.class) + * public void personIsAdult(int age, boolean expectIsAdult) { ... } + * + * private static final class IsAdultValueProvider implements TestParametersValuesProvider { + * {@literal @}Override public {@literal List<TestParametersValues>} provideValues() { + * return ImmutableList.of( + * TestParametersValues.builder() + * .name("teenager") + * .addParameter("age", 17) + * .addParameter("expectIsAdult", false) + * .build(), + * TestParametersValues.builder() + * .name("young adult") + * .addParameter("age", 22) + * .addParameter("expectIsAdult", true) + * .build() + * ); + * } + * } + * </pre> + */ + Class<? extends TestParametersValuesProvider> valuesProvider() default + DefaultTestParametersValuesProvider.class; + + /** Interface for custom providers of test parameter values. */ + interface TestParametersValuesProvider { + List<TestParametersValues> provideValues(); + } + + /** A set of parameters for a single method invocation. */ + @AutoValue + abstract class TestParametersValues { + + /** + * A name for this set of parameters that will be used for describing this test. + * + * <p>Example: If a test method is called "personIsAdult" and this name is "teenager", the name + * of the resulting test will be "personIsAdult[teenager]". + */ + public abstract String name(); + + /** A map, mapping parameter names to their values. */ + @SuppressWarnings("AutoValueImmutableFields") // intentional to allow null values + public abstract Map<String, Object> parametersMap(); + + public static Builder builder() { + return new Builder(); + } + + // Avoid instantiations other than the AutoValue one. + TestParametersValues() {} + + /** Builder for {@link TestParametersValues}. */ + public static final class Builder { + private String name; + private final LinkedHashMap<String, Object> parametersMap = new LinkedHashMap<>(); + + /** + * Sets a name for this set of parameters that will be used for describing this test. + * + * <p>Setting a name is optional. If unset, one will be generated from the parameter values. + * + * <p>Example: If a test method is called "personIsAdult" and this name is "teenager", the + * name of the resulting test will be "personIsAdult[teenager]". + */ + public Builder name(String name) { + this.name = name.replaceAll("\\s+", " "); + return this; + } + + /** + * Adds a parameter by its name. + * + * @param parameterName The name of the parameter of the test method + * @param value A value of the same type as the method parameter + */ + public Builder addParameter(String parameterName, @Nullable Object value) { + this.parametersMap.put(parameterName, value); + return this; + } + + /** Adds parameters by thris names. */ + public Builder addParameters(Map<String, Object> parameterNameToValueMap) { + this.parametersMap.putAll(parameterNameToValueMap); + return this; + } + + public TestParametersValues build() { + if (name == null) { + // Name is not set. Auto-generate one based on the parameter name and values + StringBuilder nameBuilder = new StringBuilder(); + nameBuilder.append('{'); + for (String parameterName : parametersMap.keySet()) { + if (nameBuilder.length() > 1) { + nameBuilder.append(", "); + } + nameBuilder.append( + ParameterValueParsing.formatTestNameString( + Optional.of(parameterName), parametersMap.get(parameterName))); + } + nameBuilder.append('}'); + name = nameBuilder.toString(); + } + return new AutoValue_TestParameters_TestParametersValues( + name, unmodifiableMap(new LinkedHashMap<>(parametersMap))); + } + } + } + + /** Default {@link TestParametersValuesProvider} implementation that does nothing. */ + class DefaultTestParametersValuesProvider implements TestParametersValuesProvider { + @Override + public List<TestParametersValues> provideValues() { + return ImmutableList.of(); + } + } + + /** + * Holder annotation for multiple @TestParameters annotations. This should never be used directly. + */ + @Retention(RUNTIME) + @Target({CONSTRUCTOR, METHOD}) + @interface RepeatedTestParameters { + TestParameters[] value(); + } +} diff --git a/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java new file mode 100644 index 0000000..7dffc29 --- /dev/null +++ b/junit4/src/main/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessor.java @@ -0,0 +1,468 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Verify.verify; + +import com.google.auto.value.AutoAnnotation; +import com.google.common.base.Optional; +import com.google.common.base.Throwables; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.common.primitives.Primitives; +import com.google.common.reflect.TypeToken; +import com.google.common.util.concurrent.UncheckedExecutionException; +import com.google.testing.junit.testparameterinjector.TestInfo.TestInfoParameter; +import com.google.testing.junit.testparameterinjector.TestParameters.DefaultTestParametersValuesProvider; +import com.google.testing.junit.testparameterinjector.TestParameters.RepeatedTestParameters; +import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValues; +import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValuesProvider; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** {@code TestMethodProcessor} implementation for supporting {@link TestParameters}. */ +@SuppressWarnings("AndroidJdkLibsChecker") // Parameter is not available on old Android SDKs. +final class TestParametersMethodProcessor implements TestMethodProcessor { + + private final LoadingCache<Executable, ImmutableList<TestParametersValues>> + parameterValuesByConstructorOrMethodCache = + CacheBuilder.newBuilder() + .maximumSize(1000) + .build(CacheLoader.from(TestParametersMethodProcessor::toParameterValuesList)); + + @Override + public ExecutableValidationResult validateConstructor(Constructor<?> constructor) { + if (hasRelevantAnnotation(constructor)) { + try { + // This method throws an exception if there is a validation error + getConstructorParameters(constructor); + } catch (Throwable t) { + return ExecutableValidationResult.validated(t); + } + return ExecutableValidationResult.valid(); + } else { + return ExecutableValidationResult.notValidated(); + } + } + + @Override + public ExecutableValidationResult validateTestMethod(Method testMethod, Class<?> testClass) { + if (hasRelevantAnnotation(testMethod)) { + try { + // This method throws an exception if there is a validation error + getMethodParameters(testMethod); + } catch (Throwable t) { + return ExecutableValidationResult.validated(t); + } + return ExecutableValidationResult.valid(); + } else { + return ExecutableValidationResult.notValidated(); + } + } + + @Override + public List<TestInfo> calculateTestInfos(TestInfo originalTest) { + boolean constructorIsParameterized = + hasRelevantAnnotation( + TestParameterInjectorUtils.getOnlyConstructor(originalTest.getTestClass())); + boolean methodIsParameterized = hasRelevantAnnotation(originalTest.getMethod()); + + if (!constructorIsParameterized && !methodIsParameterized) { + return ImmutableList.of(originalTest); + } + + ImmutableList.Builder<TestInfo> testInfos = ImmutableList.builder(); + + ImmutableList<Optional<TestParametersValues>> constructorParametersList = + getConstructorParametersOrSingleAbsentElement(originalTest.getTestClass()); + ImmutableList<Optional<TestParametersValues>> methodParametersList = + getMethodParametersOrSingleAbsentElement(originalTest.getMethod()); + for (int constructorParametersIndex = 0; + constructorParametersIndex < constructorParametersList.size(); + ++constructorParametersIndex) { + Optional<TestParametersValues> constructorParameters = + constructorParametersList.get(constructorParametersIndex); + + for (int methodParametersIndex = 0; + methodParametersIndex < methodParametersList.size(); + ++methodParametersIndex) { + Optional<TestParametersValues> methodParameters = + methodParametersList.get(methodParametersIndex); + + // Making final copies of non-final integers for use in lambda + int constructorParametersIndexCopy = constructorParametersIndex; + int methodParametersIndexCopy = methodParametersIndex; + + testInfos.add( + originalTest + .withExtraParameters( + FluentIterable.of( + constructorParameters.transform( + param -> + TestInfoParameter.create( + param.name(), + param.parametersMap(), + constructorParametersIndexCopy)), + methodParameters.transform( + param -> + TestInfoParameter.create( + param.name(), + param.parametersMap(), + methodParametersIndexCopy))) + .filter(Optional::isPresent) + .transform(Optional::get) + .toList()) + .withExtraAnnotation( + TestIndexHolderFactory.create( + constructorParametersIndex, methodParametersIndex))); + } + } + return testInfos.build(); + } + + private ImmutableList<Optional<TestParametersValues>> + getConstructorParametersOrSingleAbsentElement(Class<?> testClass) { + Constructor<?> constructor = TestParameterInjectorUtils.getOnlyConstructor(testClass); + return hasRelevantAnnotation(constructor) + ? FluentIterable.from(getConstructorParameters(constructor)) + .transform(Optional::of) + .toList() + : ImmutableList.of(Optional.absent()); + } + + private ImmutableList<Optional<TestParametersValues>> getMethodParametersOrSingleAbsentElement( + Method method) { + return hasRelevantAnnotation(method) + ? FluentIterable.from(getMethodParameters(method)).transform(Optional::of).toList() + : ImmutableList.of(Optional.absent()); + } + + @Override + public Optional<List<Object>> maybeGetConstructorParameters( + Constructor<?> constructor, TestInfo testInfo) { + if (hasRelevantAnnotation(constructor)) { + ImmutableList<TestParametersValues> parameterValuesList = + getConstructorParameters(constructor); + TestParametersValues parametersValues = + parameterValuesList.get( + testInfo.getAnnotation(TestIndexHolder.class).constructorParametersIndex()); + + return Optional.of(toParameterList(parametersValues, constructor.getParameters())); + } else { + return Optional.absent(); + } + } + + @Override + public Optional<List<Object>> maybeGetTestMethodParameters(TestInfo testInfo) { + Method testMethod = testInfo.getMethod(); + if (hasRelevantAnnotation(testMethod)) { + ImmutableList<TestParametersValues> parameterValuesList = getMethodParameters(testMethod); + TestParametersValues parametersValues = + parameterValuesList.get( + testInfo.getAnnotation(TestIndexHolder.class).methodParametersIndex()); + + return Optional.of(toParameterList(parametersValues, testMethod.getParameters())); + } else { + return Optional.absent(); + } + } + + @Override + public void postProcessTestInstance(Object testInstance, TestInfo testInfo) {} + + private ImmutableList<TestParametersValues> getConstructorParameters(Constructor<?> constructor) { + try { + return parameterValuesByConstructorOrMethodCache.getUnchecked(constructor); + } catch (UncheckedExecutionException e) { + // Rethrow IllegalStateException because they can be caused by user mistakes and the user + // doesn't need to know that the caching layer is in between. + Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); + throw e; + } + } + + private ImmutableList<TestParametersValues> getMethodParameters(Method method) { + try { + return parameterValuesByConstructorOrMethodCache.getUnchecked(method); + } catch (UncheckedExecutionException e) { + // Rethrow IllegalStateException because they can be caused by user mistakes and the user + // doesn't need to know that the caching layer is in between. + Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); + throw e; + } + } + + private static ImmutableList<TestParametersValues> toParameterValuesList(Executable executable) { + checkParameterNamesArePresent(executable); + ImmutableList<Parameter> parametersList = ImmutableList.copyOf(executable.getParameters()); + + if (executable.isAnnotationPresent(TestParameters.class)) { + checkState( + !executable.isAnnotationPresent(RepeatedTestParameters.class), + "Unexpected situation: Both @TestParameters and @RepeatedTestParameters annotating the" + + " same method"); + TestParameters annotation = executable.getAnnotation(TestParameters.class); + boolean valueIsSet = annotation.value().length > 0; + boolean valuesProviderIsSet = + !annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class); + + checkState( + !(valueIsSet && valuesProviderIsSet), + "It is not allowed to specify both value and valuesProvider in @TestParameters(value=%s," + + " valuesProvider=%s) on %s()", + Arrays.toString(annotation.value()), + annotation.valuesProvider().getSimpleName(), + executable.getName()); + checkState( + valueIsSet || valuesProviderIsSet, + "Either a value or a valuesProvider must be set in @TestParameters on %s()", + executable.getName()); + if (!annotation.customName().isEmpty()) { + checkState( + annotation.value().length == 1, + "Setting @TestParameters.customName is only allowed if there is exactly one YAML string" + + " in @TestParameters.value (on %s())", + executable.getName()); + } + + if (valueIsSet) { + return FluentIterable.from(annotation.value()) + .transform( + yamlMap -> toParameterValues(yamlMap, parametersList, annotation.customName())) + .toList(); + } else { + return toParameterValuesList(annotation.valuesProvider(), parametersList); + } + } else { // Not annotated with @TestParameters + verify( + executable.isAnnotationPresent(RepeatedTestParameters.class), + "This method should only be called for executables with at least one relevant" + + " annotation"); + + return FluentIterable.from(executable.getAnnotation(RepeatedTestParameters.class).value()) + .transform( + annotation -> + toParameterValues( + validateAndGetSingleValueFromRepeatedAnnotation(annotation, executable), + parametersList, + annotation.customName())) + .toList(); + } + } + + private static ImmutableList<TestParametersValues> toParameterValuesList( + Class<? extends TestParametersValuesProvider> valuesProvider, List<Parameter> parameters) { + try { + Constructor<? extends TestParametersValuesProvider> constructor = + valuesProvider.getDeclaredConstructor(); + constructor.setAccessible(true); + List<TestParametersValues> testParametersValues = constructor.newInstance().provideValues(); + for (TestParametersValues testParametersValue : testParametersValues) { + validateThatValuesMatchParameters(testParametersValue, parameters); + } + return ImmutableList.copyOf(testParametersValues); + } catch (NoSuchMethodException e) { + if (!Modifier.isStatic(valuesProvider.getModifiers()) && valuesProvider.isMemberClass()) { + throw new IllegalStateException( + String.format( + "Could not find a no-arg constructor for %s, probably because it is a not-static" + + " inner class. You can fix this by making %s static.", + valuesProvider.getSimpleName(), valuesProvider.getSimpleName()), + e); + } else { + throw new IllegalStateException( + String.format( + "Could not find a no-arg constructor for %s.", valuesProvider.getSimpleName()), + e); + } + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + } + + private static void checkParameterNamesArePresent(Executable executable) { + checkState( + FluentIterable.from(executable.getParameters()).allMatch(Parameter::isNamePresent), + "" + + "No parameter name could be found for %s, which likely means that parameter names" + + " aren't available at runtime. Please ensure that the this test was built with the" + + " -parameters compiler option.\n" + + "\n" + + "In Maven, you do this by adding <parameters>true</parameters> to the" + + " maven-compiler-plugin's configuration. For example:\n" + + "\n" + + "<build>\n" + + " <plugins>\n" + + " <plugin>\n" + + " <groupId>org.apache.maven.plugins</groupId>\n" + + " <artifactId>maven-compiler-plugin</artifactId>\n" + + " <version>3.8.1</version>\n" + + " <configuration>\n" + + " <compilerArgs>\n" + + " <arg>-parameters</arg>\n" + + " </compilerArgs>\n" + + " </configuration>\n" + + " </plugin>\n" + + " </plugins>\n" + + "</build>\n" + + "\n" + + "Don't forget to run `mvn clean` after making this change.", + executable.getName()); + } + + private static String validateAndGetSingleValueFromRepeatedAnnotation( + TestParameters annotation, Executable executable) { + checkState( + annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class), + "Setting a valuesProvider is not supported for methods/constructors with" + + " multiple @TestParameters annotations on %s()", + executable.getName()); + checkState( + annotation.value().length > 0, + "Either a value or a valuesProvider must be set in @TestParameters on %s()", + executable.getName()); + checkState( + annotation.value().length == 1, + "When specifying more than one @TestParameter for a method/constructor, each annotation" + + " must have exactly one value. Instead, got %s values on %s(): %s", + annotation.value().length, + executable.getName(), + Arrays.toString(annotation.value())); + + return annotation.value()[0]; + } + + private static void validateThatValuesMatchParameters( + TestParametersValues testParametersValues, List<Parameter> parameters) { + ImmutableMap<String, Parameter> parametersByName = + Maps.uniqueIndex(parameters, Parameter::getName); + + checkState( + testParametersValues.parametersMap().keySet().equals(parametersByName.keySet()), + "Cannot map the given TestParametersValues to parameters %s (Given TestParametersValues" + + " are %s)", + parametersByName.keySet(), + testParametersValues); + + testParametersValues + .parametersMap() + .forEach( + (paramName, paramValue) -> { + Class<?> expectedClass = Primitives.wrap(parametersByName.get(paramName).getType()); + if (paramValue != null) { + checkState( + expectedClass.isInstance(paramValue), + "Cannot map value '%s' (class = %s) to parameter %s (class = %s) (for" + + " TestParametersValues %s)", + paramValue, + paramValue.getClass(), + paramName, + expectedClass, + testParametersValues); + } + }); + } + + private static TestParametersValues toParameterValues( + String yamlString, List<Parameter> parameters, String maybeCustomName) { + Object yamlMapObject = ParameterValueParsing.parseYamlStringToObject(yamlString); + checkState( + yamlMapObject instanceof Map, + "Cannot map YAML string '%s' to parameters because it is not a mapping", + yamlString); + Map<?, ?> yamlMap = (Map<?, ?>) yamlMapObject; + + ImmutableMap<String, Parameter> parametersByName = + Maps.uniqueIndex(parameters, Parameter::getName); + checkState( + yamlMap.keySet().equals(parametersByName.keySet()), + "Cannot map YAML string '%s' to parameters %s", + yamlString, + parametersByName.keySet()); + + @SuppressWarnings("unchecked") + Map<String, Object> checkedYamlMap = (Map<String, Object>) yamlMap; + + return TestParametersValues.builder() + .name(maybeCustomName.isEmpty() ? yamlString : maybeCustomName) + .addParameters( + Maps.transformEntries( + checkedYamlMap, + (parameterName, parsedYaml) -> + ParameterValueParsing.parseYamlObjectToJavaType( + parsedYaml, + TypeToken.of(parametersByName.get(parameterName).getParameterizedType())))) + .build(); + } + + // Note: We're not using the Executable interface here because it isn't supported by Java 7 and + // this code is called even if only @TestParameter is used. In other places, Executable is usable + // because @TestParameters only works for Java 8 anyway. + private static boolean hasRelevantAnnotation(Constructor<?> executable) { + return executable.isAnnotationPresent(TestParameters.class) + || executable.isAnnotationPresent(RepeatedTestParameters.class); + } + + private static boolean hasRelevantAnnotation(Method executable) { + return executable.isAnnotationPresent(TestParameters.class) + || executable.isAnnotationPresent(RepeatedTestParameters.class); + } + + private static List<Object> toParameterList( + TestParametersValues parametersValues, Parameter[] parameters) { + return Arrays.asList( + FluentIterable.from(Arrays.asList(parameters)) + .transform(Parameter::getName) + .transform(name -> parametersValues.parametersMap().get(name)) + .toArray(Object.class)); + } + + /** + * This mechanism is a workaround to be able to store the test index in the annotation list of the + * {@link TestInfo}, since we cannot carry other information through the test runner. + */ + @Retention(RetentionPolicy.RUNTIME) + @interface TestIndexHolder { + int constructorParametersIndex(); + + int methodParametersIndex(); + } + + /** Factory for {@link TestIndexHolder}. */ + static class TestIndexHolderFactory { + @AutoAnnotation + static TestIndexHolder create(int constructorParametersIndex, int methodParametersIndex) { + return new AutoAnnotation_TestParametersMethodProcessor_TestIndexHolderFactory_create( + constructorParametersIndex, methodParametersIndex); + } + + private TestIndexHolderFactory() {} + } +} diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java new file mode 100644 index 0000000..a9336b7 --- /dev/null +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/ParameterValueParsingTest.java @@ -0,0 +1,206 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Optional; +import com.google.common.primitives.UnsignedLong; +import com.google.protobuf.ByteString; +import java.math.BigInteger; +import javax.annotation.Nullable; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public class ParameterValueParsingTest { + + @Test + public void parseEnum_success() throws Exception { + Enum<?> result = ParameterValueParsing.parseEnum("BBB", TestEnum.class); + + assertThat(result).isEqualTo(TestEnum.BBB); + } + + @Test + @TestParameters({ + "{yamlString: '{a: b, c: 15}', valid: true}", + "{yamlString: '{a: b c: 15', valid: false}", + "{yamlString: 'a: b c: 15', valid: false}", + }) + public void isValidYamlString_success(String yamlString, boolean valid) throws Exception { + boolean result = ParameterValueParsing.isValidYamlString(yamlString); + + assertThat(result).isEqualTo(valid); + } + + enum ParseYamlValueToJavaTypeCases { + STRING_TO_STRING( + /* yamlString= */ "abc", /* javaClass= */ String.class, /* expectedResult= */ "abc"), + BOOLEAN_TO_STRING( + /* yamlString= */ "true", /* javaClass= */ String.class, /* expectedResult= */ "true"), + INT_TO_STRING( + /* yamlString= */ "123", /* javaClass= */ String.class, /* expectedResult= */ "123"), + LONG_TO_STRING( + /* yamlString= */ "442147483648", + /* javaClass= */ String.class, + /* expectedResult= */ "442147483648"), + BIG_INTEGER_TO_BIGINTEGER( + /* yamlString= */ "1000000000000000000000000000", + /* javaClass= */ BigInteger.class, + /* expectedResult= */ new BigInteger("1000000000000000000000000000")), + BIG_INTEGER_TO_UNSIGNED_LONG( + /* yamlString= */ "18446744073709551615", // This is UnsignedLong.MAX_VALUE. + /* javaClass= */ UnsignedLong.class, + /* expectedResult= */ UnsignedLong.MAX_VALUE), + LONG_TO_UNSIGNED_LONG( + /* yamlString= */ "10000000000000", + /* javaClass= */ UnsignedLong.class, + /* expectedResult= */ UnsignedLong.fromLongBits(10000000000000L)), + LONG_TO_BIG_INTEGER( + /* yamlString= */ "10000000000000", + /* javaClass= */ BigInteger.class, + /* expectedResult= */ BigInteger.valueOf(10000000000000L)), + INTEGER_TO_BIG_INTEGER( + /* yamlString= */ "1000000", + /* javaClass= */ BigInteger.class, + /* expectedResult= */ BigInteger.valueOf(1000000)), + INTEGER_TO_UNSIGNED_LONG( + /* yamlString= */ "1000000", + /* javaClass= */ UnsignedLong.class, + /* expectedResult= */ UnsignedLong.fromLongBits(1000000)), + DOUBLE_TO_STRING( + /* yamlString= */ "1.23", /* javaClass= */ String.class, /* expectedResult= */ "1.23"), + + BOOLEAN_TO_BOOLEAN( + /* yamlString= */ "true", /* javaClass= */ Boolean.class, /* expectedResult= */ true), + + INT_TO_INT(/* yamlString= */ "123", /* javaClass= */ int.class, /* expectedResult= */ 123), + + LONG_TO_LONG( + /* yamlString= */ "442147483648", + /* javaClass= */ long.class, + /* expectedResult= */ 442147483648L), + INT_TO_LONG(/* yamlString= */ "123", /* javaClass= */ Long.class, /* expectedResult= */ 123L), + + DOUBLE_TO_DOUBLE( + /* yamlString= */ "1.23", /* javaClass= */ Double.class, /* expectedResult= */ 1.23), + INT_TO_DOUBLE( + /* yamlString= */ "123", /* javaClass= */ Double.class, /* expectedResult= */ 123.0), + LONG_TO_DOUBLE( + /* yamlString= */ "442147483648", + /* javaClass= */ Double.class, + /* expectedResult= */ 442147483648.0), + NAN_TO_DOUBLE( + /* yamlString= */ "NaN", /* javaClass= */ Double.class, /* expectedResult= */ Double.NaN), + INFINITY_TO_DOUBLE( + /* yamlString= */ "Infinity", + /* javaClass= */ Double.class, + /* expectedResult= */ Double.POSITIVE_INFINITY), + POSITIVE_INFINITY_TO_DOUBLE( + /* yamlString= */ "+Infinity", + /* javaClass= */ Double.class, + /* expectedResult= */ Double.POSITIVE_INFINITY), + NEGATIVE_INFINITY_TO_DOUBLE( + /* yamlString= */ "-Infinity", + /* javaClass= */ Double.class, + /* expectedResult= */ Double.NEGATIVE_INFINITY), + + DOUBLE_TO_FLOAT( + /* yamlString= */ "1.23", /* javaClass= */ Float.class, /* expectedResult= */ 1.23F), + INT_TO_FLOAT(/* yamlString= */ "123", /* javaClass= */ Float.class, /* expectedResult= */ 123F), + + STRING_TO_ENUM( + /* yamlString= */ "AAA", + /* javaClass= */ TestEnum.class, + /* expectedResult= */ TestEnum.AAA), + + STRING_TO_BYTES( + /* yamlString= */ "data", + /* javaClass= */ byte[].class, + /* expectedResult= */ "data".getBytes()), + + BYTES_TO_BYTES( + /* yamlString= */ "!!binary 'ZGF0YQ=='", + /* javaClass= */ byte[].class, + /* expectedResult= */ "data".getBytes()), + + STRING_TO_BYTESTRING( + /* yamlString= */ "'data'", + /* javaClass= */ ByteString.class, + /* expectedResult= */ ByteString.copyFromUtf8("data")), + + BINARY_TO_BYTESTRING( + /* yamlString= */ "!!binary 'ZGF0YQ=='", + /* javaClass= */ ByteString.class, + /* expectedResult= */ ByteString.copyFromUtf8("data")); + + final String yamlString; + final Class<?> javaClass; + final Object expectedResult; + + ParseYamlValueToJavaTypeCases(String yamlString, Class<?> javaClass, Object expectedResult) { + this.yamlString = yamlString; + this.javaClass = javaClass; + this.expectedResult = expectedResult; + } + } + + @Test + public void parseYamlStringToJavaType_success( + @TestParameter ParseYamlValueToJavaTypeCases parseYamlValueToJavaTypeCases) throws Exception { + Object result = + ParameterValueParsing.parseYamlStringToJavaType( + parseYamlValueToJavaTypeCases.yamlString, parseYamlValueToJavaTypeCases.javaClass); + + assertThat(result).isEqualTo(parseYamlValueToJavaTypeCases.expectedResult); + } + + enum FormatTestNameStringTestCases { + NULL_REFERENCE(/* value= */ null, /* expectedResult= */ "param=null"), + BOOLEAN(/* value= */ false, /* expectedResult= */ "param=false"), + INTEGER(/* value= */ 123, /* expectedResult= */ "param=123"), + REGULAR_STRING(/* value= */ "abc", /* expectedResult= */ "abc"), + EMPTY_STRING(/* value= */ "", /* expectedResult= */ "param="), + NULL_STRING(/* value= */ "null", /* expectedResult= */ "param=null"), + INTEGER_STRING(/* value= */ "123", /* expectedResult= */ "param=123"), + ARRAY(/* value= */ new byte[] {2, 3, 4}, /* expectedResult= */ "[2, 3, 4]"), + CHAR_MATCHER(/* value= */ CharMatcher.any(), /* expectedResult= */ "CharMatcher.any()"); + + @Nullable final Object value; + final String expectedResult; + + FormatTestNameStringTestCases(@Nullable Object value, String expectedResult) { + this.value = value; + this.expectedResult = expectedResult; + } + } + + @Test + public void formatTestNameString_success(@TestParameter FormatTestNameStringTestCases testCase) + throws Exception { + String result = + ParameterValueParsing.formatTestNameString( + /* parameterName= */ Optional.of("param"), /* value= */ testCase.value); + + assertThat(result).isEqualTo(testCase.expectedResult); + } + + private enum TestEnum { + AAA, + BBB; + } +} diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java new file mode 100644 index 0000000..f7afd79 --- /dev/null +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/PluggableTestRunnerTest.java @@ -0,0 +1,213 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.MethodRule; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.Statement; + +@RunWith(JUnit4.class) +public class PluggableTestRunnerTest { + + private static ArrayList<String> ruleInvocations; + private static int testMethodInvocationCount; + private static List<String> testOrder; + + @Before + public void setUp() { + ruleInvocations = new ArrayList<>(); + testMethodInvocationCount = 0; + testOrder = new ArrayList<>(); + } + + @Retention(RetentionPolicy.RUNTIME) + private @interface CustomTest {} + + static class TestAndMethodRule implements MethodRule, TestRule { + private final String name; + + TestAndMethodRule() { + this("DEFAULT_NAME"); + } + + TestAndMethodRule(String name) { + this.name = name; + } + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + ruleInvocations.add(name); + base.evaluate(); + } + }; + } + + @Override + public Statement apply(Statement base, FrameworkMethod method, Object target) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + ruleInvocations.add(name); + base.evaluate(); + } + }; + } + } + + @RunWith(PluggableTestRunner.class) + public static class TestAndMethodRuleTestClass { + + @Rule public TestAndMethodRule rule = new TestAndMethodRule(); + + @Test + public void test() { + // no-op + } + } + + @Test + public void ruleThatIsBothTestRuleAndMethodRuleIsInvokedOnceOnly() throws Exception { + SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures( + new PluggableTestRunner(TestAndMethodRuleTestClass.class) { + @Override + protected TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.empty(); + } + }); + + assertThat(ruleInvocations).hasSize(1); + } + + @RunWith(PluggableTestRunner.class) + public static class RuleOrderingTestClassWithExplicitOrder { + + @Rule(order = 3) + public TestAndMethodRule ruleA = new TestAndMethodRule("A"); + + @Rule(order = 1) + public TestAndMethodRule ruleB = new TestAndMethodRule("B"); + + @Rule(order = 2) + public TestAndMethodRule ruleC = new TestAndMethodRule("C"); + + @Test + public void test() { + // no-op + } + } + + @Test + public void rulesAreSortedCorrectly_withExplicitOrder() throws Exception { + SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures( + new PluggableTestRunner(RuleOrderingTestClassWithExplicitOrder.class) { + @Override + protected TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.empty(); + } + }); + + assertThat(ruleInvocations).containsExactly("B", "C", "A").inOrder(); + } + + @RunWith(PluggableTestRunner.class) + public static class CustomTestAnnotationTestClass { + @SuppressWarnings("JUnit4TestNotRun") + @CustomTest + public void customTestAnnotatedTest() { + testMethodInvocationCount++; + } + + @Test + public void testAnnotatedTest() { + testMethodInvocationCount++; + } + } + + @Test + public void testMarkedWithCustomClassIsInvoked() throws Exception { + testMethodInvocationCount = 0; + SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures( + new PluggableTestRunner(CustomTestAnnotationTestClass.class) { + @Override + protected TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.empty(); + } + + @Override + protected ImmutableList<Class<? extends Annotation>> getSupportedTestAnnotations() { + return ImmutableList.of(Test.class, CustomTest.class); + } + }); + + assertThat(testMethodInvocationCount).isEqualTo(2); + } + + @RunWith(PluggableTestRunner.class) + public static class SortedPluggableTestRunnerTestClass { + @Test + public void a() { + testOrder.add("a"); + } + + @Test + public void b() { + testOrder.add("b"); + } + + @Test + public void c() { + testOrder.add("c"); + } + } + + @Test + public void testsAreSortedCorrectly() throws Exception { + testOrder.clear(); + SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures( + new PluggableTestRunner(SortedPluggableTestRunnerTestClass.class) { + @Override + protected TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.empty(); + } + + @Override + protected ImmutableList<FrameworkMethod> sortTestMethods( + ImmutableList<FrameworkMethod> methods) { + return FluentIterable.from(methods) + .toSortedList((o1, o2) -> o2.getName().compareTo(o1.getName())); // reversed + } + }); + assertThat(testOrder).containsExactly("c", "b", "a"); + } +} diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java new file mode 100644 index 0000000..5dfe610 --- /dev/null +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/SharedTestUtilitiesJUnit4.java @@ -0,0 +1,153 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.truth.Truth.assertWithMessage; + +import com.google.common.base.Joiner; +import com.google.common.base.Throwables; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.rules.TestName; +import org.junit.runner.Runner; +import org.junit.runner.notification.Failure; +import org.junit.runner.notification.RunListener; +import org.junit.runner.notification.RunNotifier; + +/** Shared utility code for TestParameterInjector (JUnit4) tests. */ +class SharedTestUtilitiesJUnit4 { + + /** + * Runs the given test runner. + * + * @throws AssertionError if the test instance reports any failures + */ + static void runTestsAndAssertNoFailures(Runner testRunner) { + ImmutableList<Failure> failures = runTestsAndGetFailures(testRunner); + + if (failures.size() == 1) { + throw new AssertionError(getOnlyElement(failures).getException()); + } else if (failures.size() > 1) { + throw new AssertionError( + String.format( + "Test failed unexpectedly:\n\n%s", + FluentIterable.from(failures) + .transform( + f -> + String.format( + "<<%s>> %s", + f.getDescription(), + Throwables.getStackTraceAsString(f.getException()))) + .join(Joiner.on("\n------------------------------------\n")))); + } + } + + /** + * Runs the given test runner. + * + * @return all failures reported by the test instance. + */ + static ImmutableList<Failure> runTestsAndGetFailures(Runner testRunner) { + final ImmutableList.Builder<Failure> failures = ImmutableList.builder(); + RunNotifier notifier = new RunNotifier(); + notifier.addFirstListener( + new RunListener() { + @Override + public void testFailure(Failure failure) throws Exception { + failures.add(failure); + } + }); + + testRunner.run(notifier); + + return failures.build(); + } + + private static String toCopyPastableJavaString(Map<String, String> map) { + StringBuilder resultBuilder = new StringBuilder(); + resultBuilder.append("\n----------------------\n"); + resultBuilder.append("ImmutableMap.<String, String>builder()\n"); + for (Entry<String, String> entry : map.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + resultBuilder.append(String.format(" .put(\"%s\", \"%s\")\n", key, value)); + } + resultBuilder.append(" .build()\n"); + resultBuilder.append("----------------------\n"); + return resultBuilder.toString(); + } + + /** + * Base class for a test class that acts as a test case testing a single property of a + * TestParameterInjector-run test. + */ + abstract static class SuccessfulTestCaseBase { + + @Rule public TestName testName = new TestName(); + + private static Map<String, String> testNameToStringifiedParameters; + private static ImmutableMap<String, String> expectedTestNameToStringifiedParameters; + + @BeforeClass + public static void checkStaticFieldAreNull() { + checkState(testNameToStringifiedParameters == null); + checkState(expectedTestNameToStringifiedParameters == null); + } + + final void storeTestParametersForThisTest(Object... params) { + if (testNameToStringifiedParameters == null) { + testNameToStringifiedParameters = new LinkedHashMap<>(); + // Copying this into a static field because @AfterAll methods have to be static + expectedTestNameToStringifiedParameters = expectedTestNameToStringifiedParameters(); + } + checkState( + !testNameToStringifiedParameters.containsKey(testName.getMethodName()), + "Parameters for the test with name '%s' are already stored. This might mean that there" + + " are duplicate test names", + testName.getMethodName()); + testNameToStringifiedParameters.put( + testName.getMethodName(), + FluentIterable.from(params).transform(String::valueOf).join(Joiner.on(":"))); + } + + abstract ImmutableMap<String, String> expectedTestNameToStringifiedParameters(); + + @AfterClass + public static void completedAllTests() { + checkNotNull( + testNameToStringifiedParameters, "storeTestParametersForThisTest() was never called"); + try { + assertWithMessage(toCopyPastableJavaString(testNameToStringifiedParameters)) + .that(testNameToStringifiedParameters) + .isEqualTo(expectedTestNameToStringifiedParameters); + } finally { + testNameToStringifiedParameters = null; + expectedTestNameToStringifiedParameters = null; + } + } + } + + private SharedTestUtilitiesJUnit4() {} +} diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java new file mode 100644 index 0000000..46af6c4 --- /dev/null +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestInfoTest.java @@ -0,0 +1,278 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.truth.Truth.assertThat; +import static java.util.stream.Collectors.toList; + +import com.google.common.collect.ImmutableList; +import com.google.common.truth.IterableSubject; +import com.google.testing.junit.testparameterinjector.TestInfo.TestInfoParameter; +import java.util.List; +import java.util.stream.IntStream; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class TestInfoTest { + + @Test + public void shortenNamesIfNecessary_emptyTestInfos() throws Exception { + ImmutableList<TestInfo> result = TestInfo.shortenNamesIfNecessary(ImmutableList.of()); + + assertThat(result).isEmpty(); + } + + @Test + public void shortenNamesIfNecessary_noParameters() throws Exception { + ImmutableList<TestInfo> givenTestInfos = ImmutableList.of(fakeTestInfo()); + + ImmutableList<TestInfo> result = TestInfo.shortenNamesIfNecessary(givenTestInfos); + + assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder(); + } + + @Test + public void shortenNamesIfNecessary_veryLongTestMethodName_noParameters() throws Exception { + ImmutableList<TestInfo> givenTestInfos = + ImmutableList.of( + TestInfo.createWithoutParameters( + getClass() + .getMethod( + "unusedMethodThatHasAVeryLongNameForTest000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000"), + getClass(), + /* annotations= */ ImmutableList.of())); + + ImmutableList<TestInfo> result = TestInfo.shortenNamesIfNecessary(givenTestInfos); + + assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder(); + } + + @Test + public void shortenNamesIfNecessary_noShorteningNeeded() throws Exception { + ImmutableList<TestInfo> givenTestInfos = + ImmutableList.of( + fakeTestInfo( + TestInfoParameter.create( + /* valueInTestName= */ "short", /* value= */ 1, /* indexInValueSource= */ 1), + TestInfoParameter.create( + /* valueInTestName= */ "shorter", + /* value= */ null, + /* indexInValueSource= */ 3)), + fakeTestInfo( + TestInfoParameter.create( + /* valueInTestName= */ "short", /* value= */ 1, /* indexInValueSource= */ 1), + TestInfoParameter.create( + /* valueInTestName= */ "shortest", + /* value= */ 20, + /* indexInValueSource= */ 0))); + + ImmutableList<TestInfo> result = TestInfo.shortenNamesIfNecessary(givenTestInfos); + + assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder(); + } + + @Test + public void shortenNamesIfNecessary_singleParameterTooLong_twoParameters() throws Exception { + ImmutableList<TestInfo> result = + TestInfo.shortenNamesIfNecessary( + ImmutableList.of( + fakeTestInfo( + TestInfoParameter.create( + /* valueInTestName= */ "short", + /* value= */ 1, + /* indexInValueSource= */ 0), + TestInfoParameter.create( + /* valueInTestName= */ "shorter", + /* value= */ null, + /* indexInValueSource= */ 0)), + fakeTestInfo( + TestInfoParameter.create( + /* valueInTestName= */ "short", + /* value= */ 1, + /* indexInValueSource= */ 0), + TestInfoParameter.create( + /* valueInTestName= */ "very long parameter name for test" + + " 00000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000", + /* value= */ 20, + /* indexInValueSource= */ 1)))); + + assertThatTestNamesOf(result) + .containsExactly( + "toLowerCase[short,1.shorter]", + "toLowerCase[short,2.very long parameter name for test " + + "0000000000000000000000000000000000000000000000000000...]") + .inOrder(); + } + + @Test + public void shortenNamesIfNecessary_singleParameterTooLong_onlyParameter() throws Exception { + ImmutableList<TestInfo> result = + TestInfo.shortenNamesIfNecessary( + ImmutableList.of( + fakeTestInfo( + TestInfoParameter.create( + /* valueInTestName= */ "shorter", + /* value= */ null, + /* indexInValueSource= */ 0)), + fakeTestInfo( + TestInfoParameter.create( + /* valueInTestName= */ "very long parameter name for test" + + " 00000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000", + /* value= */ 20, + /* indexInValueSource= */ 1)))); + + assertThatTestNamesOf(result) + .containsExactly( + "toLowerCase[1.shorter]", + "toLowerCase[2.very long parameter name for test" + + " 000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "00000000000000000000000000000000000000000000000000000000000000000000...]") + .inOrder(); + } + + @Test + public void shortenNamesIfNecessary_tooManyParameters() throws Exception { + TestInfo testInfoWithManyParams = + fakeTestInfo( + IntStream.range(0, 50) + .mapToObj( + i -> + TestInfoParameter.create( + /* valueInTestName= */ "short", + /* value= */ i, + /* indexInValueSource= */ i)) + .toArray(TestInfoParameter[]::new)); + + ImmutableList<TestInfo> result = + TestInfo.shortenNamesIfNecessary(ImmutableList.of(testInfoWithManyParams)); + + assertThatTestNamesOf(result) + .containsExactly( + "toLowerCase[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26," + + "27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50]"); + } + + @Test + public void deduplicateTestNames_noDuplicates() throws Exception { + ImmutableList<TestInfo> givenTestInfos = + ImmutableList.of( + fakeTestInfo( + TestInfoParameter.create( + /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1), + TestInfoParameter.create( + /* valueInTestName= */ "bbb", /* value= */ null, /* indexInValueSource= */ 3)), + fakeTestInfo( + TestInfoParameter.create( + /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1), + TestInfoParameter.create( + /* valueInTestName= */ "ccc", /* value= */ 1, /* indexInValueSource= */ 0))); + + ImmutableList<TestInfo> result = TestInfo.deduplicateTestNames(givenTestInfos); + + assertThat(result).containsExactlyElementsIn(givenTestInfos).inOrder(); + assertThatTestNamesOf(result) + .containsExactly("toLowerCase[aaa,bbb]", "toLowerCase[aaa,ccc]") + .inOrder(); + } + + @Test + public void deduplicateTestNames_duplicateParameterNamesWithDifferentTypes() throws Exception { + ImmutableList<TestInfo> result = + TestInfo.deduplicateTestNames( + ImmutableList.of( + fakeTestInfo( + TestInfoParameter.create( + /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1), + TestInfoParameter.create( + /* valueInTestName= */ "null", + /* value= */ null, + /* indexInValueSource= */ 3)), + fakeTestInfo( + TestInfoParameter.create( + /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1), + TestInfoParameter.create( + /* valueInTestName= */ "null", + /* value= */ "null", + /* indexInValueSource= */ 0)), + fakeTestInfo( + TestInfoParameter.create( + /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 1), + TestInfoParameter.create( + /* valueInTestName= */ "bbb", + /* value= */ "b", + /* indexInValueSource= */ 0)))); + + assertThatTestNamesOf(result) + .containsExactly( + "toLowerCase[aaa,null (null reference)]", + "toLowerCase[aaa,null (String)]", + "toLowerCase[aaa,bbb]") + .inOrder(); + } + + @Test + public void deduplicateTestNames_duplicateParameterNamesWithSameTypes() throws Exception { + ImmutableList<TestInfo> result = + TestInfo.deduplicateTestNames( + ImmutableList.of( + fakeTestInfo( + TestInfoParameter.create( + /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 0), + TestInfoParameter.create( + /* valueInTestName= */ "bbb", /* value= */ 1, /* indexInValueSource= */ 0)), + fakeTestInfo( + TestInfoParameter.create( + /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 0), + TestInfoParameter.create( + /* valueInTestName= */ "bbb", /* value= */ 1, /* indexInValueSource= */ 1)), + fakeTestInfo( + TestInfoParameter.create( + /* valueInTestName= */ "aaa", /* value= */ 1, /* indexInValueSource= */ 0), + TestInfoParameter.create( + /* valueInTestName= */ "ccc", + /* value= */ "b", + /* indexInValueSource= */ 2)))); + + assertThatTestNamesOf(result) + .containsExactly( + "toLowerCase[1.aaa,1.bbb]", "toLowerCase[1.aaa,2.bbb]", "toLowerCase[1.aaa,3.ccc]") + .inOrder(); + } + + private static TestInfo fakeTestInfo(TestInfoParameter... parameters) + throws NoSuchMethodException { + return TestInfo.createWithoutParameters( + String.class.getMethod("toLowerCase"), + String.class, + /* annotations= */ ImmutableList.of()) + .withExtraParameters(ImmutableList.copyOf(parameters)); + } + + private static IterableSubject assertThatTestNamesOf(List<TestInfo> result) { + return assertThat(result.stream().map(TestInfo::getName).collect(toList())); + } + + public void + unusedMethodThatHasAVeryLongNameForTest000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000() {} +} diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java new file mode 100644 index 0000000..458b623 --- /dev/null +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterAnnotationMethodProcessorTest.java @@ -0,0 +1,866 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.Lists.newArrayList; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.testing.junit.testparameterinjector.SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.junit.runners.model.TestClass; + +/** + * Test class to test the PluggableTestRunner test harness works with {@link + * TestParameterAnnotation}s. + */ +@RunWith(Parameterized.class) +public class TestParameterAnnotationMethodProcessorTest { + + @Retention(RUNTIME) + @interface ClassTestResult { + Result value(); + } + + enum Result { + /** + * A successful test run is expected in both for + * TestParameterAnnotationMethodProcessor#forAllAnnotationPlacements and + * TestParameterAnnotationMethodProcessor#onlyForFieldsAndParameters. + */ + SUCCESS_ALWAYS, + SUCCESS_FOR_ALL_PLACEMENTS_ONLY, + FAILURE, + } + + public enum TestEnum { + UNDEFINED, + ONE, + TWO, + THREE, + FOUR, + FIVE + } + + @Retention(RUNTIME) + @TestParameterAnnotation + public @interface EnumParameter { + TestEnum[] value() default {}; + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class SingleAnnotationClass extends SuccessfulTestCaseBase { + + @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) + TestEnum enumParameter; + + @Test + public void test() { + storeTestParametersForThisTest(enumParameter); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test[ONE]", "ONE") + .put("test[TWO]", "TWO") + .put("test[THREE]", "THREE") + .build(); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class MultipleAllEnumValuesAnnotationClass extends SuccessfulTestCaseBase { + + @TestParameter({"ONE", "THREE"}) + TestEnum enumParameter1; + + @TestParameter TestEnum2 enumParameter2; + + @Test + public void test(@TestParameter TestEnum2 enumParameter3) { + storeTestParametersForThisTest(enumParameter1, enumParameter2, enumParameter3); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test[ONE,A,A]", "ONE:A:A") + .put("test[ONE,A,B]", "ONE:A:B") + .put("test[ONE,B,A]", "ONE:B:A") + .put("test[ONE,B,B]", "ONE:B:B") + .put("test[THREE,A,A]", "THREE:A:A") + .put("test[THREE,A,B]", "THREE:A:B") + .put("test[THREE,B,A]", "THREE:B:A") + .put("test[THREE,B,B]", "THREE:B:B") + .build(); + } + + enum TestEnum2 { + A, + B; + } + } + + @ClassTestResult(Result.SUCCESS_FOR_ALL_PLACEMENTS_ONLY) + public static class SingleParameterAnnotationClass extends SuccessfulTestCaseBase { + + @Test + @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) + public void test(TestEnum enumParameter) { + storeTestParametersForThisTest(enumParameter); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test[ONE]", "ONE") + .put("test[TWO]", "TWO") + .put("test[THREE]", "THREE") + .build(); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class SingleAnnotatedParameterAnnotationClass extends SuccessfulTestCaseBase { + + @Test + public void test( + @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) TestEnum enumParameter) { + storeTestParametersForThisTest(enumParameter); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test[ONE]", "ONE") + .put("test[TWO]", "TWO") + .put("test[THREE]", "THREE") + .build(); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class AnnotatedSuperclassParameter extends SuccessfulTestCaseBase { + + @Test + public void test( + @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) Object enumParameter) { + storeTestParametersForThisTest(enumParameter); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test[ONE]", "ONE") + .put("test[TWO]", "TWO") + .put("test[THREE]", "THREE") + .build(); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class DuplicatedAnnotatedParameterAnnotationClass extends SuccessfulTestCaseBase { + + @Test + public void test( + @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) TestEnum enumParameter, + @EnumParameter({TestEnum.FOUR, TestEnum.FIVE}) TestEnum enumParameter2) { + storeTestParametersForThisTest(enumParameter, enumParameter2); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test[ONE,FOUR]", "ONE:FOUR") + .put("test[ONE,FIVE]", "ONE:FIVE") + .put("test[TWO,FOUR]", "TWO:FOUR") + .put("test[TWO,FIVE]", "TWO:FIVE") + .put("test[THREE,FOUR]", "THREE:FOUR") + .put("test[THREE,FIVE]", "THREE:FIVE") + .build(); + } + } + + @ClassTestResult(Result.FAILURE) + public static class SingleAnnotatedParameterAnnotationClassWithMissingValue { + + @Test + public void test(@EnumParameter TestEnum enumParameter) {} + } + + @ClassTestResult(Result.SUCCESS_FOR_ALL_PLACEMENTS_ONLY) + public static class MultipleAnnotationTestClass extends SuccessfulTestCaseBase { + + @EnumParameter({TestEnum.ONE, TestEnum.TWO}) + TestEnum enumParameter; + + @Test + @EnumParameter({TestEnum.THREE}) + public void parameterized() { + storeTestParametersForThisTest(enumParameter); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder().put("parameterized[THREE]", "THREE").build(); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class TooLongTestNamesShortened extends SuccessfulTestCaseBase { + + @Test + public void test1( + @TestParameter({ + "ABC", + "This is a very long string (240 characters) that would normally cause Sponge+Tin to" + + " exceed the filename limit of 255 characters." + + " ===========================================================================" + + "===================================" + }) + String testString) { + storeTestParametersForThisTest(testString); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test1[1.ABC]", "ABC") + .put( + "test1[2.This is a very long string (240 characters) that would normally cause" + + " Sponge+Tin to exceed the filename limit of 255 characters." + + " =========================================================...]", + "This is a very long string (240 characters) that would normally cause Sponge+Tin to" + + " exceed the filename limit of 255 characters." + + " ============================================================================" + + "==================================") + .build(); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class DuplicateTestNames extends SuccessfulTestCaseBase { + + @Test + public void test1(@TestParameter({"ABC", "ABC"}) String testString) { + storeTestParametersForThisTest(testString); + } + + private static final class Test2Provider extends TestParameterValuesProvider { + @Override + public List<Object> provideValues(TestParameterValuesProvider.Context context) { + return newArrayList(123, "123", "null", null); + } + } + + @Test + public void test2(@TestParameter(valuesProvider = Test2Provider.class) Object testObject) { + storeTestParametersForThisTest(testObject); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test1[1.ABC]", "ABC") + .put("test1[2.ABC]", "ABC") + .put("test2[testObject=123 (Integer)]", "123") + .put("test2[testObject=123 (String)]", "123") + .put("test2[testObject=null (String)]", "null") + .put("test2[testObject=null (null reference)]", "null") + .build(); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class DuplicateFieldAnnotationTestClass extends SuccessfulTestCaseBase { + + @TestParameter({"foo", "bar"}) + String stringParameter; + + @TestParameter({"baz", "qux"}) + String stringParameter2; + + @Test + public void test() { + storeTestParametersForThisTest(stringParameter, stringParameter2); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test[foo,baz]", "foo:baz") + .put("test[foo,qux]", "foo:qux") + .put("test[bar,baz]", "bar:baz") + .put("test[bar,qux]", "bar:qux") + .build(); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class DuplicateIdenticalFieldAnnotationTestClass extends SuccessfulTestCaseBase { + + @TestParameter({"foo", "bar"}) + String stringParameter; + + @TestParameter({"foo", "bar"}) + String stringParameter2; + + @Test + public void test() { + storeTestParametersForThisTest(stringParameter, stringParameter2); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test[foo,foo]", "foo:foo") + .put("test[foo,bar]", "foo:bar") + .put("test[bar,foo]", "bar:foo") + .put("test[bar,bar]", "bar:bar") + .build(); + } + } + + @ClassTestResult(Result.FAILURE) + public static class ErrorDuplicateFieldAnnotationTestClass { + + @EnumParameter({TestEnum.ONE, TestEnum.TWO}) + TestEnum parameter1; + + @EnumParameter({TestEnum.THREE, TestEnum.FOUR}) + TestEnum parameter2; + + @Test + @EnumParameter(TestEnum.FIVE) + public void test() {} + } + + @ClassTestResult(Result.FAILURE) + public static class ErrorDuplicateFieldAndClassAnnotationTestClass { + + @EnumParameter({TestEnum.ONE, TestEnum.TWO}) + TestEnum parameter; + + @EnumParameter(TestEnum.FIVE) + public ErrorDuplicateFieldAndClassAnnotationTestClass() {} + + @Test + public void test() {} + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class SingleAnnotationTestClassWithAnnotation extends SuccessfulTestCaseBase { + + @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) + TestEnum enumParameter; + + @Test + public void test() { + storeTestParametersForThisTest(enumParameter); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test[ONE]", "ONE") + .put("test[TWO]", "TWO") + .put("test[THREE]", "THREE") + .build(); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class MultipleAnnotationTestClassWithAnnotation extends SuccessfulTestCaseBase { + + @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) + TestEnum enumParameter; + + @Test + public void parameterized(@TestParameter({"foo", "bar"}) String stringParameter) { + storeTestParametersForThisTest(enumParameter, stringParameter); + } + + @Test + public void nonParameterized() { + storeTestParametersForThisTest(enumParameter); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("parameterized[ONE,foo]", "ONE:foo") + .put("parameterized[ONE,bar]", "ONE:bar") + .put("parameterized[TWO,foo]", "TWO:foo") + .put("parameterized[TWO,bar]", "TWO:bar") + .put("parameterized[THREE,foo]", "THREE:foo") + .put("parameterized[THREE,bar]", "THREE:bar") + .put("nonParameterized[ONE]", "ONE") + .put("nonParameterized[TWO]", "TWO") + .put("nonParameterized[THREE]", "THREE") + .build(); + } + } + + public abstract static class BaseClassWithSingleTest extends SuccessfulTestCaseBase { + @Test + public void testInBase(@TestParameter boolean b) { + storeTestParametersForThisTest(b); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class SimpleTestInheritedFromBaseClass extends BaseClassWithSingleTest { + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("testInBase[b=false]", "false") + .put("testInBase[b=true]", "true") + .build(); + } + } + + public abstract static class BaseClassWithAnnotations extends SuccessfulTestCaseBase { + + @TestParameter boolean boolInBase; + + @Test + public void testInBase(@TestParameter({"ONE", "TWO"}) TestEnum enumInBase) { + storeTestParametersForThisTest(boolInBase, enumInBase); + } + + @Test + public abstract void abstractTestInBase(); + + @Test + public void overridableTestInBase() { + throw new UnsupportedOperationException("Expected the base class to override this"); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class AnnotationInheritedFromBaseClass extends BaseClassWithAnnotations { + + @TestParameter boolean boolInChild; + + @Test + public void testInChild(@TestParameter({"TWO", "THREE"}) TestEnum enumInChild) { + storeTestParametersForThisTest(boolInBase, boolInChild, enumInChild); + } + + @Override + public void abstractTestInBase() { + storeTestParametersForThisTest(boolInBase, boolInChild); + } + + @Override + public void overridableTestInBase() { + storeTestParametersForThisTest(boolInBase, boolInChild); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("testInChild[boolInChild=false,boolInBase=false,TWO]", "false:false:TWO") + .put("testInChild[boolInChild=false,boolInBase=false,THREE]", "false:false:THREE") + .put("testInChild[boolInChild=false,boolInBase=true,TWO]", "true:false:TWO") + .put("testInChild[boolInChild=false,boolInBase=true,THREE]", "true:false:THREE") + .put("testInChild[boolInChild=true,boolInBase=false,TWO]", "false:true:TWO") + .put("testInChild[boolInChild=true,boolInBase=false,THREE]", "false:true:THREE") + .put("testInChild[boolInChild=true,boolInBase=true,TWO]", "true:true:TWO") + .put("testInChild[boolInChild=true,boolInBase=true,THREE]", "true:true:THREE") + .put("abstractTestInBase[boolInChild=false,boolInBase=false]", "false:false") + .put("abstractTestInBase[boolInChild=false,boolInBase=true]", "true:false") + .put("abstractTestInBase[boolInChild=true,boolInBase=false]", "false:true") + .put("abstractTestInBase[boolInChild=true,boolInBase=true]", "true:true") + .put("overridableTestInBase[boolInChild=false,boolInBase=false]", "false:false") + .put("overridableTestInBase[boolInChild=false,boolInBase=true]", "true:false") + .put("overridableTestInBase[boolInChild=true,boolInBase=false]", "false:true") + .put("overridableTestInBase[boolInChild=true,boolInBase=true]", "true:true") + .put("testInBase[boolInChild=false,boolInBase=false,ONE]", "false:ONE") + .put("testInBase[boolInChild=false,boolInBase=false,TWO]", "false:TWO") + .put("testInBase[boolInChild=false,boolInBase=true,ONE]", "true:ONE") + .put("testInBase[boolInChild=false,boolInBase=true,TWO]", "true:TWO") + .put("testInBase[boolInChild=true,boolInBase=false,ONE]", "false:ONE") + .put("testInBase[boolInChild=true,boolInBase=false,TWO]", "false:TWO") + .put("testInBase[boolInChild=true,boolInBase=true,ONE]", "true:ONE") + .put("testInBase[boolInChild=true,boolInBase=true,TWO]", "true:TWO") + .build(); + } + } + + @Retention(RUNTIME) + @TestParameterAnnotation(validator = TestEnumValidator.class) + public @interface EnumEvaluatorParameter { + TestEnum[] value() default {}; + } + + public static class TestEnumValidator implements TestParameterValidator { + + @Override + public boolean shouldSkip(Context context) { + return context.has(EnumEvaluatorParameter.class, TestEnum.THREE); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class MethodEvaluatorClass extends SuccessfulTestCaseBase { + + @Test + public void test( + @EnumEvaluatorParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) TestEnum value) { + storeTestParametersForThisTest(value); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test[ONE]", "ONE") + .put("test[TWO]", "TWO") + .build(); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class FieldEvaluatorClass extends SuccessfulTestCaseBase { + + @EnumEvaluatorParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) + TestEnum value; + + @Test + public void test() { + storeTestParametersForThisTest(value); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test[ONE]", "ONE") + .put("test[TWO]", "TWO") + .build(); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class ConstructorClass extends SuccessfulTestCaseBase { + + final TestEnum enumParameter; + + public ConstructorClass( + @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) TestEnum enumParameter) { + this.enumParameter = enumParameter; + } + + @Test + public void test() { + storeTestParametersForThisTest(enumParameter); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test[ONE]", "ONE") + .put("test[TWO]", "TWO") + .put("test[THREE]", "THREE") + .build(); + } + } + + @ClassTestResult(Result.SUCCESS_FOR_ALL_PLACEMENTS_ONLY) + public static class MethodFieldOverrideClass extends SuccessfulTestCaseBase { + + @EnumParameter({TestEnum.ONE, TestEnum.TWO}) + TestEnum enumParameter; + + @Test + @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) + public void test() { + storeTestParametersForThisTest(enumParameter); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test[ONE]", "ONE") + .put("test[TWO]", "TWO") + .put("test[THREE]", "THREE") + .build(); + } + } + + @ClassTestResult(Result.SUCCESS_FOR_ALL_PLACEMENTS_ONLY) + public static class ErrorDuplicatedConstructorMethodAnnotation extends SuccessfulTestCaseBase { + + final TestEnum enumParameter; + + @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) + public ErrorDuplicatedConstructorMethodAnnotation(TestEnum enumParameter) { + this.enumParameter = enumParameter; + } + + @Test + @EnumParameter({TestEnum.ONE, TestEnum.TWO}) + public void test(TestEnum otherParameter) { + storeTestParametersForThisTest(enumParameter, otherParameter); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test[ONE,ONE]", "ONE:ONE") + .put("test[ONE,TWO]", "ONE:TWO") + .put("test[TWO,ONE]", "TWO:ONE") + .put("test[TWO,TWO]", "TWO:TWO") + .put("test[THREE,ONE]", "THREE:ONE") + .put("test[THREE,TWO]", "THREE:TWO") + .build(); + } + } + + @ClassTestResult(Result.FAILURE) + @EnumParameter({TestEnum.ONE, TestEnum.TWO, TestEnum.THREE}) + public static class ErrorDuplicatedClassFieldAnnotation { + + @EnumParameter({TestEnum.ONE, TestEnum.TWO}) + TestEnum enumParameter; + + @Test + public void test() {} + } + + @ClassTestResult(Result.FAILURE) + public static class ErrorNonStaticProviderClass { + + @Test + public void test(@TestParameter(valuesProvider = NonStaticProvider.class) int i) {} + + @SuppressWarnings("ClassCanBeStatic") + class NonStaticProvider extends TestParameterValuesProvider { + @Override + public List<?> provideValues(TestParameterValuesProvider.Context context) { + return ImmutableList.of(); + } + } + } + + @ClassTestResult(Result.FAILURE) + public static class ErrorNonPublicTestMethod { + + @Test + void test(@TestParameter boolean b) {} + } + + @ClassTestResult(Result.FAILURE) + public static class ErrorPackagePrivateConstructor { + ErrorPackagePrivateConstructor() {} + + @Test + public void test1() {} + } + + public enum EnumA { + A1, + A2 + } + + public enum EnumB { + B1, + B2 + } + + public enum EnumC { + C1, + C2, + C3 + } + + @Retention(RUNTIME) + @TestParameterAnnotation(validator = TestBaseValidatorValidator.class) + public @interface EnumAParameter { + EnumA[] value() default {EnumA.A1, EnumA.A2}; + } + + @Retention(RUNTIME) + @TestParameterAnnotation(validator = TestBaseValidatorValidator.class) + public @interface EnumBParameter { + EnumB[] value() default {EnumB.B1, EnumB.B2}; + } + + @Retention(RUNTIME) + @TestParameterAnnotation(validator = TestBaseValidatorValidator.class) + public @interface EnumCParameter { + EnumC[] value() default {EnumC.C1, EnumC.C2, EnumC.C3}; + } + + public static class TestBaseValidatorValidator extends BaseTestParameterValidator { + + @Override + protected List<List<Class<? extends Annotation>>> getIndependentParameters(Context context) { + return ImmutableList.of( + ImmutableList.of(EnumAParameter.class, EnumBParameter.class, EnumCParameter.class)); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class IndependentAnnotation extends SuccessfulTestCaseBase { + + @EnumAParameter EnumA enumA; + @EnumBParameter EnumB enumB; + @EnumCParameter EnumC enumC; + + @Test + public void test() { + storeTestParametersForThisTest(enumA, enumB, enumC); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test[A1,B1,C1]", "A1:B1:C1") + .put("test[A2,B2,C2]", "A2:B2:C2") + .put("test[A2,B2,C3]", "A2:B2:C3") + .build(); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class TestNamesTest extends SuccessfulTestCaseBase { + + @TestParameter("8") + long fieldParam; + + @Test + public void withPrimitives( + @TestParameter("true") boolean param1, @TestParameter("2") int param2) { + storeTestParametersForThisTest(fieldParam, param1, param2); + } + + @Test + public void withString(@TestParameter("AAA") String param1) { + storeTestParametersForThisTest(fieldParam, param1); + } + + @Test + public void withEnum(@EnumParameter(TestEnum.TWO) TestEnum param1) { + storeTestParametersForThisTest(fieldParam, param1); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("withString[fieldParam=8,AAA]", "8:AAA") + .put("withEnum[fieldParam=8,TWO]", "8:TWO") + .put("withPrimitives[fieldParam=8,param1=true,param2=2]", "8:true:2") + .build(); + } + } + + @ClassTestResult(Result.SUCCESS_ALWAYS) + public static class MethodNameContainsOrderedParameterNames extends SuccessfulTestCaseBase { + + @Test + public void pretest(@TestParameter({"a", "b"}) String foo) { + storeTestParametersForThisTest(foo); + } + + @Test + public void test( + @EnumParameter({TestEnum.ONE, TestEnum.TWO}) TestEnum e, @TestParameter({"c"}) String foo) { + storeTestParametersForThisTest(e, foo); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("pretest[a]", "a") + .put("pretest[b]", "b") + .put("test[ONE,c]", "ONE:c") + .put("test[TWO,c]", "TWO:c") + .build(); + } + } + + @Parameters(name = "{0}:{2}") + public static Collection<Object[]> parameters() { + return Arrays.stream(TestParameterAnnotationMethodProcessorTest.class.getClasses()) + .filter(cls -> cls.isAnnotationPresent(ClassTestResult.class)) + .map( + cls -> + new Object[] { + cls.getSimpleName(), cls, cls.getAnnotation(ClassTestResult.class).value() + }) + .collect(toImmutableList()); + } + + private final Class<?> testClass; + private final Result result; + + public TestParameterAnnotationMethodProcessorTest( + String name, Class<?> testClass, Result result) { + this.testClass = testClass; + this.result = result; + } + + @Test + public void test() throws Exception { + switch (result) { + case SUCCESS_ALWAYS: + SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures( + newTestRunnerWithParameterizedSupport( + testClass -> TestMethodProcessorList.createNewParameterizedProcessors())); + break; + + case SUCCESS_FOR_ALL_PLACEMENTS_ONLY: + assertThrows( + Exception.class, + () -> + SharedTestUtilitiesJUnit4.runTestsAndGetFailures( + newTestRunnerWithParameterizedSupport( + testClass -> TestMethodProcessorList.createNewParameterizedProcessors()))); + break; + + case FAILURE: + assertThrows( + Exception.class, + () -> + SharedTestUtilitiesJUnit4.runTestsAndGetFailures( + newTestRunnerWithParameterizedSupport( + testClass -> TestMethodProcessorList.createNewParameterizedProcessors()))); + break; + } + } + + private PluggableTestRunner newTestRunnerWithParameterizedSupport( + Function<TestClass, TestMethodProcessorList> processorListGenerator) throws Exception { + return new PluggableTestRunner(testClass) { + @Override + protected TestMethodProcessorList createTestMethodProcessorList() { + return processorListGenerator.apply(getTestClass()); + } + }; + } +} diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt new file mode 100644 index 0000000..10ce60e --- /dev/null +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterInjectorKotlinTest.kt @@ -0,0 +1,278 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import com.google.testing.junit.testparameterinjector.SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase +import java.util.Arrays +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters + +@RunWith(Parameterized::class) +class TestParameterInjectorKotlinTest { + + // ********** Test cases ********** // + // These test classes are all expected to run successfully + + @RunAsTest + internal class TestParameter_MethodParam : SuccessfulTestCaseBase() { + @Test + fun testString(@TestParameter("a", "b") param: String) { + storeTestParametersForThisTest(param) + } + + @Test + fun testEnum_selection(@TestParameter("RED", "GREEN") param: Color) { + storeTestParametersForThisTest(param) + } + + @Test + fun testEnum_all(@TestParameter param: Color) { + storeTestParametersForThisTest(param) + } + + @Test + fun testMultiple( + @TestParameter("1", "8") width: Int, + @TestParameter("1", "5.5") height: Double, + ) { + storeTestParametersForThisTest(width, height) + } + + override fun expectedTestNameToStringifiedParameters(): ImmutableMap<String, String> { + return ImmutableMap.builder<String, String>() + .put("testString[a]", "a") + .put("testString[b]", "b") + .put("testEnum_selection[RED]", "RED") + .put("testEnum_selection[GREEN]", "GREEN") + .put("testEnum_all[RED]", "RED") + .put("testEnum_all[BLUE]", "BLUE") + .put("testEnum_all[GREEN]", "GREEN") + .put("testMultiple[width=1,height=1.0]", "1:1.0") + .put("testMultiple[width=1,height=5.5]", "1:5.5") + .put("testMultiple[width=8,height=1.0]", "8:1.0") + .put("testMultiple[width=8,height=5.5]", "8:5.5") + .buildOrThrow() + } + } + + @RunAsTest + internal class TestParameter_MethodParam_WithValueClasses : SuccessfulTestCaseBase() { + @Test + fun testString(@TestParameter("a", "b") param: StringValueClass) { + storeTestParametersForThisTest(param.onlyValue) + } + + @Test + fun testEnum_selection(@TestParameter("RED", "GREEN") param: ColorValueClass) { + storeTestParametersForThisTest(param.onlyValue) + } + + @Test + fun testEnum_all(@TestParameter param: ColorValueClass) { + storeTestParametersForThisTest(param.onlyValue) + } + + @Test + fun testMixed( + @TestParameter("1", "8") width: Int, + @TestParameter("1", "5.5") height: DoubleValueClass, + ) { + storeTestParametersForThisTest(width, height.onlyValue) + } + + override fun expectedTestNameToStringifiedParameters(): ImmutableMap<String, String> { + return ImmutableMap.builder<String, String>() + .put("testString-HMW45e8[a]", "a") + .put("testString-HMW45e8[b]", "b") + .put("testEnum_selection-fiSAjMM[RED]", "RED") + .put("testEnum_selection-fiSAjMM[GREEN]", "GREEN") + .put("testEnum_all-fiSAjMM[RED]", "RED") + .put("testEnum_all-fiSAjMM[BLUE]", "BLUE") + .put("testEnum_all-fiSAjMM[GREEN]", "GREEN") + .put("testMixed-lvZ97mM[width=1,height=1.0]", "1:1.0") + .put("testMixed-lvZ97mM[width=1,height=5.5]", "1:5.5") + .put("testMixed-lvZ97mM[width=8,height=1.0]", "8:1.0") + .put("testMixed-lvZ97mM[width=8,height=5.5]", "8:5.5") + .buildOrThrow() + } + } + + @RunAsTest + internal class TestParameter_Field : SuccessfulTestCaseBase() { + @TestParameter("1", "2") var width: Int? = null + + @Test + fun test() { + storeTestParametersForThisTest(width) + } + + override fun expectedTestNameToStringifiedParameters(): ImmutableMap<String, String> { + return ImmutableMap.builder<String, String>() + .put("test[width=1]", "1") + .put("test[width=2]", "2") + .buildOrThrow() + } + } + + @RunAsTest + internal class TestParameter_Field_WithValueClass : SuccessfulTestCaseBase() { + @TestParameter(valuesProvider = DoubleValueClassProvider::class) + var width: DoubleValueClass? = null + + @Test + fun test() { + storeTestParametersForThisTest(width?.onlyValue) + } + + override fun expectedTestNameToStringifiedParameters(): ImmutableMap<String, String> { + return ImmutableMap.builder<String, String>() + .put("test[DoubleValueClass(onlyValue=1.0)]", "1.0") + .put("test[DoubleValueClass(onlyValue=2.5)]", "2.5") + .buildOrThrow() + } + + private class DoubleValueClassProvider : TestParameterValuesProvider() { + override fun provideValues(context: Context): List<DoubleValueClass> { + return ImmutableList.of(DoubleValueClass(1.0), DoubleValueClass(2.5)) + } + } + } + + @RunAsTest + internal class TestParameter_ConstructorParam : SuccessfulTestCaseBase { + val width: Int + + constructor(@TestParameter("1", "2") width: Int) { + this.width = width + } + + @Test + fun test() { + storeTestParametersForThisTest(width) + } + + override fun expectedTestNameToStringifiedParameters(): ImmutableMap<String, String> { + return ImmutableMap.builder<String, String>() + .put("test[width=1]", "1") + .put("test[width=2]", "2") + .buildOrThrow() + } + } + + @RunAsTest + internal class TestParameters_MethodParam : SuccessfulTestCaseBase() { + @TestParameters("{width: 3, height: 8}") + @TestParameters("{width: 5, height: 2.5}") + @Test + fun test(width: Int, height: Double) { + storeTestParametersForThisTest(width, height) + } + + override fun expectedTestNameToStringifiedParameters(): ImmutableMap<String, String> { + return ImmutableMap.builder<String, String>() + .put("test[{width: 3, height: 8}]", "3:8.0") + .put("test[{width: 5, height: 2.5}]", "5:2.5") + .buildOrThrow() + } + } + + @RunAsTest + internal class TestParameters_MethodParam_WithValueClasses : SuccessfulTestCaseBase() { + @TestParameters("{width: 3, height: 8}") + @TestParameters("{width: 5, height: 2.5}") + @Test + fun test(width: Int, height: DoubleValueClass) { + storeTestParametersForThisTest(width, height.onlyValue) + } + + override fun expectedTestNameToStringifiedParameters(): ImmutableMap<String, String> { + return ImmutableMap.builder<String, String>() + .put("test-lvZ97mM[{width: 3, height: 8}]", "3:8.0") + .put("test-lvZ97mM[{width: 5, height: 2.5}]", "5:2.5") + .buildOrThrow() + } + } + + @RunAsTest + internal class TestParameters_ConstructorParam : SuccessfulTestCaseBase { + val width: Int + + @TestParameters("{width: 1}") + @TestParameters("{width: 2}") + constructor(width: Int) { + this.width = width + } + + @Test + fun test() { + storeTestParametersForThisTest(width) + } + + override fun expectedTestNameToStringifiedParameters(): ImmutableMap<String, String> { + return ImmutableMap.builder<String, String>() + .put("test[{width: 1}]", "1") + .put("test[{width: 2}]", "2") + .buildOrThrow() + } + } + + // ********** Test infrastructure ********** // + + private val testClass: Class<*> + + constructor(@Suppress("UNUSED_PARAMETER") testName: String?, testClass: Class<*>) { + this.testClass = testClass + } + + @Test + fun test_success() { + SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures( + object : PluggableTestRunner(testClass) { + override fun createTestMethodProcessorList(): TestMethodProcessorList { + return TestMethodProcessorList.createNewParameterizedProcessors() + } + } + ) + } + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun parameters(): Collection<Array<Any>> { + return Arrays.stream(TestParameterInjectorKotlinTest::class.java.classes) + .filter { cls: Class<*> -> cls.isAnnotationPresent(RunAsTest::class.java) } + .map { cls: Class<*> -> arrayOf<Any>(cls.simpleName, cls) } + .collect(ImmutableList.toImmutableList()) + } + } + + annotation class RunAsTest + + enum class Color { + RED, + BLUE, + GREEN + } + + @JvmInline value class ColorValueClass(val onlyValue: Color) + + @JvmInline value class StringValueClass(val onlyValue: String) + + @JvmInline value class DoubleValueClass(val onlyValue: Double) +} diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java new file mode 100644 index 0000000..7c915ea --- /dev/null +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParameterTest.java @@ -0,0 +1,254 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.Lists.newArrayList; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.base.CharMatcher; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.testing.junit.testparameterinjector.SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase; +import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider.Context; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** Test class to test the @TestParameter's value provider. */ +@RunWith(Parameterized.class) +public class TestParameterTest { + + @Retention(RUNTIME) + @interface RunAsTest {} + + public enum TestEnum { + ONE, + TWO, + THREE, + } + + @RunAsTest + public static class AnnotatedField extends SuccessfulTestCaseBase { + + @TestParameter TestEnum enumParameter; + + @Test + public void test() { + storeTestParametersForThisTest(enumParameter); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test[ONE]", "ONE") + .put("test[TWO]", "TWO") + .put("test[THREE]", "THREE") + .build(); + } + } + + @RunAsTest + public static class AnnotatedConstructorParameter extends SuccessfulTestCaseBase { + + private final TestEnum constructorEnum; + + @TestParameter TestEnum fieldEnum; + + public AnnotatedConstructorParameter(@TestParameter TestEnum constructorEnum) { + this.constructorEnum = constructorEnum; + } + + @Test + public void test() { + storeTestParametersForThisTest(fieldEnum, constructorEnum); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test[ONE,ONE]", "ONE:ONE") + .put("test[ONE,TWO]", "ONE:TWO") + .put("test[ONE,THREE]", "ONE:THREE") + .put("test[TWO,ONE]", "TWO:ONE") + .put("test[TWO,TWO]", "TWO:TWO") + .put("test[TWO,THREE]", "TWO:THREE") + .put("test[THREE,ONE]", "THREE:ONE") + .put("test[THREE,TWO]", "THREE:TWO") + .put("test[THREE,THREE]", "THREE:THREE") + .build(); + } + } + + @RunAsTest + public static class MultipleAnnotatedParameters extends SuccessfulTestCaseBase { + + @Test + public void test( + @TestParameter TestEnum enumParameterA, + @TestParameter({"TWO", "THREE"}) TestEnum enumParameterB, + @TestParameter({"!!binary 'ZGF0YQ=='", "data2"}) byte[] bytes) { + storeTestParametersForThisTest(enumParameterA, enumParameterB, new String(bytes)); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test[ONE,TWO,[100, 97, 116, 97]]", "ONE:TWO:data") + .put("test[ONE,TWO,[100, 97, 116, 97, 50]]", "ONE:TWO:data2") + .put("test[ONE,THREE,[100, 97, 116, 97]]", "ONE:THREE:data") + .put("test[ONE,THREE,[100, 97, 116, 97, 50]]", "ONE:THREE:data2") + .put("test[TWO,TWO,[100, 97, 116, 97]]", "TWO:TWO:data") + .put("test[TWO,TWO,[100, 97, 116, 97, 50]]", "TWO:TWO:data2") + .put("test[TWO,THREE,[100, 97, 116, 97]]", "TWO:THREE:data") + .put("test[TWO,THREE,[100, 97, 116, 97, 50]]", "TWO:THREE:data2") + .put("test[THREE,TWO,[100, 97, 116, 97]]", "THREE:TWO:data") + .put("test[THREE,TWO,[100, 97, 116, 97, 50]]", "THREE:TWO:data2") + .put("test[THREE,THREE,[100, 97, 116, 97]]", "THREE:THREE:data") + .put("test[THREE,THREE,[100, 97, 116, 97, 50]]", "THREE:THREE:data2") + .build(); + } + } + + @RunAsTest + public static class WithValuesProvider extends SuccessfulTestCaseBase { + + private final int number1; + + @TestParameter(valuesProvider = TestNumberProvider.class) + private int number2; + + public WithValuesProvider( + @TestParameter(valuesProvider = TestNumberProvider.class) int number1) { + this.number1 = number1; + } + + @Test + public void stringTest( + @TestParameter(valuesProvider = TestStringProvider.class) String stringParam) { + storeTestParametersForThisTest(number1, number2, stringParam); + } + + @Test + public void charMatcherTest( + @TestParameter(valuesProvider = CharMatcherProvider.class) CharMatcher charMatcher) { + storeTestParametersForThisTest(number1, number2, charMatcher); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("stringTest[one,one,A]", "1:1:A") + .put("stringTest[one,one,B]", "1:1:B") + .put("stringTest[one,one,stringParam=null]", "1:1:null") + .put("stringTest[one,one,nothing]", "1:1:null") + .put("stringTest[one,one,wizard]", "1:1:harry") + .put("stringTest[one,number1=2,A]", "2:1:A") + .put("stringTest[one,number1=2,B]", "2:1:B") + .put("stringTest[one,number1=2,stringParam=null]", "2:1:null") + .put("stringTest[one,number1=2,nothing]", "2:1:null") + .put("stringTest[one,number1=2,wizard]", "2:1:harry") + .put("stringTest[number2=2,one,A]", "1:2:A") + .put("stringTest[number2=2,one,B]", "1:2:B") + .put("stringTest[number2=2,one,stringParam=null]", "1:2:null") + .put("stringTest[number2=2,one,nothing]", "1:2:null") + .put("stringTest[number2=2,one,wizard]", "1:2:harry") + .put("stringTest[number2=2,number1=2,A]", "2:2:A") + .put("stringTest[number2=2,number1=2,B]", "2:2:B") + .put("stringTest[number2=2,number1=2,stringParam=null]", "2:2:null") + .put("stringTest[number2=2,number1=2,nothing]", "2:2:null") + .put("stringTest[number2=2,number1=2,wizard]", "2:2:harry") + .put("charMatcherTest[one,one,CharMatcher.any()]", "1:1:CharMatcher.any()") + .put("charMatcherTest[one,one,CharMatcher.ascii()]", "1:1:CharMatcher.ascii()") + .put("charMatcherTest[one,one,CharMatcher.whitespace()]", "1:1:CharMatcher.whitespace()") + .put("charMatcherTest[one,number1=2,CharMatcher.any()]", "2:1:CharMatcher.any()") + .put("charMatcherTest[one,number1=2,CharMatcher.ascii()]", "2:1:CharMatcher.ascii()") + .put( + "charMatcherTest[one,number1=2,CharMatcher.whitespace()]", + "2:1:CharMatcher.whitespace()") + .put("charMatcherTest[number2=2,one,CharMatcher.any()]", "1:2:CharMatcher.any()") + .put("charMatcherTest[number2=2,one,CharMatcher.ascii()]", "1:2:CharMatcher.ascii()") + .put( + "charMatcherTest[number2=2,one,CharMatcher.whitespace()]", + "1:2:CharMatcher.whitespace()") + .put("charMatcherTest[number2=2,number1=2,CharMatcher.any()]", "2:2:CharMatcher.any()") + .put( + "charMatcherTest[number2=2,number1=2,CharMatcher.ascii()]", "2:2:CharMatcher.ascii()") + .put( + "charMatcherTest[number2=2,number1=2,CharMatcher.whitespace()]", + "2:2:CharMatcher.whitespace()") + .build(); + } + + private static final class TestNumberProvider extends TestParameterValuesProvider { + @Override + public List<?> provideValues(Context context) { + return newArrayList(value(1).withName("one"), 2); + } + } + + private static final class TestStringProvider extends TestParameterValuesProvider { + @Override + public List<?> provideValues(Context context) { + return newArrayList( + "A", "B", null, value(null).withName("nothing"), value("harry").withName("wizard")); + } + } + + private static final class CharMatcherProvider extends TestParameterValuesProvider { + @Override + public List<CharMatcher> provideValues(Context context) { + return newArrayList(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace()); + } + } + } + + @Parameters(name = "{0}") + public static Collection<Object[]> parameters() { + return Arrays.stream(TestParameterTest.class.getClasses()) + .filter(cls -> cls.isAnnotationPresent(RunAsTest.class)) + .map(cls -> new Object[] {cls.getSimpleName(), cls}) + .collect(toImmutableList()); + } + + private final Class<?> testClass; + + public TestParameterTest(String name, Class<?> testClass) { + this.testClass = testClass; + } + + @Test + public void test() throws Exception { + SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures( + new PluggableTestRunner(testClass) { + @Override + protected TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.createNewParameterizedProcessors(); + } + }); + } + + private static ImmutableList<Class<? extends Annotation>> annotationTypes( + Iterable<Annotation> annotations) { + return FluentIterable.from(annotations).transform(Annotation::annotationType).toList(); + } +} diff --git a/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java new file mode 100644 index 0000000..5628330 --- /dev/null +++ b/junit4/src/test/java/com/google/testing/junit/testparameterinjector/TestParametersMethodProcessorTest.java @@ -0,0 +1,554 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.testing.junit.testparameterinjector.SharedTestUtilitiesJUnit4.SuccessfulTestCaseBase; +import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValues; +import com.google.testing.junit.testparameterinjector.TestParameters.TestParametersValuesProvider; +import java.lang.annotation.Retention; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class TestParametersMethodProcessorTest { + + @Retention(RUNTIME) + @interface RunAsTest { + String failsWithMessage() default ""; + } + + public enum TestEnum { + ONE, + TWO, + THREE; + } + + private static final class TestEnumValuesProvider implements TestParametersValuesProvider { + @Override + public List<TestParametersValues> provideValues() { + return ImmutableList.of( + TestParametersValues.builder().name("one").addParameter("testEnum", TestEnum.ONE).build(), + TestParametersValues.builder().addParameter("testEnum", TestEnum.TWO).build(), + TestParametersValues.builder().name("null-case").addParameter("testEnum", null).build()); + } + } + + @RunAsTest + public static class SimpleMethodAnnotation extends SuccessfulTestCaseBase { + + @Test + @TestParameters("{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}") + @TestParameters("{testEnum: TWO,\ntestLong: 22,\ntestBoolean: true,\r\n\r\n testString: 'DEF'}") + @TestParameters("{testEnum: null, testLong: 33, testBoolean: false, testString: null}") + public void test(TestEnum testEnum, long testLong, boolean testBoolean, String testString) { + storeTestParametersForThisTest(testEnum, testLong, testBoolean, testString); + } + + @Test + @TestParameters({ + "{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}", + "{testEnum: TWO,\ntestLong: 22,\ntestBoolean: true,\r\n\r\n testString: 'DEF'}", + "{testEnum: null, testLong: 33, testBoolean: false, testString: null}", + }) + public void test_singleAnnotation( + TestEnum testEnum, long testLong, boolean testBoolean, String testString) { + storeTestParametersForThisTest(testEnum, testLong, testBoolean, testString); + } + + @Test + @TestParameters("{testString: ABC}") + @TestParameters( + "{testString: 'This is a very long string (240 characters) that would normally cause" + + " Sponge+Tin to exceed the filename limit of 255 characters." + + " =================================================================================" + + "=============='}") + public void test2_withLongNames(String testString) { + storeTestParametersForThisTest(testString); + } + + @Test + @TestParameters( + "{testEnums: [ONE, TWO, THREE], testLongs: [11, 4], testBooleans: [false, true]," + + " testStrings: [ABC, '123']}") + @TestParameters( + "{testEnums: [TWO],\ntestLongs: [22],\ntestBooleans: [true],\r\n\r\n testStrings: ['DEF']}") + @TestParameters("{testEnums: [], testLongs: [], testBooleans: [], testStrings: []}") + public void test3_withRepeatedParams( + List<TestEnum> testEnums, + List<Long> testLongs, + List<Boolean> testBooleans, + List<String> testStrings) { + storeTestParametersForThisTest(testEnums, testLongs, testBooleans, testStrings); + } + + @Test + @TestParameters(customName = "custom1", value = "{testEnum: ONE}") + @TestParameters("{testEnum: TWO}") + @TestParameters(customName = "custom3", value = "{testEnum: THREE}") + public void test4_withCustomName(TestEnum testEnum) { + storeTestParametersForThisTest(testEnum); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put( + "test[{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}]", + "ONE:11:false:ABC") + .put( + "test[{testEnum: TWO, testLong: 22, testBoolean: true, testString: 'DEF'}]", + "TWO:22:true:DEF") + .put( + "test[{testEnum: null, testLong: 33, testBoolean: false, testString: null}]", + "null:33:false:null") + .put( + "test_singleAnnotation[{testEnum: ONE, testLong: 11, testBoolean: false, testString:" + + " ABC}]", + "ONE:11:false:ABC") + .put( + "test_singleAnnotation[{testEnum: TWO, testLong: 22, testBoolean: true, testString:" + + " 'DEF'}]", + "TWO:22:true:DEF") + .put( + "test_singleAnnotation[{testEnum: null, testLong: 33, testBoolean: false, testString:" + + " null}]", + "null:33:false:null") + .put("test2_withLongNames[1.{testString: ABC}]", "ABC") + .put( + "test2_withLongNames[2.{testString: 'This is a very long string (240 characters) that" + + " would normally cause Sponge+Tin to exceed the filename limit of 255" + + " characters. =============================...]", + "This is a very long string (240 characters) that would normally cause Sponge+Tin to" + + " exceed the filename limit of 255 characters." + + " ===============================================================================================") + .put( + "test3_withRepeatedParams[{testEnums: [ONE, TWO, THREE], testLongs: [11, 4]," + + " testBooleans: [false, true], testStrings: [ABC, '123']}]", + "[ONE, TWO, THREE]:[11, 4]:[false, true]:[ABC, 123]") + .put( + "test3_withRepeatedParams[{testEnums: [TWO], testLongs: [22], testBooleans: [true]," + + " testStrings: ['DEF']}]", + "[TWO]:[22]:[true]:[DEF]") + .put( + "test3_withRepeatedParams[{testEnums: [], testLongs: [], testBooleans: []," + + " testStrings: []}]", + "[]:[]:[]:[]") + .put("test4_withCustomName[custom1]", "ONE") + .put("test4_withCustomName[{testEnum: TWO}]", "TWO") + .put("test4_withCustomName[custom3]", "THREE") + .build(); + } + } + + @RunAsTest + public static class SimpleConstructorAnnotation extends SuccessfulTestCaseBase { + + private final TestEnum testEnum; + private final long testLong; + private final boolean testBoolean; + private final String testString; + + @TestParameters({ + "{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}", + "{testEnum: TWO, testLong: 22, testBoolean: true, testString: DEF}", + "{testEnum: null, testLong: 33, testBoolean: false, testString: null}", + }) + public SimpleConstructorAnnotation( + TestEnum testEnum, long testLong, boolean testBoolean, String testString) { + this.testEnum = testEnum; + this.testLong = testLong; + this.testBoolean = testBoolean; + this.testString = testString; + } + + @Test + public void test1() { + storeTestParametersForThisTest(testEnum, testLong, testBoolean, testString); + } + + @Test + public void test2() { + storeTestParametersForThisTest(testEnum, testLong, testBoolean, testString); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put( + "test1[{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}]", + "ONE:11:false:ABC") + .put( + "test1[{testEnum: TWO, testLong: 22, testBoolean: true, testString: DEF}]", + "TWO:22:true:DEF") + .put( + "test1[{testEnum: null, testLong: 33, testBoolean: false, testString: null}]", + "null:33:false:null") + .put( + "test2[{testEnum: ONE, testLong: 11, testBoolean: false, testString: ABC}]", + "ONE:11:false:ABC") + .put( + "test2[{testEnum: TWO, testLong: 22, testBoolean: true, testString: DEF}]", + "TWO:22:true:DEF") + .put( + "test2[{testEnum: null, testLong: 33, testBoolean: false, testString: null}]", + "null:33:false:null") + .build(); + } + } + + @RunAsTest + public static class ConstructorAnnotationWithProvider extends SuccessfulTestCaseBase { + + private final TestEnum testEnum; + + @TestParameters(valuesProvider = TestEnumValuesProvider.class) + public ConstructorAnnotationWithProvider(TestEnum testEnum) { + this.testEnum = testEnum; + } + + @Test + public void test() { + storeTestParametersForThisTest(testEnum); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test[one]", "ONE") + .put("test[{TWO}]", "TWO") + .put("test[null-case]", "null") + .build(); + } + } + + @RunAsTest + public static class MethodAnnotationWithProvider extends SuccessfulTestCaseBase { + + @TestParameters(valuesProvider = CustomProvider.class) + @Test + public void test(int testInt, TestEnum testEnum) { + storeTestParametersForThisTest(testInt, testEnum); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test[{testInt=5, ONE}]", "5:ONE") + .put("test[{testInt=10, TWO}]", "10:TWO") + .build(); + } + + private static final class CustomProvider implements TestParametersValuesProvider { + @Override + public List<TestParametersValues> provideValues() { + return ImmutableList.of( + TestParametersValues.builder() + .addParameter("testInt", 5) + .addParameter("testEnum", TestEnum.ONE) + .build(), + TestParametersValues.builder() + .addParameter("testInt", 10) + .addParameter("testEnum", TestEnum.TWO) + .build()); + } + } + } + + public abstract static class BaseClassWithMethodAnnotation extends SuccessfulTestCaseBase { + + @Test + @TestParameters("{testEnum: ONE}") + @TestParameters("{testEnum: TWO}") + public void testInBase(TestEnum testEnum) { + storeTestParametersForThisTest(testEnum); + } + } + + @RunAsTest + public static class AnnotationInheritedFromBaseClass extends BaseClassWithMethodAnnotation { + + @Test + @TestParameters({"{testEnum: TWO}", "{testEnum: THREE}"}) + public void testInChild(TestEnum testEnum) { + storeTestParametersForThisTest(testEnum); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("testInChild[{testEnum: TWO}]", "TWO") + .put("testInChild[{testEnum: THREE}]", "THREE") + .put("testInBase[{testEnum: ONE}]", "ONE") + .put("testInBase[{testEnum: TWO}]", "TWO") + .build(); + } + } + + @RunAsTest + public static class MixedWithTestParameterMethodAnnotation extends SuccessfulTestCaseBase { + + private final TestEnum testEnumFromConstructor; + + @TestParameters("{testEnum: ONE}") + @TestParameters("{testEnum: TWO}") + public MixedWithTestParameterMethodAnnotation(TestEnum testEnum) { + this.testEnumFromConstructor = testEnum; + } + + @Test + public void test1(@TestParameter TestEnum testEnum) { + storeTestParametersForThisTest(testEnumFromConstructor, testEnum); + } + + @Test + @TestParameters("{testString: ABC}") + @TestParameters("{testString: DEF}") + public void test2(String testString) { + storeTestParametersForThisTest(testEnumFromConstructor, testString); + } + + @Test + @TestParameters("{testString: ABC}") + @TestParameters( + "{testString: 'This is a very long string (240 characters) that would normally cause" + + " Sponge+Tin to exceed the filename limit of 255 characters." + + " =================================================================================" + + "=============='}") + public void test3_withLongNames(String testString) { + storeTestParametersForThisTest(testEnumFromConstructor, testString); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test1[{testEnum: ONE},ONE]", "ONE:ONE") + .put("test1[{testEnum: ONE},TWO]", "ONE:TWO") + .put("test1[{testEnum: ONE},THREE]", "ONE:THREE") + .put("test1[{testEnum: TWO},ONE]", "TWO:ONE") + .put("test1[{testEnum: TWO},TWO]", "TWO:TWO") + .put("test1[{testEnum: TWO},THREE]", "TWO:THREE") + .put("test2[{testEnum: ONE},{testString: ABC}]", "ONE:ABC") + .put("test2[{testEnum: ONE},{testString: DEF}]", "ONE:DEF") + .put("test2[{testEnum: TWO},{testString: ABC}]", "TWO:ABC") + .put("test2[{testEnum: TWO},{testString: DEF}]", "TWO:DEF") + .put("test3_withLongNames[{testEnum: ONE},1.{testString: ABC}]", "ONE:ABC") + .put( + "test3_withLongNames[{testEnum: ONE},2.{testString: 'This is a very long string (240" + + " characters) that would normally caus...]", + "ONE:This is a very long string (240 characters) that would normally cause Sponge+Tin" + + " to exceed the filename limit of 255 characters." + + " ===================================================================" + + "============================") + .put("test3_withLongNames[{testEnum: TWO},1.{testString: ABC}]", "TWO:ABC") + .put( + "test3_withLongNames[{testEnum: TWO},2.{testString: 'This is a very long string (240" + + " characters) that would normally caus...]", + "TWO:This is a very long string (240 characters) that would normally cause Sponge+Tin" + + " to exceed the filename limit of 255 characters." + + " ======================================================================" + + "=========================") + .build(); + } + } + + @RunAsTest + public static class MixedWithTestParameterFieldAnnotation extends SuccessfulTestCaseBase { + + private final TestEnum testEnumB; + + @TestParameter TestEnum testEnumA; + + @TestParameters("{testEnumB: ONE}") + @TestParameters("{testEnumB: TWO}") + public MixedWithTestParameterFieldAnnotation(TestEnum testEnumB) { + this.testEnumB = testEnumB; + } + + @Test + @TestParameters({"{testString: ABC}", "{testString: DEF}"}) + public void test1(String testString) { + storeTestParametersForThisTest(testEnumA, testEnumB, testString); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test1[{testEnumB: ONE},{testString: ABC},ONE]", "ONE:ONE:ABC") + .put("test1[{testEnumB: ONE},{testString: ABC},TWO]", "TWO:ONE:ABC") + .put("test1[{testEnumB: ONE},{testString: ABC},THREE]", "THREE:ONE:ABC") + .put("test1[{testEnumB: ONE},{testString: DEF},ONE]", "ONE:ONE:DEF") + .put("test1[{testEnumB: ONE},{testString: DEF},TWO]", "TWO:ONE:DEF") + .put("test1[{testEnumB: ONE},{testString: DEF},THREE]", "THREE:ONE:DEF") + .put("test1[{testEnumB: TWO},{testString: ABC},ONE]", "ONE:TWO:ABC") + .put("test1[{testEnumB: TWO},{testString: ABC},TWO]", "TWO:TWO:ABC") + .put("test1[{testEnumB: TWO},{testString: ABC},THREE]", "THREE:TWO:ABC") + .put("test1[{testEnumB: TWO},{testString: DEF},ONE]", "ONE:TWO:DEF") + .put("test1[{testEnumB: TWO},{testString: DEF},TWO]", "TWO:TWO:DEF") + .put("test1[{testEnumB: TWO},{testString: DEF},THREE]", "THREE:TWO:DEF") + .build(); + } + } + + @RunAsTest( + failsWithMessage = + "Either a value or a valuesProvider must be set in @TestParameters on test1()") + public static class InvalidTestBecauseEmptyAnnotation { + @Test + @TestParameters + public void test1() {} + } + + @RunAsTest( + failsWithMessage = + "Either a value or a valuesProvider must be set in @TestParameters on" + + " com.google.testing.junit.testparameterinjector.TestParametersMethodProcessorTest" + + "$InvalidTestBecauseEmptyAnnotationOnConstructor()") + public static class InvalidTestBecauseEmptyAnnotationOnConstructor { + @TestParameters + public InvalidTestBecauseEmptyAnnotationOnConstructor() {} + + @Test + public void test1() {} + } + + @RunAsTest( + failsWithMessage = + "It is not allowed to specify both value and valuesProvider in" + + " @TestParameters(value=[{testEnum: ONE}], valuesProvider=TestEnumValuesProvider)" + + " on test1()") + public static class InvalidTestBecauseCombiningValueWithProvider { + + @Test + @TestParameters(value = "{testEnum: ONE}", valuesProvider = TestEnumValuesProvider.class) + public void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "Either a value or a valuesProvider must be set in @TestParameters on test1()") + public static class InvalidTestBecauseRepeatedAnnotationIsEmpty { + @Test + @TestParameters(value = "{testEnum: ONE}") + @TestParameters + public void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "When specifying more than one @TestParameter for a method/constructor, each annotation" + + " must have exactly one value. Instead, got 2 values on test1(): [{testEnum: TWO}," + + " {testEnum: THREE}]") + public static class InvalidTestBecauseRepeatedAnnotationHasMultipleValues { + @Test + @TestParameters(value = "{testEnum: ONE}") + @TestParameters(value = {"{testEnum: TWO}", "{testEnum: THREE}"}) + public void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "Setting a valuesProvider is not supported for methods/constructors with" + + " multiple @TestParameters annotations on test1()") + public static class InvalidTestBecauseRepeatedAnnotationHasProvider { + @Test + @TestParameters(valuesProvider = TestEnumValuesProvider.class) + @TestParameters(valuesProvider = TestEnumValuesProvider.class) + public void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "Setting @TestParameters.customName is only allowed if there is exactly one YAML string" + + " in @TestParameters.value (on test1())") + public static class InvalidTestBecauseNamedAnnotationHasMultipleValues { + @Test + @TestParameters( + customName = "custom", + value = {"{testEnum: TWO}", "{testEnum: THREE}"}) + public void test1(TestEnum testEnum) {} + } + + @RunAsTest(failsWithMessage = "Test class should have exactly one public constructor") + public static class InvalidTestBecausePackagePrivateConstructor { + InvalidTestBecausePackagePrivateConstructor() {} + + @Test + public void test1() {} + } + + @Parameters(name = "{0}") + public static Collection<Object[]> parameters() { + return Arrays.stream(TestParametersMethodProcessorTest.class.getClasses()) + .filter(cls -> cls.isAnnotationPresent(RunAsTest.class)) + .map( + cls -> + new Object[] { + cls.getSimpleName(), cls, cls.getAnnotation(RunAsTest.class).failsWithMessage() + }) + .collect(toImmutableList()); + } + + private final Class<?> testClass; + private final Optional<String> maybeFailureMessage; + + public TestParametersMethodProcessorTest( + String name, Class<?> testClass, String failsWithMessage) { + this.testClass = testClass; + this.maybeFailureMessage = + failsWithMessage.isEmpty() ? Optional.empty() : Optional.of(failsWithMessage); + } + + @Test + public void test_success() throws Exception { + assume().that(maybeFailureMessage.isPresent()).isFalse(); + + SharedTestUtilitiesJUnit4.runTestsAndAssertNoFailures(newTestRunner()); + } + + @Test + public void test_failure() throws Exception { + assume().that(maybeFailureMessage.isPresent()).isTrue(); + + Exception exception = + assertThrows( + Exception.class, + () -> SharedTestUtilitiesJUnit4.runTestsAndGetFailures(newTestRunner())); + + assertThat(exception).hasMessageThat().contains(maybeFailureMessage.get()); + } + + private PluggableTestRunner newTestRunner() throws Exception { + return new PluggableTestRunner(testClass) { + @Override + protected TestMethodProcessorList createTestMethodProcessorList() { + return TestMethodProcessorList.createNewParameterizedProcessors(); + } + }; + } +} diff --git a/junit5/pom.xml b/junit5/pom.xml new file mode 100644 index 0000000..345604b --- /dev/null +++ b/junit5/pom.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Copyright 2021 Google Inc. + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>com.google.testparameterinjector</groupId> + <artifactId>test-parameter-injector-parent</artifactId> + <version>HEAD-SNAPSHOT</version> + </parent> + + <artifactId>test-parameter-injector-junit5</artifactId> + + <name>TestParameterInjector for JUnit5</name> + + <dependencies> + <!-- Compile-time dependencies --> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter</artifactId> + <version>5.8.1</version> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <version>5.8.1</version> + </dependency> + + <!-- Test dependencies --> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-params</artifactId> + <version>5.8.1</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.platform</groupId> + <artifactId>junit-platform-launcher</artifactId> + <version>1.8.1</version> + <scope>test</scope> + </dependency> + </dependencies> +</project> diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/BaseTestParameterValidator.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/BaseTestParameterValidator.java new file mode 100644 index 0000000..2386278 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/BaseTestParameterValidator.java @@ -0,0 +1,90 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static java.lang.Math.min; + +import com.google.common.collect.FluentIterable; +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Default base class for {@link TestParameterValidator}, simplifying how validators can exclude + * variable independent test parameters annotations. + */ +abstract class BaseTestParameterValidator implements TestParameterValidator { + + @Override + public boolean shouldSkip(Context context) { + for (List<Class<? extends Annotation>> parameters : getIndependentParameters(context)) { + checkArgument(!parameters.isEmpty()); + // For independent test parameters, the only allowed tests will be those that use the same + // Nth specified parameter, except for parameter values that have less specified values than + // others. + + // For example, if parameter A has values a1 and a2, parameter B has values b1 and b2, and + // parameter C has values c1, c2 and c3, given that A, B and C are independent, the only + // tests that will not be skipped will be {(a1, b1, c1), (a2, b2, c2), (a2, b2, c3)}, + // instead of 12 tests that would constitute their cartesian product. + + // First, find the largest specified value count (parameter C in the example above), + // so that we can easily determine which parameter value should be used for validating the + // other parameters (e.g. should this test be for (a1, b1, c1), (a2, b2, c2), or + // (a2, b2, c3). The test parameter 'C' will be the 'leadingParameter'. + + Class<? extends Annotation> leadingParameter = + FluentIterable.from(parameters) + .toSortedList( + (o1, o2) -> + Integer.compare( + context.getSpecifiedValues(o1).size(), + context.getSpecifiedValues(o2).size())) + .reverse() + .get(0); + + // Second, determine which index is the current value in the specified value list of + // the leading parameter. In the example above, the index of the current value 'c2' of the + // leading parameter 'C' would be '1', given the specified values (c1, c2, c3). + int leadingParameterValueIndex = + getValueIndex(context, leadingParameter, context.getValue(leadingParameter).get()); + checkState(leadingParameterValueIndex >= 0); + // Each independent test parameter should be the same index, or the last available index. + // For example, if the parameter is A, and the leading parameter (C) index is 2, the A's index + // should be 1, since a2 is the only available value. + for (Class<? extends Annotation> parameter : parameters) { + List<Object> specifiedValues = context.getSpecifiedValues(parameter); + int valueIndex = specifiedValues.indexOf(context.getValue(parameter).get()); + int requiredValueIndex = min(leadingParameterValueIndex, specifiedValues.size() - 1); + if (valueIndex != requiredValueIndex) { + return true; + } + } + } + return false; + } + + private int getValueIndex(Context context, Class<? extends Annotation> annotation, Object value) { + return context.getSpecifiedValues(annotation).indexOf(value); + } + + /** + * Returns a list of TestParameterAnnotation annotated annotation types that are mutually + * independent, and therefore the combinations of their values do not need to be tested. + */ + protected abstract List<List<Class<? extends Annotation>>> getIndependentParameters( + Context context); +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ByteStringReflection.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ByteStringReflection.java new file mode 100644 index 0000000..80cac0b --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ByteStringReflection.java @@ -0,0 +1,98 @@ +/* + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableMap; +import java.lang.reflect.InvocationTargetException; + +/** + * Utility methods to interact with com.google.protobuf.ByteString via reflection. + * + * <p>This is a hack to avoid the open source project to depend on protobuf-lite/javalite, which is + * causing conflicts for users (see https://github.com/google/TestParameterInjector/issues/24). + */ +final class ByteStringReflection { + + static final Optional<Class<?>> MAYBE_BYTE_STRING_CLASS = maybeGetByteStringClass(); + + /** Equivalent of {@code object instanceof ByteString} */ + static boolean isInstanceOfByteString(Object object) { + if (MAYBE_BYTE_STRING_CLASS.isPresent()) { + return MAYBE_BYTE_STRING_CLASS.get().isInstance(object); + } else { + return false; + } + } + + /** Eqvuivalent of {@code ((ByteString) byteString).toByteArray()} */ + static byte[] byteStringToByteArray(Object byteString) { + return (byte[]) + invokeByteStringMethod("toByteArray", /* obj= */ byteString, /* args= */ ImmutableMap.of()); + } + + /** + * Eqvuivalent of {@code ByteString.copyFromUtf8(text)}. + * + * <p>Encodes {@code text} into a sequence of UTF-8 bytes and returns the result as a {@code + * ByteString}. + */ + static Object copyFromUtf8(String text) { + return invokeByteStringMethod( + "copyFromUtf8", /* obj= */ null, /* args= */ ImmutableMap.of(String.class, text)); + } + + /** + * Eqvuivalent of {@code ByteString.copyFrom(bytes)}. + * + * <p>Copies the given bytes into a {@code ByteString}. + */ + static Object copyFrom(byte[] bytes) { + return invokeByteStringMethod( + "copyFrom", /* obj= */ null, /* args= */ ImmutableMap.of(byte[].class, bytes)); + } + + @SuppressWarnings("UseMultiCatch") + private static Object invokeByteStringMethod( + String methodName, Object obj, ImmutableMap<Class<?>, ?> args) { + try { + return MAYBE_BYTE_STRING_CLASS + .get() + .getMethod(methodName, args.keySet().toArray(new Class<?>[0])) + .invoke(obj, args.values().toArray()); + /* + * Do not merge the 3 catch blocks below. javac would infer a type of + * ReflectiveOperationException, which Animal Sniffer would reject. (Old versions of + * Android don't *seem* to mind, but there might be edge cases of which we're unaware.) + */ + } catch (IllegalAccessException e) { + throw new LinkageError(String.format("Accessing %s()", methodName), e); + } catch (InvocationTargetException e) { + throw new LinkageError(String.format("Calling %s()", methodName), e); + } catch (NoSuchMethodException e) { + throw new LinkageError(String.format("Calling %s()", methodName), e); + } + } + + private static Optional<Class<?>> maybeGetByteStringClass() { + try { + return Optional.of(Class.forName("com.google.protobuf.ByteString")); + } catch (ClassNotFoundException | LinkageError unused) { + return Optional.absent(); + } + } + + private ByteStringReflection() {} // Inhibit instantiation +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ExecutableValidationResult.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ExecutableValidationResult.java new file mode 100644 index 0000000..4c0c40d --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ExecutableValidationResult.java @@ -0,0 +1,72 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.Iterables.getOnlyElement; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import java.util.Collection; + +/** + * Value class that captures the result of a validating a single constructor or test method. + * + * <p>If the validation is not validated by any processor, it will be validated using the default + * validator. If a processor validates a constructor/test method, the remaining processors will + * *not* be called. + */ +@AutoValue +abstract class ExecutableValidationResult { + + /** Returns true if the properties of the given constructor/test method were validated. */ + public abstract boolean wasValidated(); + + /** Returns the validation errors, if any. */ + public abstract ImmutableList<Throwable> validationErrors(); + + static ExecutableValidationResult notValidated() { + return of(/* wasValidated= */ false, /* validationErrors= */ ImmutableList.of()); + } + + static ExecutableValidationResult validated(Collection<Throwable> errors) { + return of(/* wasValidated= */ true, /* validationErrors= */ errors); + } + + static ExecutableValidationResult validated(Throwable error) { + return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of(error)); + } + + static ExecutableValidationResult valid() { + return of(/* wasValidated= */ true, /* validationErrors= */ ImmutableList.of()); + } + + private static ExecutableValidationResult of( + boolean wasValidated, Collection<Throwable> validationErrors) { + checkArgument(wasValidated || validationErrors.isEmpty()); + return new AutoValue_ExecutableValidationResult( + wasValidated, ImmutableList.copyOf(validationErrors)); + } + + void assertValid() { + if (wasValidated() && !validationErrors().isEmpty()) { + if (validationErrors().size() == 1) { + throw new AssertionError(getOnlyElement(validationErrors())); + } else { + throw new AssertionError(String.format("Found validation errors: %s", validationErrors())); + } + } + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/GenericParameterContext.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/GenericParameterContext.java new file mode 100644 index 0000000..02e5367 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/GenericParameterContext.java @@ -0,0 +1,191 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.collect.Iterables.getOnlyElement; + +import com.google.common.base.Function; +import com.google.common.base.Joiner; +import com.google.common.base.Optional; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Ordering; +import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.Proxy; +import java.util.NoSuchElementException; + +/** A value class that contains extra information about the context of a field or parameter. */ +final class GenericParameterContext { + + private final ImmutableList<Annotation> annotationsOnParameter; + + /** Same contract as #getAnnotations */ + private final Function<Class<? extends Annotation>, ImmutableList<? extends Annotation>> + getAnnotationsFunction; + + private final Class<?> testClass; + + private GenericParameterContext( + ImmutableList<Annotation> annotationsOnParameter, + Function<Class<? extends Annotation>, ImmutableList<? extends Annotation>> + getAnnotationsFunction, + Class<?> testClass) { + this.annotationsOnParameter = annotationsOnParameter; + this.getAnnotationsFunction = getAnnotationsFunction; + this.testClass = testClass; + } + + // Field.getAnnotationsByType() is not available on old Android SDKs. There is a fallback in that + // case in this method. + @SuppressWarnings("AndroidJdkLibsChecker") + static GenericParameterContext create(Field field, Class<?> testClass) { + return new GenericParameterContext( + ImmutableList.copyOf(field.getAnnotations()), + /* getAnnotationsFunction= */ annotationType -> { + try { + return ImmutableList.copyOf(field.getAnnotationsByType(annotationType)); + } catch (NoSuchMethodError ignored) { + return getAnnotationsFallback( + ImmutableList.copyOf(field.getAnnotations()), annotationType); + } + }, + testClass); + } + + // Parameter is not available on old Android SDKs, and isn't desugared. That's why this method + // should only be called with a fallback. + @SuppressWarnings("AndroidJdkLibsChecker") + static GenericParameterContext create(Parameter parameter, Class<?> testClass) { + return new GenericParameterContext( + ImmutableList.copyOf(parameter.getAnnotations()), + /* getAnnotationsFunction= */ annotationType -> + ImmutableList.copyOf(parameter.getAnnotationsByType(annotationType)), + testClass); + } + + static GenericParameterContext createWithRepeatableAnnotationsFallback( + Annotation[] annotationsOnParameter, Class<?> testClass) { + return new GenericParameterContext( + ImmutableList.copyOf(annotationsOnParameter), + /* getAnnotationsFunction= */ annotationType -> + getAnnotationsFallback(ImmutableList.copyOf(annotationsOnParameter), annotationType), + testClass); + } + + static GenericParameterContext createWithoutParameterAnnotations(Class<?> testClass) { + return new GenericParameterContext( + /* annotationsOnParameter= */ ImmutableList.of(), + /* getAnnotationsFunction= */ annotationType -> + getAnnotationsFallback(ImmutableList.of(), annotationType), + testClass); + } + + /** + * Returns the only annotation with the given type on the field or parameter. + * + * @throws NoSuchElementException if this there is no annotation with the given type + * @throws IllegalArgumentException if there are multiple annotations with the given type + */ + @SuppressWarnings("unchecked") // Safe because of the filter operation + <A extends Annotation> A getAnnotation(Class<A> annotationType) { + return (A) + getOnlyElement( + FluentIterable.from(annotationsOnParameter) + .filter(annotation -> annotation.annotationType().equals(annotationType)) + .toList()); + } + + /** + * Returns the annotations with the given type on the field or parameter. + * + * <p>Returns an empty list if this there is no annotation with the given type. + */ + @SuppressWarnings("unchecked") // Safe because of the getAnnotationsFunction contract + <A extends Annotation> ImmutableList<A> getAnnotations(Class<A> annotationType) { + return (ImmutableList<A>) getAnnotationsFunction.apply(annotationType); + } + + /** The class that contains the test that is currently being run. */ + Class<?> testClass() { + return testClass; + } + + /** A list of all annotations on the field or parameter. */ + ImmutableList<Annotation> annotationsOnParameter() { + return annotationsOnParameter; + } + + @Override + public String toString() { + return String.format( + "context(annotationsOnParameter=[%s],testClass=%s)", + FluentIterable.from( + ImmutableList.sortedCopyOf( + Ordering.natural().onResultOf(Annotation::toString), annotationsOnParameter)) + .transform( + annotation -> String.format("@%s", annotation.annotationType().getSimpleName())) + .join(Joiner.on(',')), + testClass().getSimpleName()); + } + + private static ImmutableList<Annotation> getAnnotationsFallback( + ImmutableList<Annotation> annotationsOnParameter, + Class<? extends Annotation> annotationType) { + ImmutableList<Annotation> candidates = + FluentIterable.from(annotationsOnParameter) + .filter(annotation -> annotation.annotationType().equals(annotationType)) + .toList(); + if (candidates.isEmpty() && getContainerType(annotationType).isPresent()) { + ImmutableList<Annotation> containerAnnotations = + getAnnotationsFallback(annotationsOnParameter, getContainerType(annotationType).get()); + if (containerAnnotations.size() == 1) { + Annotation containerAnnotation = getOnlyElement(containerAnnotations); + try { + Method annotationValueMethod = + containerAnnotation.annotationType().getDeclaredMethod("value"); + annotationValueMethod.setAccessible(true); + return ImmutableList.copyOf( + (Annotation[]) + Proxy.getInvocationHandler(containerAnnotation) + .invoke(containerAnnotation, annotationValueMethod, null)); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + return ImmutableList.of(); + } else { + return candidates; + } + } + + private static Optional<Class<? extends Annotation>> getContainerType( + Class<? extends Annotation> annotationType) { + try { + Repeatable repeatable = annotationType.getAnnotation(Repeatable.class); + if (repeatable == null) { + return Optional.absent(); + } else { + return Optional.of(repeatable.value()); + } + } catch (NoClassDefFoundError ignored) { + // If @Repeatable does not exist, then there is no container type by definition + return Optional.absent(); + } + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java new file mode 100644 index 0000000..130c186 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/ParameterValueParsing.java @@ -0,0 +1,303 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.common.collect.Lists; +import com.google.common.primitives.Primitives; +import com.google.common.primitives.UnsignedLong; +import com.google.common.reflect.TypeToken; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.lang.reflect.Array; +import java.lang.reflect.ParameterizedType; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import javax.annotation.Nullable; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; + +/** A helper class for parsing parameter values from strings. */ +final class ParameterValueParsing { + + @SuppressWarnings("unchecked") + static <E extends Enum<E>> Enum<?> parseEnum(String str, Class<?> enumType) { + return Enum.valueOf((Class<E>) enumType, str); + } + + static boolean isValidYamlString(String yamlString) { + try { + new Yaml(new SafeConstructor(new LoaderOptions())).load(yamlString); + return true; + } catch (RuntimeException e) { + return false; + } + } + + static Object parseYamlStringToJavaType(String yamlString, Class<?> javaType) { + return parseYamlObjectToJavaType(parseYamlStringToObject(yamlString), TypeToken.of(javaType)); + } + + static Object parseYamlStringToObject(String yamlString) { + return new Yaml(new SafeConstructor(new LoaderOptions())).load(yamlString); + } + + private static UnsignedLong parseYamlSignedLongToUnsignedLong(long number) { + checkState(number >= 0, "%s should be greater than or equal to zero", number); + return UnsignedLong.fromLongBits(number); + } + + @SuppressWarnings({"unchecked"}) + static Object parseYamlObjectToJavaType(Object parsedYaml, TypeToken<?> javaType) { + // Pass along null so we don't have to worry about it below + if (parsedYaml == null) { + return null; + } + + YamlValueTransformer yamlValueTransformer = + new YamlValueTransformer(parsedYaml, javaType.getRawType()); + + yamlValueTransformer + .ifJavaType(String.class) + .supportParsedType(String.class, self -> self) + // Also support other primitives because it's easy to accidentally write e.g. a number when + // a string was intended in YAML + .supportParsedType(Boolean.class, Object::toString) + .supportParsedType(Integer.class, Object::toString) + .supportParsedType(Long.class, Object::toString) + .supportParsedType(Double.class, Object::toString); + + yamlValueTransformer.ifJavaType(Boolean.class).supportParsedType(Boolean.class, self -> self); + + yamlValueTransformer.ifJavaType(Integer.class).supportParsedType(Integer.class, self -> self); + + yamlValueTransformer + .ifJavaType(Long.class) + .supportParsedType(Long.class, self -> self) + .supportParsedType(Integer.class, Integer::longValue); + + yamlValueTransformer + .ifJavaType(UnsignedLong.class) + .supportParsedType(Long.class, self -> parseYamlSignedLongToUnsignedLong(self.longValue())) + .supportParsedType( + Integer.class, self -> parseYamlSignedLongToUnsignedLong(self.longValue())) + // UnsignedLong::valueOf(BigInteger) will validate that BigInteger is in the valid range and + // throws otherwise. + .supportParsedType(BigInteger.class, UnsignedLong::valueOf); + + yamlValueTransformer + .ifJavaType(BigInteger.class) + .supportParsedType(Long.class, self -> BigInteger.valueOf(self.longValue())) + .supportParsedType(Integer.class, self -> BigInteger.valueOf(self.longValue())) + .supportParsedType(BigInteger.class, self -> self); + + yamlValueTransformer + .ifJavaType(Float.class) + .supportParsedType(Float.class, self -> self) + .supportParsedType(Double.class, Double::floatValue) + .supportParsedType(Integer.class, Integer::floatValue) + .supportParsedType(String.class, Float::valueOf); + + yamlValueTransformer + .ifJavaType(Double.class) + .supportParsedType(Double.class, self -> self) + .supportParsedType(Integer.class, Integer::doubleValue) + .supportParsedType(Long.class, Long::doubleValue) + .supportParsedType(String.class, Double::valueOf); + + yamlValueTransformer + .ifJavaType(Enum.class) + .supportParsedType( + String.class, str -> ParameterValueParsing.parseEnum(str, javaType.getRawType())); + + yamlValueTransformer + .ifJavaType(byte[].class) + .supportParsedType(byte[].class, self -> self) + // Uses String based charset because StandardCharsets was not introduced until later + // versions of Android + // See https://developer.android.com/reference/java/nio/charset/StandardCharsets. + .supportParsedType(String.class, s -> s.getBytes(Charset.forName("UTF-8"))); + + if (ByteStringReflection.MAYBE_BYTE_STRING_CLASS.isPresent()) { + yamlValueTransformer + .ifJavaType((Class<Object>) ByteStringReflection.MAYBE_BYTE_STRING_CLASS.get()) + .supportParsedType(String.class, ByteStringReflection::copyFromUtf8) + .supportParsedType(byte[].class, ByteStringReflection::copyFrom); + } + + // Added mainly for protocol buffer parsing + yamlValueTransformer + .ifJavaType(List.class) + .supportParsedType( + List.class, + list -> + Lists.transform( + list, + e -> + parseYamlObjectToJavaType( + e, getGenericParameterType(javaType, /* parameterIndex= */ 0)))); + yamlValueTransformer + .ifJavaType(Map.class) + .supportParsedType(Map.class, map -> parseYamlMapToJavaMap(map, javaType)); + + return yamlValueTransformer.transformedJavaValue(); + } + + private static Map<?, ?> parseYamlMapToJavaMap(Map<?, ?> map, TypeToken<?> javaType) { + Map<Object, Object> returnedMap = new LinkedHashMap<>(); + for (Entry<?, ?> entry : map.entrySet()) { + returnedMap.put( + parseYamlObjectToJavaType( + entry.getKey(), getGenericParameterType(javaType, /* parameterIndex= */ 0)), + parseYamlObjectToJavaType( + entry.getValue(), getGenericParameterType(javaType, /* parameterIndex= */ 1))); + } + return returnedMap; + } + + private static TypeToken<?> getGenericParameterType(TypeToken<?> typeToken, int parameterIndex) { + checkArgument( + typeToken.getType() instanceof ParameterizedType, + "Could not parse the generic parameter of type %s", + typeToken); + + ParameterizedType parameterizedType = (ParameterizedType) typeToken.getType(); + return TypeToken.of(parameterizedType.getActualTypeArguments()[parameterIndex]); + } + + private static final class YamlValueTransformer { + private final Object parsedYaml; + private final Class<?> javaType; + @Nullable private Object transformedJavaValue; + + YamlValueTransformer(Object parsedYaml, Class<?> javaType) { + this.parsedYaml = parsedYaml; + this.javaType = javaType; + } + + <JavaT> SupportedJavaType<JavaT> ifJavaType(Class<JavaT> supportedJavaType) { + return new SupportedJavaType<>(supportedJavaType); + } + + Object transformedJavaValue() { + checkArgument( + transformedJavaValue != null, + "Could not map YAML value %s (class = %s) to java class %s", + parsedYaml, + parsedYaml.getClass(), + javaType); + return transformedJavaValue; + } + + final class SupportedJavaType<JavaT> { + + private final Class<JavaT> supportedJavaType; + + private SupportedJavaType(Class<JavaT> supportedJavaType) { + this.supportedJavaType = supportedJavaType; + } + + @SuppressWarnings("unchecked") + @CanIgnoreReturnValue + <ParsedYamlT> SupportedJavaType<JavaT> supportParsedType( + Class<ParsedYamlT> parsedYamlType, Function<ParsedYamlT, JavaT> transformation) { + if (Primitives.wrap(supportedJavaType).isAssignableFrom(Primitives.wrap(javaType))) { + if (Primitives.wrap(parsedYamlType).isInstance(parsedYaml)) { + checkState( + transformedJavaValue == null, + "This case is already handled. This is a bug in" + + " testparameterinjector.TestParametersMethodProcessor."); + try { + transformedJavaValue = checkNotNull(transformation.apply((ParsedYamlT) parsedYaml)); + } catch (Exception e) { + throw new IllegalArgumentException( + String.format( + "Could not map YAML value %s (class = %s) to java class %s", + parsedYaml, parsedYaml.getClass(), javaType), + e); + } + } + } + + return this; + } + } + } + + static String formatTestNameString(Optional<String> parameterName, @Nullable Object value) { + Object unwrappedValue; + Optional<String> customName; + + if (value instanceof TestParameterValue) { + TestParameterValue tpValue = (TestParameterValue) value; + unwrappedValue = tpValue.getWrappedValue(); + customName = tpValue.getCustomName(); + } else { + unwrappedValue = value; + customName = Optional.absent(); + } + + String result = customName.or(() -> valueAsString(unwrappedValue)); + if (parameterName.isPresent() && !customName.isPresent()) { + if (unwrappedValue == null + || + // Primitives are often ambiguous + Primitives.unwrap(unwrappedValue.getClass()).isPrimitive() + // Ambiguous String cases + || unwrappedValue.equals("null") + || (unwrappedValue instanceof CharSequence + && CharMatcher.anyOf("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + .matchesNoneOf((CharSequence) unwrappedValue))) { + // Prefix the parameter value with its field name. This is to avoid test names + // such as myMethod_success[true,false,2]. Instead, it'll be + // myMethod_success[dryRun=true,experimentFlag=false,retries=2]. + result = String.format("%s=%s", parameterName.get(), valueAsString(unwrappedValue)); + } + } + return result.trim().replaceAll("\\s+", " "); + } + + private static String valueAsString(Object value) { + if (value != null && value.getClass().isArray()) { + StringBuilder resultBuider = new StringBuilder(); + resultBuider.append("["); + for (int i = 0; i < Array.getLength(value); i++) { + if (i > 0) { + resultBuider.append(", "); + } + resultBuider.append(Array.get(value, i)); + } + resultBuider.append("]"); + return resultBuider.toString(); + } else if (ByteStringReflection.isInstanceOfByteString(value)) { + return Arrays.toString(ByteStringReflection.byteStringToByteArray(value)); + } else { + return String.valueOf(value); + } + } + + private ParameterValueParsing() {} +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java new file mode 100644 index 0000000..7ed3412 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java @@ -0,0 +1,325 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Joiner; +import com.google.common.collect.ContiguousSet; +import com.google.common.collect.DiscreteDomain; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; +import com.google.common.collect.Range; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import javax.annotation.Nullable; + +/** A POJO containing information about a test (name and anotations). */ +@AutoValue +abstract class TestInfo { + + /** + * The maximum amount of characters that {@link #getName()} can have. + * + * <p>See b/168325767 for the reason behind this. tl;dr the name is put into a Unix file with max + * 255 characters. The surrounding constant characters take up 31 characters. The max is reduced + * by an additional 24 characters to account for future changes. + */ + static final int MAX_TEST_NAME_LENGTH = 200; + + public abstract Method getMethod(); + + /** + * The test class that is being run. + * + * <p>Note that this is not always the same as the class that declares {@link #getMethod()} + * because test methods can be inherited. + */ + public abstract Class<?> getTestClass(); + + public final String getName() { + if (getParameters().isEmpty()) { + return getMethod().getName(); + } else { + return String.format( + "%s[%s]", + getMethod().getName(), + FluentIterable.from(getParameters()) + .transform(TestInfoParameter::getValueInTestName) + .join(Joiner.on(","))); + } + } + + abstract ImmutableList<TestInfoParameter> getParameters(); + + public abstract ImmutableList<Annotation> getAnnotations(); + + @Nullable + public final <T extends Annotation> T getAnnotation(Class<T> annotationClass) { + for (Annotation annotation : getAnnotations()) { + if (annotationClass.isInstance(annotation)) { + return annotationClass.cast(annotation); + } + } + return null; + } + + final TestInfo withExtraParameters(List<TestInfoParameter> parameters) { + return new AutoValue_TestInfo( + getMethod(), + getTestClass(), + ImmutableList.<TestInfoParameter>builder() + .addAll(this.getParameters()) + .addAll(parameters) + .build(), + getAnnotations()); + } + + final TestInfo withExtraAnnotation(Annotation annotation) { + ImmutableList<Annotation> newAnnotations = + ImmutableList.<Annotation>builder().addAll(this.getAnnotations()).add(annotation).build(); + return new AutoValue_TestInfo(getMethod(), getTestClass(), getParameters(), newAnnotations); + } + + /** + * Returns a new TestInfo instance with updated parameter names. + * + * @param parameterWithIndexToNewName A function of the parameter and its index in the {@link + * #getParameters()} list to the new name. + */ + private TestInfo withUpdatedParameterNames( + Java8BiFunction<TestInfoParameter, Integer, String> parameterWithIndexToNewName) { + return new AutoValue_TestInfo( + getMethod(), + getTestClass(), + FluentIterable.from( + ContiguousSet.create( + Range.closedOpen(0, getParameters().size()), DiscreteDomain.integers())) + .transform( + parameterIndex -> { + TestInfoParameter parameter = getParameters().get(parameterIndex); + return parameter.withValueInTestName( + parameterWithIndexToNewName.apply(parameter, parameterIndex)); + }) + .toList(), + getAnnotations()); + } + + public static TestInfo legacyCreate( + Method method, Class<?> testClass, String name, List<Annotation> annotations) { + return new AutoValue_TestInfo( + method, testClass, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations)); + } + + static TestInfo createWithoutParameters( + Method method, Class<?> testClass, List<Annotation> annotations) { + return new AutoValue_TestInfo( + method, testClass, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations)); + } + + static ImmutableList<TestInfo> shortenNamesIfNecessary(List<TestInfo> testInfos) { + if (FluentIterable.from(testInfos) + .anyMatch(info -> info.getName().length() > MAX_TEST_NAME_LENGTH)) { + int numberOfParameters = testInfos.get(0).getParameters().size(); + + if (numberOfParameters == 0) { + return ImmutableList.copyOf(testInfos); + } else { + Set<Integer> parameterIndicesThatNeedUpdate = + FluentIterable.from( + ContiguousSet.create( + Range.closedOpen(0, numberOfParameters), DiscreteDomain.integers())) + .filter( + parameterIndex -> + FluentIterable.from(testInfos) + .anyMatch( + info -> + info.getParameters() + .get(parameterIndex) + .getValueInTestName() + .length() + > getMaxCharactersPerParameter(info, numberOfParameters))) + .toSet(); + + return FluentIterable.from(testInfos) + .transform( + info -> + info.withUpdatedParameterNames( + (parameter, parameterIndex) -> + parameterIndicesThatNeedUpdate.contains(parameterIndex) + ? getShortenedName( + parameter, + getMaxCharactersPerParameter(info, numberOfParameters)) + : info.getParameters().get(parameterIndex).getValueInTestName())) + .toList(); + } + } else { + return ImmutableList.copyOf(testInfos); + } + } + + private static int getMaxCharactersPerParameter(TestInfo testInfo, int numberOfParameters) { + int maxLengthOfAllParameters = + // Subtract 2 characters for square brackets + MAX_TEST_NAME_LENGTH - testInfo.getMethod().getName().length() - 2; + + // Subtract 4 characters to leave place for joining commas and the parameter index. + return maxLengthOfAllParameters / numberOfParameters - 4; + } + + static ImmutableList<TestInfo> deduplicateTestNames(List<TestInfo> testInfos) { + long uniqueTestNameCount = + FluentIterable.from(testInfos).transform(TestInfo::getName).toSet().size(); + if (testInfos.size() == uniqueTestNameCount) { + // Return early if there are no duplicates + return ImmutableList.copyOf(testInfos); + } else { + return deduplicateWithNumberPrefixes(maybeAddTypesIfDuplicate(testInfos)); + } + } + + private static String getShortenedName( + TestInfoParameter parameter, int maxCharactersPerParameter) { + if (maxCharactersPerParameter < 4) { + // Not enough characters for "..." suffix + return String.valueOf(parameter.getIndexInValueSource() + 1); + } else { + String shortenedName = + parameter.getValueInTestName().length() > maxCharactersPerParameter + ? parameter.getValueInTestName().substring(0, maxCharactersPerParameter - 3) + "..." + : parameter.getValueInTestName(); + return String.format("%s.%s", parameter.getIndexInValueSource() + 1, shortenedName); + } + } + + private static ImmutableList<TestInfo> maybeAddTypesIfDuplicate(List<TestInfo> testInfos) { + Multimap<String, TestInfo> testNameToInfo = + MultimapBuilder.linkedHashKeys().arrayListValues().build(); + for (TestInfo testInfo : testInfos) { + testNameToInfo.put(testInfo.getName(), testInfo); + } + + return FluentIterable.from(testNameToInfo.keySet()) + .transformAndConcat( + testName -> { + Collection<TestInfo> matchedInfos = testNameToInfo.get(testName); + if (matchedInfos.size() == 1) { + // There was only one method with this name, so no deduplication is necessary + return matchedInfos; + } else { + // Found tests with duplicate test names + int numParameters = matchedInfos.iterator().next().getParameters().size(); + Set<Integer> indicesThatShouldGetSuffix = + // Find parameter indices for which a suffix would allow the reader to + // differentiate + FluentIterable.from( + ContiguousSet.create( + Range.closedOpen(0, numParameters), DiscreteDomain.integers())) + .filter( + parameterIndex -> + FluentIterable.from(matchedInfos) + .transform( + info -> + getTypeSuffix( + info.getParameters() + .get(parameterIndex) + .getValue())) + .toSet() + .size() + > 1) + .toSet(); + + return FluentIterable.from(matchedInfos) + .transform( + testInfo -> + testInfo.withUpdatedParameterNames( + (parameter, parameterIndex) -> + indicesThatShouldGetSuffix.contains(parameterIndex) + ? parameter.getValueInTestName() + + getTypeSuffix(parameter.getValue()) + : parameter.getValueInTestName())); + } + }) + .toList(); + } + + private static String getTypeSuffix(@Nullable Object value) { + if (value == null) { + return " (null reference)"; + } else { + return String.format(" (%s)", value.getClass().getSimpleName()); + } + } + + private static ImmutableList<TestInfo> deduplicateWithNumberPrefixes( + ImmutableList<TestInfo> testInfos) { + long uniqueTestNameCount = + FluentIterable.from(testInfos).transform(TestInfo::getName).toSet().size(); + if (testInfos.size() == uniqueTestNameCount) { + return ImmutableList.copyOf(testInfos); + } else { + // There are still duplicates, even after adding type suffixes. As a last resort: add a + // counter to all parameters to guarantee that each case is unique. + return FluentIterable.from(testInfos) + .transform( + testInfo -> + testInfo.withUpdatedParameterNames( + (parameter, parameterIndex) -> + String.format( + "%s.%s", + parameter.getIndexInValueSource() + 1, + parameter.getValueInTestName()))) + .toList(); + } + } + + @AutoValue + abstract static class TestInfoParameter { + + abstract String getValueInTestName(); + + @Nullable + abstract Object getValue(); + + /** + * The index of this parameter value in the list of all values provided by the provider that + * returned this value. + */ + abstract int getIndexInValueSource(); + + final TestInfoParameter withValueInTestName(String newValueInTestName) { + return create(newValueInTestName, getValue(), getIndexInValueSource()); + } + + static TestInfoParameter create( + String valueInTestName, @Nullable Object value, int indexInValueSource) { + checkArgument(indexInValueSource >= 0); + return new AutoValue_TestInfo_TestInfoParameter( + checkNotNull(valueInTestName), value, indexInValueSource); + } + } + + /** Copy of Java8's java.util.BiFunction which is not available in older versions of the JDK */ + interface Java8BiFunction<I, J, K> { + K apply(I a, J b); + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessor.java new file mode 100644 index 0000000..48e9a5e --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessor.java @@ -0,0 +1,66 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import com.google.common.base.Optional; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; + +/** + * Interface to change the list of methods used in a test. + * + * <p>Note: Implementations of this interface are expected to be immutable, i.e. they no longer + * change after construction. + */ +interface TestMethodProcessor { + + /** Allows to transform the test information (name and annotations). */ + List<TestInfo> calculateTestInfos(TestInfo originalTest); + + /** + * If this processor can handle the given constructor, returns the parameters with which it should + * be invoked. + * + * <p>This method is never called for a parameterless constructor. + */ + Optional<List<Object>> maybeGetConstructorParameters( + Constructor<?> constructor, TestInfo testInfo); + + /** + * If this processor can handle the given test, returns the parameters with which {@code + * testInfo.getMethod()} should be invoked. + * + * <p>This method is never called for a parameterless {@code testInfo.getMethod()}. + */ + Optional<List<Object>> maybeGetTestMethodParameters(TestInfo testInfo); + + /** + * Optionally process the test instance right after construction to ready it for the given test + * instance. + */ + void postProcessTestInstance(Object testInstance, TestInfo testInfo); + + /** Optionally validates the given constructor. */ + ExecutableValidationResult validateConstructor(Constructor<?> constructor); + + /** + * Optionally validates the given method. + * + * <p>Note that the given method is not necessarily declared in the given class because test + * methods can be inherited. + */ + ExecutableValidationResult validateTestMethod(Method testMethod, Class<?> testClass); +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessorList.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessorList.java new file mode 100644 index 0000000..d1020c8 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestMethodProcessorList.java @@ -0,0 +1,146 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import com.google.common.base.Optional; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +/** + * Combined version of all {@link TestMethodProcessor} implementations that this package supports. + */ +final class TestMethodProcessorList { + + private final ImmutableList<TestMethodProcessor> testMethodProcessors; + + private TestMethodProcessorList(ImmutableList<TestMethodProcessor> testMethodProcessors) { + this.testMethodProcessors = testMethodProcessors; + } + + /** + * Returns a TestMethodProcessorList that supports all features that this package supports, except + * the following legacy features: + * + * <ul> + * <li>No support for {@link org.junit.runners.Parameterized} + * <li>No support for class and method-level parameters, except for @TestParameters + * </ul> + */ + public static TestMethodProcessorList createNewParameterizedProcessors() { + return new TestMethodProcessorList( + ImmutableList.of( + new TestParametersMethodProcessor(), + TestParameterAnnotationMethodProcessor.onlyForFieldsAndParameters())); + } + + static TestMethodProcessorList empty() { + return new TestMethodProcessorList(ImmutableList.of()); + } + + /** + * Calculates the TestInfo instances for the given test method. Each TestInfo corresponds to a + * single test. + * + * <p>The returned list always contains at least one element. If there is no parameterization, + * this would be the TestInfo for running the test method without parameters. + */ + public List<TestInfo> calculateTestInfos(Method testMethod, Class<?> testClass) { + List<TestInfo> testInfos = + ImmutableList.of( + TestInfo.createWithoutParameters( + testMethod, testClass, ImmutableList.copyOf(testMethod.getAnnotations()))); + + for (final TestMethodProcessor testMethodProcessor : testMethodProcessors) { + List<TestInfo> list = new ArrayList<>(); + for (TestInfo lastTestInfo : testInfos) { + list.addAll(testMethodProcessor.calculateTestInfos(lastTestInfo)); + } + testInfos = list; + } + + testInfos = TestInfo.deduplicateTestNames(TestInfo.shortenNamesIfNecessary(testInfos)); + + return testInfos; + } + + /** + * Returns the parameters with which it should be invoked. + * + * <p>This method is never called for a parameterless constructor. + */ + public List<Object> getConstructorParameters(Constructor<?> constructor, TestInfo testInfo) { + return FluentIterable.from(testMethodProcessors) + .transform(processor -> processor.maybeGetConstructorParameters(constructor, testInfo)) + .filter(Optional::isPresent) + .transform(Optional::get) + .first() + .or( + () -> { + throw new IllegalStateException( + String.format( + "Could not generate parameter values for %s. Did you forget an annotation?", + constructor)); + }); + } + + /** + * Returns the parameters with which {@code testInfo.getMethod()} should be invoked. + * + * <p>This method is never called for a parameterless {@code testInfo.getMethod()}. + */ + public List<Object> getTestMethodParameters(TestInfo testInfo) { + return FluentIterable.from(testMethodProcessors) + .transform(processor -> processor.maybeGetTestMethodParameters(testInfo)) + .filter(Optional::isPresent) + .transform(Optional::get) + .first() + .or( + () -> { + throw new IllegalStateException( + String.format( + "Could not generate parameter values for %s. Did you forget an annotation?", + testInfo.getMethod())); + }); + } + + /** + * Optionally process the test instance right after construction to ready it for the given test. + */ + public void postProcessTestInstance(Object testInstance, TestInfo testInfo) { + for (TestMethodProcessor testMethodProcessor : testMethodProcessors) { + testMethodProcessor.postProcessTestInstance(testInstance, testInfo); + } + } + + /** Optionally validates the given constructor. */ + public ExecutableValidationResult validateConstructor(Constructor<?> constructor) { + return FluentIterable.from(testMethodProcessors) + .transform(processor -> processor.validateConstructor(constructor)) + .firstMatch(ExecutableValidationResult::wasValidated) + .or(ExecutableValidationResult.notValidated()); + } + + /** Optionally validates the given method. */ + public ExecutableValidationResult validateTestMethod(Method testMethod, Class<?> testClass) { + return FluentIterable.from(testMethodProcessors) + .transform(processor -> processor.validateTestMethod(testMethod, testClass)) + .firstMatch(ExecutableValidationResult::wasValidated) + .or(ExecutableValidationResult.notValidated()); + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java new file mode 100644 index 0000000..c26c8ed --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameter.java @@ -0,0 +1,260 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkState; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.base.Optional; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.Lists; +import com.google.common.primitives.Primitives; +import com.google.testing.junit.testparameterinjector.junit5.TestParameter.InternalImplementationOfThisParameter; +import com.google.testing.junit.testparameterinjector.junit5.TestParameterValuesProvider.Context; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Test parameter annotation that defines the values that a single parameter can have. + * + * <p>For enums and booleans, the values can be automatically derived as all possible values: + * + * <pre> + * {@literal @}Test + * public void test1(@TestParameter MyEnum myEnum, @TestParameter boolean myBoolean) { + * // ... will run for [(A,false), (A,true), (B,false), (B,true), (C,false), (C,true)] + * } + * + * enum MyEnum { A, B, C } + * </pre> + * + * <p>The values can be explicitly defined as a parsed string: + * + * <pre> + * public void test1( + * {@literal @}TestParameter({"{name: Hermione, age: 18}", "{name: Dumbledore, age: 115}"}) + * UpdateCharacterRequest request, + * {@literal @}TestParameter({"1", "4"}) int bookNumber) { + * // ... will run for [(Hermione,1), (Hermione,4), (Dumbledore,1), (Dumbledore,4)] + * } + * </pre> + * + * <p>For more flexibility, see {{@link #valuesProvider()}}. If you don't want to test all possible + * combinations but instead want to specify sets of parameters explicitly, use @{@link + * TestParameters}. + */ +@Retention(RUNTIME) +@Target({FIELD, PARAMETER}) +@TestParameterAnnotation(valueProvider = InternalImplementationOfThisParameter.class) +public @interface TestParameter { + + /** + * Array of stringified values for the annotated type. + * + * <p>Types that are supported: + * + * <ul> + * <li>String: No parsing happens + * <li>boolean: Specified as YAML boolean + * <li>long and int: Specified as YAML integer + * <li>float and double: Specified as YAML floating point or integer + * <li>Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()} + * <li>Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML bytes + * (example: "!!binary 'ZGF0YQ=='") + * </ul> + * + * <p>For dynamic sets of parameters or parameter types that are not supported here, use {@link + * #valuesProvider()} and leave this field empty. + * + * <p>For examples, see {@link TestParameter}. + */ + String[] value() default {}; + + /** + * Sets a provider that will return a list of parameter values. + * + * <p>If this field is set, {@link #value()} must be empty and vice versa. + * + * <p><b>Example</b> + * + * <pre> + * import com.google.testing.junit.testparameterinjector.junit5.TestParameterValuesProvider; + * + * {@literal @}Test + * public void matchesAllOf_throwsOnNull( + * {@literal @}TestParameter(valuesProvider = CharMatcherProvider.class) + * CharMatcher charMatcher) { + * assertThrows(NullPointerException.class, () -> charMatcher.matchesAllOf(null)); + * } + * + * private static final class CharMatcherProvider extends TestParameterValuesProvider { + * {@literal @}Override + * public {@literal List<CharMatcher>} provideValues(Context context) { + * return ImmutableList.of(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace()); + * } + * } + * </pre> + */ + Class<? extends TestParameterValuesProvider> valuesProvider() default + DefaultTestParameterValuesProvider.class; + + /** + * Interface for custom providers of test parameter values. + * + * @deprecated Use {@link + * com.google.testing.junit.testparameterinjector.junit5.TestParameterValuesProvider} instead. The + * replacement implements this same interface, but with an additional Context parameter. + */ + @Deprecated + interface TestParameterValuesProvider { + List<?> provideValues(); + + /** + * Wraps the given value in an object that allows you to give the parameter value a different + * name. The TestParameterInjector framework will recognize the returned {@link + * TestParameterValue} instances and unwrap them at injection time. + * + * <p>Usage: {@code value(file.content).withName(file.name)}. + * + * <p>Do not override this method. + */ + default TestParameterValue value(@javax.annotation.Nullable Object wrappedValue) { + return TestParameterValue.wrap(wrappedValue); + } + } + + /** Default {@link TestParameterValuesProvider} implementation that does nothing. */ + class DefaultTestParameterValuesProvider implements TestParameterValuesProvider { + @Override + public List<Object> provideValues() { + return com.google.common.collect.ImmutableList.of(); + } + } + + /** Implementation of this parameter annotation. */ + final class InternalImplementationOfThisParameter implements TestParameterValueProvider { + @Override + public List<Object> provideValues( + Annotation uncastAnnotation, + Optional<Class<?>> maybeParameterClass, + GenericParameterContext context) { + TestParameter annotation = (TestParameter) uncastAnnotation; + Class<?> parameterClass = getValueType(annotation.annotationType(), maybeParameterClass); + + boolean valueIsSet = annotation.value().length > 0; + boolean valuesProviderIsSet = + !annotation.valuesProvider().equals(DefaultTestParameterValuesProvider.class); + checkState( + !(valueIsSet && valuesProviderIsSet), + "It is not allowed to specify both value and valuesProvider on annotation %s", + annotation); + + if (valueIsSet) { + return Lists.newArrayList( + FluentIterable.from(annotation.value()) + .transform(v -> parseStringValue(v, parameterClass)) + .toArray(Object.class)); + } else if (valuesProviderIsSet) { + return getValuesFromProvider(annotation.valuesProvider(), new Context(context)); + } else { + if (Enum.class.isAssignableFrom(parameterClass)) { + return Arrays.asList((Object[]) parameterClass.asSubclass(Enum.class).getEnumConstants()); + } else if (Primitives.wrap(parameterClass).equals(Boolean.class)) { + return Arrays.asList(false, true); + } else { + throw new IllegalStateException( + String.format( + "A @TestParameter without values can only be placed at an enum or a boolean, but" + + " was placed by a %s", + parameterClass)); + } + } + } + + @Override + public Class<?> getValueType( + Class<? extends Annotation> annotationType, Optional<Class<?>> parameterClass) { + if (parameterClass.isPresent()) { + return parameterClass.get(); + } + throw new AssertionError( + String.format( + "An empty parameter class should not be possible since" + + " @TestParameter can only target FIELD or PARAMETER, both" + + " of which are supported for annotation %s.", + annotationType)); + } + + private static Object parseStringValue(String value, Class<?> parameterClass) { + if (parameterClass.equals(String.class)) { + return value.equals("null") ? null : value; + } else if (Enum.class.isAssignableFrom(parameterClass)) { + return value.equals("null") ? null : ParameterValueParsing.parseEnum(value, parameterClass); + } else { + return ParameterValueParsing.parseYamlStringToJavaType(value, parameterClass); + } + } + + private static List<Object> getValuesFromProvider( + Class<? extends TestParameterValuesProvider> valuesProvider, Context context) { + try { + Constructor<? extends TestParameterValuesProvider> constructor = + valuesProvider.getDeclaredConstructor(); + constructor.setAccessible(true); + TestParameterValuesProvider instance = constructor.newInstance(); + if (instance + instanceof com.google.testing.junit.testparameterinjector.junit5.TestParameterValuesProvider) { + return new ArrayList<>( + ((com.google.testing.junit.testparameterinjector.junit5.TestParameterValuesProvider) + instance) + .provideValues(context)); + } else { + return new ArrayList<>(instance.provideValues()); + } + } catch (NoSuchMethodException e) { + if (!Modifier.isStatic(valuesProvider.getModifiers()) && valuesProvider.isMemberClass()) { + throw new IllegalStateException( + String.format( + "Could not find a no-arg constructor for %s, probably because it is a not-static" + + " inner class. You can fix this by making %s static.", + valuesProvider.getSimpleName(), valuesProvider.getSimpleName()), + e); + } else { + throw new IllegalStateException( + String.format( + "Could not find a no-arg constructor for %s.", valuesProvider.getSimpleName()), + e); + } + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } catch (Exception e) { + // Catch any unchecked exception that may come from `provideValues(Context)` + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } else { + throw new IllegalStateException(e); + } + } + } + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotation.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotation.java new file mode 100644 index 0000000..255e4f5 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotation.java @@ -0,0 +1,244 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Verify.verify; +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Primitives; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; + +/** + * Annotation to define a test annotation used to have parameterized methods, in either a + * parameterized or non parameterized test. + * + * <p>Parameterized tests enabled by defining a annotation (see {@link TestParameter} as an example) + * for the type of the parameter, defining a member variable annotated with this annotation, and + * specifying the parameter with the same annotation for each test, or for the whole class, for + * example: + * + * <pre>{@code + * @RunWith(TestParameterInjector.class) + * public class ColorTest { + * @Retention(RUNTIME) + * @Target({TYPE, METHOD, FIELD}) + * @TestParameterAnnotation + * public @interface ColorParameter { + * Color[] value() default {}; + * } + * + * @ColorParameter({BLUE, WHITE, RED}) private Color color; + * + * @Test + * public void test() { + * assertThat(paint(color)).isSuccessful(); + * } + * } + * }</pre> + * + * <p>An alternative is to use a method parameter for injection: + * + * <pre>{@code + * @RunWith(TestParameterInjector.class) + * public class ColorTest { + * @Retention(RUNTIME) + * @Target({TYPE, METHOD, FIELD}) + * @TestParameterAnnotation + * public @interface ColorParameter { + * Color[] value() default {}; + * } + * + * @Test + * @ColorParameter({BLUE, WHITE, RED}) + * public void test(Color color) { + * assertThat(paint(color)).isSuccessful(); + * } + * } + * }</pre> + * + * <p>Yet another alternative is to use a method parameter for injection, but with the annotation + * specified on the parameter itself, which helps when multiple arguments share the + * same @TestParameterAnnotation annotation. + * + * <pre>{@code + * @RunWith(TestParameterInjector.class) + * public class ColorTest { + * @Retention(RUNTIME) + * @Target({TYPE, METHOD, FIELD}) + * @TestParameterAnnotation + * public @interface ColorParameter { + * Color[] value() default {}; + * } + * + * @Test + * public void test(@ColorParameter({BLUE, WHITE}) Color color1, + * @ColorParameter({WHITE, RED}) Color color2) { + * assertThat(paint(color1. color2)).isSuccessful(); + * } + * } + * }</pre> + * + * <p>Class constructors can also be annotated with @TestParameterAnnotation annotations, as shown + * below: + * + * <pre>{@code + * @RunWith(TestParameterInjector.class) + * public class ColorTest { + * @Retention(RUNTIME) + * @Target({TYPE, METHOD, FIELD}) + * public @TestParameterAnnotation + * public @interface ColorParameter { + * Color[] value() default {}; + * } + * + * public ColorTest(@ColorParameter({BLUE, WHITE}) Color color) { + * ... + * } + * + * @Test + * public void test() {...} + * } + * }</pre> + * + * <p>Each field that needs to be injected from a parameter requires its dedicated distinct + * annotation. + * + * <p>If the same annotation is defined both on the class and method, the method parameter values + * take precedence. + * + * <p>If the same annotation is defined both on the class and constructor, the constructor parameter + * values take precedence. + * + * <p>Annotations cannot be duplicated between the constructor or constructor parameters and a + * method or method parameter. + * + * <p>Since the parameter values must be specified in an annotation return value, they are + * restricted to the annotation method return type set (primitive, Class, Enum, String, etc...). If + * parameters have to be dynamically generated, the conventional Parameterized mechanism with {@code + * Parameters} has to be used instead. + */ +@Retention(RUNTIME) +@Target({ANNOTATION_TYPE}) +@interface TestParameterAnnotation { + + /** Specifies a validator for the parameter to determine whether test should be skipped. */ + Class<? extends TestParameterValidator> validator() default DefaultValidator.class; + + /** Specifies a value provider for the parameter to provide the values to test. */ + Class<? extends TestParameterValueProvider> valueProvider() default DefaultValueProvider.class; + + /** Default {@link TestParameterValidator} implementation which skips no test. */ + class DefaultValidator implements TestParameterValidator { + + @Override + public boolean shouldSkip(Context context) { + return false; + } + } + + /** + * Default {@link TestParameterValueProvider} implementation that gets its values from the + * annotation's `value` method. + */ + class DefaultValueProvider implements TestParameterValueProvider { + + @Override + public List<Object> provideValues(Annotation annotation, Optional<Class<?>> parameterClass) { + Object parameters = getParametersAnnotationValues(annotation, annotation.annotationType()); + checkState( + parameters.getClass().isArray(), + "The return value of the value method should be an array"); + + int parameterCount = Array.getLength(parameters); + ImmutableList.Builder<Object> resultBuilder = ImmutableList.builder(); + for (int i = 0; i < parameterCount; i++) { + Object value = Array.get(parameters, i); + if (parameterClass.isPresent()) { + verify( + Primitives.wrap(parameterClass.get()).isInstance(value), + "Found %s annotation next to a parameter of type %s which doesn't match" + + " (annotation = %s)", + annotation.annotationType().getSimpleName(), + parameterClass.get().getSimpleName(), + annotation); + } + resultBuilder.add(value); + } + return resultBuilder.build(); + } + + @Override + public Class<?> getValueType( + Class<? extends Annotation> annotationType, Optional<Class<?>> parameterClass) { + try { + Method valueMethod = annotationType.getMethod("value"); + return valueMethod.getReturnType().getComponentType(); + } catch (NoSuchMethodException e) { + throw new RuntimeException( + "The @TestParameterAnnotation annotation should have a single value() method.", e); + } + } + + /** + * Returns the parameters of the test parameter, by calling the {@code value} method on the + * annotation. + */ + private static Object getParametersAnnotationValues( + Annotation annotation, Class<? extends Annotation> annotationType) { + Method valueMethod; + try { + valueMethod = annotationType.getMethod("value"); + } catch (NoSuchMethodException e) { + throw new RuntimeException( + "The @TestParameterAnnotation annotation should have a single value() method.", e); + } + Object parameters; + try { + parameters = valueMethod.invoke(annotation); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof IllegalAccessError) { + // There seems to be a bug or at least something weird with the JVM that causes + // IllegalAccessError to be thrown because the return value is not visible when it is a + // non-public nested type. See + // http://mail.openjdk.java.net/pipermail/core-libs-dev/2014-January/024180.html for more + // info. + throw new RuntimeException( + String.format( + "Could not access %s.value(). This is probably because %s is not visible to the" + + " annotation proxy. To fix this, make %s public.", + annotationType.getSimpleName(), + valueMethod.getReturnType().getSimpleName(), + valueMethod.getReturnType().getSimpleName())); + // Note: Not chaining the exception to reduce the clutter for the reader + } else { + throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e); + } + } catch (Exception e) { + throw new RuntimeException("Unexpected exception while invoking " + valueMethod, e); + } + return parameters; + } + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java new file mode 100644 index 0000000..7fd2336 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterAnnotationMethodProcessor.java @@ -0,0 +1,1369 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Verify.verify; +import static com.google.common.collect.Lists.newArrayList; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.auto.value.AutoAnnotation; +import com.google.auto.value.AutoValue; +import com.google.common.base.Optional; +import com.google.common.base.Throwables; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ContiguousSet; +import com.google.common.collect.DiscreteDomain; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; +import com.google.common.collect.Range; +import com.google.common.util.concurrent.UncheckedExecutionException; +import com.google.testing.junit.testparameterinjector.junit5.TestInfo.TestInfoParameter; +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import javax.annotation.Nullable; + +/** + * {@code TestMethodProcessor} implementation for supporting parameterized tests annotated with + * {@link TestParameterAnnotation}. + * + * @see TestParameterAnnotation + */ +final class TestParameterAnnotationMethodProcessor implements TestMethodProcessor { + + /** + * Class to hold an annotation type and origin and one of the values as returned by the {@code + * value()} method. + */ + @AutoValue + abstract static class TestParameterValueHolder implements Serializable { + + private static final long serialVersionUID = -6491624726743872379L; + + /** + * Annotation type and origin of the annotation annotated with {@link TestParameterAnnotation}. + */ + abstract AnnotationTypeOrigin annotationTypeOrigin(); + + /** + * The value used for the test as returned by the @TestParameterAnnotation annotated + * annotation's {@code value()} method (e.g. 'true' or 'false' in the case of a Boolean + * parameter). + */ + abstract TestParameterValue wrappedValue(); + + /** The index of this value in {@link #specifiedValues()}. */ + abstract int valueIndex(); + + /** + * The list of values specified by the @TestParameterAnnotation annotated annotation's {@code + * value()} method (e.g. {true, false} in the case of a boolean parameter). + */ + @SuppressWarnings("AutoValueImmutableFields") // intentional to allow null values + abstract List<Object> specifiedValues(); + + /** + * The name of the parameter or field that is being annotated. In case the annotation is + * annotating a method, constructor or class, {@code paramName} is an absent optional. + */ + abstract Optional<String> paramName(); + + /** + * Returns {@link #wrappedValue()} without the {@link TestParameterValue} wrapper if it exists. + */ + @Nullable + Object unwrappedValue() { + return wrappedValue().getWrappedValue(); + } + + /** + * Returns a String that represents this value and is fit for use in a test name (between + * brackets). + */ + String toTestNameString() { + return ParameterValueParsing.formatTestNameString(paramName(), wrappedValue()); + } + + public static ImmutableList<TestParameterValueHolder> create( + AnnotationWithMetadata annotationWithMetadata, Origin origin) { + List<TestParameterValue> specifiedValues = + getParametersAnnotationValues(annotationWithMetadata); + checkState( + !specifiedValues.isEmpty(), + "The number of parameter values should not be 0" + + ", otherwise the parameter would cause the test to be skipped."); + return FluentIterable.from( + ContiguousSet.create( + Range.closedOpen(0, specifiedValues.size()), DiscreteDomain.integers())) + .transform( + valueIndex -> + (TestParameterValueHolder) + new AutoValue_TestParameterAnnotationMethodProcessor_TestParameterValueHolder( + AnnotationTypeOrigin.create( + annotationWithMetadata.annotation().annotationType(), origin), + specifiedValues.get(valueIndex), + valueIndex, + newArrayList( + FluentIterable.from(specifiedValues) + .transform(TestParameterValue::getWrappedValue)), + annotationWithMetadata.paramName())) + .toList(); + } + } + + /** + * Returns a {@link TestParameterValues} for retrieving the {@link TestParameterAnnotation} + * annotation values for a the {@code testInfo}. + */ + public static TestParameterValues getTestParameterValues(TestInfo testInfo) { + TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); + if (testIndexHolder == null) { + return annotationType -> Optional.absent(); + } else { + return annotationType -> + FluentIterable.from( + new TestParameterAnnotationMethodProcessor( + /* onlyForFieldsAndParameters= */ false) + .getParameterValuesForTest(testIndexHolder, testInfo.getTestClass())) + .filter( + testParameterValue -> + testParameterValue + .annotationTypeOrigin() + .annotationType() + .equals(annotationType)) + .transform(TestParameterValueHolder::unwrappedValue) + .first(); + } + } + + /** + * Returns a {@link TestParameterAnnotation} value for the current test as specified by {@code + * testInfo}, or {@link Optional#absent()} if the {@code annotationType} is not found. + */ + public static Optional<Object> getTestParameterValue( + TestInfo testInfo, Class<? extends Annotation> annotationType) { + return getTestParameterValues(testInfo).getValue(annotationType); + } + + private static ImmutableList<TestParameterValue> getParametersAnnotationValues( + AnnotationWithMetadata annotationWithMetadata) { + Annotation annotation = annotationWithMetadata.annotation(); + TestParameterAnnotation testParameter = + annotation.annotationType().getAnnotation(TestParameterAnnotation.class); + Class<? extends TestParameterValueProvider> valueProvider = testParameter.valueProvider(); + try { + return FluentIterable.from( + valueProvider + .getConstructor() + .newInstance() + .provideValues( + annotation, + annotationWithMetadata.paramClass(), + annotationWithMetadata.context())) + .transform( + value -> + (value instanceof TestParameterValue) + ? (TestParameterValue) value + : TestParameterValue.wrap(value)) + .toList(); + } catch (ReflectiveOperationException e) { + throw new RuntimeException( + "Unexpected exception while invoking value provider " + valueProvider, e); + } + } + + /** The origin of an annotation type. */ + enum Origin { + CLASS, + FIELD, + METHOD, + METHOD_PARAMETER, + CONSTRUCTOR, + CONSTRUCTOR_PARAMETER, + } + + /** Class to hold an annotation type and the element where it was declared. */ + @AutoValue + abstract static class AnnotationTypeOrigin implements Serializable { + + private static final long serialVersionUID = 4909750539931241385L; + + /** Annotation type of the @TestParameterAnnotation annotated annotation. */ + abstract Class<? extends Annotation> annotationType(); + + /** Where the annotation was declared. */ + abstract Origin origin(); + + public static AnnotationTypeOrigin create( + Class<? extends Annotation> annotationType, Origin origin) { + return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationTypeOrigin( + annotationType, origin); + } + + @Override + public final String toString() { + return annotationType().getSimpleName() + ":" + origin(); + } + } + + /** Class to hold an annotation type and metadata about the annotated parameter. */ + @AutoValue + abstract static class AnnotationWithMetadata implements Serializable { + + /** + * The annotation whose interface is itself annotated by the @TestParameterAnnotation + * annotation. + */ + abstract Annotation annotation(); + + /** + * The class of the parameter or field that is being annotated. In case the annotation is + * annotating a method, constructor or class, {@code paramClass} is an absent optional. + */ + abstract Optional<Class<?>> paramClass(); + + /** + * The name of the parameter or field that is being annotated. In case the annotation is + * annotating a method, constructor or class, {@code paramName} is an absent optional. + */ + abstract Optional<String> paramName(); + + /** + * A value class that contains extra information about the context of this parameter. + * + * <p>In case the annotation is annotating a method, constructor or class (deprecated + * functionality), the annotations in the context will be empty. + */ + abstract GenericParameterContext context(); + + public static AnnotationWithMetadata withMetadata( + Annotation annotation, + Class<?> paramClass, + String paramName, + GenericParameterContext context) { + return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( + annotation, Optional.of(paramClass), Optional.of(paramName), context); + } + + public static AnnotationWithMetadata withMetadata( + Annotation annotation, Class<?> paramClass, GenericParameterContext context) { + return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( + annotation, Optional.of(paramClass), Optional.absent(), context); + } + + public static AnnotationWithMetadata withoutMetadata( + Annotation annotation, GenericParameterContext context) { + return new AutoValue_TestParameterAnnotationMethodProcessor_AnnotationWithMetadata( + annotation, + /* paramClass= */ Optional.absent(), + /* paramName= */ Optional.absent(), + context); + } + + // Prevent anyone relying on equals() and hashCode() so that it remains possible to add fields + // to this class without breaking existing code. + @Override + public final boolean equals(Object other) { + throw new UnsupportedOperationException("Equality is not supported"); + } + + @Override + public final int hashCode() { + throw new UnsupportedOperationException("hashCode() is not supported"); + } + } + + private final boolean onlyForFieldsAndParameters; + private final LoadingCache<Class<?>, ImmutableList<AnnotationTypeOrigin>> + annotationTypeOriginsCache = + CacheBuilder.newBuilder() + .maximumSize(1000) + .build(CacheLoader.from(this::calculateAnnotationTypeOrigins)); + private final Cache<Method, List<List<TestParameterValueHolder>>> parameterValuesCache = + CacheBuilder.newBuilder().maximumSize(1000).build(); + + private TestParameterAnnotationMethodProcessor(boolean onlyForFieldsAndParameters) { + this.onlyForFieldsAndParameters = onlyForFieldsAndParameters; + } + + /** + * Constructs a new {@link TestMethodProcessor} that handles {@link + * TestParameterAnnotation}-annotated annotations that are placed anywhere: + * + * <ul> + * <li>At a method / constructor parameter + * <li>At a field + * <li>At a method / constructor on the class + * <li>At the test class + * </ul> + */ + static TestMethodProcessor forAllAnnotationPlacements() { + return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ false); + } + + /** + * Constructs a new {@link TestMethodProcessor} that handles {@link + * TestParameterAnnotation}-annotated annotations that are placed at fields or parameters. + * + * <p>Note that this excludes class and method-level annotations, as is the default (using the + * constructor). + */ + static TestMethodProcessor onlyForFieldsAndParameters() { + return new TestParameterAnnotationMethodProcessor(/* onlyForFieldsAndParameters= */ true); + } + + private ImmutableList<AnnotationTypeOrigin> calculateAnnotationTypeOrigins(Class<?> testClass) { + // Collect all annotations used in declared fields and methods that have themselves a + // @TestParameterAnnotation annotation. + List<AnnotationTypeOrigin> fieldAnnotations = + extractTestParameterAnnotations( + FluentIterable.from(listWithParents(testClass)) + .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields())) + .transformAndConcat(field -> Arrays.asList(field.getAnnotations())) + .toList(), + Origin.FIELD); + List<AnnotationTypeOrigin> methodAnnotations = + extractTestParameterAnnotations( + FluentIterable.from(testClass.getMethods()) + .transformAndConcat(method -> Arrays.asList(method.getAnnotations())) + .toList(), + Origin.METHOD); + List<AnnotationTypeOrigin> parameterAnnotations = + extractTestParameterAnnotations( + FluentIterable.from(listWithParents(testClass)) + .transformAndConcat(c -> Arrays.asList(c.getDeclaredMethods())) + .transformAndConcat(method -> Arrays.asList(method.getParameterAnnotations())) + .transformAndConcat(Arrays::asList) + .toList(), + Origin.METHOD_PARAMETER); + List<AnnotationTypeOrigin> classAnnotations = + extractTestParameterAnnotations(Arrays.asList(testClass.getAnnotations()), Origin.CLASS); + List<AnnotationTypeOrigin> constructorAnnotations = + extractTestParameterAnnotations( + FluentIterable.from(testClass.getDeclaredConstructors()) + .transformAndConcat(constructor -> Arrays.asList(constructor.getAnnotations())) + .toList(), + Origin.CONSTRUCTOR); + List<AnnotationTypeOrigin> constructorParameterAnnotations = + extractTestParameterAnnotations( + FluentIterable.from(testClass.getDeclaredConstructors()) + .transformAndConcat( + constructor -> + FluentIterable.from(Arrays.asList(constructor.getParameterAnnotations())) + .transformAndConcat(Arrays::asList)) + .toList(), + Origin.CONSTRUCTOR_PARAMETER); + + checkDuplicatedClassAndFieldAnnotations( + constructorAnnotations, classAnnotations, fieldAnnotations); + + checkDuplicatedFieldsAnnotations(methodAnnotations, fieldAnnotations); + + checkState( + FluentIterable.from(constructorAnnotations).toSet().size() == constructorAnnotations.size(), + "Annotations should not be duplicated on the constructor."); + + checkState( + FluentIterable.from(classAnnotations).toSet().size() == classAnnotations.size(), + "Annotations should not be duplicated on the class."); + + if (onlyForFieldsAndParameters) { + checkState( + methodAnnotations.isEmpty(), + "This test runner (constructed by the testparameterinjector package) was configured" + + " to disallow method-level annotations that could be field/parameter" + + " annotations, but found %s", + methodAnnotations); + checkState( + classAnnotations.isEmpty(), + "This test runner (constructed by the testparameterinjector package) was configured" + + " to disallow class-level annotations that could be field/parameter annotations," + + " but found %s", + classAnnotations); + checkState( + constructorAnnotations.isEmpty(), + "This test runner (constructed by the testparameterinjector package) was configured" + + " to disallow constructor-level annotations that could be field/parameter" + + " annotations, but found %s", + constructorAnnotations); + } + + // The order matters, since it will determine which annotation processor is + // called first. + return FluentIterable.from(classAnnotations) + .append(fieldAnnotations) + .append(constructorAnnotations) + .append(constructorParameterAnnotations) + .append(methodAnnotations) + .append(parameterAnnotations) + .toSet() + .asList(); + } + + private ImmutableList<AnnotationTypeOrigin> getAnnotationTypeOrigins( + Class<?> testClass, Origin firstOrigin, Origin... otherOrigins) { + Set<Origin> originsToFilterBy = + ImmutableSet.<Origin>builder().add(firstOrigin).add(otherOrigins).build(); + try { + return FluentIterable.from(annotationTypeOriginsCache.getUnchecked(testClass)) + .filter(annotationTypeOrigin -> originsToFilterBy.contains(annotationTypeOrigin.origin())) + .toList(); + } catch (UncheckedExecutionException e) { + Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); + throw e; + } + } + + private void checkDuplicatedFieldsAnnotations( + List<AnnotationTypeOrigin> methodAnnotations, List<AnnotationTypeOrigin> fieldAnnotations) { + // If an annotation is duplicated on two fields, then it becomes specific, and cannot be + // overridden by a method. + if (FluentIterable.from(fieldAnnotations).toSet().size() != fieldAnnotations.size()) { + List<Class<? extends Annotation>> methodOrFieldAnnotations = + new ArrayList<>( + FluentIterable.from(methodAnnotations) + .append(new HashSet<>(fieldAnnotations)) + .transform(AnnotationTypeOrigin::annotationType) + .toList()); + + checkState( + FluentIterable.from(methodOrFieldAnnotations).toSet().size() + == methodOrFieldAnnotations.size(), + "Annotations should not be duplicated on a method and field" + + " if they are present on multiple fields"); + } + } + + private void checkDuplicatedClassAndFieldAnnotations( + List<AnnotationTypeOrigin> constructorAnnotations, + List<AnnotationTypeOrigin> classAnnotations, + List<AnnotationTypeOrigin> fieldAnnotations) { + ImmutableSet<? extends Class<? extends Annotation>> classAnnotationTypes = + FluentIterable.from(classAnnotations) + .transform(AnnotationTypeOrigin::annotationType) + .toSet(); + + ImmutableSet<? extends Class<? extends Annotation>> uniqueFieldAnnotations = + FluentIterable.from(fieldAnnotations) + .transform(AnnotationTypeOrigin::annotationType) + .toSet(); + ImmutableSet<? extends Class<? extends Annotation>> uniqueConstructorAnnotations = + FluentIterable.from(constructorAnnotations) + .transform(AnnotationTypeOrigin::annotationType) + .toSet(); + + checkState( + Collections.disjoint(classAnnotationTypes, uniqueFieldAnnotations), + "Annotations should not be duplicated on a class and field"); + + checkState( + Collections.disjoint(classAnnotationTypes, uniqueConstructorAnnotations), + "Annotations should not be duplicated on a class and constructor"); + + checkState( + Collections.disjoint(uniqueConstructorAnnotations, uniqueFieldAnnotations), + "Annotations should not be duplicated on a field and constructor"); + } + + private List<AnnotationTypeOrigin> extractTestParameterAnnotations( + List<Annotation> annotations, Origin origin) { + return new ArrayList<>( + FluentIterable.from(annotations) + .transform(Annotation::annotationType) + .filter( + annotationType -> annotationType.isAnnotationPresent(TestParameterAnnotation.class)) + .transform(annotationType -> AnnotationTypeOrigin.create(annotationType, origin)) + .toList()); + } + + @Override + public ExecutableValidationResult validateConstructor(Constructor<?> constructor) { + Class<?>[] parameterTypes = constructor.getParameterTypes(); + if (parameterTypes.length == 0) { + return ExecutableValidationResult.notValidated(); + } + // The constructor has parameters, they must be injected by a TestParameterAnnotation + // annotation. + Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); + Class<?> testClass = constructor.getDeclaringClass(); + return ExecutableValidationResult.validated( + validateMethodOrConstructorParameters( + removeOverrides( + getAnnotationTypeOrigins( + testClass, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER), + testClass), + testClass, + constructor, + parameterTypes, + parameterAnnotations)); + } + + @Override + public ExecutableValidationResult validateTestMethod(Method testMethod, Class<?> testClass) { + Class<?>[] methodParameterTypes = testMethod.getParameterTypes(); + if (methodParameterTypes.length == 0) { + return ExecutableValidationResult.notValidated(); + } else { + // The method has parameters, they must be injected by a TestParameterAnnotation annotation. + return ExecutableValidationResult.validated( + validateMethodOrConstructorParameters( + getAnnotationTypeOrigins( + testClass, Origin.CLASS, Origin.METHOD, Origin.METHOD_PARAMETER), + testClass, + testMethod, + methodParameterTypes, + testMethod.getParameterAnnotations())); + } + } + + private List<Throwable> validateMethodOrConstructorParameters( + List<AnnotationTypeOrigin> annotationTypeOrigins, + Class<?> testClass, + AnnotatedElement methodOrConstructor, + Class<?>[] parameterTypes, + Annotation[][] parametersAnnotations) { + List<Throwable> errors = new ArrayList<>(); + + for (int parameterIndex = 0; parameterIndex < parameterTypes.length; parameterIndex++) { + Class<?> parameterType = parameterTypes[parameterIndex]; + Annotation[] parameterAnnotations = parametersAnnotations[parameterIndex]; + boolean matchingTestParameterAnnotationFound = false; + // First, handle the case where the method parameter specifies the test parameter explicitly, + // e.g. {@code public void test(@ColorParameter({...}) Color c)}. + for (AnnotationTypeOrigin testParameterAnnotationType : annotationTypeOrigins) { + for (Annotation parameterAnnotation : parameterAnnotations) { + if (parameterAnnotation + .annotationType() + .equals(testParameterAnnotationType.annotationType())) { + // Verify that the type is assignable with the return type of the 'value' method. + Class<?> valueMethodReturnType = + getValueMethodReturnType( + testParameterAnnotationType.annotationType(), + /* paramClass= */ Optional.of(parameterType)); + if (!parameterType.isAssignableFrom(valueMethodReturnType)) { + errors.add( + new IllegalStateException( + String.format( + "Parameter of type %s annotated with %s does not match" + + " expected type %s in method/constructor %s", + parameterType.getName(), + testParameterAnnotationType.annotationType().getName(), + valueMethodReturnType.getName(), + methodOrConstructor))); + } else { + matchingTestParameterAnnotationFound = true; + } + } + } + } + // Second, handle the case where the method parameter does not specify the test parameter, + // and instead relies on the type matching, e.g. {@code public void test(Color c)}. + if (!matchingTestParameterAnnotationFound) { + ImmutableList<? extends Class<? extends Annotation>> testParameterAnnotationTypes = + getTestParameterAnnotations( + // Do not include METHOD_PARAMETER or CONSTRUCTOR_PARAMETER since they have already + // been evaluated. + filterAnnotationTypeOriginsByOrigin( + annotationTypeOrigins, Origin.CLASS, Origin.CONSTRUCTOR, Origin.METHOD), + testClass, + methodOrConstructor); + // If no annotation is present, simply compare the type. + for (Class<? extends Annotation> testParameterAnnotationType : + testParameterAnnotationTypes) { + if (parameterType.isAssignableFrom( + getValueMethodReturnType( + testParameterAnnotationType, /* paramClass= */ Optional.absent()))) { + if (matchingTestParameterAnnotationFound) { + errors.add( + new IllegalStateException( + String.format( + "Ambiguous method/constructor parameter type, matching multiple" + + " annotations for parameter of type %s in method %s", + parameterType.getName(), methodOrConstructor))); + } + matchingTestParameterAnnotationFound = true; + } + } + } + if (!matchingTestParameterAnnotationFound) { + errors.add( + new IllegalStateException( + String.format( + "No matching test parameter annotation found" + + " for parameter of type %s in method/constructor %s", + parameterType.getName(), methodOrConstructor))); + } + } + return errors; + } + + @Override + public Optional<List<Object>> maybeGetConstructorParameters( + Constructor<?> constructor, TestInfo testInfo) { + if (testInfo.getAnnotation(TestIndexHolder.class) == null + // Explicitly skip @TestParameters annotated methods to ensure compatibility. + // + // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported) + // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;). + // Without the @TestParameters check below, this class would try to find parameters for + // these methods. When there are no method parameters, this is a no-op, but when the method + // is annotated with @TestParameters, this throws an exception (because there are method + // parameters that this processor has no values for - they are provided by the + // @TestParameters processor). + || constructor.isAnnotationPresent(TestParameters.class)) { + return Optional.absent(); + } else { + TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); + List<TestParameterValueHolder> testParameterValues = + getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()); + + Class<?>[] parameterTypes = constructor.getParameterTypes(); + Annotation[][] parameterAnnotations = constructor.getParameterAnnotations(); + List<Object> parameterValues = new ArrayList<>(/* initialCapacity= */ parameterTypes.length); + List<Class<? extends Annotation>> processedAnnotationTypes = new ArrayList<>(); + List<TestParameterValueHolder> parameterValuesForConstructor = + filterByOrigin( + testParameterValues, Origin.CLASS, Origin.CONSTRUCTOR, Origin.CONSTRUCTOR_PARAMETER); + for (int i = 0; i < parameterTypes.length; i++) { + // Initialize each parameter value from the corresponding TestParameterAnnotation value. + parameterValues.add( + getParameterValue( + parameterValuesForConstructor, + parameterTypes[i], + parameterAnnotations[i], + processedAnnotationTypes)); + } + return Optional.of(parameterValues); + } + } + + @Override + public Optional<List<Object>> maybeGetTestMethodParameters(TestInfo testInfo) { + Method testMethod = testInfo.getMethod(); + if (testInfo.getAnnotation(TestIndexHolder.class) == null + // Explicitly skip @TestParameters annotated methods to ensure compatibility. + // + // Reason (see b/175678220): @TestIndexHolder will even be present when the only (supported) + // parameterization is at the field level (e.g. @TestParameter private TestEnum enum;). + // Without the @TestParameters check below, this class would try to find parameters for + // these methods. When there are no method parameters, this is a no-op, but when the method + // is annotated with @TestParameters, this throws an exception (because there are method + // parameters that this processor has no values for - they are provided by the + // @TestParameters processor). + || testMethod.isAnnotationPresent(TestParameters.class)) { + return Optional.absent(); + } else { + TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); + checkState(testIndexHolder != null); + List<TestParameterValueHolder> testParameterValues = + filterByOrigin( + getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()), + Origin.CLASS, + Origin.METHOD, + Origin.METHOD_PARAMETER); + + Class<?>[] parameterTypes = testMethod.getParameterTypes(); + Annotation[][] parametersAnnotations = testMethod.getParameterAnnotations(); + ArrayList<Object> parameterValues = + new ArrayList<>(/* initialCapacity= */ parameterTypes.length); + + List<Class<? extends Annotation>> processedAnnotationTypes = new ArrayList<>(); + for (int i = 0; i < parameterTypes.length; i++) { + parameterValues.add( + getParameterValue( + testParameterValues, + parameterTypes[i], + parametersAnnotations[i], + processedAnnotationTypes)); + } + + return Optional.of(parameterValues); + } + } + + /** + * Returns the {@link TestInfo}, one for each result of the cartesian product of each test + * parameter values. + * + * <p>For example, given the annotation {@code @ColorParameter({BLUE, WHITE, RED})} on a method, + * it method will return the TestParameterValues: "(@ColorParameter, BLUE), (@ColorParameter, + * WHITE), (@ColorParameter, RED)}). + * + * <p>For multiple annotations (say, {@code @TestParameter("foo", "bar")} and + * {@code @ColorParameter({BLUE, WHITE})}), it will generate the following result: + * + * <ul> + * <li>("foo", BLUE) + * <li>("foo", WHITE) + * <li>("bar", BLUE) + * <li>("bar", WHITE) + * <li> + * </ul> + * + * corresponding to the cartesian product of both annotations. + */ + @Override + public List<TestInfo> calculateTestInfos(TestInfo originalTest) { + List<List<TestParameterValueHolder>> parameterValuesForMethod = + getParameterValuesForMethod(originalTest.getMethod(), originalTest.getTestClass()); + + if (parameterValuesForMethod.equals(ImmutableList.of(ImmutableList.of()))) { + // This test is not parameterized + return ImmutableList.of(originalTest); + } + + ImmutableList.Builder<TestInfo> testInfos = ImmutableList.builder(); + for (int parametersIndex = 0; + parametersIndex < parameterValuesForMethod.size(); + ++parametersIndex) { + List<TestParameterValueHolder> testParameterValues = + parameterValuesForMethod.get(parametersIndex); + testInfos.add( + originalTest + .withExtraParameters( + FluentIterable.from(testParameterValues) + .transform( + param -> + TestInfoParameter.create( + param.toTestNameString(), + param.unwrappedValue(), + param.valueIndex())) + .toList()) + .withExtraAnnotation( + TestIndexHolderFactory.create( + /* methodIndex= */ strictIndexOf( + getMethodsIncludingParentsSorted(originalTest.getTestClass()), + originalTest.getMethod()), + parametersIndex, + originalTest.getTestClass().getName()))); + } + + return testInfos.build(); + } + + private List<List<TestParameterValueHolder>> getParameterValuesForMethod( + Method method, Class<?> testClass) { + try { + return parameterValuesCache.get( + method, + () -> { + List<List<TestParameterValueHolder>> testParameterValuesList = + getAnnotationValuesForUsedAnnotationTypes(method, testClass); + + return FluentIterable.from(Lists.cartesianProduct(testParameterValuesList)) + .filter( + // Skip tests based on the annotations' {@link Validator#shouldSkip} return + // value. + testParameterValues -> + FluentIterable.from(testParameterValues) + .filter( + testParameterValue -> + callShouldSkip( + testParameterValue.annotationTypeOrigin().annotationType(), + testParameterValues)) + .isEmpty()) + .toList(); + }); + } catch (ExecutionException | UncheckedExecutionException e) { + Throwables.throwIfUnchecked(e.getCause()); + throw new RuntimeException(e); + } + } + + private List<TestParameterValueHolder> getParameterValuesForTest( + TestIndexHolder testIndexHolder, Class<?> testClass) { + verify( + testIndexHolder.testClassName().equals(testClass.getName()), + "The class for which the given annotation was created (%s) is not the same as the test" + + " class that this runner is handling (%s)", + testIndexHolder.testClassName(), + testClass.getName()); + Method testMethod = + getMethodsIncludingParentsSorted(testClass).get(testIndexHolder.methodIndex()); + return getParameterValuesForMethod(testMethod, testClass) + .get(testIndexHolder.parametersIndex()); + } + + /** + * Returns the list of annotation index for all annotations defined in a given test method and its + * class. + */ + private ImmutableList<List<TestParameterValueHolder>> getAnnotationValuesForUsedAnnotationTypes( + Method method, Class<?> testClass) { + ImmutableList<AnnotationTypeOrigin> annotationTypes = + FluentIterable.from(getAnnotationTypeOrigins(testClass, Origin.CLASS)) + .append(getAnnotationTypeOrigins(testClass, Origin.FIELD)) + .append(getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR)) + .append(getAnnotationTypeOrigins(testClass, Origin.CONSTRUCTOR_PARAMETER)) + .append(getAnnotationTypeOrigins(testClass, Origin.METHOD)) + .append( + ImmutableList.sortedCopyOf( + annotationComparator(method.getParameterAnnotations()), + getAnnotationTypeOrigins(testClass, Origin.METHOD_PARAMETER))) + .toList(); + + return FluentIterable.from(removeOverrides(annotationTypes, testClass, method)) + .transform( + annotationTypeOrigin -> + getAnnotationFromParametersOrTestOrClass(annotationTypeOrigin, method, testClass)) + .filter(l -> !l.isEmpty()) + .transformAndConcat(i -> i) + .toList(); + } + + private Comparator<AnnotationTypeOrigin> annotationComparator( + Annotation[][] parameterAnnotations) { + ImmutableList<String> annotationOrdering = + FluentIterable.from(parameterAnnotations) + .transformAndConcat(Arrays::asList) + .transform(Annotation::annotationType) + .transform(Class::getName) + .toList(); + return (annotationTypeOrigin, t1) -> + Integer.compare( + annotationOrdering.indexOf(annotationTypeOrigin.annotationType().getName()), + annotationOrdering.indexOf(t1.annotationType().getName())); + } + + /** + * Returns a list of {@link AnnotationTypeOrigin} where the overridden annotation are removed for + * the current {@code originalTest} and {@code testClass}. + * + * <p>Specifically, annotation defined on CLASS and FIELD elements will be removed if they are + * also defined on the method, method parameter, constructor, or constructor parameters. + */ + private List<AnnotationTypeOrigin> removeOverrides( + List<AnnotationTypeOrigin> annotationTypeOrigins, Class<?> testClass, Method method) { + return removeOverrides( + new ArrayList<>( + FluentIterable.from(annotationTypeOrigins) + .filter( + annotationTypeOrigin -> { + switch (annotationTypeOrigin.origin()) { + case FIELD: // Fall through. + case CLASS: + return getAnnotationListWithType( + method.getAnnotations(), annotationTypeOrigin.annotationType()) + .isEmpty(); + default: + return true; + } + }) + .toList()), + testClass); + } + + /** + * @see #removeOverrides(List, Class) + */ + private List<AnnotationTypeOrigin> removeOverrides( + List<AnnotationTypeOrigin> annotationTypeOrigins, Class<?> testClass) { + return new ArrayList<>( + FluentIterable.from(annotationTypeOrigins) + .filter( + annotationTypeOrigin -> { + switch (annotationTypeOrigin.origin()) { + case FIELD: // Fall through. + case CLASS: + return getAnnotationListWithType( + TestParameterInjectorUtils.getOnlyConstructor(testClass) + .getAnnotations(), + annotationTypeOrigin.annotationType()) + .isEmpty(); + default: + return true; + } + }) + .toList()); + } + + /** + * Returns the given annotations defined either on the method parameters, method or the test + * class. + * + * <p>The annotation from the parameters takes precedence over the same annotation defined on the + * method, and the one defined on the method takes precedence over the same annotation defined on + * the class. + */ + private ImmutableList<List<TestParameterValueHolder>> getAnnotationFromParametersOrTestOrClass( + AnnotationTypeOrigin annotationTypeOrigin, Method method, Class<?> testClass) { + Origin origin = annotationTypeOrigin.origin(); + Class<? extends Annotation> annotationType = annotationTypeOrigin.annotationType(); + if (origin == Origin.CONSTRUCTOR_PARAMETER) { + Constructor<?> constructor = TestParameterInjectorUtils.getOnlyConstructor(testClass); + List<AnnotationWithMetadata> annotations = + getAnnotationWithMetadataListWithType(constructor, annotationType, testClass); + + if (!annotations.isEmpty()) { + return toTestParameterValueList(annotations, origin); + } + } else if (origin == Origin.CONSTRUCTOR) { + Annotation annotation = + TestParameterInjectorUtils.getOnlyConstructor(testClass).getAnnotation(annotationType); + if (annotation != null) { + return ImmutableList.of( + TestParameterValueHolder.create( + AnnotationWithMetadata.withoutMetadata( + annotation, + GenericParameterContext.createWithoutParameterAnnotations(testClass)), + origin)); + } + + } else if (origin == Origin.METHOD_PARAMETER) { + List<AnnotationWithMetadata> annotations = + getAnnotationWithMetadataListWithType(method, annotationType, testClass); + if (!annotations.isEmpty()) { + return toTestParameterValueList(annotations, origin); + } + } else if (origin == Origin.METHOD) { + if (method.isAnnotationPresent(annotationType)) { + return ImmutableList.of( + TestParameterValueHolder.create( + AnnotationWithMetadata.withoutMetadata( + method.getAnnotation(annotationType), + GenericParameterContext.createWithoutParameterAnnotations(testClass)), + origin)); + } + } else if (origin == Origin.FIELD) { + List<AnnotationWithMetadata> annotations = + new ArrayList<>( + FluentIterable.from(listWithParents(testClass)) + .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields())) + .transformAndConcat( + field -> + FluentIterable.from( + getAnnotationListWithType(field.getAnnotations(), annotationType)) + .transform( + annotation -> + AnnotationWithMetadata.withMetadata( + annotation, + field.getType(), + field.getName(), + GenericParameterContext.create(field, testClass)))) + .toList()); + if (!annotations.isEmpty()) { + return toTestParameterValueList(annotations, origin); + } + } else if (origin == Origin.CLASS) { + Annotation annotation = testClass.getAnnotation(annotationType); + if (annotation != null) { + return ImmutableList.of( + TestParameterValueHolder.create( + AnnotationWithMetadata.withoutMetadata( + annotation, + GenericParameterContext.createWithoutParameterAnnotations(testClass)), + origin)); + } + } + return ImmutableList.of(); + } + + private static ImmutableList<List<TestParameterValueHolder>> toTestParameterValueList( + List<AnnotationWithMetadata> annotationWithMetadatas, Origin origin) { + return FluentIterable.from(annotationWithMetadatas) + .transform( + annotationWithMetadata -> + (List<TestParameterValueHolder>) + new ArrayList<>( + TestParameterValueHolder.create(annotationWithMetadata, origin))) + .toList(); + } + + private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType( + Method callable, Class<? extends Annotation> annotationType, Class<?> testClass) { + try { + return getAnnotationWithMetadataListWithType( + callable.getParameters(), annotationType, testClass); + } catch (NoSuchMethodError ignored) { + return getAnnotationWithMetadataListWithType( + callable.getParameterTypes(), + callable.getParameterAnnotations(), + annotationType, + testClass); + } + } + + private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType( + Constructor<?> callable, Class<? extends Annotation> annotationType, Class<?> testClass) { + try { + return getAnnotationWithMetadataListWithType( + callable.getParameters(), annotationType, testClass); + } catch (NoSuchMethodError ignored) { + return getAnnotationWithMetadataListWithType( + callable.getParameterTypes(), + callable.getParameterAnnotations(), + annotationType, + testClass); + } + } + + // Parameter is not available on old Android SDKs, and isn't desugared. That's why this method + // has a fallback that takes the parameter types and annotations (without the parameter names, + // which are optional anyway). + @SuppressWarnings("AndroidJdkLibsChecker") + private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType( + Parameter[] parameters, Class<? extends Annotation> annotationType, Class<?> testClass) { + return FluentIterable.from(parameters) + .transform( + parameter -> { + Annotation annotation = parameter.getAnnotation(annotationType); + return annotation == null + ? null + : parameter.isNamePresent() + ? AnnotationWithMetadata.withMetadata( + annotation, + parameter.getType(), + parameter.getName(), + GenericParameterContext.create(parameter, testClass)) + : AnnotationWithMetadata.withMetadata( + annotation, + parameter.getType(), + GenericParameterContext.create(parameter, testClass)); + }) + .filter(Objects::nonNull) + .toList(); + } + + private static ImmutableList<AnnotationWithMetadata> getAnnotationWithMetadataListWithType( + Class<?>[] parameterTypes, + Annotation[][] annotations, + Class<? extends Annotation> annotationType, + Class<?> testClass) { + checkArgument(parameterTypes.length == annotations.length); + + ImmutableList.Builder<AnnotationWithMetadata> resultBuilder = ImmutableList.builder(); + for (int i = 0; i < annotations.length; i++) { + for (Annotation annotation : annotations[i]) { + if (annotation.annotationType().equals(annotationType)) { + resultBuilder.add( + AnnotationWithMetadata.withMetadata( + annotation, + parameterTypes[i], + GenericParameterContext.createWithRepeatableAnnotationsFallback( + annotations[i], testClass))); + } + } + } + return resultBuilder.build(); + } + + private ImmutableList<Annotation> getAnnotationListWithType( + Annotation[] annotations, Class<? extends Annotation> annotationType) { + return FluentIterable.from(annotations) + .filter(annotation -> annotation.annotationType().equals(annotationType)) + .toList(); + } + + @Override + public void postProcessTestInstance(Object testInstance, TestInfo testInfo) { + TestIndexHolder testIndexHolder = testInfo.getAnnotation(TestIndexHolder.class); + try { + if (testIndexHolder != null) { + List<TestParameterValueHolder> testParameterValues = + getParameterValuesForTest(testIndexHolder, testInfo.getTestClass()); + + // Do not include {@link Origin#METHOD_PARAMETER} nor {@link Origin#CONSTRUCTOR_PARAMETER} + // annotations. + List<TestParameterValueHolder> testParameterValuesForFieldInjection = + filterByOrigin(testParameterValues, Origin.CLASS, Origin.FIELD, Origin.METHOD); + // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class + // in the example above. + List<TestParameterValueHolder> remainingTestParameterValuesForFieldInjection = + new ArrayList<>(testParameterValuesForFieldInjection); + for (Field declaredField : + FluentIterable.from(listWithParents(testInstance.getClass())) + .transformAndConcat(c -> Arrays.asList(c.getDeclaredFields())) + .toList()) { + for (TestParameterValueHolder testParameterValue : + remainingTestParameterValuesForFieldInjection) { + if (declaredField.isAnnotationPresent( + testParameterValue.annotationTypeOrigin().annotationType())) { + if (testParameterValue.paramName().isPresent() + && !declaredField.getName().equals(testParameterValue.paramName().get())) { + // names don't match + continue; + } + declaredField.setAccessible(true); + declaredField.set(testInstance, testParameterValue.unwrappedValue()); + remainingTestParameterValuesForFieldInjection.remove(testParameterValue); + break; + } + } + } + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns an {@link TestParameterValueHolder} list that contains only the values originating from + * one of the {@code origins}. + */ + private static ImmutableList<TestParameterValueHolder> filterByOrigin( + List<TestParameterValueHolder> testParameterValues, Origin... origins) { + Set<Origin> originsToFilterBy = ImmutableSet.copyOf(origins); + return FluentIterable.from(testParameterValues) + .filter( + testParameterValue -> + originsToFilterBy.contains(testParameterValue.annotationTypeOrigin().origin())) + .toList(); + } + + /** + * Returns an {@link AnnotationTypeOrigin} list that contains only the values originating from one + * of the {@code origins}. + */ + private static ImmutableList<AnnotationTypeOrigin> filterAnnotationTypeOriginsByOrigin( + List<AnnotationTypeOrigin> annotationTypeOrigins, Origin... origins) { + List<Origin> originList = Arrays.asList(origins); + return FluentIterable.from(annotationTypeOrigins) + .filter(annotationTypeOrigin -> originList.contains(annotationTypeOrigin.origin())) + .toList(); + } + + /** Returns a {@link TestParameterAnnotation}'s value for a method or constructor parameter. */ + private Object getParameterValue( + List<TestParameterValueHolder> testParameterValues, + Class<?> methodParameterType, + Annotation[] parameterAnnotations, + List<Class<? extends Annotation>> processedAnnotationTypes) { + List<Class<? extends Annotation>> iteratedAnnotationTypes = new ArrayList<>(); + for (TestParameterValueHolder testParameterValue : testParameterValues) { + // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class + // in the example above. + for (Annotation parameterAnnotation : parameterAnnotations) { + Class<? extends Annotation> annotationType = + testParameterValue.annotationTypeOrigin().annotationType(); + if (parameterAnnotation.annotationType().equals(annotationType)) { + // If multiple annotations exist, ensure that the proper one is selected. + // For instance, for: + // <code> + // test(@FooParameter(1,2) Foo foo, @FooParameter(3,4) Foo bar) {} + // </code> + // Verifies that the correct @FooParameter annotation value will be assigned to the + // corresponding variable. + if (Collections.frequency(processedAnnotationTypes, annotationType) + == Collections.frequency(iteratedAnnotationTypes, annotationType)) { + processedAnnotationTypes.add(annotationType); + return testParameterValue.unwrappedValue(); + } + iteratedAnnotationTypes.add(annotationType); + } + } + } + // If no annotation matches, use the method parameter type. + for (TestParameterValueHolder testParameterValue : testParameterValues) { + // The annotationType corresponding to the annotationIndex, e.g. ColorParameter.class + // in the example above. + if (methodParameterType.isAssignableFrom( + getValueMethodReturnType( + testParameterValue.annotationTypeOrigin().annotationType(), + /* paramClass= */ Optional.absent()))) { + return testParameterValue.unwrappedValue(); + } + } + throw new IllegalStateException( + "The method parameter should have matched a TestParameterAnnotation"); + } + + /** + * This mechanism is a workaround to be able to store the annotation values in the annotation list + * of the {@link TestInfo}, since we cannot carry other information through the test runner. + */ + @Retention(RUNTIME) + @interface TestIndexHolder { + + /** The index of the test method in {@code getMethodsIncludingParentsSorted(testClass)} */ + int methodIndex(); + + /** + * The index of the set of parameters to run the test method with in the list produced by {@link + * #getParameterValuesForMethod}. + */ + int parametersIndex(); + + /** + * The full name of the test class. Only used for verifying that assumptions about the above + * indices are valid. + */ + String testClassName(); + } + + /** Factory for {@link TestIndexHolder}. */ + static class TestIndexHolderFactory { + @AutoAnnotation + static TestIndexHolder create(int methodIndex, int parametersIndex, String testClassName) { + return new AutoAnnotation_TestParameterAnnotationMethodProcessor_TestIndexHolderFactory_create( + methodIndex, parametersIndex, testClassName); + } + + private TestIndexHolderFactory() {} + } + + /** + * Returns whether the test should be skipped according to the {@code annotationType}'s {@link + * TestParameterValidator} and the current list of {@link TestParameterValueHolder}. + */ + private static boolean callShouldSkip( + Class<? extends Annotation> annotationType, + List<TestParameterValueHolder> testParameterValues) { + TestParameterAnnotation annotation = + annotationType.getAnnotation(TestParameterAnnotation.class); + Class<? extends TestParameterValidator> validator = annotation.validator(); + try { + return validator + .getConstructor() + .newInstance() + .shouldSkip(new ValidatorContext(testParameterValues)); + } catch (Exception e) { + throw new RuntimeException("Unexpected exception while invoking validator " + validator, e); + } + } + + private static class ValidatorContext implements TestParameterValidator.Context { + + private final List<TestParameterValueHolder> testParameterValues; + private final Set<Object> valueList; + + public ValidatorContext(List<TestParameterValueHolder> testParameterValues) { + this.testParameterValues = testParameterValues; + this.valueList = + FluentIterable.from(testParameterValues) + .transform(TestParameterValueHolder::unwrappedValue) + .filter(Objects::nonNull) + .toSet(); + } + + @Override + public boolean has(Class<? extends Annotation> testParameter, Object value) { + return getValue(testParameter).transform(value::equals).or(false); + } + + @Override + public <T extends Enum<T>, U extends Enum<U>> boolean has(T value1, U value2) { + return valueList.contains(value1) && valueList.contains(value2); + } + + @Override + public Optional<Object> getValue(Class<? extends Annotation> testParameter) { + return getParameter(testParameter).transform(TestParameterValueHolder::unwrappedValue); + } + + @Override + public List<Object> getSpecifiedValues(Class<? extends Annotation> testParameter) { + return getParameter(testParameter) + .transform(TestParameterValueHolder::specifiedValues) + .or(ImmutableList.of()); + } + + private Optional<TestParameterValueHolder> getParameter( + Class<? extends Annotation> testParameter) { + return FluentIterable.from(testParameterValues) + .firstMatch(value -> value.annotationTypeOrigin().annotationType().equals(testParameter)); + } + } + + /** + * Returns the class of the list elements returned by {@code provideValues()}. + * + * @param annotationType The type of the annotation that was encountered in the test class. The + * definition of this annotation is itself annotated with the {@link TestParameterAnnotation} + * annotation. + * @param paramClass The class of the parameter or field that is being annotated. In case the + * annotation is annotating a method, constructor or class, {@code paramClass} is an absent + * optional. + */ + private static Class<?> getValueMethodReturnType( + Class<? extends Annotation> annotationType, Optional<Class<?>> paramClass) { + TestParameterAnnotation testParameter = + annotationType.getAnnotation(TestParameterAnnotation.class); + Class<? extends TestParameterValueProvider> valueProvider = testParameter.valueProvider(); + try { + return valueProvider.getConstructor().newInstance().getValueType(annotationType, paramClass); + } catch (Exception e) { + throw new RuntimeException( + "Unexpected exception while invoking value provider " + valueProvider, e); + } + } + + /** Returns the TestParameterAnnotation annotation types defined for a method or constructor. */ + private ImmutableList<? extends Class<? extends Annotation>> getTestParameterAnnotations( + List<AnnotationTypeOrigin> annotationTypeOrigins, + final Class<?> testClass, + AnnotatedElement methodOrConstructor) { + return FluentIterable.from(annotationTypeOrigins) + .transform(AnnotationTypeOrigin::annotationType) + .filter( + annotationType -> + testClass.isAnnotationPresent(annotationType) + || methodOrConstructor.isAnnotationPresent(annotationType)) + .toList(); + } + + private <T> int strictIndexOf(List<T> haystack, T needle) { + int index = haystack.indexOf(needle); + checkArgument(index >= 0, "Could not find '%s' in %s", needle, haystack); + return index; + } + + private ImmutableList<Method> getMethodsIncludingParentsSorted(Class<?> clazz) { + ImmutableList.Builder<Method> resultBuilder = ImmutableList.builder(); + while (clazz != null) { + resultBuilder.add(clazz.getDeclaredMethods()); + clazz = clazz.getSuperclass(); + } + // Because getDeclaredMethods()'s order is not specified, there is the theoretical possibility + // that the order of methods is unstable. To partly fix this, we sort the result based on method + // name. This is still not perfect because of method overloading, but that should be + // sufficiently rare for test names. + return ImmutableList.sortedCopyOf( + Ordering.natural().onResultOf(Method::getName), resultBuilder.build()); + } + + private static ImmutableList<Class<?>> listWithParents(Class<?> clazz) { + ImmutableList.Builder<Class<?>> resultBuilder = ImmutableList.builder(); + + Class<?> currentClass = clazz; + while (currentClass != null) { + resultBuilder.add(currentClass); + currentClass = currentClass.getSuperclass(); + } + + return resultBuilder.build(); + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorExtension.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorExtension.java new file mode 100644 index 0000000..6d6aa51 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorExtension.java @@ -0,0 +1,140 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Iterables.getOnlyElement; + +import com.google.auto.value.AutoValue; +import com.google.auto.value.extension.memoized.Memoized; +import com.google.common.collect.ImmutableList; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.TestInstancePostProcessor; +import org.junit.jupiter.api.extension.TestTemplateInvocationContext; +import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; + +/** Implements the TestParameterInjector logic for JUnit5 (Jupiter). */ +class TestParameterInjectorExtension implements TestTemplateInvocationContextProvider { + + private static final TestMethodProcessorList testMethodProcessors = + TestMethodProcessorList.createNewParameterizedProcessors(); + + @Override + public boolean supportsTestTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts( + ExtensionContext extensionContext) { + validateTestMethodAndConstructor( + extensionContext.getRequiredTestMethod(), extensionContext.getRequiredTestClass()); + List<TestInfo> testInfos = + testMethodProcessors.calculateTestInfos( + extensionContext.getRequiredTestMethod(), extensionContext.getRequiredTestClass()); + + return testInfos.stream().map(CustomInvocationContext::of); + } + + private void validateTestMethodAndConstructor(Method testMethod, Class<?> testClass) { + checkState( + testClass.getDeclaredConstructors().length == 1, + "Only a single constructor is allowed, but found %s in %s", + testClass.getDeclaredConstructors().length, + testClass.getSimpleName()); + Constructor<?> constructor = + getOnlyElement(ImmutableList.copyOf(testClass.getDeclaredConstructors())); + + testMethodProcessors.validateConstructor(constructor).assertValid(); + + testMethodProcessors.validateTestMethod(testMethod, testClass).assertValid(); + + checkState( + testMethod.getAnnotation(TestParameterInjectorTest.class) != null, + "Each test method handled by this extension should be annotated with" + + " @TestParameterInjectorTest"); + } + + @AutoValue + abstract static class CustomInvocationContext implements TestTemplateInvocationContext { + + abstract TestInfo testInfo(); + + @Memoized + List<Object> getConstructorParameters() { + Constructor<?> constructor = + getOnlyElement(ImmutableList.copyOf(testInfo().getTestClass().getDeclaredConstructors())); + + return testMethodProcessors.getConstructorParameters(constructor, testInfo()); + } + + @Memoized + List<Object> getTestMethodParameters() { + return testMethodProcessors.getTestMethodParameters(testInfo()); + } + + static CustomInvocationContext of(TestInfo testInfo) { + return new AutoValue_TestParameterInjectorExtension_CustomInvocationContext(testInfo); + } + + @Override + public String getDisplayName(int invocationIndex) { + return testInfo().getName(); + } + + @Override + public List<Extension> getAdditionalExtensions() { + return ImmutableList.of(new CustomAdditionalExtension()); + } + + class CustomAdditionalExtension implements ParameterResolver, TestInstancePostProcessor { + + @Override + public boolean supportsParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) { + if (parameterContext.getDeclaringExecutable() instanceof Constructor) { + return true; + } else { + return parameterContext + .getDeclaringExecutable() + .isAnnotationPresent(TestParameterInjectorTest.class); + } + } + + @Override + public Object resolveParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) { + if (parameterContext.getDeclaringExecutable() instanceof Constructor) { + return getConstructorParameters().get(parameterContext.getIndex()); + } else { + return getTestMethodParameters().get(parameterContext.getIndex()); + } + } + + @Override + public void postProcessTestInstance(Object testInstance, ExtensionContext extensionContext) + throws Exception { + testMethodProcessors.postProcessTestInstance(testInstance, testInfo()); + } + } + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorTest.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorTest.java new file mode 100644 index 0000000..e17179a --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorTest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Replacement for JUnit5's @Test for test methods that want to use @TestParameter[s]. + * + * <p>Example: + * + * <pre> + * class MyTest { + * {@literal @}TestParameterInjectorTest + * void withParameter_success({@literal @}TestParameter boolean bool) { + * // ... + * } + * + * {@literal @}TestParameterInjectorTest + * {@literal @}TestParameters("{name: 1, number: 3.3}") + * {@literal @}TestParameters("{name: abc, number: 5}") + * void withParameters_success(String name, double number) { + * // ... + * } + * } + * </pre> + */ +@TestTemplate +@ExtendWith(TestParameterInjectorExtension.class) +@Retention(RUNTIME) +@Target({METHOD}) +public @interface TestParameterInjectorTest {} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorUtils.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorUtils.java new file mode 100644 index 0000000..cd47ea1 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorUtils.java @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Iterables.getOnlyElement; + +import com.google.common.collect.ImmutableList; +import java.lang.reflect.Constructor; + +/** Shared utility methods. */ +class TestParameterInjectorUtils { + + /** + * Return the only public constructor of the given test class. If there is none, return the only + * constructor. + * + * <p>Normally, there should be exactly one constructor (public or other), but some frameworks + * introduce an extra non-public constructor (see + * https://github.com/google/TestParameterInjector/issues/40). + */ + static Constructor<?> getOnlyConstructor(Class<?> testClass) { + ImmutableList<Constructor<?>> constructors = ImmutableList.copyOf(testClass.getConstructors()); + if (constructors.isEmpty()) { + // There are no public constructors. This is likely a JUnit5 test, so we should take the only + // non-public constructor instead. + constructors = ImmutableList.copyOf(testClass.getDeclaredConstructors()); + } + checkState( + constructors.size() == 1, "Expected exactly one constructor, but got %s", constructors); + return getOnlyElement(constructors); + } + + private TestParameterInjectorUtils() {} +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValidator.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValidator.java new file mode 100644 index 0000000..70db746 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValidator.java @@ -0,0 +1,68 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import com.google.common.base.Optional; +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Validator interface which allows {@link TestParameterAnnotation} annotations to validate the set + * of annotation values for a given test instance, and to selectively skip the test. + */ +interface TestParameterValidator { + + /** + * This interface allows to access information on the current testwhen implementing {@link + * TestParameterValidator}. + */ + interface Context { + + /** Returns whether the current test has the {@link TestParameterAnnotation} value(s). */ + boolean has(Class<? extends Annotation> testParameter, Object value); + + /** + * Returns whether the current test has the two {@link TestParameterAnnotation} values, granted + * that the value is an enum, and each enum corresponds to a unique annotation. + */ + <T extends Enum<T>, U extends Enum<U>> boolean has(T value1, U value2); + + /** + * Returns all the current test value for a given {@link TestParameterAnnotation} annotated + * annotation. + */ + Optional<Object> getValue(Class<? extends Annotation> testParameter); + + /** + * Returns all the values specified for a given {@link TestParameterAnnotation} annotated + * annotation in the test. + * + * <p>For example, if the test annotates '@Foo(a,b,c)', getSpecifiedValues(Foo.class) will + * return [a,b,c]. + */ + List<Object> getSpecifiedValues(Class<? extends Annotation> testParameter); + } + + /** + * Returns whether the test should be skipped based on the annotations' values. + * + * <p>The {@code testParameterValues} list contains all {@link TestParameterAnnotation} + * annotations, including those specified at the class, field, method, method parameter, + * constructor, and constructor parameter for a given test. + * + * <p>This method is not invoked in the context of a running test statement. + */ + boolean shouldSkip(Context context); +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValue.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValue.java new file mode 100644 index 0000000..f748521 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValue.java @@ -0,0 +1,57 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Optional; +import javax.annotation.Nullable; + +/** + * Wrapper class around a parameter value. Use this to give a value a name that is different from + * its {@code toString()} method. + */ +public class TestParameterValue { + private final @Nullable Object wrappedValue; + private final Optional<String> customName; + + private TestParameterValue(@Nullable Object wrappedValue, Optional<String> customName) { + this.wrappedValue = wrappedValue; + this.customName = checkNotNull(customName); + } + + /** Wraps the given value. */ + public static TestParameterValue wrap(@Nullable Object wrappedValue) { + return new TestParameterValue(wrappedValue, /* customName= */ Optional.absent()); + } + + /** + * Returns a new {@link TestParameterValue} instance that stores the given name. The + * TestParameterInjector framework will use this name instead of {@code wrappedValue.toString()} + * when generating the test name. + */ + public TestParameterValue withName(String name) { + return new TestParameterValue(wrappedValue, Optional.of(name)); + } + + @Nullable + Object getWrappedValue() { + return wrappedValue; + } + + Optional<String> getCustomName() { + return customName; + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java new file mode 100644 index 0000000..9cc9f88 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValueProvider.java @@ -0,0 +1,94 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import com.google.common.base.Optional; +import java.lang.annotation.Annotation; +import java.util.List; + +/** + * Interface which allows {@link TestParameterAnnotation} annotations to provide the values to test + * in a dynamic way. + */ +interface TestParameterValueProvider { + + /** + * Returns the parameter values for which the test should run. + * + * @param annotation The annotation instance that was encountered in the test class. The + * definition of this annotation is itself annotated with the {@link TestParameterAnnotation} + * annotation. + * @param parameterClass The class of the parameter or field that is being annotated. In case the + * annotation is annotating a method, constructor or class, {@code parameterClass} is an empty + * optional. + */ + default List<Object> provideValues(Annotation annotation, Optional<Class<?>> parameterClass) { + throw new UnsupportedOperationException( + "If this is called by TestParameterInjector, it means that neither of the" + + " provideValues()-type methods have been implemented"); + } + + /** + * Extension of {@link #provideValues(Annotation, Optional<Class<?>>)} with extra context. + * + * @param annotation The annotation instance that was encountered in the test class. The + * definition of this annotation is itself annotated with the {@link TestParameterAnnotation} + * annotation. + * @param otherAnnotations A list of all other annotations on the field or parameter that was + * annotated with {@code annotation}. + * <p>For example, if the test code is as follows: + * <pre> + * @Test + * public void myTest_success( + * @CustomAnnotation(123) @TestParameter(valuesProvider=MyProvider.class) Foo foo) { + * ... + * } + * </pre> + * then this list will contain a single element: @CustomAnnotation(123). + * <p>In case the annotation is annotating a method, constructor or class, {@code + * parameterClass} is an empty list. + * @param parameterClass The class of the parameter or field that is being annotated. In case the + * annotation is annotating a method, constructor or class, {@code parameterClass} is an empty + * optional. + * @param testClass The class that contains the test that is currently being run. + * <p>Having this can be useful when sharing providers between tests that have the same base + * class. In those cases, an abstract method can be called as follows: + * <pre> + * ((MyBaseClass) context.testClass().newInstance()).myAbstractMethod() + * </pre> + * + * @deprecated Don't use this method outside of the testparameterinjector codebase, as it is prone + * to being changed. + */ + @Deprecated + default List<Object> provideValues( + Annotation annotation, Optional<Class<?>> parameterClass, GenericParameterContext context) { + return provideValues(annotation, parameterClass); + } + + /** + * Returns the class of the list elements returned by {@link #provideValues(Annotation, + * Optional)}. + * + * @param annotationType The type of the annotation that was encountered in the test class. The + * definition of this annotation is itself annotated with the {@link TestParameterAnnotation} + * annotation. + * @param parameterClass The class of the parameter or field that is being annotated. In case the + * annotation is annotating a method, constructor or class, {@code parameterClass} is an empty + * optional. + */ + Class<?> getValueType( + Class<? extends Annotation> annotationType, Optional<Class<?>> parameterClass); +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValues.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValues.java new file mode 100644 index 0000000..b2c88a6 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValues.java @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import com.google.common.base.Optional; +import java.lang.annotation.Annotation; + +/** Interface to retrieve the {@link TestParameterAnnotation} values for a test. */ +interface TestParameterValues { + /** + * Returns a {@link TestParameterAnnotation} value for the current test as specified by {@code + * testInfo}, or {@link Optional#absent()} if the {@code annotationType} is not found. + */ + Optional<Object> getValue(Class<? extends Annotation> annotationType); +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java new file mode 100644 index 0000000..29f945f --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterValuesProvider.java @@ -0,0 +1,162 @@ +/* + * Copyright 2024 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.NoSuchElementException; +import javax.annotation.Nullable; + +/** + * Abstract class for custom providers of @TestParameter values. + * + * <p>This is a replacement for {@link TestParameter.TestParameterValuesProvider}, which will soon + * be deprecated. The difference with the former interface is that this class provides a {@code + * Context} instance when invoking {@link #provideValues}. + */ +public abstract class TestParameterValuesProvider + implements TestParameter.TestParameterValuesProvider { + + protected abstract List<?> provideValues(Context context) throws Exception; + + /** + * @deprecated This method should never be called as it will simply throw an {@link + * UnsupportedOperationException}. + */ + @Override + @Deprecated + public final List<?> provideValues() { + throw new UnsupportedOperationException( + "The TestParameterInjector framework should never call this method, and instead call" + + " #provideValues(Context)"); + } + + /** + * Wraps the given value in an object that allows you to give the parameter value a different + * name. The TestParameterInjector framework will recognize the returned {@link + * TestParameterValue} instances and unwrap them at injection time. + * + * <p>Usage: {@code value(file.content).withName(file.name)}. + */ + @Override + public final TestParameterValue value(@Nullable Object wrappedValue) { + // Overriding this method as final because it is not supposed to be overwritten + return TestParameterValue.wrap(wrappedValue); + } + + /** + * An immutable value class that contains extra information about the context of the parameter for + * which values are being provided. + */ + public static final class Context { + + private final GenericParameterContext delegate; + + Context(GenericParameterContext delegate) { + this.delegate = delegate; + } + + /** + * Returns the only annotation with the given type on the field or parameter that was annotated + * with @TestParameter. + * + * <p>For example, if the test code is as follows: + * + * <pre> + * {@literal @}Test + * public void myTest_success( + * {@literal @}CustomAnnotation(123) {@literal @}TestParameter(valuesProvider=MyProvider.class) Foo foo) { + * ... + * } + * </pre> + * + * then {@code context.getOtherAnnotation(CustomAnnotation.class).value()} will equal 123. + * + * @throws NoSuchElementException if this there is no annotation with the given type + * @throws IllegalArgumentException if there are multiple annotations with the given type + * @throws IllegalArgumentException if the argument it TestParameter.class because it is already + * handled by the TestParameterInjector framework. + */ + public <A extends Annotation> A getOtherAnnotation(Class<A> annotationType) { + checkArgument( + !TestParameter.class.equals(annotationType), + "Getting the @TestParameter annotating the field or parameter is not allowed because" + + " it is already handled by the TestParameterInjector framework."); + return delegate.getAnnotation(annotationType); + } + + /** + * Returns the only annotation with the given type on the field or parameter that was annotated + * with @TestParameter. + * + * <p>For example, if the test code is as follows: + * + * <pre> + * {@literal @}Test + * public void myTest_success( + * {@literal @}CustomAnnotation(123) + * {@literal @}CustomAnnotation(456) + * {@literal @}TestParameter(valuesProvider=MyProvider.class) + * Foo foo) { + * ... + * } + * </pre> + * + * then {@code context.getOtherAnnotations(CustomAnnotation.class)} will return the annotation + * with 123 and 456. + * + * <p>Returns an empty list if this there is no annotation with the given type. + * + * @throws IllegalArgumentException if the argument it TestParameter.class because it is already + * handled by the TestParameterInjector framework. + */ + public <A extends Annotation> ImmutableList<A> getOtherAnnotations(Class<A> annotationType) { + checkArgument( + !TestParameter.class.equals(annotationType), + "Getting the @TestParameter annotating the field or parameter is not allowed because" + + " it is already handled by the TestParameterInjector framework."); + return delegate.getAnnotations(annotationType); + } + + /** + * The class that contains the test that is currently being run. + * + * <p>Having this can be useful when sharing providers between tests that have the same base + * class. In those cases, an abstract method can be called as follows: + * + * <pre> + * ((MyBaseClass) context.testClass().newInstance()).myAbstractMethod() + * </pre> + */ + public Class<?> testClass() { + return delegate.testClass(); + } + + /** A list of all annotations on the field or parameter. */ + @VisibleForTesting + ImmutableList<Annotation> annotationsOnParameter() { + return delegate.annotationsOnParameter(); + } + + @Override + public String toString() { + return delegate.toString(); + } + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameters.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameters.java new file mode 100644 index 0000000..07d0fff --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParameters.java @@ -0,0 +1,276 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.util.Collections.unmodifiableMap; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValuesProvider; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * Annotation that can be placed (repeatedly) on @Test-methods or a test constructor to indicate the + * sets of parameters that it should be invoked with. + * + * <p>For @Test-methods, the method will be invoked for every set of parameters that is specified. + * For constructors, all the tests in the test class will be invoked on a class instance that was + * constructed by each set of parameters. + * + * <p>Note: If this annotation is used in a test class, the other methods in that class can still + * use other types of parameterization, such as {@linkplain TestParameter @TestParameter}. + * + * <p>See {@link #value()} for simple examples. + * + * <p>Warning: This annotation can only be used if the compiled java code contains the parameter + * names. This is typically done by passing the {@code -parameters} option to the Java compiler, + * which requires using Java 8 or higher and may not be available on Android. + */ +@Retention(RUNTIME) +@Target({CONSTRUCTOR, METHOD}) +@Repeatable(TestParameters.RepeatedTestParameters.class) +public @interface TestParameters { + + /** + * Specifies one or more stringified sets of parameters in YAML format. Each set corresponds to a + * single invocation of a test method. + * + * <p>Each element in this array is a full parameter set, formatted as a YAML mapping. The mapping + * keys must match the parameter names and the mapping values will be converted to the parameter + * type if possible. See yaml.org for the YAML syntax and the section below on the supported + * parameter types. + * + * <p>There are two distinct ways of using this annotation: repeated vs single: + * + * <p><b>Recommended usage: Separate annotation per parameter set</b> + * + * <p>This approach uses multiple @TestParameters annotations, one for each set of parameters, for + * example: + * + * <pre> + * {@literal @}Test + * {@literal @}TestParameters("{age: 17, expectIsAdult: false}") + * {@literal @}TestParameters("{age: 22, expectIsAdult: true}") + * public void personIsAdult(int age, boolean expectIsAdult) { ... } + * + * {@literal @}Test + * {@literal @}TestParameters("{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}") + * {@literal @}TestParameters("{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}") + * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... } + * </pre> + * + * <p><b>Old discouraged usage: Single annotation with all parameter sets</b> + * + * <p>This approach uses a single @TestParameter annotation for all parameter sets, for example: + * + * <pre> + * {@literal @}Test + * {@literal @}TestParameters({ + * "{age: 17, expectIsAdult: false}", + * "{age: 22, expectIsAdult: true}", + * }) + * public void personIsAdult(int age, boolean expectIsAdult) { ... } + * + * {@literal @}Test + * {@literal @}TestParameters({ + * "{updateRequest: {country_code: BE}, expectedResultType: SUCCESS}", + * "{updateRequest: {country_code: XYZ}, expectedResultType: FAILURE}", + * }) + * public void update(UpdateRequest updateRequest, ResultType expectedResultType) { ... } + * </pre> + * + * <p><b>Supported parameter types</b> + * + * <ul> + * <li>YAML primitives: + * <ul> + * <li>String: Specified as YAML string + * <li>boolean: Specified as YAML boolean + * <li>long and int: Specified as YAML integer + * <li>float and double: Specified as YAML floating point or integer + * </ul> + * <li> + * <li>Parsed types: + * <ul> + * <li>Enum value: Specified as a String that can be parsed by {@code Enum.valueOf()} + * <li>Byte array or com.google.protobuf.ByteString: Specified as an UTF8 String or YAML + * bytes (example: "!!binary 'ZGF0YQ=='") + * </ul> + * <li> + * </ul> + * + * <p>For dynamic sets of parameters or parameter types that are not supported here, use {@link + * #valuesProvider()} and leave this field empty. + */ + String[] value() default {}; + + /** + * Overrides the name of the parameter set that is used in the test name. + * + * <p>This can only be set if {@link #value()} has exactly one element. If not set, the YAML + * string in {@link #value()} is used in the test name. + * + * <p>For example: If this name is set to "young adult", then the test name might be + * "personIsAdult[young adult]" where the default might have been "personIsAdult[{age: 17, + * expectIsAdult: false}]". + */ + String customName() default ""; + + /** + * Sets a provider that will return a list of parameter sets. Each element in the returned list + * corresponds to a single invocation of a test method. + * + * <p>If this field is set, {@link #value()} must be empty and vice versa. + * + * <p><b>Example</b> + * + * <pre> + * {@literal @}Test + * {@literal @}TestParameters(valuesProvider = IsAdultValueProvider.class) + * public void personIsAdult(int age, boolean expectIsAdult) { ... } + * + * private static final class IsAdultValueProvider implements TestParametersValuesProvider { + * {@literal @}Override public {@literal List<TestParametersValues>} provideValues() { + * return ImmutableList.of( + * TestParametersValues.builder() + * .name("teenager") + * .addParameter("age", 17) + * .addParameter("expectIsAdult", false) + * .build(), + * TestParametersValues.builder() + * .name("young adult") + * .addParameter("age", 22) + * .addParameter("expectIsAdult", true) + * .build() + * ); + * } + * } + * </pre> + */ + Class<? extends TestParametersValuesProvider> valuesProvider() default + DefaultTestParametersValuesProvider.class; + + /** Interface for custom providers of test parameter values. */ + interface TestParametersValuesProvider { + List<TestParametersValues> provideValues(); + } + + /** A set of parameters for a single method invocation. */ + @AutoValue + abstract class TestParametersValues { + + /** + * A name for this set of parameters that will be used for describing this test. + * + * <p>Example: If a test method is called "personIsAdult" and this name is "teenager", the name + * of the resulting test will be "personIsAdult[teenager]". + */ + public abstract String name(); + + /** A map, mapping parameter names to their values. */ + @SuppressWarnings("AutoValueImmutableFields") // intentional to allow null values + public abstract Map<String, Object> parametersMap(); + + public static Builder builder() { + return new Builder(); + } + + // Avoid instantiations other than the AutoValue one. + TestParametersValues() {} + + /** Builder for {@link TestParametersValues}. */ + public static final class Builder { + private String name; + private final LinkedHashMap<String, Object> parametersMap = new LinkedHashMap<>(); + + /** + * Sets a name for this set of parameters that will be used for describing this test. + * + * <p>Setting a name is optional. If unset, one will be generated from the parameter values. + * + * <p>Example: If a test method is called "personIsAdult" and this name is "teenager", the + * name of the resulting test will be "personIsAdult[teenager]". + */ + public Builder name(String name) { + this.name = name.replaceAll("\\s+", " "); + return this; + } + + /** + * Adds a parameter by its name. + * + * @param parameterName The name of the parameter of the test method + * @param value A value of the same type as the method parameter + */ + public Builder addParameter(String parameterName, @Nullable Object value) { + this.parametersMap.put(parameterName, value); + return this; + } + + /** Adds parameters by thris names. */ + public Builder addParameters(Map<String, Object> parameterNameToValueMap) { + this.parametersMap.putAll(parameterNameToValueMap); + return this; + } + + public TestParametersValues build() { + if (name == null) { + // Name is not set. Auto-generate one based on the parameter name and values + StringBuilder nameBuilder = new StringBuilder(); + nameBuilder.append('{'); + for (String parameterName : parametersMap.keySet()) { + if (nameBuilder.length() > 1) { + nameBuilder.append(", "); + } + nameBuilder.append( + ParameterValueParsing.formatTestNameString( + Optional.of(parameterName), parametersMap.get(parameterName))); + } + nameBuilder.append('}'); + name = nameBuilder.toString(); + } + return new AutoValue_TestParameters_TestParametersValues( + name, unmodifiableMap(new LinkedHashMap<>(parametersMap))); + } + } + } + + /** Default {@link TestParametersValuesProvider} implementation that does nothing. */ + class DefaultTestParametersValuesProvider implements TestParametersValuesProvider { + @Override + public List<TestParametersValues> provideValues() { + return ImmutableList.of(); + } + } + + /** + * Holder annotation for multiple @TestParameters annotations. This should never be used directly. + */ + @Retention(RUNTIME) + @Target({CONSTRUCTOR, METHOD}) + @interface RepeatedTestParameters { + TestParameters[] value(); + } +} diff --git a/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParametersMethodProcessor.java b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParametersMethodProcessor.java new file mode 100644 index 0000000..26a1e65 --- /dev/null +++ b/junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestParametersMethodProcessor.java @@ -0,0 +1,468 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Verify.verify; + +import com.google.auto.value.AutoAnnotation; +import com.google.common.base.Optional; +import com.google.common.base.Throwables; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.common.primitives.Primitives; +import com.google.common.reflect.TypeToken; +import com.google.common.util.concurrent.UncheckedExecutionException; +import com.google.testing.junit.testparameterinjector.junit5.TestInfo.TestInfoParameter; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters.DefaultTestParametersValuesProvider; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters.RepeatedTestParameters; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValues; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValuesProvider; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** {@code TestMethodProcessor} implementation for supporting {@link TestParameters}. */ +@SuppressWarnings("AndroidJdkLibsChecker") // Parameter is not available on old Android SDKs. +final class TestParametersMethodProcessor implements TestMethodProcessor { + + private final LoadingCache<Executable, ImmutableList<TestParametersValues>> + parameterValuesByConstructorOrMethodCache = + CacheBuilder.newBuilder() + .maximumSize(1000) + .build(CacheLoader.from(TestParametersMethodProcessor::toParameterValuesList)); + + @Override + public ExecutableValidationResult validateConstructor(Constructor<?> constructor) { + if (hasRelevantAnnotation(constructor)) { + try { + // This method throws an exception if there is a validation error + getConstructorParameters(constructor); + } catch (Throwable t) { + return ExecutableValidationResult.validated(t); + } + return ExecutableValidationResult.valid(); + } else { + return ExecutableValidationResult.notValidated(); + } + } + + @Override + public ExecutableValidationResult validateTestMethod(Method testMethod, Class<?> testClass) { + if (hasRelevantAnnotation(testMethod)) { + try { + // This method throws an exception if there is a validation error + getMethodParameters(testMethod); + } catch (Throwable t) { + return ExecutableValidationResult.validated(t); + } + return ExecutableValidationResult.valid(); + } else { + return ExecutableValidationResult.notValidated(); + } + } + + @Override + public List<TestInfo> calculateTestInfos(TestInfo originalTest) { + boolean constructorIsParameterized = + hasRelevantAnnotation( + TestParameterInjectorUtils.getOnlyConstructor(originalTest.getTestClass())); + boolean methodIsParameterized = hasRelevantAnnotation(originalTest.getMethod()); + + if (!constructorIsParameterized && !methodIsParameterized) { + return ImmutableList.of(originalTest); + } + + ImmutableList.Builder<TestInfo> testInfos = ImmutableList.builder(); + + ImmutableList<Optional<TestParametersValues>> constructorParametersList = + getConstructorParametersOrSingleAbsentElement(originalTest.getTestClass()); + ImmutableList<Optional<TestParametersValues>> methodParametersList = + getMethodParametersOrSingleAbsentElement(originalTest.getMethod()); + for (int constructorParametersIndex = 0; + constructorParametersIndex < constructorParametersList.size(); + ++constructorParametersIndex) { + Optional<TestParametersValues> constructorParameters = + constructorParametersList.get(constructorParametersIndex); + + for (int methodParametersIndex = 0; + methodParametersIndex < methodParametersList.size(); + ++methodParametersIndex) { + Optional<TestParametersValues> methodParameters = + methodParametersList.get(methodParametersIndex); + + // Making final copies of non-final integers for use in lambda + int constructorParametersIndexCopy = constructorParametersIndex; + int methodParametersIndexCopy = methodParametersIndex; + + testInfos.add( + originalTest + .withExtraParameters( + FluentIterable.of( + constructorParameters.transform( + param -> + TestInfoParameter.create( + param.name(), + param.parametersMap(), + constructorParametersIndexCopy)), + methodParameters.transform( + param -> + TestInfoParameter.create( + param.name(), + param.parametersMap(), + methodParametersIndexCopy))) + .filter(Optional::isPresent) + .transform(Optional::get) + .toList()) + .withExtraAnnotation( + TestIndexHolderFactory.create( + constructorParametersIndex, methodParametersIndex))); + } + } + return testInfos.build(); + } + + private ImmutableList<Optional<TestParametersValues>> + getConstructorParametersOrSingleAbsentElement(Class<?> testClass) { + Constructor<?> constructor = TestParameterInjectorUtils.getOnlyConstructor(testClass); + return hasRelevantAnnotation(constructor) + ? FluentIterable.from(getConstructorParameters(constructor)) + .transform(Optional::of) + .toList() + : ImmutableList.of(Optional.absent()); + } + + private ImmutableList<Optional<TestParametersValues>> getMethodParametersOrSingleAbsentElement( + Method method) { + return hasRelevantAnnotation(method) + ? FluentIterable.from(getMethodParameters(method)).transform(Optional::of).toList() + : ImmutableList.of(Optional.absent()); + } + + @Override + public Optional<List<Object>> maybeGetConstructorParameters( + Constructor<?> constructor, TestInfo testInfo) { + if (hasRelevantAnnotation(constructor)) { + ImmutableList<TestParametersValues> parameterValuesList = + getConstructorParameters(constructor); + TestParametersValues parametersValues = + parameterValuesList.get( + testInfo.getAnnotation(TestIndexHolder.class).constructorParametersIndex()); + + return Optional.of(toParameterList(parametersValues, constructor.getParameters())); + } else { + return Optional.absent(); + } + } + + @Override + public Optional<List<Object>> maybeGetTestMethodParameters(TestInfo testInfo) { + Method testMethod = testInfo.getMethod(); + if (hasRelevantAnnotation(testMethod)) { + ImmutableList<TestParametersValues> parameterValuesList = getMethodParameters(testMethod); + TestParametersValues parametersValues = + parameterValuesList.get( + testInfo.getAnnotation(TestIndexHolder.class).methodParametersIndex()); + + return Optional.of(toParameterList(parametersValues, testMethod.getParameters())); + } else { + return Optional.absent(); + } + } + + @Override + public void postProcessTestInstance(Object testInstance, TestInfo testInfo) {} + + private ImmutableList<TestParametersValues> getConstructorParameters(Constructor<?> constructor) { + try { + return parameterValuesByConstructorOrMethodCache.getUnchecked(constructor); + } catch (UncheckedExecutionException e) { + // Rethrow IllegalStateException because they can be caused by user mistakes and the user + // doesn't need to know that the caching layer is in between. + Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); + throw e; + } + } + + private ImmutableList<TestParametersValues> getMethodParameters(Method method) { + try { + return parameterValuesByConstructorOrMethodCache.getUnchecked(method); + } catch (UncheckedExecutionException e) { + // Rethrow IllegalStateException because they can be caused by user mistakes and the user + // doesn't need to know that the caching layer is in between. + Throwables.throwIfInstanceOf(e.getCause(), IllegalStateException.class); + throw e; + } + } + + private static ImmutableList<TestParametersValues> toParameterValuesList(Executable executable) { + checkParameterNamesArePresent(executable); + ImmutableList<Parameter> parametersList = ImmutableList.copyOf(executable.getParameters()); + + if (executable.isAnnotationPresent(TestParameters.class)) { + checkState( + !executable.isAnnotationPresent(RepeatedTestParameters.class), + "Unexpected situation: Both @TestParameters and @RepeatedTestParameters annotating the" + + " same method"); + TestParameters annotation = executable.getAnnotation(TestParameters.class); + boolean valueIsSet = annotation.value().length > 0; + boolean valuesProviderIsSet = + !annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class); + + checkState( + !(valueIsSet && valuesProviderIsSet), + "It is not allowed to specify both value and valuesProvider in @TestParameters(value=%s," + + " valuesProvider=%s) on %s()", + Arrays.toString(annotation.value()), + annotation.valuesProvider().getSimpleName(), + executable.getName()); + checkState( + valueIsSet || valuesProviderIsSet, + "Either a value or a valuesProvider must be set in @TestParameters on %s()", + executable.getName()); + if (!annotation.customName().isEmpty()) { + checkState( + annotation.value().length == 1, + "Setting @TestParameters.customName is only allowed if there is exactly one YAML string" + + " in @TestParameters.value (on %s())", + executable.getName()); + } + + if (valueIsSet) { + return FluentIterable.from(annotation.value()) + .transform( + yamlMap -> toParameterValues(yamlMap, parametersList, annotation.customName())) + .toList(); + } else { + return toParameterValuesList(annotation.valuesProvider(), parametersList); + } + } else { // Not annotated with @TestParameters + verify( + executable.isAnnotationPresent(RepeatedTestParameters.class), + "This method should only be called for executables with at least one relevant" + + " annotation"); + + return FluentIterable.from(executable.getAnnotation(RepeatedTestParameters.class).value()) + .transform( + annotation -> + toParameterValues( + validateAndGetSingleValueFromRepeatedAnnotation(annotation, executable), + parametersList, + annotation.customName())) + .toList(); + } + } + + private static ImmutableList<TestParametersValues> toParameterValuesList( + Class<? extends TestParametersValuesProvider> valuesProvider, List<Parameter> parameters) { + try { + Constructor<? extends TestParametersValuesProvider> constructor = + valuesProvider.getDeclaredConstructor(); + constructor.setAccessible(true); + List<TestParametersValues> testParametersValues = constructor.newInstance().provideValues(); + for (TestParametersValues testParametersValue : testParametersValues) { + validateThatValuesMatchParameters(testParametersValue, parameters); + } + return ImmutableList.copyOf(testParametersValues); + } catch (NoSuchMethodException e) { + if (!Modifier.isStatic(valuesProvider.getModifiers()) && valuesProvider.isMemberClass()) { + throw new IllegalStateException( + String.format( + "Could not find a no-arg constructor for %s, probably because it is a not-static" + + " inner class. You can fix this by making %s static.", + valuesProvider.getSimpleName(), valuesProvider.getSimpleName()), + e); + } else { + throw new IllegalStateException( + String.format( + "Could not find a no-arg constructor for %s.", valuesProvider.getSimpleName()), + e); + } + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + } + + private static void checkParameterNamesArePresent(Executable executable) { + checkState( + FluentIterable.from(executable.getParameters()).allMatch(Parameter::isNamePresent), + "" + + "No parameter name could be found for %s, which likely means that parameter names" + + " aren't available at runtime. Please ensure that the this test was built with the" + + " -parameters compiler option.\n" + + "\n" + + "In Maven, you do this by adding <parameters>true</parameters> to the" + + " maven-compiler-plugin's configuration. For example:\n" + + "\n" + + "<build>\n" + + " <plugins>\n" + + " <plugin>\n" + + " <groupId>org.apache.maven.plugins</groupId>\n" + + " <artifactId>maven-compiler-plugin</artifactId>\n" + + " <version>3.8.1</version>\n" + + " <configuration>\n" + + " <compilerArgs>\n" + + " <arg>-parameters</arg>\n" + + " </compilerArgs>\n" + + " </configuration>\n" + + " </plugin>\n" + + " </plugins>\n" + + "</build>\n" + + "\n" + + "Don't forget to run `mvn clean` after making this change.", + executable.getName()); + } + + private static String validateAndGetSingleValueFromRepeatedAnnotation( + TestParameters annotation, Executable executable) { + checkState( + annotation.valuesProvider().equals(DefaultTestParametersValuesProvider.class), + "Setting a valuesProvider is not supported for methods/constructors with" + + " multiple @TestParameters annotations on %s()", + executable.getName()); + checkState( + annotation.value().length > 0, + "Either a value or a valuesProvider must be set in @TestParameters on %s()", + executable.getName()); + checkState( + annotation.value().length == 1, + "When specifying more than one @TestParameter for a method/constructor, each annotation" + + " must have exactly one value. Instead, got %s values on %s(): %s", + annotation.value().length, + executable.getName(), + Arrays.toString(annotation.value())); + + return annotation.value()[0]; + } + + private static void validateThatValuesMatchParameters( + TestParametersValues testParametersValues, List<Parameter> parameters) { + ImmutableMap<String, Parameter> parametersByName = + Maps.uniqueIndex(parameters, Parameter::getName); + + checkState( + testParametersValues.parametersMap().keySet().equals(parametersByName.keySet()), + "Cannot map the given TestParametersValues to parameters %s (Given TestParametersValues" + + " are %s)", + parametersByName.keySet(), + testParametersValues); + + testParametersValues + .parametersMap() + .forEach( + (paramName, paramValue) -> { + Class<?> expectedClass = Primitives.wrap(parametersByName.get(paramName).getType()); + if (paramValue != null) { + checkState( + expectedClass.isInstance(paramValue), + "Cannot map value '%s' (class = %s) to parameter %s (class = %s) (for" + + " TestParametersValues %s)", + paramValue, + paramValue.getClass(), + paramName, + expectedClass, + testParametersValues); + } + }); + } + + private static TestParametersValues toParameterValues( + String yamlString, List<Parameter> parameters, String maybeCustomName) { + Object yamlMapObject = ParameterValueParsing.parseYamlStringToObject(yamlString); + checkState( + yamlMapObject instanceof Map, + "Cannot map YAML string '%s' to parameters because it is not a mapping", + yamlString); + Map<?, ?> yamlMap = (Map<?, ?>) yamlMapObject; + + ImmutableMap<String, Parameter> parametersByName = + Maps.uniqueIndex(parameters, Parameter::getName); + checkState( + yamlMap.keySet().equals(parametersByName.keySet()), + "Cannot map YAML string '%s' to parameters %s", + yamlString, + parametersByName.keySet()); + + @SuppressWarnings("unchecked") + Map<String, Object> checkedYamlMap = (Map<String, Object>) yamlMap; + + return TestParametersValues.builder() + .name(maybeCustomName.isEmpty() ? yamlString : maybeCustomName) + .addParameters( + Maps.transformEntries( + checkedYamlMap, + (parameterName, parsedYaml) -> + ParameterValueParsing.parseYamlObjectToJavaType( + parsedYaml, + TypeToken.of(parametersByName.get(parameterName).getParameterizedType())))) + .build(); + } + + // Note: We're not using the Executable interface here because it isn't supported by Java 7 and + // this code is called even if only @TestParameter is used. In other places, Executable is usable + // because @TestParameters only works for Java 8 anyway. + private static boolean hasRelevantAnnotation(Constructor<?> executable) { + return executable.isAnnotationPresent(TestParameters.class) + || executable.isAnnotationPresent(RepeatedTestParameters.class); + } + + private static boolean hasRelevantAnnotation(Method executable) { + return executable.isAnnotationPresent(TestParameters.class) + || executable.isAnnotationPresent(RepeatedTestParameters.class); + } + + private static List<Object> toParameterList( + TestParametersValues parametersValues, Parameter[] parameters) { + return Arrays.asList( + FluentIterable.from(Arrays.asList(parameters)) + .transform(Parameter::getName) + .transform(name -> parametersValues.parametersMap().get(name)) + .toArray(Object.class)); + } + + /** + * This mechanism is a workaround to be able to store the test index in the annotation list of the + * {@link TestInfo}, since we cannot carry other information through the test runner. + */ + @Retention(RetentionPolicy.RUNTIME) + @interface TestIndexHolder { + int constructorParametersIndex(); + + int methodParametersIndex(); + } + + /** Factory for {@link TestIndexHolder}. */ + static class TestIndexHolderFactory { + @AutoAnnotation + static TestIndexHolder create(int constructorParametersIndex, int methodParametersIndex) { + return new AutoAnnotation_TestParametersMethodProcessor_TestIndexHolderFactory_create( + constructorParametersIndex, methodParametersIndex); + } + + private TestIndexHolderFactory() {} + } +} diff --git a/junit5/src/test/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorJUnit5Test.java b/junit5/src/test/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorJUnit5Test.java new file mode 100644 index 0000000..0ebf54b --- /dev/null +++ b/junit5/src/test/java/com/google/testing/junit/testparameterinjector/junit5/TestParameterInjectorJUnit5Test.java @@ -0,0 +1,608 @@ +/* + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.testing.junit.testparameterinjector.junit5_otherpackage; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.testing.junit.testparameterinjector.junit5.TestParameter; +import com.google.testing.junit.testparameterinjector.junit5.TestParameter.TestParameterValuesProvider; +import com.google.testing.junit.testparameterinjector.junit5.TestParameterInjectorTest; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValues; +import com.google.testing.junit.testparameterinjector.junit5.TestParameters.TestParametersValuesProvider; +import java.lang.annotation.Retention; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.TestExecutionResult.Status; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestIdentifier; +import org.junit.platform.launcher.TestPlan; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.launcher.core.LauncherFactory; + +/** Tests the full feature set of TestParameterInjector with JUnit5 (Jupiter). */ +class TestParameterInjectorJUnit5Test { + + abstract static class SuccessfulTestCaseBase { + private static Map<String, String> testNameToStringifiedParameters; + private static ImmutableMap<String, String> expectedTestNameToStringifiedParameters; + private String testName; + + @BeforeAll + private static void checkStaticFieldAreNull() { + checkState(testNameToStringifiedParameters == null); + checkState(expectedTestNameToStringifiedParameters == null); + } + + @BeforeEach + private void storeTestName(org.junit.jupiter.api.TestInfo testInfo) { + testName = testInfo.getDisplayName(); + } + + final void storeTestParametersForThisTest(Object... params) { + if (testNameToStringifiedParameters == null) { + testNameToStringifiedParameters = new LinkedHashMap<>(); + // Copying this into a static field because @AfterAll methods have to be static + expectedTestNameToStringifiedParameters = expectedTestNameToStringifiedParameters(); + } + checkState( + !testNameToStringifiedParameters.containsKey(testName), + "Parameters for the test with name '%s' are already stored. This might mean that there" + + " are duplicate test names", + testName); + testNameToStringifiedParameters.put( + testName, stream(params).map(String::valueOf).collect(joining(":"))); + } + + abstract ImmutableMap<String, String> expectedTestNameToStringifiedParameters(); + + @AfterAll + private static void completedAllTests() { + try { + assertWithMessage(toCopyPastableJavaString(testNameToStringifiedParameters)) + .that(testNameToStringifiedParameters) + .isEqualTo(expectedTestNameToStringifiedParameters); + } finally { + testNameToStringifiedParameters = null; + expectedTestNameToStringifiedParameters = null; + } + } + } + + @RunAsTest + static class SimpleCases_WithoutExplicitConstructor extends SuccessfulTestCaseBase { + @Test + void withoutCustomAnnotation() { + storeTestParametersForThisTest(); + } + + @TestParameterInjectorTest + void withoutParameters() { + storeTestParametersForThisTest(); + } + + @TestParameterInjectorTest + @TestParameters("{name: 1, number: 3.3}") + @TestParameters("{name: abc, number: 5}") + void withParameters_success(String name, double number) { + storeTestParametersForThisTest(name, number); + } + + @TestParameterInjectorTest + void withParameter_success( + @TestParameter({"2", "xyz"}) String name, @TestParameter boolean bool) { + storeTestParametersForThisTest(name, bool); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("withoutCustomAnnotation()", "") + .put("withoutParameters", "") + .put("withParameters_success[{name: 1, number: 3.3}]", "1:3.3") + .put("withParameters_success[{name: abc, number: 5}]", "abc:5.0") + .put("withParameter_success[name=2,bool=false]", "2:false") + .put("withParameter_success[name=2,bool=true]", "2:true") + .put("withParameter_success[xyz,bool=false]", "xyz:false") + .put("withParameter_success[xyz,bool=true]", "xyz:true") + .build(); + } + } + + @RunAsTest + static class SimpleCases_WithParameterizedConstructor_TestParameter + extends SuccessfulTestCaseBase { + private final boolean constr; + + @TestParameter({"AAA", "BBB"}) + private String field; + + SimpleCases_WithParameterizedConstructor_TestParameter(@TestParameter boolean constr) { + this.constr = constr; + } + + @TestParameterInjectorTest + void withoutParameters() { + storeTestParametersForThisTest(constr, field); + } + + @TestParameterInjectorTest + @TestParameters("{name: 1}") + @TestParameters("{name: abc}") + void withParameters_success(String name) { + storeTestParametersForThisTest(constr, field, name); + } + + @TestParameterInjectorTest + void withParameter_success(@TestParameter({"2", "xyz"}) String name) { + storeTestParametersForThisTest(constr, field, name); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("withParameters_success[{name: 1},AAA,constr=false]", "false:AAA:1") + .put("withParameters_success[{name: 1},AAA,constr=true]", "true:AAA:1") + .put("withParameters_success[{name: 1},BBB,constr=false]", "false:BBB:1") + .put("withParameters_success[{name: 1},BBB,constr=true]", "true:BBB:1") + .put("withParameters_success[{name: abc},AAA,constr=false]", "false:AAA:abc") + .put("withParameters_success[{name: abc},AAA,constr=true]", "true:AAA:abc") + .put("withParameters_success[{name: abc},BBB,constr=false]", "false:BBB:abc") + .put("withParameters_success[{name: abc},BBB,constr=true]", "true:BBB:abc") + .put("withParameter_success[AAA,constr=false,name=2]", "false:AAA:2") + .put("withParameter_success[AAA,constr=false,xyz]", "false:AAA:xyz") + .put("withParameter_success[AAA,constr=true,name=2]", "true:AAA:2") + .put("withParameter_success[AAA,constr=true,xyz]", "true:AAA:xyz") + .put("withParameter_success[BBB,constr=false,name=2]", "false:BBB:2") + .put("withParameter_success[BBB,constr=false,xyz]", "false:BBB:xyz") + .put("withParameter_success[BBB,constr=true,name=2]", "true:BBB:2") + .put("withParameter_success[BBB,constr=true,xyz]", "true:BBB:xyz") + .put("withoutParameters[AAA,constr=false]", "false:AAA") + .put("withoutParameters[AAA,constr=true]", "true:AAA") + .put("withoutParameters[BBB,constr=false]", "false:BBB") + .put("withoutParameters[BBB,constr=true]", "true:BBB") + .build(); + } + } + + @RunAsTest + static class SimpleCases_WithParameterizedConstructor_TestParameters + extends SuccessfulTestCaseBase { + private final boolean constr; + + @TestParameter({"AAA", "BBB"}) + private String field; + + @TestParameters("{constr: true}") + @TestParameters("{constr: false}") + SimpleCases_WithParameterizedConstructor_TestParameters(boolean constr) { + this.constr = constr; + } + + @TestParameterInjectorTest + void withoutParameters() { + storeTestParametersForThisTest(constr, field); + } + + @TestParameterInjectorTest + @TestParameters("{name: 1}") + @TestParameters("{name: abc}") + void withParameters_success(String name) { + storeTestParametersForThisTest(constr, field, name); + } + + @TestParameterInjectorTest + void withParameter_success(@TestParameter({"2", "xyz"}) String name) { + storeTestParametersForThisTest(constr, field, name); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("withParameters_success[{constr: true},{name: 1},AAA]", "true:AAA:1") + .put("withParameters_success[{constr: true},{name: 1},BBB]", "true:BBB:1") + .put("withParameters_success[{constr: true},{name: abc},AAA]", "true:AAA:abc") + .put("withParameters_success[{constr: true},{name: abc},BBB]", "true:BBB:abc") + .put("withParameters_success[{constr: false},{name: 1},AAA]", "false:AAA:1") + .put("withParameters_success[{constr: false},{name: 1},BBB]", "false:BBB:1") + .put("withParameters_success[{constr: false},{name: abc},AAA]", "false:AAA:abc") + .put("withParameters_success[{constr: false},{name: abc},BBB]", "false:BBB:abc") + .put("withParameter_success[{constr: true},AAA,name=2]", "true:AAA:2") + .put("withParameter_success[{constr: true},AAA,xyz]", "true:AAA:xyz") + .put("withParameter_success[{constr: true},BBB,name=2]", "true:BBB:2") + .put("withParameter_success[{constr: true},BBB,xyz]", "true:BBB:xyz") + .put("withParameter_success[{constr: false},AAA,name=2]", "false:AAA:2") + .put("withParameter_success[{constr: false},AAA,xyz]", "false:AAA:xyz") + .put("withParameter_success[{constr: false},BBB,name=2]", "false:BBB:2") + .put("withParameter_success[{constr: false},BBB,xyz]", "false:BBB:xyz") + .put("withoutParameters[{constr: true},AAA]", "true:AAA") + .put("withoutParameters[{constr: true},BBB]", "true:BBB") + .put("withoutParameters[{constr: false},AAA]", "false:AAA") + .put("withoutParameters[{constr: false},BBB]", "false:BBB") + .build(); + } + } + + @RunAsTest + public static class AdvancedCases_WithValuesProvider extends SuccessfulTestCaseBase { + private final TestEnum testEnum; + + @TestParameters(valuesProvider = TestEnumValuesProvider.class) + public AdvancedCases_WithValuesProvider(TestEnum testEnum) { + this.testEnum = testEnum; + } + + @TestParameterInjectorTest + void test1() { + storeTestParametersForThisTest(testEnum); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("test1[one]", "ONE") + .put("test1[two]", "TWO") + .put("test1[null-case]", "null") + .build(); + } + } + + @RunAsTest + public static class AdvancedCases_WithValueProvider extends SuccessfulTestCaseBase { + @TestParameterInjectorTest + void stringTest(@TestParameter(valuesProvider = TestStringProvider.class) String string) { + storeTestParametersForThisTest(string); + } + + @TestParameterInjectorTest + void charMatcherTest( + @TestParameter(valuesProvider = CharMatcherProvider.class) CharMatcher charMatcher) { + storeTestParametersForThisTest(charMatcher); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("stringTest[A]", "A") + .put("stringTest[B]", "B") + .put("stringTest[string=null]", "null") + .put("stringTest[wizard]", "harry") + .put("charMatcherTest[CharMatcher.any()]", "CharMatcher.any()") + .put("charMatcherTest[CharMatcher.ascii()]", "CharMatcher.ascii()") + .put("charMatcherTest[CharMatcher.whitespace()]", "CharMatcher.whitespace()") + .build(); + } + + private static final class TestStringProvider implements TestParameterValuesProvider { + @Override + public List<?> provideValues() { + return newArrayList("A", "B", null, value("harry").withName("wizard")); + } + } + + private static final class CharMatcherProvider implements TestParameterValuesProvider { + @Override + public List<CharMatcher> provideValues() { + return newArrayList(CharMatcher.any(), CharMatcher.ascii(), CharMatcher.whitespace()); + } + } + } + + public abstract static class BaseClassWithTestParametersMethod extends SuccessfulTestCaseBase { + @TestParameterInjectorTest + @TestParameters("{testEnum: ONE}") + @TestParameters("{testEnum: TWO}") + void testInBase(TestEnum testEnum) { + storeTestParametersForThisTest(testEnum); + } + } + + @RunAsTest + public static class AdvancedCases_WithBaseClass_TestParametersMethodInBase + extends BaseClassWithTestParametersMethod { + @TestParameterInjectorTest + @TestParameters({"{testEnum: TWO}", "{testEnum: THREE}"}) + void testInChild(TestEnum testEnum) { + storeTestParametersForThisTest(testEnum); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("testInBase[{testEnum: ONE}]", "ONE") + .put("testInBase[{testEnum: TWO}]", "TWO") + .put("testInChild[{testEnum: TWO}]", "TWO") + .put("testInChild[{testEnum: THREE}]", "THREE") + .build(); + } + } + + public abstract static class BaseClassWithTestParameterMethod extends SuccessfulTestCaseBase { + @TestParameterInjectorTest + void testInBase(@TestParameter({"ONE", "TWO"}) TestEnum testEnum) { + storeTestParametersForThisTest(testEnum); + } + } + + @RunAsTest + public static class AdvancedCases_WithBaseClass_TestParameterMethodInBase + extends BaseClassWithTestParameterMethod { + @TestParameterInjectorTest + @TestParameters({"{testEnum: TWO}", "{testEnum: THREE}"}) + void testInChild(TestEnum testEnum) { + storeTestParametersForThisTest(testEnum); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("testInBase[ONE]", "ONE") + .put("testInBase[TWO]", "TWO") + .put("testInChild[{testEnum: TWO}]", "TWO") + .put("testInChild[{testEnum: THREE}]", "THREE") + .build(); + } + } + + public abstract static class BaseClassWithTestParameterField extends SuccessfulTestCaseBase { + @TestParameter TestEnum fieldInBase; + } + + @RunAsTest + public static class AdvancedCases_WithBaseClass_TestParameterFieldInBase + extends BaseClassWithTestParameterField { + @TestParameterInjectorTest + void testInChild() { + storeTestParametersForThisTest(fieldInBase); + } + + @Override + ImmutableMap<String, String> expectedTestNameToStringifiedParameters() { + return ImmutableMap.<String, String>builder() + .put("testInChild[ONE]", "ONE") + .put("testInChild[TWO]", "TWO") + .put("testInChild[THREE]", "THREE") + .build(); + } + } + + @RunAsTest( + failsWithMessage = + "Either a value or a valuesProvider must be set in @TestParameters on test1()") + public static class InvalidTest_TestParameters_EmptyAnnotation { + @TestParameterInjectorTest + @TestParameters + void test1() {} + } + + @RunAsTest( + failsWithMessage = "Either a value or a valuesProvider must be set in @TestParameters on ") + public static class InvalidTest_TestParameters_EmptyAnnotationOnConstructor { + @TestParameters + public InvalidTest_TestParameters_EmptyAnnotationOnConstructor() {} + + @TestParameterInjectorTest + void test1() {} + } + + @RunAsTest( + failsWithMessage = + "It is not allowed to specify both value and valuesProvider in" + + " @TestParameters(value=[{testEnum: ONE}], valuesProvider=TestEnumValuesProvider)" + + " on test1()") + public static class InvalidTest_TestParameters_CombiningValueWithProvider { + @TestParameterInjectorTest + @TestParameters(value = "{testEnum: ONE}", valuesProvider = TestEnumValuesProvider.class) + void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "Either a value or a valuesProvider must be set in @TestParameters on test1()") + public static class InvalidTest_TestParameters_RepeatedAnnotationIsEmpty { + @TestParameterInjectorTest + @TestParameters(value = "{testEnum: ONE}") + @TestParameters + void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "When specifying more than one @TestParameter for a method/constructor, each annotation" + + " must have exactly one value. Instead, got 2 values on test1(): [{testEnum: TWO}," + + " {testEnum: THREE}]") + public static class InvalidTest_TestParameters_RepeatedAnnotationHasMultipleValues { + @TestParameterInjectorTest + @TestParameters(value = "{testEnum: ONE}") + @TestParameters(value = {"{testEnum: TWO}", "{testEnum: THREE}"}) + void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "Setting a valuesProvider is not supported for methods/constructors with" + + " multiple @TestParameters annotations on test1()") + public static class InvalidTest_TestParameters_RepeatedAnnotationHasProvider { + @TestParameterInjectorTest + @TestParameters(valuesProvider = TestEnumValuesProvider.class) + @TestParameters(valuesProvider = TestEnumValuesProvider.class) + void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "Setting @TestParameters.customName is only allowed if there is exactly one YAML string" + + " in @TestParameters.value (on test1())") + public static class InvalidTest_TestParameters_NamedAnnotationHasMultipleValues { + @TestParameterInjectorTest + @TestParameters( + customName = "custom", + value = {"{testEnum: TWO}", "{testEnum: THREE}"}) + void test1(TestEnum testEnum) {} + } + + @RunAsTest( + failsWithMessage = + "Could not find a no-arg constructor for NonStaticProvider, probably because it is a" + + " not-static inner class. You can fix this by making NonStaticProvider static.") + public static class InvalidTest_TestParameter_NonStaticProviderClass { + @TestParameterInjectorTest + void test(@TestParameter(valuesProvider = NonStaticProvider.class) int i) {} + + @SuppressWarnings("ClassCanBeStatic") + class NonStaticProvider implements TestParameterValuesProvider { + @Override + public List<?> provideValues() { + return ImmutableList.of(); + } + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("provideTestClassesThatExpectSuccess") + void runTest_success(Class<?> testClass) { + FailureListener listener = new FailureListener(); + LauncherDiscoveryRequest request = + LauncherDiscoveryRequestBuilder.request() + .selectors(DiscoverySelectors.selectClass(testClass)) + .build(); + Launcher launcher = LauncherFactory.create(); + launcher.registerTestExecutionListeners(listener); + launcher.execute(request); + + assertNoFailures(listener.failures); + } + + @ParameterizedTest(name = "{0} fails with '{1}'") + @MethodSource("provideTestClassesThatExpectFailure") + void runTest_failure(Class<?> testClass, String failureMessage) { + FailureListener listener = new FailureListener(); + LauncherDiscoveryRequest request = + LauncherDiscoveryRequestBuilder.request() + .selectors(DiscoverySelectors.selectClass(testClass)) + .build(); + Launcher launcher = LauncherFactory.create(); + TestPlan testPlan = launcher.discover(request); + launcher.registerTestExecutionListeners(listener); + launcher.execute(request); + + assertThat(listener.failures).hasSize(1); + assertThat(getOnlyElement(listener.failures)).contains(failureMessage); + } + + private static Stream<Class<?>> provideTestClassesThatExpectSuccess() { + return stream(TestParameterInjectorJUnit5Test.class.getDeclaredClasses()) + .filter( + cls -> + cls.isAnnotationPresent(RunAsTest.class) + && cls.getAnnotation(RunAsTest.class).failsWithMessage().isEmpty()); + } + + private static Stream<Arguments> provideTestClassesThatExpectFailure() { + return stream(TestParameterInjectorJUnit5Test.class.getDeclaredClasses()) + .filter( + cls -> + cls.isAnnotationPresent(RunAsTest.class) + && !cls.getAnnotation(RunAsTest.class).failsWithMessage().isEmpty()) + .map(cls -> Arguments.of(cls, cls.getAnnotation(RunAsTest.class).failsWithMessage())); + } + + private static void assertNoFailures(List<String> failures) { + if (failures.size() == 1) { + throw new AssertionError(getOnlyElement(failures)); + } else if (failures.size() > 1) { + throw new AssertionError( + String.format( + "Test failed unexpectedly:\n\n%s", + failures.stream().collect(joining("\n------------------------------------\n")))); + } + } + + private static String toCopyPastableJavaString(Map<String, String> map) { + StringBuilder resultBuilder = new StringBuilder(); + resultBuilder.append("\n----------------------\n"); + resultBuilder.append("ImmutableMap.<String, String>builder()\n"); + map.forEach( + (key, value) -> + resultBuilder.append(String.format(" .put(\"%s\", \"%s\")\n", key, value))); + resultBuilder.append(" .build()\n"); + resultBuilder.append("----------------------\n"); + return resultBuilder.toString(); + } + + class FailureListener implements TestExecutionListener { + final List<String> failures = new ArrayList<>(); + + @Override + public void executionFinished( + TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { + if (testExecutionResult.getStatus() != Status.SUCCESSFUL) { + failures.add( + String.format( + "%s --> %s", + testIdentifier.getDisplayName(), + testExecutionResult.getThrowable().isPresent() + ? Throwables.getStackTraceAsString(testExecutionResult.getThrowable().get()) + : testExecutionResult)); + } + } + } + + @Retention(RUNTIME) + @interface RunAsTest { + String failsWithMessage() default ""; + } + + public enum TestEnum { + ONE, + TWO, + THREE; + } + + private static final class TestEnumValuesProvider implements TestParametersValuesProvider { + @Override + public List<TestParametersValues> provideValues() { + return ImmutableList.of( + TestParametersValues.builder().name("one").addParameter("testEnum", TestEnum.ONE).build(), + TestParametersValues.builder().name("two").addParameter("testEnum", TestEnum.TWO).build(), + TestParametersValues.builder().name("null-case").addParameter("testEnum", null).build()); + } + } +} @@ -21,10 +21,17 @@ <modelVersion>4.0.0</modelVersion> <groupId>com.google.testparameterinjector</groupId> - <artifactId>test-parameter-injector</artifactId> + <artifactId>test-parameter-injector-parent</artifactId> <version>HEAD-SNAPSHOT</version> - <name>TestParameterInjector</name> + <packaging>pom</packaging> + + <modules> + <module>junit4</module> + <module>junit5</module> + </modules> + + <name>TestParameterInjector parent project for internal use</name> <description> A simple yet powerful parameterized test runner. @@ -71,17 +78,6 @@ </roles> <timezone>+0</timezone> </developer> - <developer> - <id>ajurkowski</id> - <name>Alex Jurkowski</name> - <email>ajurkowski@google.com</email> - <organization>Google Inc.</organization> - <organizationUrl>http://www.google.com/</organizationUrl> - <roles> - <role>developer</role> - </roles> - <timezone>-6</timezone> - </developer> </developers> <scm> @@ -130,22 +126,12 @@ <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> - <version>30.1-jre</version> - </dependency> - <dependency> - <groupId>com.google.protobuf</groupId> - <artifactId>protobuf-lite</artifactId> - <version>3.0.1</version> - </dependency> - <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - <version>4.13.2</version> + <version>32.0.0-jre</version> </dependency> <dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> - <version>1.27</version> + <version>2.0</version> </dependency> <!-- Test dependencies --> @@ -155,6 +141,12 @@ <version>1.1.2</version> <scope>test</scope> </dependency> + <dependency> + <groupId>com.google.protobuf</groupId> + <artifactId>protobuf-javalite</artifactId> + <version>3.20.3</version> + <scope>test</scope> + </dependency> </dependencies> @@ -173,6 +165,7 @@ <testSource>1.8</testSource> <testTarget>1.8</testTarget> <parameters>true</parameters> + <compilerArgument>-Xlint:deprecation</compilerArgument> <annotationProcessorPaths> <path> <groupId>com.google.auto.value</groupId> |