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