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