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