blob: 6d43808c80958ccefba73e53d7fd0d98902dbf08 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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.android.bedstead.harrier;
import androidx.annotation.Nullable;
import com.android.bedstead.harrier.annotations.AnnotationRunPrecedence;
import com.android.bedstead.harrier.annotations.enterprise.CanSetPolicyTest;
import com.android.bedstead.harrier.annotations.enterprise.CannotSetPolicyTest;
import com.android.bedstead.harrier.annotations.enterprise.EnterprisePolicy;
import com.android.bedstead.harrier.annotations.enterprise.NegativePolicyTest;
import com.android.bedstead.harrier.annotations.enterprise.PositivePolicyTest;
import com.android.bedstead.harrier.annotations.meta.ParameterizedAnnotation;
import com.android.bedstead.harrier.annotations.meta.RepeatingAnnotation;
import com.android.bedstead.harrier.annotations.parameterized.IncludeNone;
import com.android.bedstead.nene.exceptions.NeneException;
import com.google.common.base.Objects;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.TestClass;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* A JUnit test runner for use with Bedstead.
*/
public final class BedsteadJUnit4 extends BlockJUnit4ClassRunner {
private static final String BEDSTEAD_PACKAGE_NAME = "com.android.bedstead";
// These are annotations which are not included indirectly
private static final Set<String> sIgnoredAnnotationPackages = new HashSet<>();
static {
sIgnoredAnnotationPackages.add("java.lang.annotation");
sIgnoredAnnotationPackages.add("com.android.bedstead.harrier.annotations.meta");
sIgnoredAnnotationPackages.add("kotlin.*");
sIgnoredAnnotationPackages.add("org.junit");
}
private static int annotationSorter(Annotation a, Annotation b) {
return getAnnotationWeight(a) - getAnnotationWeight(b);
}
private static int getAnnotationWeight(Annotation a) {
if (!a.annotationType().getPackage().getName().startsWith(BEDSTEAD_PACKAGE_NAME)) {
return AnnotationRunPrecedence.FIRST;
}
try {
return (int) a.annotationType().getMethod("weight").invoke(a);
} catch (NoSuchMethodException e) {
// Default to PRECEDENCE_NOT_IMPORTANT if no weight is found on the annotation.
return AnnotationRunPrecedence.PRECEDENCE_NOT_IMPORTANT;
} catch (IllegalAccessException | InvocationTargetException e) {
throw new NeneException("Failed to invoke weight on this annotation: " + a, e);
}
}
/**
* {@link FrameworkMethod} subclass which allows modifying the test name and annotations.
*/
public static final class BedsteadFrameworkMethod extends FrameworkMethod {
private final Class<? extends Annotation> mParameterizedAnnotation;
private final Map<Class<? extends Annotation>, Annotation> mAnnotationsMap =
new HashMap<>();
private Annotation[] mAnnotations;
public BedsteadFrameworkMethod(Method method) {
this(method, /* parameterizedAnnotation= */ null);
}
public BedsteadFrameworkMethod(Method method, Annotation parameterizedAnnotation) {
super(method);
this.mParameterizedAnnotation = (parameterizedAnnotation == null) ? null
: parameterizedAnnotation.annotationType();
calculateAnnotations();
}
private void calculateAnnotations() {
List<Annotation> annotations =
new ArrayList<>(Arrays.asList(getDeclaringClass().getAnnotations()));
annotations.sort(BedsteadJUnit4::annotationSorter);
annotations.addAll(Arrays.stream(getMethod().getAnnotations())
.sorted(BedsteadJUnit4::annotationSorter)
.collect(Collectors.toList()));
parseEnterpriseAnnotations(annotations);
resolveRecursiveAnnotations(annotations, mParameterizedAnnotation);
this.mAnnotations = annotations.toArray(new Annotation[0]);
for (Annotation annotation : annotations) {
mAnnotationsMap.put(annotation.annotationType(), annotation);
}
}
@Override
public String getName() {
if (mParameterizedAnnotation == null) {
return super.getName();
}
return super.getName() + "[" + mParameterizedAnnotation.getSimpleName() + "]";
}
@Override
public boolean equals(Object obj) {
if (!super.equals(obj)) {
return false;
}
if (!(obj instanceof BedsteadFrameworkMethod)) {
return false;
}
BedsteadFrameworkMethod other = (BedsteadFrameworkMethod) obj;
return Objects.equal(mParameterizedAnnotation, other.mParameterizedAnnotation);
}
@Override
public Annotation[] getAnnotations() {
return mAnnotations;
}
@Override
public <T extends Annotation> T getAnnotation(Class<T> annotationType) {
return (T) mAnnotationsMap.get(annotationType);
}
}
/**
* Resolve annotations recursively.
*
* @param parameterizedAnnotation The class of the parameterized annotation to expand, if any
*/
public static void resolveRecursiveAnnotations(List<Annotation> annotations,
@Nullable Class<? extends Annotation> parameterizedAnnotation) {
int index = 0;
while (index < annotations.size()) {
Annotation annotation = annotations.get(index);
annotations.remove(index);
List<Annotation> replacementAnnotations =
getReplacementAnnotations(annotation, parameterizedAnnotation);
replacementAnnotations.sort(BedsteadJUnit4::annotationSorter);
annotations.addAll(index, replacementAnnotations);
index += replacementAnnotations.size();
}
}
private static List<Annotation> getReplacementAnnotations(Annotation annotation,
@Nullable Class<? extends Annotation> parameterizedAnnotation) {
List<Annotation> replacementAnnotations = new ArrayList<>();
if (annotation.annotationType().getAnnotation(RepeatingAnnotation.class) != null) {
try {
Annotation[] annotations =
(Annotation[]) annotation.annotationType()
.getMethod("value").invoke(annotation);
Collections.addAll(replacementAnnotations, annotations);
return replacementAnnotations;
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new NeneException("Error expanding repeated annotations", e);
}
}
if (annotation.annotationType().getAnnotation(ParameterizedAnnotation.class) != null
&& !annotation.annotationType().equals(parameterizedAnnotation)) {
return replacementAnnotations;
}
for (Annotation indirectAnnotation : annotation.annotationType().getAnnotations()) {
String annotationPackage = indirectAnnotation.annotationType().getPackage().getName();
if (shouldSkipAnnotation(annotationPackage)) {
continue;
}
replacementAnnotations.addAll(getReplacementAnnotations(
indirectAnnotation, parameterizedAnnotation));
}
replacementAnnotations.add(annotation);
return replacementAnnotations;
}
private static boolean shouldSkipAnnotation(String annotationPackage) {
for (String ignoredPackage : sIgnoredAnnotationPackages) {
if (ignoredPackage.endsWith(".*")) {
if (annotationPackage.startsWith(
ignoredPackage.substring(0, ignoredPackage.length() - 2))) {
return true;
}
} else if (annotationPackage.equals(ignoredPackage)) {
return true;
}
}
return false;
}
public BedsteadJUnit4(Class<?> testClass) throws InitializationError {
super(testClass);
}
@Override
protected List<FrameworkMethod> computeTestMethods() {
TestClass testClass = getTestClass();
List<FrameworkMethod> basicTests = testClass.getAnnotatedMethods(Test.class);
List<FrameworkMethod> modifiedTests = new ArrayList<>();
for (FrameworkMethod m : basicTests) {
Set<Annotation> parameterizedAnnotations = getParameterizedAnnotations(m);
if (parameterizedAnnotations.isEmpty()) {
// Unparameterized, just add the original
modifiedTests.add(new BedsteadFrameworkMethod(m.getMethod()));
}
for (Annotation annotation : parameterizedAnnotations) {
if (annotation.annotationType().equals(IncludeNone.class)) {
// Special case - does not generate a run
continue;
}
modifiedTests.add(
new BedsteadFrameworkMethod(m.getMethod(), annotation));
}
}
sortMethodsByBedsteadAnnotations(modifiedTests);
return modifiedTests;
}
/**
* Sort methods so that methods with identical bedstead annotations are together.
*
* <p>This will also ensure that all tests methods which are not annotated for bedstead will
* run before any tests which are annotated.
*/
private void sortMethodsByBedsteadAnnotations(List<FrameworkMethod> modifiedTests) {
List<Annotation> bedsteadAnnotationsSortedByMostCommon =
bedsteadAnnotationsSortedByMostCommon(modifiedTests);
modifiedTests.sort((o1, o2) -> {
for (Annotation annotation : bedsteadAnnotationsSortedByMostCommon) {
boolean o1HasAnnotation = o1.getAnnotation(annotation.annotationType()) != null;
boolean o2HasAnnotation = o2.getAnnotation(annotation.annotationType()) != null;
if (o1HasAnnotation && !o2HasAnnotation) {
// o1 goes to the end
return 1;
} else if (o2HasAnnotation && !o1HasAnnotation) {
return -1;
}
}
return 0;
});
}
private List<Annotation> bedsteadAnnotationsSortedByMostCommon(List<FrameworkMethod> methods) {
Map<Annotation, Integer> annotationCounts = countAnnotations(methods);
List<Annotation> annotations = new ArrayList<>(annotationCounts.keySet());
annotations.removeIf(
annotation ->
!annotation.annotationType()
.getCanonicalName().contains(BEDSTEAD_PACKAGE_NAME));
annotations.sort(Comparator.comparingInt(annotationCounts::get));
Collections.reverse(annotations);
return annotations;
}
private Map<Annotation, Integer> countAnnotations(List<FrameworkMethod> methods) {
Map<Annotation, Integer> annotationCounts = new HashMap<>();
for (FrameworkMethod method : methods) {
for (Annotation annotation : method.getAnnotations()) {
annotationCounts.put(
annotation, annotationCounts.getOrDefault(annotation, 0) + 1);
}
}
return annotationCounts;
}
private Set<Annotation> getParameterizedAnnotations(FrameworkMethod method) {
Set<Annotation> parameterizedAnnotations = new HashSet<>();
List<Annotation> annotations = new ArrayList<>(Arrays.asList(method.getAnnotations()));
// TODO(scottjonathan): We're doing this twice... does it matter?
parseEnterpriseAnnotations(annotations);
for (Annotation annotation : annotations) {
if (annotation.annotationType().getAnnotation(ParameterizedAnnotation.class) != null) {
parameterizedAnnotations.add(annotation);
}
}
return parameterizedAnnotations;
}
/**
* Parse enterprise-specific annotations.
*
* <p>To be used before general annotation processing.
*/
private static void parseEnterpriseAnnotations(List<Annotation> annotations) {
int index = 0;
while (index < annotations.size()) {
Annotation annotation = annotations.get(index);
if (annotation instanceof PositivePolicyTest) {
annotations.remove(index);
Class<?> policy = ((PositivePolicyTest) annotation).policy();
EnterprisePolicy enterprisePolicy =
policy.getAnnotation(EnterprisePolicy.class);
List<Annotation> replacementAnnotations =
Policy.positiveStates(policy.getName(), enterprisePolicy);
replacementAnnotations.sort(BedsteadJUnit4::annotationSorter);
annotations.addAll(index, replacementAnnotations);
index += replacementAnnotations.size();
} else if (annotation instanceof NegativePolicyTest) {
annotations.remove(index);
Class<?> policy = ((NegativePolicyTest) annotation).policy();
EnterprisePolicy enterprisePolicy =
policy.getAnnotation(EnterprisePolicy.class);
List<Annotation> replacementAnnotations =
Policy.negativeStates(policy.getName(), enterprisePolicy);
replacementAnnotations.sort(BedsteadJUnit4::annotationSorter);
annotations.addAll(index, replacementAnnotations);
index += replacementAnnotations.size();
} else if (annotation instanceof CannotSetPolicyTest) {
annotations.remove(index);
Class<?> policy = ((CannotSetPolicyTest) annotation).policy();
EnterprisePolicy enterprisePolicy =
policy.getAnnotation(EnterprisePolicy.class);
List<Annotation> replacementAnnotations =
Policy.cannotSetPolicyStates(policy.getName(), enterprisePolicy);
replacementAnnotations.sort(BedsteadJUnit4::annotationSorter);
annotations.addAll(index, replacementAnnotations);
index += replacementAnnotations.size();
} else if (annotation instanceof CanSetPolicyTest) {
annotations.remove(index);
Class<?> policy = ((CanSetPolicyTest) annotation).policy();
boolean singleTestOnly = ((CanSetPolicyTest) annotation).singleTestOnly();
EnterprisePolicy enterprisePolicy =
policy.getAnnotation(EnterprisePolicy.class);
List<Annotation> replacementAnnotations =
Policy.canSetPolicyStates(
policy.getName(), enterprisePolicy, singleTestOnly);
replacementAnnotations.sort(BedsteadJUnit4::annotationSorter);
annotations.addAll(index, replacementAnnotations);
index += replacementAnnotations.size();
} else {
index++;
}
}
}
@Override
protected List<TestRule> classRules() {
List<TestRule> rules = super.classRules();
for (TestRule rule : rules) {
if (rule instanceof DeviceState) {
DeviceState deviceState = (DeviceState) rule;
deviceState.setSkipTestTeardown(true);
deviceState.setUsingBedsteadJUnit4(true);
break;
}
}
return rules;
}
}