xref: /aosp_15_r20/cts/common/device-side/bedstead/harrier/src/main/java/com/android/bedstead/harrier/BedsteadJUnit4.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1 /*
2  * Copyright (C) 2023 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 static com.android.bedstead.permissions.annotations.EnsureDoesNotHavePermissionKt.ensureDoesNotHavePermission;
20 import static com.android.bedstead.permissions.annotations.EnsureHasPermissionKt.ensureHasPermission;
21 
22 import com.android.bedstead.enterprise.annotations.CanSetPolicyTest;
23 import com.android.bedstead.enterprise.annotations.CannotSetPolicyTest;
24 import com.android.bedstead.enterprise.annotations.EnsureHasWorkProfile;
25 import com.android.bedstead.enterprise.annotations.MostImportantCoexistenceTest;
26 import com.android.bedstead.enterprise.annotations.MostRestrictiveCoexistenceTest;
27 import com.android.bedstead.enterprise.annotations.PolicyAppliesTest;
28 import com.android.bedstead.enterprise.annotations.PolicyDoesNotApplyTest;
29 import com.android.bedstead.enterprise.annotations.RequireRunOnWorkProfile;
30 import com.android.bedstead.harrier.annotations.AnnotationCostRunPrecedence;
31 import com.android.bedstead.harrier.annotations.AnnotationPriorityRunPrecedence;
32 import com.android.bedstead.harrier.annotations.CrossUserTest;
33 import com.android.bedstead.harrier.annotations.EnumTestParameter;
34 import com.android.bedstead.harrier.annotations.HiddenApiTest;
35 import com.android.bedstead.harrier.annotations.IntTestParameter;
36 import com.android.bedstead.harrier.annotations.PermissionTest;
37 import com.android.bedstead.harrier.annotations.PolicyArgument;
38 import com.android.bedstead.harrier.annotations.RequireRunOnInitialUser;
39 import com.android.bedstead.harrier.annotations.StringTestParameter;
40 import com.android.bedstead.harrier.annotations.UserPair;
41 import com.android.bedstead.harrier.annotations.UserTest;
42 import com.android.bedstead.harrier.annotations.meta.ParameterizedAnnotation;
43 import com.android.bedstead.harrier.annotations.meta.RepeatingAnnotation;
44 import com.android.bedstead.harrier.annotations.parameterized.IncludeNone;
45 import com.android.bedstead.harrier.exceptions.RestartTestException;
46 import com.android.bedstead.multiuser.annotations.EnsureHasAdditionalUser;
47 import com.android.bedstead.multiuser.annotations.EnsureHasCloneProfile;
48 import com.android.bedstead.multiuser.annotations.EnsureHasPrivateProfile;
49 import com.android.bedstead.multiuser.annotations.EnsureHasSecondaryUser;
50 import com.android.bedstead.multiuser.annotations.EnsureHasTvProfile;
51 import com.android.bedstead.multiuser.annotations.OtherUser;
52 import com.android.bedstead.multiuser.annotations.RequireNotHeadlessSystemUserMode;
53 import com.android.bedstead.multiuser.annotations.RequireRunOnAdditionalUser;
54 import com.android.bedstead.multiuser.annotations.RequireRunOnCloneProfile;
55 import com.android.bedstead.multiuser.annotations.RequireRunOnPrimaryUser;
56 import com.android.bedstead.multiuser.annotations.RequireRunOnPrivateProfile;
57 import com.android.bedstead.multiuser.annotations.RequireRunOnSecondaryUser;
58 import com.android.bedstead.multiuser.annotations.RequireRunOnSystemUser;
59 import com.android.bedstead.multiuser.annotations.RequireRunOnTvProfile;
60 import com.android.bedstead.nene.exceptions.NeneException;
61 import com.android.bedstead.nene.types.OptionalBoolean;
62 import com.android.bedstead.performanceanalyzer.annotations.PerformanceTest;
63 import com.android.queryable.annotations.Query;
64 
65 import com.google.auto.value.AutoAnnotation;
66 import com.google.common.collect.ImmutableMap;
67 
68 import org.junit.Test;
69 import org.junit.rules.TestRule;
70 import org.junit.runner.Description;
71 import org.junit.runner.notification.RunNotifier;
72 import org.junit.runners.BlockJUnit4ClassRunner;
73 import org.junit.runners.model.FrameworkMethod;
74 import org.junit.runners.model.InitializationError;
75 import org.junit.runners.model.Statement;
76 import org.junit.runners.model.TestClass;
77 
78 import java.lang.annotation.Annotation;
79 import java.lang.reflect.InvocationTargetException;
80 import java.lang.reflect.Parameter;
81 import java.util.ArrayList;
82 import java.util.Arrays;
83 import java.util.Collections;
84 import java.util.Comparator;
85 import java.util.HashMap;
86 import java.util.HashSet;
87 import java.util.List;
88 import java.util.Map;
89 import java.util.Objects;
90 import java.util.Set;
91 import java.util.function.BiFunction;
92 import java.util.stream.Collectors;
93 import java.util.stream.Stream;
94 
95 /**
96  * A JUnit test runner for use with Bedstead.
97  */
98 // Annotating this class with @Query as a workaround to add this as a data type to a field
99 // in annotations that are called upon by @AutoAnnotation (for e.g. EnsureHasWorkProfile).
100 // @AutoAnnotation is not able to set default value for a field with an annotated data type,
101 // so we try to pass the default value explicitly that is accessed via reflection through this
102 // class.
103 @SuppressWarnings("AndroidJdkLibsChecker")
104 @Query
105 public final class BedsteadJUnit4 extends BlockJUnit4ClassRunner {
106 
107     private static final Set<TestLifecycleListener> sLifecycleListeners = new HashSet<>();
108 
109     private static final Map<Annotation, Integer> ANNOTATION_COST_CACHE = new HashMap<>();
110     private static final Map<Annotation, Integer> ANNOTATION_PRIORITY_CACHE = new HashMap<>();
111 
112     private static final String LOG_TAG = "BedsteadJUnit4";
113     private boolean mHasManualHarrierRule = false;
114 
115     @AutoAnnotation
requireRunOnSystemUser()116     private static RequireRunOnSystemUser requireRunOnSystemUser() {
117         return new AutoAnnotation_BedsteadJUnit4_requireRunOnSystemUser();
118     }
119 
requireRunOnPrimaryUser()120     private static RequireRunOnPrimaryUser requireRunOnPrimaryUser() {
121         return requireRunOnPrimaryUser(OptionalBoolean.ANY);
122     }
123 
124     @AutoAnnotation
requireRunOnPrimaryUser(OptionalBoolean switchedToUser)125     private static RequireRunOnPrimaryUser requireRunOnPrimaryUser(OptionalBoolean switchedToUser) {
126         return new AutoAnnotation_BedsteadJUnit4_requireRunOnPrimaryUser(switchedToUser);
127     }
128 
requireRunOnSecondaryUser()129     private static RequireRunOnSecondaryUser requireRunOnSecondaryUser() {
130         return requireRunOnSecondaryUser(OptionalBoolean.ANY);
131     }
132 
133     @AutoAnnotation
requireRunOnSecondaryUser( OptionalBoolean switchedToUser)134     private static RequireRunOnSecondaryUser requireRunOnSecondaryUser(
135             OptionalBoolean switchedToUser) {
136         return new AutoAnnotation_BedsteadJUnit4_requireRunOnSecondaryUser(switchedToUser);
137     }
138 
139     @AutoAnnotation
requireRunOnAdditionalUser()140     private static RequireRunOnAdditionalUser requireRunOnAdditionalUser() {
141         return new AutoAnnotation_BedsteadJUnit4_requireRunOnAdditionalUser();
142     }
143 
144     @AutoAnnotation
requireRunOnWorkProfile(Query dpc)145     private static RequireRunOnWorkProfile requireRunOnWorkProfile(Query dpc) {
146         return new AutoAnnotation_BedsteadJUnit4_requireRunOnWorkProfile(dpc);
147     }
148 
149     @AutoAnnotation
requireRunOnTvProfile()150     private static RequireRunOnTvProfile requireRunOnTvProfile() {
151         return new AutoAnnotation_BedsteadJUnit4_requireRunOnTvProfile();
152     }
153 
154     @AutoAnnotation
requireRunOnCloneProfile()155     private static RequireRunOnCloneProfile requireRunOnCloneProfile() {
156         return new AutoAnnotation_BedsteadJUnit4_requireRunOnCloneProfile();
157     }
158 
159     @AutoAnnotation
requireRunOnPrivateProfile()160     private static RequireRunOnPrivateProfile requireRunOnPrivateProfile() {
161         return new AutoAnnotation_BedsteadJUnit4_requireRunOnPrivateProfile();
162     }
163 
164     @AutoAnnotation
requireRunOnInitialUser(OptionalBoolean switchedToUser)165     static RequireRunOnInitialUser requireRunOnInitialUser(OptionalBoolean switchedToUser) {
166         return new AutoAnnotation_BedsteadJUnit4_requireRunOnInitialUser(switchedToUser);
167     }
168 
requireRunOnInitialUser()169     static RequireRunOnInitialUser requireRunOnInitialUser() {
170         return requireRunOnInitialUser(OptionalBoolean.TRUE);
171     }
172 
173     @AutoAnnotation
ensureHasSecondaryUser()174     private static EnsureHasSecondaryUser ensureHasSecondaryUser() {
175         return new AutoAnnotation_BedsteadJUnit4_ensureHasSecondaryUser();
176     }
177 
178     @AutoAnnotation
ensureHasAdditionalUser()179     private static EnsureHasAdditionalUser ensureHasAdditionalUser() {
180         return new AutoAnnotation_BedsteadJUnit4_ensureHasAdditionalUser();
181     }
182 
183     @AutoAnnotation
ensureHasWorkProfile(Query dpc)184     private static EnsureHasWorkProfile ensureHasWorkProfile(Query dpc) {
185         return new AutoAnnotation_BedsteadJUnit4_ensureHasWorkProfile(dpc);
186     }
187 
188     @AutoAnnotation
ensureHasTvProfile()189     private static EnsureHasTvProfile ensureHasTvProfile() {
190         return new AutoAnnotation_BedsteadJUnit4_ensureHasTvProfile();
191     }
192 
193     @AutoAnnotation
ensureHasCloneProfile()194     private static EnsureHasCloneProfile ensureHasCloneProfile() {
195         return new AutoAnnotation_BedsteadJUnit4_ensureHasCloneProfile();
196     }
197 
198     @AutoAnnotation
ensureHasPrivateProfile()199     private static EnsureHasPrivateProfile ensureHasPrivateProfile() {
200         return new AutoAnnotation_BedsteadJUnit4_ensureHasPrivateProfile();
201     }
202 
203     @AutoAnnotation
otherUser(UserType value)204     private static OtherUser otherUser(UserType value) {
205         return new AutoAnnotation_BedsteadJUnit4_otherUser(value);
206     }
207 
208     @AutoAnnotation
requireNotHeadlessSystemUserMode(String reason)209     private static RequireNotHeadlessSystemUserMode requireNotHeadlessSystemUserMode(String reason) {
210         return new AutoAnnotation_BedsteadJUnit4_requireNotHeadlessSystemUserMode(reason);
211     }
212 
213     // Get @Query annotation via BedsteadJunit4 class as a workaround to enable adding Query
214     // fields to annotations that rely on @AutoAnnotation (for e.g. @EnsureHasWorkProfile)
query()215     private static Query query() {
216         try {
217             return Class.forName("com.android.bedstead.harrier.BedsteadJUnit4")
218                     .getAnnotation(Query.class);
219         } catch (ClassNotFoundException e) {
220             throw new RuntimeException(
221                     "Unable to get BedsteadJunit4 class when trying to get "
222                             + "@Query annotation", e);
223         }
224     }
225 
226 
227     // These are annotations which are not included indirectly
228     private static final Set<String> sIgnoredAnnotationPackages = new HashSet<>();
229 
230     static {
231         sIgnoredAnnotationPackages.add("java.lang.annotation");
232         sIgnoredAnnotationPackages.add("com.android.bedstead.harrier.annotations.meta");
233         sIgnoredAnnotationPackages.add("kotlin.*");
234         sIgnoredAnnotationPackages.add("org.junit");
235     }
236 
237     /**
238      * Annotation sorter using the priority method added to an annotation,
239      * higher priority numbers are earlier in the list, if a priority is not provided
240      * {@link AnnotationPriorityRunPrecedence#PRECEDENCE_NOT_IMPORTANT} will be used
241      */
annotationSorter(Annotation a, Annotation b)242     public static int annotationSorter(Annotation a, Annotation b) {
243         return getAnnotationPriority(a) - getAnnotationPriority(b);
244     }
245 
getAnnotationCost(Annotation annotation)246     private static int getAnnotationCost(Annotation annotation) {
247         return ANNOTATION_COST_CACHE.computeIfAbsent(
248                 annotation, BedsteadJUnit4::computeAnnotationCost);
249     }
250 
getAnnotationPriority(Annotation annotation)251     private static int getAnnotationPriority(Annotation annotation) {
252         return ANNOTATION_PRIORITY_CACHE.computeIfAbsent(
253                 annotation, BedsteadJUnit4::computeAnnotationPriority);
254     }
255 
computeAnnotationCost(Annotation annotation)256     private static int computeAnnotationCost(Annotation annotation) {
257         try {
258             return (int) annotation.annotationType().getMethod("cost").invoke(annotation);
259         } catch (NoSuchMethodException e) {
260             // Default to MIDDLE if no cost is found on the annotation.
261             return AnnotationCostRunPrecedence.MIDDLE;
262         } catch (IllegalAccessException | InvocationTargetException e) {
263             throw new NeneException("Failed to invoke cost on this annotation: " + annotation, e);
264         }
265     }
266 
computeAnnotationPriority(Annotation annotation)267     private static int computeAnnotationPriority(Annotation annotation) {
268         if (annotation instanceof DynamicParameterizedAnnotation) {
269             // Special case, not important
270             return AnnotationPriorityRunPrecedence.PRECEDENCE_NOT_IMPORTANT;
271         }
272 
273         try {
274             return (int) annotation.annotationType().getMethod("priority").invoke(annotation);
275         } catch (NoSuchMethodException e) {
276             // Default to PRECEDENCE_NOT_IMPORTANT if no priority is found on the annotation.
277             return AnnotationPriorityRunPrecedence.PRECEDENCE_NOT_IMPORTANT;
278         } catch (IllegalAccessException | InvocationTargetException e) {
279             throw new NeneException(
280                     "Failed to invoke priority on this annotation: " + annotation, e);
281         }
282     }
283 
getParameterName(Annotation annotation)284     static String getParameterName(Annotation annotation) {
285         if (annotation instanceof DynamicParameterizedAnnotation) {
286             return ((DynamicParameterizedAnnotation) annotation).name();
287         }
288         return annotation.annotationType().getSimpleName();
289     }
290 
291     /**
292      * Resolves annotations recursively.
293      *
294      * @param parameterizedAnnotations The class of the parameterized annotations to expand, if any
295      */
resolveRecursiveAnnotations( List<Annotation> annotations, List<Annotation> parameterizedAnnotations)296     public void resolveRecursiveAnnotations(
297             List<Annotation> annotations, List<Annotation> parameterizedAnnotations) {
298         resolveRecursiveAnnotations(getHarrierRule(), annotations, parameterizedAnnotations);
299     }
300 
301     /**
302      * Resolves annotations recursively.
303      *
304      * @param parameterizedAnnotations The class of the parameterized annotation to expand, if any
305      */
resolveRecursiveAnnotations( HarrierRule harrierRule, List<Annotation> annotations, List<Annotation> parameterizedAnnotations)306     public static void resolveRecursiveAnnotations(
307             HarrierRule harrierRule,
308             List<Annotation> annotations,
309             List<Annotation> parameterizedAnnotations) {
310         int index = 0;
311         while (index < annotations.size()) {
312             Annotation annotation = annotations.get(index);
313             annotations.remove(index);
314             List<Annotation> replacementAnnotations =
315                     getReplacementAnnotations(harrierRule, annotation, parameterizedAnnotations);
316             replacementAnnotations.sort(BedsteadJUnit4::annotationSorter);
317             annotations.addAll(index, replacementAnnotations);
318             index += replacementAnnotations.size();
319         }
320     }
321 
isParameterizedAnnotation(Annotation annotation)322     private static boolean isParameterizedAnnotation(Annotation annotation) {
323         if (annotation instanceof DynamicParameterizedAnnotation) {
324             return true;
325         }
326 
327         return annotation.annotationType().getAnnotation(ParameterizedAnnotation.class) != null;
328     }
329 
isAnnotationClassParameterizedAnnotation(Annotation annotation)330     private static boolean isAnnotationClassParameterizedAnnotation(Annotation annotation) {
331         return annotation.annotationType() != null
332                 && annotation.annotationType().getAnnotation(ParameterizedAnnotation.class) != null;
333     }
334 
getIndirectAnnotations(Annotation annotation)335     private static Annotation[] getIndirectAnnotations(Annotation annotation) {
336         if (annotation instanceof DynamicParameterizedAnnotation) {
337             return ((DynamicParameterizedAnnotation) annotation).annotations();
338         }
339         return annotation.annotationType().getAnnotations();
340     }
341 
isRepeatingAnnotation(Annotation annotation)342     private static boolean isRepeatingAnnotation(Annotation annotation) {
343         if (annotation instanceof DynamicParameterizedAnnotation) {
344             return false;
345         }
346 
347         return annotation.annotationType().getAnnotation(RepeatingAnnotation.class) != null;
348     }
349 
350     private HarrierRule mHarrierRule;
351 
352     private static final ImmutableMap<
353                     Class<? extends Annotation>,
354                     BiFunction<HarrierRule, Annotation, Stream<Annotation>>>
355             ANNOTATION_REPLACEMENTS =
356                     ImmutableMap.of(
357                             RequireRunOnInitialUser.class,
358                             (harrierRule, a) -> {
359                                 RequireRunOnInitialUser requireRunOnInitialUserAnnotation =
360                                         (RequireRunOnInitialUser) a;
361 
362                                 if (harrierRule.isHeadlessSystemUserMode()) {
363                                     return Stream.of(
364                                             a,
365                                             ensureHasSecondaryUser(),
366                                             requireRunOnSecondaryUser(
367                                                     requireRunOnInitialUserAnnotation
368                                                             .switchedToUser()));
369                                 } else {
370                                     return Stream.of(
371                                             a,
372                                             requireRunOnPrimaryUser(
373                                                     requireRunOnInitialUserAnnotation
374                                                             .switchedToUser()));
375                                 }
376                             },
377                             RequireRunOnAdditionalUser.class,
378                             (harrierRule, a) -> {
379                                 RequireRunOnAdditionalUser requireRunOnAdditionalUserAnnotation =
380                                         (RequireRunOnAdditionalUser) a;
381                                 if (harrierRule.isHeadlessSystemUserMode()) {
382                                     return Stream.of(ensureHasSecondaryUser(), a);
383                                 } else {
384                                     return Stream.of(
385                                             a,
386                                             requireRunOnSecondaryUser(
387                                                     requireRunOnAdditionalUserAnnotation
388                                                             .switchedToUser()));
389                                 }
390                             });
391 
getReplacementAnnotations( HarrierRule harrierRule, Annotation annotation, List<Annotation> parameterizedAnnotations)392     static List<Annotation> getReplacementAnnotations(
393             HarrierRule harrierRule,
394             Annotation annotation,
395             List<Annotation> parameterizedAnnotations) {
396         BiFunction<HarrierRule, Annotation, Stream<Annotation>> specialReplaceFunction =
397                 ANNOTATION_REPLACEMENTS.get(annotation.annotationType());
398 
399         if (specialReplaceFunction != null) {
400             List<Annotation> replacement =
401                     specialReplaceFunction.apply(harrierRule, annotation)
402                             .collect(Collectors.toList());
403             return replacement;
404         }
405 
406         List<Annotation> replacementAnnotations = new ArrayList<>();
407 
408         if (isRepeatingAnnotation(annotation)) {
409             try {
410                 Annotation[] annotations =
411                         (Annotation[]) annotation.annotationType()
412                                 .getMethod("value").invoke(annotation);
413                 Collections.addAll(replacementAnnotations, annotations);
414                 return replacementAnnotations;
415             } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
416                 throw new NeneException("Error expanding repeated annotations", e);
417             }
418         }
419 
420         if (isParameterizedAnnotation(annotation)
421                 && !parameterizedAnnotations.contains(annotation)) {
422             return replacementAnnotations;
423         }
424 
425         for (Annotation indirectAnnotation : getIndirectAnnotations(annotation)) {
426             if (shouldSkipAnnotation(annotation)) {
427                 continue;
428             }
429 
430             replacementAnnotations.addAll(
431                     getReplacementAnnotations(
432                             harrierRule, indirectAnnotation, parameterizedAnnotations));
433         }
434 
435         if (!(annotation instanceof DynamicParameterizedAnnotation)) {
436             // We drop the fake annotation once it's replaced
437             replacementAnnotations.add(annotation);
438         }
439 
440         return replacementAnnotations;
441     }
442 
shouldSkipAnnotation(Annotation annotation)443     private static boolean shouldSkipAnnotation(Annotation annotation) {
444         if (annotation instanceof DynamicParameterizedAnnotation) {
445             return false;
446         }
447 
448         if(annotation.annotationType().equals(IncludeNone.class)) {
449             return true;
450         }
451 
452         String annotationPackage = annotation.annotationType().getPackage().getName();
453 
454         for (String ignoredPackage : sIgnoredAnnotationPackages) {
455             if (ignoredPackage.endsWith(".*")) {
456                 if (annotationPackage.startsWith(
457                         ignoredPackage.substring(0, ignoredPackage.length() - 2))) {
458                     return true;
459                 }
460             } else if (annotationPackage.equals(ignoredPackage)) {
461                 return true;
462             }
463         }
464 
465         return false;
466     }
467 
BedsteadJUnit4(Class<?> testClass)468     public BedsteadJUnit4(Class<?> testClass) throws InitializationError {
469         super(testClass);
470     }
471 
getBasicTests(TestClass testClass)472     private static List<FrameworkMethod> getBasicTests(TestClass testClass) {
473         Set<FrameworkMethod> methods = new HashSet<>();
474 
475         methods.addAll(testClass.getAnnotatedMethods(Test.class));
476         methods.addAll(testClass.getAnnotatedMethods(PolicyAppliesTest.class));
477         methods.addAll(testClass.getAnnotatedMethods(PolicyDoesNotApplyTest.class));
478         methods.addAll(testClass.getAnnotatedMethods(CanSetPolicyTest.class));
479         methods.addAll(testClass.getAnnotatedMethods(CannotSetPolicyTest.class));
480         methods.addAll(testClass.getAnnotatedMethods(UserTest.class));
481         methods.addAll(testClass.getAnnotatedMethods(CrossUserTest.class));
482         methods.addAll(testClass.getAnnotatedMethods(PermissionTest.class));
483         methods.addAll(testClass.getAnnotatedMethods(MostRestrictiveCoexistenceTest.class));
484         methods.addAll(testClass.getAnnotatedMethods(MostImportantCoexistenceTest.class));
485         methods.addAll(testClass.getAnnotatedMethods(HiddenApiTest.class));
486         methods.addAll(testClass.getAnnotatedMethods(PerformanceTest.class));
487 
488         return new ArrayList<>(methods);
489     }
490 
491     /**
492      * Groups list of annotations of type [ParameterizedAnnotation] by its [scope].
493      *
494      * @param parameterizedAnnotations the list of annotations of type [ParameterizedAnnotation]
495      * @return list of list of [ParameterizedAnnotation] where each sub list corresponds to
496      *     annotations of one scope.
497      */
getParameterizedAnnotationsGroupedByScope( Set<Annotation> parameterizedAnnotations)498     private List<List<Annotation>> getParameterizedAnnotationsGroupedByScope(
499             Set<Annotation> parameterizedAnnotations) {
500         Map<String, List<Annotation>> annotationsPerScope = new HashMap<>();
501         for (Annotation annotation : parameterizedAnnotations) {
502             if (isAnnotationClassParameterizedAnnotation(annotation)
503                     && !shouldSkipAnnotation(annotation)) {
504                 ParameterizedAnnotation parameterizedAnnotation =
505                         annotation.annotationType().getAnnotation(ParameterizedAnnotation.class);
506                 annotationsPerScope.putIfAbsent(
507                         parameterizedAnnotation.scope().name(), new ArrayList<>());
508                 annotationsPerScope.get(parameterizedAnnotation.scope().name()).add(annotation);
509             }
510         }
511 
512         return new ArrayList<>(annotationsPerScope.values());
513     }
514 
515     /**
516      * Generates a cartesian product of multiple sets of annotations. For example: If the
517      * [annotations] param has value [[A1, A2], [A3, A4]] then it will return [[A1, A3], [A1, A4],
518      * [A2, A3], [A2, A4]].
519      *
520      * @param annotations list of list of annotations whose cartesian product we want to generate.
521      * @return cartesian product of the annotation sets.
522      */
calculateCartesianProductOfAnnotationSets( List<List<Annotation>> annotations)523     private static List<List<Annotation>> calculateCartesianProductOfAnnotationSets(
524             List<List<Annotation>> annotations) {
525         List<List<Annotation>> result = new ArrayList<>();
526         if (!annotations.isEmpty()) {
527             generateCartesianProductOfAnnotationSets(annotations, 0, result, new ArrayList<>());
528         }
529         return result;
530     }
531 
532     /**
533      * Generates a cartesian product of multiple sets of annotations. This method is an internal
534      * helper method for {@code calculateCartesianProductOfAnnotationSets()}. Refer {@code
535      * calculateCartesianProductOfAnnotationSets()} for an example.
536      */
generateCartesianProductOfAnnotationSets( List<List<Annotation>> annotations, int position, List<List<Annotation>> result, List<Annotation> subResult)537     private static void generateCartesianProductOfAnnotationSets(
538             List<List<Annotation>> annotations,
539             int position,
540             List<List<Annotation>> result,
541             List<Annotation> subResult) {
542         if (position == annotations.size()) {
543             if (!subResult.isEmpty()) {
544                 result.add(new ArrayList<>(subResult));
545             }
546             return;
547         }
548         for (int i = 0; i < annotations.get(position).size(); i++) {
549             subResult.add(annotations.get(position).get(i));
550             generateCartesianProductOfAnnotationSets(annotations, position + 1, result, subResult);
551             subResult.remove(subResult.size() - 1);
552         }
553     }
554 
555     @Override
computeTestMethods()556     protected List<FrameworkMethod> computeTestMethods() {
557         // TODO: It appears that the annotations are computed up to 8 times per run. Figure out how
558         // to cut this out (this method only seems to be called once)
559         List<FrameworkMethod> basicTests = getBasicTests(getTestClass());
560         List<FrameworkMethod> modifiedTests = new ArrayList<>();
561 
562         for (FrameworkMethod m : basicTests) {
563             Set<Annotation> parameterizedAnnotations = getParameterizedAnnotations(m.getAnnotations());
564 
565             if (parameterizedAnnotations.isEmpty()) {
566                 // Unparameterized, just add the original
567                 modifiedTests.add(new BedsteadFrameworkMethod(this, m.getMethod()));
568                 continue;
569             }
570 
571             // Create [BedsteadFrameworkMethod] for parameterized annotation of instance {@Code
572             // DynamicParameterizedAnnotation}.
573             for (Annotation annotation : parameterizedAnnotations) {
574                 if (shouldSkipAnnotation(annotation)
575                         || isAnnotationClassParameterizedAnnotation(annotation)) {
576                     // Special case - does not generate a run
577                     continue;
578                 }
579                 modifiedTests.add(
580                         new BedsteadFrameworkMethod(this, m.getMethod(), List.of(annotation)));
581             }
582 
583             List<List<Annotation>> parametrizedAnnotationsGroupedByScope =
584                     getParameterizedAnnotationsGroupedByScope(parameterizedAnnotations);
585 
586             List<List<Annotation>> cartesianProductOfAnnotationSets =
587                     calculateCartesianProductOfAnnotationSets(
588                             parametrizedAnnotationsGroupedByScope);
589 
590             // Create [BedsteadFrameworkMethod] for each parameterized annotation of type
591             // [ParameterizedAnnotation].
592             for (List<Annotation> annotationsToApplyTogether : cartesianProductOfAnnotationSets) {
593                 modifiedTests.add(
594                         new BedsteadFrameworkMethod(
595                                 this, m.getMethod(), annotationsToApplyTogether));
596             }
597         }
598 
599         modifiedTests = generateGeneralParameterisationMethods(modifiedTests);
600 
601         sortMethodsByBedsteadAnnotations(modifiedTests);
602 
603         return modifiedTests;
604     }
605 
generateGeneralParameterisationMethods( List<FrameworkMethod> modifiedTests)606     private List<FrameworkMethod> generateGeneralParameterisationMethods(
607             List<FrameworkMethod> modifiedTests) {
608         return modifiedTests.stream()
609                 .flatMap(this::generateGeneralParameterisationMethods)
610                 .collect(Collectors.toList());
611     }
612 
generateGeneralParameterisationMethods(FrameworkMethod method)613     private Stream<FrameworkMethod> generateGeneralParameterisationMethods(FrameworkMethod method) {
614         Stream<FrameworkMethod> expandedMethods = Stream.of(method);
615         if (method.getMethod().getParameterCount() == 0) {
616             return expandedMethods;
617         }
618 
619         for (Parameter parameter : method.getMethod().getParameters()) {
620             List<Annotation> annotations =
621                     new ArrayList<>(Arrays.asList(parameter.getAnnotations()));
622             resolveRecursiveAnnotations(annotations, /* parameterizedAnnotations= */ List.of());
623 
624             boolean hasParameterised = false;
625 
626             for (Annotation annotation : annotations) {
627 
628                 if (annotation instanceof PolicyArgument) {
629                     if (hasParameterised) {
630                         throw new IllegalStateException(
631                                 "Each parameter can only have a single parameterised annotation");
632                     }
633                     hasParameterised = true;
634 
635                     HarrierToEnterpriseMediator mediator =
636                             HarrierToEnterpriseMediator.Companion.getMediatorOrThrowException(
637                                     "you can't use @PolicyArgument without the enterprise module"
638                             );
639                     expandedMethods = mediator.generatePolicyArgumentTests(method, expandedMethods);
640                 } else if (annotation instanceof StringTestParameter) {
641                     if (hasParameterised) {
642                         throw new IllegalStateException(
643                                 "Each parameter can only have a single parameterised annotation");
644                     }
645                     hasParameterised = true;
646 
647                     StringTestParameter stringTestParameter = (StringTestParameter) annotation;
648 
649                     expandedMethods = expandedMethods.flatMap(
650                             i -> applyStringTestParameter(i, stringTestParameter));
651                 } else if (annotation instanceof IntTestParameter) {
652                     if (hasParameterised) {
653                         throw new IllegalStateException(
654                                 "Each parameter can only have a single parameterised annotation");
655                     }
656                     hasParameterised = true;
657 
658                     IntTestParameter intTestParameter = (IntTestParameter) annotation;
659 
660                     expandedMethods = expandedMethods.flatMap(
661                             i -> applyIntTestParameter(i, intTestParameter));
662                 } else if (annotation instanceof EnumTestParameter) {
663                     if (hasParameterised) {
664                         throw new IllegalStateException(
665                                 "Each parameter can only have a single parameterised annotation");
666                     }
667                     hasParameterised = true;
668 
669                     EnumTestParameter enumTestParameter = (EnumTestParameter) annotation;
670 
671                     expandedMethods = expandedMethods.flatMap(
672                             i -> applyEnumTestParameter(i, enumTestParameter));
673                 }
674             }
675 
676             if (!hasParameterised) {
677                 throw new IllegalStateException(
678                         "Parameter " + parameter + " must be annotated as parameterised");
679             }
680         }
681 
682         return expandedMethods;
683     }
684 
applyStringTestParameter(FrameworkMethod frameworkMethod, StringTestParameter stringTestParameter)685     private static Stream<FrameworkMethod> applyStringTestParameter(FrameworkMethod frameworkMethod,
686             StringTestParameter stringTestParameter) {
687         return Stream.of(stringTestParameter.value()).map(
688                 (i) -> new FrameworkMethodWithParameter(frameworkMethod, i)
689         );
690     }
691 
applyIntTestParameter(FrameworkMethod frameworkMethod, IntTestParameter intTestParameter)692     private static Stream<FrameworkMethod> applyIntTestParameter(FrameworkMethod frameworkMethod,
693             IntTestParameter intTestParameter) {
694         return Arrays.stream(intTestParameter.value()).mapToObj(
695                 (i) -> new FrameworkMethodWithParameter(frameworkMethod, i)
696         );
697     }
698 
applyEnumTestParameter(FrameworkMethod frameworkMethod, EnumTestParameter enumTestParameter)699     private static Stream<FrameworkMethod> applyEnumTestParameter(FrameworkMethod frameworkMethod,
700             EnumTestParameter enumTestParameter) {
701         return Arrays.stream(enumTestParameter.value().getEnumConstants()).map(
702                 (i) -> new FrameworkMethodWithParameter(frameworkMethod, i)
703         );
704     }
705 
706     /**
707      * Sort methods by cost and group the ones with identical bedstead annotations together.
708      *
709      * <p>This will also ensure that all tests methods which are not annotated for bedstead will
710      * run before any tests which are annotated.
711      */
sortMethodsByBedsteadAnnotations(List<FrameworkMethod> modifiedTests)712     private void sortMethodsByBedsteadAnnotations(List<FrameworkMethod> modifiedTests) {
713         List<Annotation> bedsteadAnnotationsSortedByCost =
714                 bedsteadAnnotationsSortedByCost(modifiedTests);
715         Comparator<FrameworkMethod> comparator = ((o1, o2) -> {
716             for (Annotation annotation : bedsteadAnnotationsSortedByCost) {
717                 boolean o1HasAnnotation = o1.getAnnotation(annotation.annotationType()) != null;
718                 boolean o2HasAnnotation = o2.getAnnotation(annotation.annotationType()) != null;
719 
720                 if (o1HasAnnotation && !o2HasAnnotation) {
721                     // o1 goes to the start
722                     return -1;
723                 } else if (o2HasAnnotation && !o1HasAnnotation) {
724                     return 1;
725                 }
726             }
727             return 0;
728         });
729 
730         List<Annotation> bedsteadAnnotationsSortedByMostCommon =
731                 bedsteadAnnotationsSortedByMostCommon(modifiedTests);
732         var unused = comparator.thenComparing((o1, o2) -> {
733             for (Annotation annotation : bedsteadAnnotationsSortedByMostCommon) {
734                 boolean o1HasAnnotation = o1.getAnnotation(annotation.annotationType()) != null;
735                 boolean o2HasAnnotation = o2.getAnnotation(annotation.annotationType()) != null;
736 
737                 if (o1HasAnnotation && !o2HasAnnotation) {
738                     // o1 goes to the end
739                     return 1;
740                 } else if (o2HasAnnotation && !o1HasAnnotation) {
741                     return -1;
742                 }
743             }
744 
745             return 0;
746         });
747 
748         modifiedTests.sort(comparator);
749     }
750 
bedsteadAnnotationsSortedByCost(List<FrameworkMethod> methods)751     private List<Annotation> bedsteadAnnotationsSortedByCost(List<FrameworkMethod> methods) {
752         Map<Annotation, Integer> annotationCosts = mapAnnotationsCost(methods);
753 
754         List<Annotation> annotations = new ArrayList<>(annotationCosts.keySet());
755         annotations.sort(Comparator.comparingInt(annotationCosts::get));
756 
757         return annotations;
758     }
759 
bedsteadAnnotationsSortedByMostCommon(List<FrameworkMethod> methods)760     private List<Annotation> bedsteadAnnotationsSortedByMostCommon(List<FrameworkMethod> methods) {
761         Map<Annotation, Integer> annotationCounts = countAnnotations(methods);
762         List<Annotation> annotations = new ArrayList<>(annotationCounts.keySet());
763         annotations.sort(Comparator.comparingInt(annotationCounts::get));
764         Collections.reverse(annotations);
765 
766         return annotations;
767     }
768 
countAnnotations(List<FrameworkMethod> methods)769     private Map<Annotation, Integer> countAnnotations(List<FrameworkMethod> methods) {
770         Map<Annotation, Integer> annotationCounts = new HashMap<>();
771 
772         for (FrameworkMethod method : methods) {
773             for (Annotation annotation : method.getAnnotations()) {
774                 annotationCounts.put(
775                         annotation, annotationCounts.getOrDefault(annotation, 0) + 1);
776             }
777         }
778 
779         return annotationCounts;
780     }
781 
mapAnnotationsCost(List<FrameworkMethod> methods)782     private Map<Annotation, Integer> mapAnnotationsCost(List<FrameworkMethod> methods) {
783         Map<Annotation, Integer> annotationCosts = new HashMap<>();
784 
785         for (FrameworkMethod method : methods) {
786             for (Annotation annotation : method.getAnnotations()) {
787                 annotationCosts.put(annotation, getAnnotationCost(annotation));
788             }
789         }
790 
791         return annotationCosts;
792     }
793 
794     /**
795      * Filters array of annotations and returns only annotations of type
796      * {@link ParameterizedAnnotation} and {@link DynamicParameterizedAnnotation}.
797      *
798      * @param methodAnnotations the array of annotations of test method
799      */
getParameterizedAnnotations(Annotation[] methodAnnotations)800     public static Set<Annotation> getParameterizedAnnotations(Annotation[] methodAnnotations) {
801         Set<Annotation> parameterizedAnnotations = new HashSet<>();
802         List<Annotation> annotations = new ArrayList<>(Arrays.asList(methodAnnotations));
803 
804         parseEnterpriseAnnotations(annotations);
805         parsePermissionAnnotations(annotations);
806         parseUserAnnotations(annotations);
807 
808         for (Annotation annotation : annotations) {
809             if (isParameterizedAnnotation(annotation)) {
810                 parameterizedAnnotations.add(annotation);
811             }
812         }
813 
814         return parameterizedAnnotations;
815     }
816 
817     /**
818      * Parse enterprise-specific annotations.
819      *
820      * <p>To be used before general annotation processing.
821      */
parseEnterpriseAnnotations(List<Annotation> annotations)822     static void parseEnterpriseAnnotations(List<Annotation> annotations) {
823         HarrierToEnterpriseMediator mediator =
824                 HarrierToEnterpriseMediator.Companion.getMediatorOrNull();
825         if (mediator == null) {
826             System.out.println(LOG_TAG + " bedstead-enterprise module is not loaded, "
827                     + "parseEnterpriseAnnotations will not be executed");
828         } else {
829             mediator.parseEnterpriseAnnotations(annotations);
830         }
831     }
832 
833     /**
834      * Parse @PermissionTest annotations.
835      *
836      * <p>To be used before general annotation processing.
837      */
parsePermissionAnnotations(List<Annotation> annotations)838     static void parsePermissionAnnotations(List<Annotation> annotations) {
839         int index = 0;
840         while (index < annotations.size()) {
841             Annotation annotation = annotations.get(index);
842             if (annotation instanceof PermissionTest) {
843                 annotations.remove(index);
844 
845                 List<Annotation> replacementAnnotations = generatePermissionAnnotations(
846                         ((PermissionTest) annotation).value());
847                 replacementAnnotations.sort(BedsteadJUnit4::annotationSorter);
848 
849                 annotations.addAll(index, replacementAnnotations);
850                 index += replacementAnnotations.size();
851             } else {
852                 index++;
853             }
854         }
855     }
856 
generatePermissionAnnotations(String[] permissions)857     private static List<Annotation> generatePermissionAnnotations(String[] permissions) {
858         Set<String> allPermissions = new HashSet<>(Arrays.asList(permissions));
859         List<Annotation> replacementAnnotations = new ArrayList<>();
860 
861         for (String permission : permissions) {
862             allPermissions.remove(permission);
863             replacementAnnotations.add(
864                     new DynamicParameterizedAnnotation(
865                             permission,
866                             new Annotation[]{
867                                     ensureHasPermission(permission),
868                                     ensureDoesNotHavePermission(allPermissions.toArray(new String[]{}))
869                             }));
870             allPermissions.add(permission);
871         }
872 
873         return replacementAnnotations;
874     }
875 
876     /**
877      * Parse @UserTest and @CrossUserTest annotations.
878      *
879      * <p>To be used before general annotation processing.
880      */
parseUserAnnotations(List<Annotation> annotations)881     static void parseUserAnnotations(List<Annotation> annotations) {
882         int index = 0;
883         while (index < annotations.size()) {
884             Annotation annotation = annotations.get(index);
885             if (annotation instanceof UserTest) {
886                 annotations.remove(index);
887 
888                 List<Annotation> replacementAnnotations = generateUserAnnotations(
889                         ((UserTest) annotation).value());
890                 replacementAnnotations.sort(BedsteadJUnit4::annotationSorter);
891 
892                 annotations.addAll(index, replacementAnnotations);
893                 index += replacementAnnotations.size();
894             } else if (annotation instanceof CrossUserTest) {
895                 annotations.remove(index);
896 
897                 CrossUserTest crossUserTestAnnotation = (CrossUserTest) annotation;
898                 List<Annotation> replacementAnnotations = generateCrossUserAnnotations(
899                         crossUserTestAnnotation.value());
900                 replacementAnnotations.sort(BedsteadJUnit4::annotationSorter);
901 
902                 annotations.addAll(index, replacementAnnotations);
903                 index += replacementAnnotations.size();
904             } else {
905                 index++;
906             }
907         }
908     }
909 
generateUserAnnotations(UserType[] userTypes)910     private static List<Annotation> generateUserAnnotations(UserType[] userTypes) {
911         List<Annotation> replacementAnnotations = new ArrayList<>();
912 
913         for (UserType userType : userTypes) {
914             Annotation runOnUserAnnotation = getRunOnAnnotation(userType, "@UserTest");
915             replacementAnnotations.add(
916                     new DynamicParameterizedAnnotation(
917                             userType.name(),
918                             new Annotation[]{runOnUserAnnotation}));
919         }
920 
921         return replacementAnnotations;
922     }
923 
generateCrossUserAnnotations(UserPair[] userPairs)924     private static List<Annotation> generateCrossUserAnnotations(UserPair[] userPairs) {
925         List<Annotation> replacementAnnotations = new ArrayList<>();
926 
927         for (UserPair userPair : userPairs) {
928             Annotation[] annotations = new Annotation[]{
929                     getRunOnAnnotation(userPair.from(), "@CrossUserTest"),
930                     otherUser(userPair.to())
931             };
932             if (userPair.from() != userPair.to()) {
933                 Annotation hasUserAnnotation =
934                         getHasUserAnnotation(userPair.to(), "@CrossUserTest");
935                 if (hasUserAnnotation != null) {
936                     annotations = new Annotation[]{
937                             annotations[0],
938                             annotations[1],
939                             hasUserAnnotation};
940                 }
941             }
942 
943             replacementAnnotations.add(
944                     new DynamicParameterizedAnnotation(
945                             userPair.from().name() + "_to_" + userPair.to().name(),
946                             annotations));
947         }
948 
949         return replacementAnnotations;
950     }
951 
getRunOnAnnotation(UserType userType, String annotationName)952     private static Annotation getRunOnAnnotation(UserType userType, String annotationName) {
953         switch (userType) {
954             case SYSTEM_USER:
955                 return requireRunOnSystemUser();
956             case CURRENT_USER:
957                 return null; // No requirement, run on current user
958             case INITIAL_USER:
959                 return requireRunOnInitialUser();
960             case ADDITIONAL_USER:
961                 return requireRunOnAdditionalUser();
962             case PRIMARY_USER:
963                 return requireRunOnPrimaryUser();
964             case SECONDARY_USER:
965                 return requireRunOnSecondaryUser();
966             case WORK_PROFILE:
967                 return requireRunOnWorkProfile(query());
968             case TV_PROFILE:
969                 return requireRunOnTvProfile();
970             case CLONE_PROFILE:
971                 return requireRunOnCloneProfile();
972             case PRIVATE_PROFILE:
973                 return requireRunOnPrivateProfile();
974             default:
975                 throw new IllegalStateException(
976                         "UserType " + userType + " is not compatible with " + annotationName);
977         }
978     }
979 
getHasUserAnnotation(UserType userType, String annotationName)980     private static Annotation getHasUserAnnotation(UserType userType, String annotationName) {
981         switch (userType) {
982             case SYSTEM_USER:
983                 return null; // We always have a system user
984             case CURRENT_USER:
985                 return null; // We always have a current user
986             case INITIAL_USER:
987                 return null; // We always have an initial user
988             case ADDITIONAL_USER:
989                 return ensureHasAdditionalUser();
990             case PRIMARY_USER:
991                 return requireNotHeadlessSystemUserMode(
992                         "Headless System User Mode Devices do not have a primary user");
993             case SECONDARY_USER:
994                 return ensureHasSecondaryUser();
995             case WORK_PROFILE:
996                 return ensureHasWorkProfile(query());
997             case TV_PROFILE:
998                 return ensureHasTvProfile();
999             case CLONE_PROFILE:
1000                 return ensureHasCloneProfile();
1001             case PRIVATE_PROFILE:
1002                 return ensureHasPrivateProfile();
1003             default:
1004                 throw new IllegalStateException(
1005                         "UserType " + userType + " is not compatible with " + annotationName);
1006         }
1007     }
1008 
getHarrierRule()1009     HarrierRule getHarrierRule() {
1010         if (mHarrierRule == null) {
1011             var unused = classRules();
1012         }
1013         return mHarrierRule;
1014     }
1015 
1016     @Override
getTestRules(Object target)1017     protected List<TestRule> getTestRules(Object target) {
1018         var testRules = super.getTestRules(target);
1019         if (mHasManualHarrierRule) {
1020             return testRules;
1021         }
1022         var harrier = findHarrier(testRules);
1023         if (harrier == null) {
1024             testRules.add(getHarrierRule());
1025         }
1026         return testRules;
1027     }
1028 
1029     @Override
classRules()1030     protected List<TestRule> classRules() {
1031         List<TestRule> rules = super.classRules();
1032 
1033         mHarrierRule = findHarrier(rules);
1034         mHasManualHarrierRule = mHarrierRule != null;
1035 
1036         if (mHarrierRule == null) {
1037             mHarrierRule = new DeviceState();
1038         }
1039         if (!rules.contains(mHarrierRule)) {
1040             rules.add(mHarrierRule);
1041         }
1042 
1043         mHarrierRule.setSkipTestTeardown(true);
1044         mHarrierRule.setUsingBedsteadJUnit4(true);
1045 
1046         return rules;
1047     }
1048 
findHarrier(List<TestRule> rules)1049     private HarrierRule findHarrier(List<TestRule> rules) {
1050         for (TestRule rule : rules) {
1051             if (rule instanceof HarrierRule) {
1052                 return (HarrierRule) rule;
1053             }
1054         }
1055         return null;
1056     }
1057 
1058     /**
1059      * True if the test is running in debug mode.
1060      *
1061      * <p>This will result in additional debugging information being added which would otherwise
1062      * be dropped to improve test performance.
1063      *
1064      * <p>To enable this, pass the "bedstead-debug" instrumentation arg as "true"
1065      */
isDebug()1066     public static boolean isDebug() {
1067         try {
1068             Class instrumentationRegistryClass = Class.forName(
1069                         "androidx.test.platform.app.InstrumentationRegistry");
1070 
1071             Object arguments = instrumentationRegistryClass.getMethod("getArguments")
1072                     .invoke(null);
1073             return Boolean.parseBoolean((String) arguments.getClass()
1074                     .getMethod("getString", String.class, String.class)
1075                     .invoke(arguments, "bedstead-debug", "false"));
1076         } catch (ClassNotFoundException e) {
1077             return false; // Must be on the host so can't access debug information
1078         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
1079             throw new IllegalStateException("Error getting isDebug", e);
1080         }
1081     }
1082 
1083     @Override
validateTestMethods(List<Throwable> errors)1084     protected void validateTestMethods(List<Throwable> errors) {
1085         // We do allow arguments - they will fail validation later on if not properly annotated
1086     }
1087 
1088     /**
1089      * Add a listener to be informed of test lifecycle events.
1090      */
addLifecycleListener(TestLifecycleListener listener)1091     public static void addLifecycleListener(TestLifecycleListener listener) {
1092         sLifecycleListeners.add(listener);
1093     }
1094 
1095     /**
1096      * Remove a listener being informed of test lifecycle events.
1097      */
removeLifecycleListener(TestLifecycleListener listener)1098     public static void removeLifecycleListener(TestLifecycleListener listener) {
1099         sLifecycleListeners.remove(listener);
1100     }
1101 
1102     @Override
runChild(final FrameworkMethod method, RunNotifier notifier)1103     protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
1104         Description description = describeChild(method);
1105         if (isIgnored(method)) {
1106             notifier.fireTestIgnored(description);
1107         } else {
1108             Statement statement = new Statement() {
1109                 @Override
1110                 public void evaluate() throws Throwable {
1111                     sLifecycleListeners.forEach(l -> l.testStarted(method.getName()));
1112                     while (true) {
1113                         try {
1114                             methodBlock(method).evaluate();
1115                             sLifecycleListeners.forEach(l -> l.testFinished(method.getName()));
1116                             return;
1117                         } catch (RestartTestException e) {
1118                             sLifecycleListeners.forEach(
1119                                     l -> l.testRestarted(method.getName(), e.getMessage()));
1120                             System.out.println(LOG_TAG + ": Restarting test(" + e.toString() + ")");
1121                         }
1122                     }
1123                 }
1124             };
1125             runLeaf(statement, description, notifier);
1126         }
1127     }
1128 }
1129