diff options
Diffstat (limited to 'junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java')
-rw-r--r-- | junit5/src/main/java/com/google/testing/junit/testparameterinjector/junit5/TestInfo.java | 325 |
1 files changed, 325 insertions, 0 deletions
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); + } +} |