| /* |
| * Copyright 2021 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except |
| * in compliance with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software distributed under the License |
| * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
| * or implied. See the License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| |
| package com.google.testing.junit.testparameterinjector; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static java.lang.Math.min; |
| import static java.util.stream.Collectors.joining; |
| import static java.util.stream.Collectors.toSet; |
| |
| import com.google.auto.value.AutoValue; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Multimap; |
| import com.google.common.collect.MultimapBuilder; |
| import java.lang.annotation.Annotation; |
| import java.lang.reflect.Method; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.function.BiFunction; |
| import java.util.stream.Collector; |
| import java.util.stream.Collectors; |
| import java.util.stream.IntStream; |
| import javax.annotation.Nullable; |
| |
| /** A POJO containing information about a test (name and anotations). */ |
| @AutoValue |
| abstract class TestInfo { |
| |
| /** |
| * The maximum amount of characters that {@link #getName()} can have. |
| * |
| * <p>See b/168325767 for the reason behind this. tl;dr the name is put into a Unix file with max |
| * 255 characters. The surrounding constant characters take up 31 characters. The max is reduced |
| * by an additional 24 characters to account for future changes. |
| */ |
| static final int MAX_TEST_NAME_LENGTH = 200; |
| |
| /** The maximum amount of characters that a single parameter can take up in {@link #getName()}. */ |
| static final int MAX_PARAMETER_NAME_LENGTH = 100; |
| |
| public abstract Method getMethod(); |
| |
| public String getName() { |
| if (getParameters().isEmpty()) { |
| return getMethod().getName(); |
| } else { |
| return String.format( |
| "%s[%s]", |
| getMethod().getName(), |
| getParameters().stream().map(TestInfoParameter::getName).collect(joining(","))); |
| } |
| } |
| |
| abstract ImmutableList<TestInfoParameter> getParameters(); |
| |
| public abstract ImmutableList<Annotation> getAnnotations(); |
| |
| @Nullable |
| public <T extends Annotation> T getAnnotation(Class<T> annotationClass) { |
| for (Annotation annotation : getAnnotations()) { |
| if (annotationClass.isInstance(annotation)) { |
| return annotationClass.cast(annotation); |
| } |
| } |
| return null; |
| } |
| |
| TestInfo withExtraParameters(List<TestInfoParameter> parameters) { |
| return new AutoValue_TestInfo( |
| getMethod(), |
| ImmutableList.<TestInfoParameter>builder() |
| .addAll(this.getParameters()) |
| .addAll(parameters) |
| .build(), |
| getAnnotations()); |
| } |
| |
| TestInfo withExtraAnnotation(Annotation annotation) { |
| ImmutableList<Annotation> newAnnotations = |
| ImmutableList.<Annotation>builder().addAll(this.getAnnotations()).add(annotation).build(); |
| return new AutoValue_TestInfo(getMethod(), getParameters(), newAnnotations); |
| } |
| |
| /** |
| * Returns a new TestInfo instance with updated parameter names. |
| * |
| * @param parameterWithIndexToNewName A function of the parameter and its index in the {@link |
| * #getParameters()} list to the new name. |
| */ |
| private TestInfo withUpdatedParameterNames( |
| BiFunction<TestInfoParameter, Integer, String> parameterWithIndexToNewName) { |
| return new AutoValue_TestInfo( |
| getMethod(), |
| IntStream.range(0, getParameters().size()) |
| .mapToObj( |
| parameterIndex -> { |
| TestInfoParameter parameter = getParameters().get(parameterIndex); |
| return parameter.withName( |
| parameterWithIndexToNewName.apply(parameter, parameterIndex)); |
| }) |
| .collect(toImmutableList()), |
| getAnnotations()); |
| } |
| |
| public static TestInfo legacyCreate(Method method, String name, List<Annotation> annotations) { |
| return new AutoValue_TestInfo( |
| method, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations)); |
| } |
| |
| static TestInfo createWithoutParameters(Method method, List<Annotation> annotations) { |
| return new AutoValue_TestInfo( |
| method, /* parameters= */ ImmutableList.of(), ImmutableList.copyOf(annotations)); |
| } |
| |
| static ImmutableList<TestInfo> shortenNamesIfNecessary(List<TestInfo> testInfos) { |
| if (testInfos.stream() |
| .anyMatch( |
| info -> |
| info.getName().length() > MAX_TEST_NAME_LENGTH |
| || info.getParameters().stream() |
| .anyMatch(param -> param.getName().length() > MAX_PARAMETER_NAME_LENGTH))) { |
| int numberOfParameters = testInfos.get(0).getParameters().size(); |
| |
| if (numberOfParameters == 0) { |
| return ImmutableList.copyOf(testInfos); |
| } else { |
| Set<Integer> parameterIndicesThatNeedUpdate = |
| IntStream.range(0, numberOfParameters) |
| .filter( |
| parameterIndex -> |
| testInfos.stream() |
| .anyMatch( |
| info -> |
| info.getParameters().get(parameterIndex).getName().length() |
| > getMaxCharactersPerParameter(info, numberOfParameters))) |
| .boxed() |
| .collect(toSet()); |
| |
| return testInfos.stream() |
| .map( |
| info -> |
| info.withUpdatedParameterNames( |
| (parameter, parameterIndex) -> |
| parameterIndicesThatNeedUpdate.contains(parameterIndex) |
| ? getShortenedName( |
| parameter, |
| getMaxCharactersPerParameter(info, numberOfParameters)) |
| : info.getParameters().get(parameterIndex).getName())) |
| .collect(toImmutableList()); |
| } |
| } else { |
| return ImmutableList.copyOf(testInfos); |
| } |
| } |
| |
| private static int getMaxCharactersPerParameter(TestInfo testInfo, int numberOfParameters) { |
| int maxLengthOfAllParameters = |
| // Subtract 2 characters for square brackets |
| MAX_TEST_NAME_LENGTH - testInfo.getMethod().getName().length() - 2; |
| return min( |
| // Subtract 4 characters to leave place for joining commas and the parameter index. |
| maxLengthOfAllParameters / numberOfParameters - 4, |
| // Subtract 3 characters to leave place for the parameter index |
| MAX_PARAMETER_NAME_LENGTH - 3); |
| } |
| |
| static ImmutableList<TestInfo> deduplicateTestNames(List<TestInfo> testInfos) { |
| long uniqueTestNameCount = testInfos.stream().map(TestInfo::getName).distinct().count(); |
| if (testInfos.size() == uniqueTestNameCount) { |
| // Return early if there are no duplicates |
| return ImmutableList.copyOf(testInfos); |
| } else { |
| return deduplicateWithNumberPrefixes(maybeAddTypesIfDuplicate(testInfos)); |
| } |
| } |
| |
| private static String getShortenedName( |
| TestInfoParameter parameter, int maxCharactersPerParameter) { |
| if (maxCharactersPerParameter < 4) { |
| // Not enough characters for "..." suffix |
| return String.valueOf(parameter.getIndexInValueSource() + 1); |
| } else { |
| String shortenedName = |
| parameter.getName().length() > maxCharactersPerParameter |
| ? parameter.getName().substring(0, maxCharactersPerParameter - 3) + "..." |
| : parameter.getName(); |
| return String.format("%s.%s", parameter.getIndexInValueSource() + 1, shortenedName); |
| } |
| } |
| |
| private static ImmutableList<TestInfo> maybeAddTypesIfDuplicate(List<TestInfo> testInfos) { |
| Multimap<String, TestInfo> testNameToInfo = |
| MultimapBuilder.linkedHashKeys().arrayListValues().build(); |
| for (TestInfo testInfo : testInfos) { |
| testNameToInfo.put(testInfo.getName(), testInfo); |
| } |
| |
| return testNameToInfo.keySet().stream() |
| .flatMap( |
| testName -> { |
| Collection<TestInfo> matchedInfos = testNameToInfo.get(testName); |
| if (matchedInfos.size() == 1) { |
| // There was only one method with this name, so no deduplication is necessary |
| return matchedInfos.stream(); |
| } else { |
| // Found tests with duplicate test names |
| int numParameters = matchedInfos.iterator().next().getParameters().size(); |
| Set<Integer> indicesThatShouldGetSuffix = |
| // Find parameter indices for which a suffix would allow the reader to |
| // differentiate |
| IntStream.range(0, numParameters) |
| .filter( |
| parameterIndex -> |
| matchedInfos.stream() |
| .map( |
| info -> |
| getTypeSuffix( |
| info.getParameters() |
| .get(parameterIndex) |
| .getValue())) |
| .distinct() |
| .count() |
| > 1) |
| .boxed() |
| .collect(toSet()); |
| |
| return matchedInfos.stream() |
| .map( |
| testInfo -> |
| testInfo.withUpdatedParameterNames( |
| (parameter, parameterIndex) -> |
| indicesThatShouldGetSuffix.contains(parameterIndex) |
| ? parameter.getName() + getTypeSuffix(parameter.getValue()) |
| : parameter.getName())); |
| } |
| }) |
| .collect(toImmutableList()); |
| } |
| |
| private static String getTypeSuffix(@Nullable Object value) { |
| if (value == null) { |
| return " (null reference)"; |
| } else { |
| return String.format(" (%s)", value.getClass().getSimpleName()); |
| } |
| } |
| |
| private static ImmutableList<TestInfo> deduplicateWithNumberPrefixes( |
| ImmutableList<TestInfo> testInfos) { |
| long uniqueTestNameCount = testInfos.stream().map(TestInfo::getName).distinct().count(); |
| if (testInfos.size() == uniqueTestNameCount) { |
| return ImmutableList.copyOf(testInfos); |
| } else { |
| // There are still duplicates, even after adding type suffixes. As a last resort: add a |
| // counter to all parameters to guarantee that each case is unique. |
| return testInfos.stream() |
| .map( |
| testInfo -> |
| testInfo.withUpdatedParameterNames( |
| (parameter, parameterIndex) -> |
| String.format( |
| "%s.%s", parameter.getIndexInValueSource() + 1, parameter.getName()))) |
| .collect(toImmutableList()); |
| } |
| } |
| |
| private static <E> Collector<E, ?, ImmutableList<E>> toImmutableList() { |
| return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf); |
| } |
| |
| @AutoValue |
| abstract static class TestInfoParameter { |
| |
| abstract String getName(); |
| |
| @Nullable |
| abstract Object getValue(); |
| |
| /** |
| * The index of this parameter value in the list of all values provided by the provider that |
| * returned this value. |
| */ |
| abstract int getIndexInValueSource(); |
| |
| TestInfoParameter withName(String newName) { |
| return create(newName, getValue(), getIndexInValueSource()); |
| } |
| |
| static TestInfoParameter create(String name, @Nullable Object value, int indexInValueSource) { |
| checkArgument(indexInValueSource >= 0); |
| return new AutoValue_TestInfo_TestInfoParameter( |
| checkNotNull(name), value, indexInValueSource); |
| } |
| } |
| } |