1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.bedstead.harrier;
18 
19 import android.os.Bundle;
20 
21 import androidx.annotation.Nullable;
22 import androidx.test.platform.app.InstrumentationRegistry;
23 
24 import com.android.bedstead.harrier.annotations.AnnotationRunPrecedence;
25 import com.android.bedstead.harrier.annotations.enterprise.CanSetPolicyTest;
26 import com.android.bedstead.harrier.annotations.enterprise.CannotSetPolicyTest;
27 import com.android.bedstead.harrier.annotations.enterprise.EnterprisePolicy;
28 import com.android.bedstead.harrier.annotations.enterprise.NegativePolicyTest;
29 import com.android.bedstead.harrier.annotations.enterprise.PositivePolicyTest;
30 import com.android.bedstead.harrier.annotations.meta.ParameterizedAnnotation;
31 import com.android.bedstead.harrier.annotations.meta.RepeatingAnnotation;
32 import com.android.bedstead.harrier.annotations.parameterized.IncludeNone;
33 import com.android.bedstead.nene.exceptions.NeneException;
34 
35 import com.google.common.base.Objects;
36 
37 import org.junit.Test;
38 import org.junit.rules.TestRule;
39 import org.junit.runners.BlockJUnit4ClassRunner;
40 import org.junit.runners.model.FrameworkMethod;
41 import org.junit.runners.model.InitializationError;
42 import org.junit.runners.model.TestClass;
43 
44 import java.lang.annotation.Annotation;
45 import java.lang.reflect.InvocationTargetException;
46 import java.lang.reflect.Method;
47 import java.util.ArrayList;
48 import java.util.Arrays;
49 import java.util.Collections;
50 import java.util.Comparator;
51 import java.util.HashMap;
52 import java.util.HashSet;
53 import java.util.List;
54 import java.util.Map;
55 import java.util.Set;
56 import java.util.stream.Collectors;
57 
58 /**
59  * A JUnit test runner for use with Bedstead.
60  */
61 public final class BedsteadJUnit4 extends BlockJUnit4ClassRunner {
62 
63     private static final String BEDSTEAD_PACKAGE_NAME = "com.android.bedstead";
64 
65     // These are annotations which are not included indirectly
66     private static final Set<String> sIgnoredAnnotationPackages = new HashSet<>();
67     static {
68         sIgnoredAnnotationPackages.add("java.lang.annotation");
69         sIgnoredAnnotationPackages.add("com.android.bedstead.harrier.annotations.meta");
70         sIgnoredAnnotationPackages.add("kotlin.*");
71         sIgnoredAnnotationPackages.add("org.junit");
72     }
73 
annotationSorter(Annotation a, Annotation b)74     private static int annotationSorter(Annotation a, Annotation b) {
75         return getAnnotationWeight(a) - getAnnotationWeight(b);
76     }
77 
getAnnotationWeight(Annotation annotation)78     private static int getAnnotationWeight(Annotation annotation) {
79         if (annotation instanceof DynamicParameterizedAnnotation) {
80             // Special case, not important
81             return AnnotationRunPrecedence.PRECEDENCE_NOT_IMPORTANT;
82         }
83 
84         if (!annotation.annotationType().getPackage().getName().startsWith(BEDSTEAD_PACKAGE_NAME)) {
85             return AnnotationRunPrecedence.FIRST;
86         }
87 
88         try {
89             return (int) annotation.annotationType().getMethod("weight").invoke(annotation);
90         } catch (NoSuchMethodException e) {
91             // Default to PRECEDENCE_NOT_IMPORTANT if no weight is found on the annotation.
92             return AnnotationRunPrecedence.PRECEDENCE_NOT_IMPORTANT;
93         } catch (IllegalAccessException | InvocationTargetException e) {
94             throw new NeneException("Failed to invoke weight on this annotation: " + annotation, e);
95         }
96     }
97 
98     /**
99      * {@link FrameworkMethod} subclass which allows modifying the test name and annotations.
100      */
101     public static final class BedsteadFrameworkMethod extends FrameworkMethod {
102 
103         private final Annotation mParameterizedAnnotation;
104         private final Map<Class<? extends Annotation>, Annotation> mAnnotationsMap =
105                 new HashMap<>();
106         private Annotation[] mAnnotations;
107 
BedsteadFrameworkMethod(Method method)108         public BedsteadFrameworkMethod(Method method) {
109             this(method, /* parameterizedAnnotation= */ null);
110         }
111 
BedsteadFrameworkMethod(Method method, Annotation parameterizedAnnotation)112         public BedsteadFrameworkMethod(Method method, Annotation parameterizedAnnotation) {
113             super(method);
114             mParameterizedAnnotation = parameterizedAnnotation;
115 
116             calculateAnnotations();
117         }
118 
calculateAnnotations()119         private void calculateAnnotations() {
120             List<Annotation> annotations =
121                     new ArrayList<>(Arrays.asList(getDeclaringClass().getAnnotations()));
122             annotations.sort(BedsteadJUnit4::annotationSorter);
123 
124             annotations.addAll(Arrays.stream(getMethod().getAnnotations())
125                     .sorted(BedsteadJUnit4::annotationSorter)
126                     .collect(Collectors.toList()));
127 
128             parseEnterpriseAnnotations(annotations);
129 
130             resolveRecursiveAnnotations(annotations, mParameterizedAnnotation);
131 
132             this.mAnnotations = annotations.toArray(new Annotation[0]);
133             for (Annotation annotation : annotations) {
134                 if (annotation instanceof DynamicParameterizedAnnotation) {
135                     continue; // don't return this
136                 }
137                 mAnnotationsMap.put(annotation.annotationType(), annotation);
138             }
139         }
140 
141         @Override
getName()142         public String getName() {
143             if (mParameterizedAnnotation == null) {
144                 return super.getName();
145             }
146             return super.getName() + "[" + getParameterName(mParameterizedAnnotation) + "]";
147         }
148 
149         @Override
equals(Object obj)150         public boolean equals(Object obj) {
151             if (!super.equals(obj)) {
152                 return false;
153             }
154 
155             if (!(obj instanceof BedsteadFrameworkMethod)) {
156                 return false;
157             }
158 
159             BedsteadFrameworkMethod other = (BedsteadFrameworkMethod) obj;
160 
161             return Objects.equal(mParameterizedAnnotation, other.mParameterizedAnnotation);
162         }
163 
164         @Override
getAnnotations()165         public Annotation[] getAnnotations() {
166             return mAnnotations;
167         }
168 
169         @Override
getAnnotation(Class<T> annotationType)170         public <T extends Annotation> T getAnnotation(Class<T> annotationType) {
171             return (T) mAnnotationsMap.get(annotationType);
172         }
173     }
174 
getParameterName(Annotation annotation)175     private static String getParameterName(Annotation annotation) {
176         if (annotation instanceof DynamicParameterizedAnnotation) {
177             return ((DynamicParameterizedAnnotation) annotation).name();
178         }
179         return annotation.annotationType().getSimpleName();
180     }
181 
182     /**
183      * Resolve annotations recursively.
184      *
185      * @param parameterizedAnnotation The class of the parameterized annotation to expand, if any
186      */
resolveRecursiveAnnotations(List<Annotation> annotations, @Nullable Annotation parameterizedAnnotation)187     public static void resolveRecursiveAnnotations(List<Annotation> annotations,
188             @Nullable Annotation parameterizedAnnotation) {
189         int index = 0;
190         while (index < annotations.size()) {
191             Annotation annotation = annotations.get(index);
192             annotations.remove(index);
193             List<Annotation> replacementAnnotations =
194                     getReplacementAnnotations(annotation, parameterizedAnnotation);
195             replacementAnnotations.sort(BedsteadJUnit4::annotationSorter);
196             annotations.addAll(index, replacementAnnotations);
197             index += replacementAnnotations.size();
198         }
199     }
200 
isParameterizedAnnotation(Annotation annotation)201     private static boolean isParameterizedAnnotation(Annotation annotation) {
202         if (annotation instanceof DynamicParameterizedAnnotation) {
203             return true;
204         }
205 
206         return annotation.annotationType().getAnnotation(ParameterizedAnnotation.class) != null;
207     }
208 
getIndirectAnnotations(Annotation annotation)209     private static Annotation[] getIndirectAnnotations(Annotation annotation) {
210         if (annotation instanceof DynamicParameterizedAnnotation) {
211             return ((DynamicParameterizedAnnotation) annotation).annotations();
212         }
213         return annotation.annotationType().getAnnotations();
214     }
215 
isRepeatingAnnotation(Annotation annotation)216     private static boolean isRepeatingAnnotation(Annotation annotation) {
217         if (annotation instanceof DynamicParameterizedAnnotation) {
218             return false;
219         }
220 
221         return annotation.annotationType().getAnnotation(RepeatingAnnotation.class) != null;
222     }
223 
getReplacementAnnotations(Annotation annotation, @Nullable Annotation parameterizedAnnotation)224     private static List<Annotation> getReplacementAnnotations(Annotation annotation,
225             @Nullable Annotation parameterizedAnnotation) {
226         List<Annotation> replacementAnnotations = new ArrayList<>();
227 
228         if (isRepeatingAnnotation(annotation)) {
229             try {
230                 Annotation[] annotations =
231                         (Annotation[]) annotation.annotationType()
232                                 .getMethod("value").invoke(annotation);
233                 Collections.addAll(replacementAnnotations, annotations);
234                 return replacementAnnotations;
235             } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
236                 throw new NeneException("Error expanding repeated annotations", e);
237             }
238         }
239 
240         if (isParameterizedAnnotation(annotation) && !annotation.equals(parameterizedAnnotation)) {
241             return replacementAnnotations;
242         }
243 
244         for (Annotation indirectAnnotation : getIndirectAnnotations(annotation)) {
245             if (shouldSkipAnnotation(annotation)) {
246                 continue;
247             }
248 
249             replacementAnnotations.addAll(getReplacementAnnotations(
250                     indirectAnnotation, parameterizedAnnotation));
251         }
252 
253         if (!(annotation instanceof DynamicParameterizedAnnotation)) {
254             // We drop the fake annotation once it's replaced
255             replacementAnnotations.add(annotation);
256         }
257 
258         return replacementAnnotations;
259     }
260 
shouldSkipAnnotation(Annotation annotation)261     private static boolean shouldSkipAnnotation(Annotation annotation) {
262         if (annotation instanceof DynamicParameterizedAnnotation) {
263             return false;
264         }
265 
266         String annotationPackage = annotation.annotationType().getPackage().getName();
267 
268         for (String ignoredPackage : sIgnoredAnnotationPackages) {
269             if (ignoredPackage.endsWith(".*")) {
270                 if (annotationPackage.startsWith(
271                     ignoredPackage.substring(0, ignoredPackage.length() - 2))) {
272                     return true;
273                 }
274             } else if (annotationPackage.equals(ignoredPackage)) {
275                 return true;
276             }
277         }
278 
279         return false;
280     }
281 
BedsteadJUnit4(Class<?> testClass)282     public BedsteadJUnit4(Class<?> testClass) throws InitializationError {
283         super(testClass);
284     }
285 
annotationShouldBeSkipped(Annotation annotation)286     private boolean annotationShouldBeSkipped(Annotation annotation) {
287         if (annotation instanceof DynamicParameterizedAnnotation) {
288             return false;
289         }
290 
291         return annotation.annotationType().equals(IncludeNone.class);
292     }
293 
294     @Override
computeTestMethods()295     protected List<FrameworkMethod> computeTestMethods() {
296         TestClass testClass = getTestClass();
297 
298         List<FrameworkMethod> basicTests = testClass.getAnnotatedMethods(Test.class);
299         List<FrameworkMethod> modifiedTests = new ArrayList<>();
300 
301         for (FrameworkMethod m : basicTests) {
302             Set<Annotation> parameterizedAnnotations = getParameterizedAnnotations(m);
303 
304             if (parameterizedAnnotations.isEmpty()) {
305                 // Unparameterized, just add the original
306                 modifiedTests.add(new BedsteadFrameworkMethod(m.getMethod()));
307             }
308 
309             for (Annotation annotation : parameterizedAnnotations) {
310                 if (annotationShouldBeSkipped(annotation)) {
311                     // Special case - does not generate a run
312                     continue;
313                 }
314                 modifiedTests.add(
315                         new BedsteadFrameworkMethod(m.getMethod(), annotation));
316             }
317         }
318 
319         sortMethodsByBedsteadAnnotations(modifiedTests);
320 
321         return modifiedTests;
322     }
323 
324     /**
325      * Sort methods so that methods with identical bedstead annotations are together.
326      *
327      * <p>This will also ensure that all tests methods which are not annotated for bedstead will
328      * run before any tests which are annotated.
329      */
sortMethodsByBedsteadAnnotations(List<FrameworkMethod> modifiedTests)330     private void sortMethodsByBedsteadAnnotations(List<FrameworkMethod> modifiedTests) {
331         List<Annotation> bedsteadAnnotationsSortedByMostCommon =
332                 bedsteadAnnotationsSortedByMostCommon(modifiedTests);
333 
334         modifiedTests.sort((o1, o2) -> {
335             for (Annotation annotation : bedsteadAnnotationsSortedByMostCommon) {
336                 boolean o1HasAnnotation = o1.getAnnotation(annotation.annotationType()) != null;
337                 boolean o2HasAnnotation = o2.getAnnotation(annotation.annotationType()) != null;
338 
339                 if (o1HasAnnotation && !o2HasAnnotation) {
340                     // o1 goes to the end
341                     return 1;
342                 } else if (o2HasAnnotation && !o1HasAnnotation) {
343                     return -1;
344                 }
345             }
346             return 0;
347         });
348     }
349 
bedsteadAnnotationsSortedByMostCommon(List<FrameworkMethod> methods)350     private List<Annotation> bedsteadAnnotationsSortedByMostCommon(List<FrameworkMethod> methods) {
351         Map<Annotation, Integer> annotationCounts = countAnnotations(methods);
352         List<Annotation> annotations = new ArrayList<>(annotationCounts.keySet());
353 
354         annotations.removeIf(
355                 annotation ->
356                         !annotation.annotationType()
357                                 .getCanonicalName().contains(BEDSTEAD_PACKAGE_NAME));
358 
359         annotations.sort(Comparator.comparingInt(annotationCounts::get));
360         Collections.reverse(annotations);
361 
362         return annotations;
363     }
364 
countAnnotations(List<FrameworkMethod> methods)365     private Map<Annotation, Integer> countAnnotations(List<FrameworkMethod> methods) {
366         Map<Annotation, Integer> annotationCounts = new HashMap<>();
367 
368         for (FrameworkMethod method : methods) {
369             for (Annotation annotation : method.getAnnotations()) {
370                 annotationCounts.put(
371                         annotation, annotationCounts.getOrDefault(annotation, 0) + 1);
372             }
373         }
374 
375         return annotationCounts;
376     }
377 
getParameterizedAnnotations(FrameworkMethod method)378     private Set<Annotation> getParameterizedAnnotations(FrameworkMethod method) {
379         Set<Annotation> parameterizedAnnotations = new HashSet<>();
380         List<Annotation> annotations = new ArrayList<>(Arrays.asList(method.getAnnotations()));
381 
382         // TODO(scottjonathan): We're doing this twice... does it matter?
383         parseEnterpriseAnnotations(annotations);
384 
385         for (Annotation annotation : annotations) {
386             if (isParameterizedAnnotation(annotation)) {
387                 parameterizedAnnotations.add(annotation);
388             }
389         }
390 
391         return parameterizedAnnotations;
392     }
393 
394     /**
395      * Parse enterprise-specific annotations.
396      *
397      * <p>To be used before general annotation processing.
398      */
parseEnterpriseAnnotations(List<Annotation> annotations)399     private static void parseEnterpriseAnnotations(List<Annotation> annotations) {
400         int index = 0;
401         while (index < annotations.size()) {
402             Annotation annotation = annotations.get(index);
403             if (annotation instanceof PositivePolicyTest) {
404                 annotations.remove(index);
405                 Class<?> policy = ((PositivePolicyTest) annotation).policy();
406 
407                 EnterprisePolicy enterprisePolicy =
408                         policy.getAnnotation(EnterprisePolicy.class);
409                 List<Annotation> replacementAnnotations =
410                         Policy.positiveStates(policy.getName(), enterprisePolicy);
411                 replacementAnnotations.sort(BedsteadJUnit4::annotationSorter);
412 
413                 annotations.addAll(index, replacementAnnotations);
414                 index += replacementAnnotations.size();
415             } else if (annotation instanceof NegativePolicyTest) {
416                 annotations.remove(index);
417                 Class<?> policy = ((NegativePolicyTest) annotation).policy();
418 
419                 EnterprisePolicy enterprisePolicy =
420                         policy.getAnnotation(EnterprisePolicy.class);
421                 List<Annotation> replacementAnnotations =
422                         Policy.negativeStates(policy.getName(), enterprisePolicy);
423                 replacementAnnotations.sort(BedsteadJUnit4::annotationSorter);
424 
425                 annotations.addAll(index, replacementAnnotations);
426                 index += replacementAnnotations.size();
427             } else if (annotation instanceof CannotSetPolicyTest) {
428                 annotations.remove(index);
429                 Class<?> policy = ((CannotSetPolicyTest) annotation).policy();
430 
431                 EnterprisePolicy enterprisePolicy =
432                         policy.getAnnotation(EnterprisePolicy.class);
433                 List<Annotation> replacementAnnotations =
434                         Policy.cannotSetPolicyStates(policy.getName(), enterprisePolicy, ((CannotSetPolicyTest) annotation).includeDeviceAdminStates(), ((CannotSetPolicyTest) annotation).includeNonDeviceAdminStates());
435                 replacementAnnotations.sort(BedsteadJUnit4::annotationSorter);
436 
437                 annotations.addAll(index, replacementAnnotations);
438                 index += replacementAnnotations.size();
439             } else if (annotation instanceof CanSetPolicyTest) {
440                 annotations.remove(index);
441                 Class<?> policy = ((CanSetPolicyTest) annotation).policy();
442                 boolean singleTestOnly = ((CanSetPolicyTest) annotation).singleTestOnly();
443 
444                 EnterprisePolicy enterprisePolicy =
445                         policy.getAnnotation(EnterprisePolicy.class);
446                 List<Annotation> replacementAnnotations =
447                         Policy.canSetPolicyStates(
448                                 policy.getName(), enterprisePolicy, singleTestOnly);
449                 replacementAnnotations.sort(BedsteadJUnit4::annotationSorter);
450 
451                 annotations.addAll(index, replacementAnnotations);
452                 index += replacementAnnotations.size();
453             } else {
454                 index++;
455             }
456         }
457     }
458 
459     @Override
classRules()460     protected List<TestRule> classRules() {
461         List<TestRule> rules = super.classRules();
462 
463         for (TestRule rule : rules) {
464             if (rule instanceof DeviceState) {
465                 DeviceState deviceState = (DeviceState) rule;
466 
467                 deviceState.setSkipTestTeardown(true);
468                 deviceState.setUsingBedsteadJUnit4(true);
469 
470                 break;
471             }
472         }
473 
474         return rules;
475     }
476 
477     /**
478      * True if the test is running in debug mode.
479      *
480      * <p>This will result in additional debugging information being added which would otherwise
481      * be dropped to improve test performance.
482      *
483      * <p>To enable this, pass the "bedstead-debug" instrumentation arg as "true"
484      */
isDebug()485     public static boolean isDebug() {
486         Bundle arguments = InstrumentationRegistry.getArguments();
487         return Boolean.parseBoolean(arguments.getString("bedstead-debug", "false"));
488     }
489 }
490