blob: 7d16b6e669a3583aa960128e8b79564ccf7a05c0 [file] [log] [blame]
/*
* 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);
}
}
}