1 /* 2 * Copyright (C) 2022 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 android.car.test; 18 19 import static java.lang.annotation.ElementType.METHOD; 20 import static java.lang.annotation.ElementType.TYPE; 21 import static java.lang.annotation.RetentionPolicy.RUNTIME; 22 23 import android.annotation.Nullable; 24 import android.car.Car; 25 import android.car.CarVersion; 26 import android.car.PlatformVersion; 27 import android.car.PlatformVersionMismatchException; 28 import android.car.annotation.AddedInOrBefore; 29 import android.car.annotation.ApiRequirements; 30 import android.car.test.ApiCheckerRule.UnsupportedVersionTest.Behavior; 31 import android.os.Build; 32 import android.text.TextUtils; 33 import android.util.Log; 34 import android.util.Pair; 35 36 import com.android.compatibility.common.util.ApiTest; 37 import com.android.compatibility.common.util.CddTest; 38 import com.android.compatibility.common.util.NonApiTest; 39 40 import org.junit.AssumptionViolatedException; 41 import org.junit.rules.TestRule; 42 import org.junit.runner.Description; 43 import org.junit.runners.model.Statement; 44 45 import java.lang.annotation.Annotation; 46 import java.lang.annotation.Retention; 47 import java.lang.annotation.Target; 48 import java.lang.reflect.Field; 49 import java.lang.reflect.Member; 50 import java.lang.reflect.Method; 51 import java.util.ArrayList; 52 import java.util.Arrays; 53 import java.util.List; 54 55 /** 56 * Rule used to validate Car API requirements on CTS tests. 57 * 58 * <p>This rule is used to verify that all tests in a class: 59 * 60 * <ol> 61 * <li>Indicate which API / CDD is being tested. 62 * <li>Properly behave on supported and unsupported versions. 63 * </ol> 64 * 65 * <p>For the former, the test must be annotated with either {@link ApiTest} or {@link CddTest} (in 66 * which case it also needs to be annotated with {@link ApiRequirements}, otherwise the test will 67 * fail (unless the rule was created with {@link Builder#disableAnnotationsCheck()}. In the case 68 * of {@link ApiTest}, the rule will also assert that the underlying APIs are annotated with either 69 * {@link ApiRequirements} or {@link AddedInOrBefore}. 70 * 71 * <p><b>Note:</b> Usually, all CTS tests should be testing public or system APIs or CDD 72 * requirements. However, in the case that they don't (especially in {@code AndroidCarApiTest}), 73 * they should be annotated with {@link NonApiTest}. This usage should also be justified. 74 * 75 * <p>For the latter, if the API declares {@link ApiRequirements}, the rule by default will make 76 * sure the test behaves properly in the supported and unsupported platform versions: 77 * <ol> 78 * <li>If the platform is supported, the test should pass as usual. 79 * <li>If the platform is not supported, the rule will assert that the test throws a 80 * {@link PlatformVersionMismatchException}. 81 * </ol> 82 * 83 * <p>There are corner cases where the default rule behavior cannot be applied for the test, like: 84 * <ol> 85 * <li>The test logic is too complex (or takes time) and should be simplified when running on 86 * unsupported versions. 87 * <li>The API being tested should behave differently on supported or unsupported versions. 88 * </ol> 89 * 90 * <p>In these cases, the test should be split in 2 tests, one for the supported version and another 91 * for the unsupported version, and annotated with {@link SupportedVersionTest} or 92 * {@link UnsupportedVersionTest} respectively; these tests <b>MUST</b> be provided in pair (in 93 * fact, these annotations take an argument pointing to the pair) and they will behave this way: 94 * 95 * <ol> 96 * <li>{@link SupportedVersionTest}: should pass on supported platform and will be ignored on 97 * unsupported platforms (by throwing an {@link ExpectedVersionAssumptionViolationException}). 98 * <li>{@link UnsupportedVersionTest}: by default, it will be ignored on supported platforms 99 * (by throwing an {@link ExpectedVersionAssumptionViolationException}), but can be changed 100 * to run on unsupported platforms as well (by setting its 101 * {@link UnsupportedVersionTest#behavior()} to {@link Behavior#EXPECT_PASS}. 102 * </ol> 103 * 104 * <p>So, back to the examples above, the tests would be: 105 * <pre>{@code 106 * 107 * @Test 108 * @ApiTest(apis = {"com.acme.Car#foo"}) 109 * @SupportedVersionTest(unsupportedVersionTest="testFoo_unsupported") 110 * public void testFoo_supported() { 111 * baz(); // takes a long time 112 * foo(); 113 * } 114 * 115 * @Test 116 * @ApiTest(apis = {"com.acme.Car#foo"}) 117 * @UnsupportedVersionTest(supportedVersionTest="testFoo_supported") 118 * public void testFoo_unsupported() { 119 * foo(); // should throw PlatformViolationException 120 * } 121 * 122 * @Test 123 * @ApiTest(apis = {"com.acme.Car#bar"}) 124 * @SupportedVersionTest(unsupportedVersionTest="testBar_unsupported") 125 * public void testBar_supported() { 126 * assertWithMessage("bar()").that(bar()).isEqualTo("BehaviorOnSupportedPlatform"); 127 * } 128 * 129 * @Test 130 * @ApiTest(apis = {"com.acme.Car#bar"}) 131 * @UnsupportedVersionTest(supportedVersionTest="testBar_supported", behavior=EXPECT_PASS) 132 * public void testFoo_unsupported() { 133 * assertWithMessage("bar()").that(bar()).isEqualTo("BehaviorOnUnsupportedPlatform"); 134 * } 135 * 136 * }</pre> 137 * 138 * For nested classes the following annotation should be used for methods: 139 * <pre>{@code 140 * @Test 141 * @ApiTest(apis = {"com.acme.Car$Inner#methodName"}) 142 * public void testMethodName() {} 143 * 144 * </code></pre> 145 * For nested classes the following annotation should be used for fields: 146 * <pre><code> 147 * @Test 148 * @ApiTest(apis = {"com.acme.Car.Inner#fieldName"}) 149 * public void testFieldName() {} 150 * 151 * }</pre> 152 */ 153 public final class ApiCheckerRule implements TestRule { 154 155 public static final String TAG = ApiCheckerRule.class.getSimpleName(); 156 157 private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); 158 159 private final boolean mEnforceTestApiAnnotations; 160 private final boolean mEnforceApiRequirements; 161 162 @Nullable 163 private String mTestMethodName; 164 165 /** 166 * Builder. 167 */ 168 public static final class Builder { 169 private boolean mEnforceTestApiAnnotations = true; 170 private boolean mEnforceApiRequirements = true; 171 172 /** 173 * Creates a new rule. 174 */ build()175 public ApiCheckerRule build() { 176 return new ApiCheckerRule(this); 177 } 178 179 /** 180 * Don't fail the test if the required annotations (like {@link ApiTest}) are missing. 181 */ disableAnnotationsCheck()182 public Builder disableAnnotationsCheck() { 183 mEnforceTestApiAnnotations = false; 184 return this; 185 } 186 187 /** 188 * Don't fail the test if it could not infer its {@link ApiRequirements}. 189 * 190 * <p>Typically used on tests for built-in APIs. 191 */ disableApiRequirementsCheck()192 public Builder disableApiRequirementsCheck() { 193 mEnforceApiRequirements = false; 194 return this; 195 } 196 } 197 ApiCheckerRule(Builder builder)198 private ApiCheckerRule(Builder builder) { 199 mEnforceTestApiAnnotations = builder.mEnforceTestApiAnnotations; 200 mEnforceApiRequirements = mEnforceTestApiAnnotations 201 ? builder.mEnforceApiRequirements 202 : false; 203 } 204 205 /** 206 * Checks whether the test is running in an environment that supports the given API. 207 * 208 * @param api API as defined by {@link ApiTest}. 209 * @return whether the test is running in an environment that supports the 210 * {@link ApiRequirements} defined in such API. 211 */ isApiSupported(String api)212 public boolean isApiSupported(String api) { 213 ApiRequirements apiRequirements = getApiRequirements(api); 214 215 if (apiRequirements == null) { 216 throw new IllegalArgumentException("No @ApiRequirements on " + api); 217 } 218 219 return isSupported(apiRequirements); 220 } 221 222 /** 223 * Gets the name of the test being executed. 224 */ 225 @Nullable getTestMethodName()226 public String getTestMethodName() { 227 return mTestMethodName; 228 } 229 isSupported(ApiRequirements apiRequirements)230 private boolean isSupported(ApiRequirements apiRequirements) { 231 PlatformVersion platformVersion = Car.getPlatformVersion(); 232 boolean isSupported = platformVersion 233 .isAtLeast(apiRequirements.minPlatformVersion().get()); 234 if (DBG) { 235 Log.d(TAG, "isSupported(" + apiRequirements + "): platformVersion=" + platformVersion 236 + ",supported=" + isSupported); 237 } 238 return isSupported; 239 } 240 getApiRequirements(String api)241 private static ApiRequirements getApiRequirements(String api) { 242 Member member = ApiHelper.resolve(api); 243 if (member == null) { 244 throw new IllegalArgumentException("API not found: " + api); 245 } 246 return getApiRequirements(member); 247 } 248 getApiRequirements(Member member)249 private static ApiRequirements getApiRequirements(Member member) { 250 return getAnnotation(ApiRequirements.class, member); 251 } 252 253 @SuppressWarnings("deprecation") getAddedInOrBefore(Member member)254 private static AddedInOrBefore getAddedInOrBefore(Member member) { 255 return getAnnotation(AddedInOrBefore.class, member); 256 } 257 getAnnotation(Class<T> annotationClass, Member member)258 private static <T extends Annotation> T getAnnotation(Class<T> annotationClass, Member member) { 259 if (member instanceof Field) { 260 return ((Field) member).getAnnotation(annotationClass); 261 } 262 if (member instanceof Method) { 263 return ((Method) member).getAnnotation(annotationClass); 264 } 265 throw new UnsupportedOperationException("Invalid member type for API: " + member); 266 } 267 268 @Override apply(Statement base, Description description)269 public Statement apply(Statement base, Description description) { 270 return new Statement() { 271 @Override 272 public void evaluate() throws Throwable { 273 base.evaluate(); 274 } 275 }; 276 } 277 278 // TODO(b/285930588):ApiCheckerRule is no longer required. But the code can be useful, 279 // Currently disabling the rule. As part of the bug, more investigation is required how to 280 // clean up API Checker Rule. 281 public Statement applyOld(Statement base, Description description) { 282 return new Statement() { 283 @Override 284 public void evaluate() throws Throwable { 285 mTestMethodName = description.getMethodName(); 286 try { 287 evaluateInternal(); 288 } finally { 289 mTestMethodName = null; 290 } 291 } 292 293 private void evaluateInternal() throws Throwable { 294 if (DBG) { 295 Log.d(TAG, "evaluating " + description.getDisplayName()); 296 } 297 298 // Need to do a basic version check first, as the rule could be used on ATS tests 299 // running on pre-mainline versions 300 if (!isPlatformSupported(description)) { 301 base.evaluate(); 302 return; 303 } 304 305 // Variables below are used to validate that all ApiRequirements are compatible 306 ApiTest apiTest = null; 307 NonApiTest nonApiTest = null; 308 ApiRequirements apiRequirementsOnApiUnderTest = null; 309 IgnoreInvalidApi ignoreInvalidApi = null; 310 311 // Optional annotations that change the behavior of the rule 312 SupportedVersionTest supportedVersionTest = null; 313 UnsupportedVersionTest unsupportedVersionTest = null; 314 315 // Other relevant annotations 316 @SuppressWarnings("deprecation") 317 AddedInOrBefore addedInOrBefore = null; 318 CddTest cddTest = null; 319 ApiRequirements apiRequirementsOnTest = null; // user only with CddTest 320 ApiRequirements effectiveApiRequirementsOnTest = null; 321 322 for (Annotation annotation : description.getAnnotations()) { 323 if (DBG) { 324 Log.d(TAG, "Annotation: " + annotation); 325 } 326 if (annotation instanceof ApiTest) { 327 apiTest = (ApiTest) annotation; 328 continue; 329 } 330 if (annotation instanceof NonApiTest) { 331 nonApiTest = (NonApiTest) annotation; 332 continue; 333 } 334 if (annotation instanceof ApiRequirements) { 335 apiRequirementsOnTest = (ApiRequirements) annotation; 336 continue; 337 } 338 if (annotation instanceof CddTest) { 339 cddTest = (CddTest) annotation; 340 continue; 341 } 342 if (annotation instanceof SupportedVersionTest) { 343 supportedVersionTest = (SupportedVersionTest) annotation; 344 continue; 345 } 346 if (annotation instanceof UnsupportedVersionTest) { 347 unsupportedVersionTest = (UnsupportedVersionTest) annotation; 348 continue; 349 } 350 if (annotation instanceof IgnoreInvalidApi) { 351 ignoreInvalidApi = (IgnoreInvalidApi) annotation; 352 continue; 353 } 354 } 355 356 if (DBG) { 357 Log.d(TAG, "Relevant annotations on test: " 358 + "ApiTest=" + apiTest 359 + " CddTest=" + cddTest 360 + " NonApiTest= " + nonApiTest 361 + " ApiRequirements=" + apiRequirementsOnTest 362 + " SupportedVersionTest=" + supportedVersionTest 363 + " UnsupportedVersionTest=" + unsupportedVersionTest 364 + " IgnoreInvalidApi=" + ignoreInvalidApi); 365 } 366 367 validateOptionalAnnotations(description.getTestClass(), description.getMethodName(), 368 supportedVersionTest, unsupportedVersionTest); 369 370 if (apiTest == null && (cddTest != null || nonApiTest != null)) { 371 validateNonApiAnnotations(cddTest, nonApiTest, apiRequirementsOnTest); 372 effectiveApiRequirementsOnTest = apiRequirementsOnTest; 373 } 374 375 if (apiTest == null && cddTest == null && nonApiTest == null) { 376 if (mEnforceTestApiAnnotations) { 377 throw new IllegalArgumentException( 378 "Test is missing @ApiTest, @NonApiTest, or @CddTest annotation"); 379 } else { 380 Log.w(TAG, "Test " + description 381 + " doesn't have @ApiTest, @NonApiTest, or @CddTest," 382 + "but rule is not enforcing it"); 383 } 384 } 385 386 if (apiTest != null) { 387 Pair<ApiRequirements, AddedInOrBefore> pair = getApiRequirementsFromApis( 388 description, apiTest, ignoreInvalidApi); 389 apiRequirementsOnApiUnderTest = pair.first; 390 if (effectiveApiRequirementsOnTest == null) { 391 // not set by CddTest 392 effectiveApiRequirementsOnTest = apiRequirementsOnApiUnderTest; 393 } 394 if (effectiveApiRequirementsOnTest == null && ignoreInvalidApi != null) { 395 effectiveApiRequirementsOnTest = apiRequirementsOnTest; 396 } 397 addedInOrBefore = pair.second; 398 } 399 400 if (DBG) { 401 Log.d(TAG, "Relevant annotations on APIs: " 402 + "ApiRequirements=" + apiRequirementsOnApiUnderTest 403 + ", AddedInOrBefore: " + addedInOrBefore); 404 } 405 406 if (apiRequirementsOnApiUnderTest != null && apiRequirementsOnTest != null) { 407 throw new IllegalArgumentException("Test cannot be annotated with both " 408 + "@ApiTest and @ApiRequirements"); 409 } 410 411 if (effectiveApiRequirementsOnTest == null) { 412 if (ignoreInvalidApi != null) { 413 if (mEnforceTestApiAnnotations) { 414 throw new IllegalArgumentException("Test contains @IgnoreInvalidApi but" 415 + " is missing @ApiRequirements"); 416 } else { 417 Log.w(TAG, "Test " + description + " contains @IgnoreInvalidApi and is " 418 + "missing @ApiRequirements, but rule is not enforcing them"); 419 } 420 } else if (addedInOrBefore == null) { 421 if (mEnforceApiRequirements) { 422 throw new IllegalArgumentException("Missing @ApiRequirements " 423 + "or @AddedInOrBefore"); 424 } else { 425 Log.w(TAG, "Test " + description + " doesn't have required " 426 + "@ApiRequirements or @AddedInOrBefore but rule is not " 427 + "enforcing them"); 428 } 429 } 430 base.evaluate(); 431 return; 432 } 433 434 // Finally, run the test and assert results depending on whether it's supported or 435 // not 436 apply(base, description, effectiveApiRequirementsOnTest, supportedVersionTest, 437 unsupportedVersionTest); 438 } 439 }; 440 } // apply 441 442 protected boolean isPlatformSupported(Description description) { 443 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { 444 Log.d(TAG, "Running " + description.getDisplayName() + " as-is on pre-TM platform build" 445 + " (" + Build.VERSION.SDK_INT + ")"); 446 return false; 447 } 448 if (!Car.isApiVersionAtLeast(Build.VERSION_CODES.TIRAMISU, /* minor= */ 1)) { 449 Log.d(TAG, "Running " + description.getDisplayName() + " as-is on pre-TM-QPR1 Car build" 450 + " (major=" + Car.getCarVersion().getMajorVersion() 451 + ", minor=" + Car.getCarVersion().getMinorVersion() + ")"); 452 return false; 453 } 454 return true; 455 } 456 457 private void validateNonApiAnnotations(CddTest cddTest, NonApiTest nonApiTest, 458 ApiRequirements apiRequirements) { 459 if (cddTest != null && nonApiTest != null) { 460 throw new IllegalArgumentException("Test contains both " + nonApiTest.annotationType() 461 + " annotation (" + nonApiTest + ") and " + cddTest.annotationType() 462 + " annotation (" + cddTest + ")"); 463 } 464 465 if (cddTest != null) { 466 validateCddTestAnnotation(cddTest, apiRequirements); 467 return; 468 } 469 470 if (nonApiTest != null) { 471 validateNonApiTestAnnotation(nonApiTest, apiRequirements); 472 } 473 } 474 475 private void validateCddTestAnnotation(CddTest cddTest, ApiRequirements apiRequirements) { 476 @SuppressWarnings("deprecation") 477 String deprecatedRequirement = cddTest.requirement(); 478 479 if (!TextUtils.isEmpty(deprecatedRequirement)) { 480 throw new IllegalArgumentException("Test contains " + cddTest.annotationType() 481 + " annotation (" + cddTest + "), but it's using the" 482 + " deprecated 'requirement' field (value=" + deprecatedRequirement + "); it " 483 + "should use 'requirements' instead"); 484 } 485 486 String[] requirements = cddTest.requirements(); 487 488 if (requirements == null || requirements.length == 0) { 489 throw new IllegalArgumentException("Test contains " + cddTest.annotationType() 490 + " annotation (" + cddTest 491 + "), but it's 'requirements' field is empty (value=" 492 + Arrays.toString(requirements) + ")"); 493 } 494 for (String requirement : requirements) { 495 String trimmedRequirement = requirement == null ? "" : requirement.trim(); 496 if (TextUtils.isEmpty(trimmedRequirement)) { 497 throw new IllegalArgumentException("Test contains " + cddTest.annotationType() 498 + " annotation (" + cddTest + "), but it contains an empty requirement" 499 + "(requirements=" + Arrays.toString(requirements) + ")"); 500 } 501 } 502 503 // CddTest itself is valid, must have ApiRequirements 504 if (apiRequirements == null) { 505 throw new IllegalArgumentException("Test contains " + cddTest.annotationType() 506 + " annotation (" + cddTest + "), but it's missing @ApiRequirements)"); 507 } 508 } 509 510 public void validateNonApiTestAnnotation(NonApiTest nonApiTest, 511 ApiRequirements apiRequirements) { 512 if (apiRequirements == null) { 513 throw new IllegalArgumentException("Test contains " + nonApiTest.annotationType() 514 + " annotation (" + nonApiTest + "), but it's missing @ApiRequirements)"); 515 } 516 } 517 518 @SuppressWarnings("deprecation") 519 private Pair<ApiRequirements, AddedInOrBefore> getApiRequirementsFromApis( 520 Description description, ApiTest apiTest, @Nullable IgnoreInvalidApi ignoreInvalidApi) { 521 ApiRequirements firstApiRequirements = null; 522 AddedInOrBefore addedInOrBefore = null; 523 List<String> allApis = new ArrayList<>(); 524 List<ApiRequirements> allApiRequirements = new ArrayList<>(); 525 boolean compatibleApis = true; 526 527 String[] apis = apiTest.apis(); 528 if (apis == null || apis.length == 0) { 529 throw new IllegalArgumentException("empty @ApiTest annotation"); 530 } 531 List<String> invalidApis = new ArrayList<>(); 532 for (String api : apis) { 533 allApis.add(api); 534 Member member = ApiHelper.resolve(api); 535 if (member == null) { 536 invalidApis.add(api); 537 continue; 538 } 539 ApiRequirements apiRequirements = getApiRequirements(member); 540 if (apiRequirements == null && addedInOrBefore == null) { 541 addedInOrBefore = getAddedInOrBefore(member); 542 if (DBG) { 543 Log.d(TAG, "No @ApiRequirements on " + api + "; trying " 544 + "@AddedInOrBefore instead: " + addedInOrBefore); 545 } 546 continue; 547 } 548 549 if (apiRequirements == null) { 550 continue; 551 } 552 allApiRequirements.add(apiRequirements); 553 if (firstApiRequirements == null) { 554 firstApiRequirements = apiRequirements; 555 continue; 556 } 557 // Make sure all ApiRequirements are compatible 558 if (!apiRequirements.minCarVersion() 559 .equals(firstApiRequirements.minCarVersion()) 560 || !apiRequirements.minPlatformVersion() 561 .equals(firstApiRequirements.minPlatformVersion())) { 562 Log.w(TAG, "Found incompatible API requirement (" + apiRequirements 563 + ") on " + api + "(first ApiRequirements is " 564 + firstApiRequirements + ")"); 565 compatibleApis = false; 566 } else { 567 Log.d(TAG, "Multiple @ApiRequirements found but they're compatible"); 568 } 569 } 570 if (!invalidApis.isEmpty()) { 571 if (ignoreInvalidApi != null) { 572 Log.i(TAG, "Could not resolve some APIs (" + invalidApis + ") on annotation (" 573 + apiTest + "), but letting it go due to " + ignoreInvalidApi); 574 } else { 575 throw new IllegalArgumentException("Could not resolve some APIs (" 576 + invalidApis + ") on annotation (" + apiTest + ")"); 577 } 578 } else if (!compatibleApis) { 579 throw new IncompatibleApiRequirementsException(allApis, allApiRequirements); 580 } 581 return new Pair<>(firstApiRequirements, addedInOrBefore); 582 } 583 584 private void validateOptionalAnnotations(Class<?> testClass, String testMethodName, 585 @Nullable SupportedVersionTest supportedVersionAnnotationOnTestMethod, 586 @Nullable UnsupportedVersionTest unsupportedVersionAnnotationOnTestMethod) { 587 if (unsupportedVersionAnnotationOnTestMethod != null 588 && supportedVersionAnnotationOnTestMethod != null) { 589 throw new IllegalArgumentException("test must be annotated with either " 590 + "supportedVersionTest or unsupportedVersionTest, not both"); 591 } 592 if (unsupportedVersionAnnotationOnTestMethod != null) { 593 validateUnsupportedVersionTest(testClass, testMethodName, 594 unsupportedVersionAnnotationOnTestMethod); 595 return; 596 } 597 if (supportedVersionAnnotationOnTestMethod != null) { 598 validateSupportedVersionTest(testClass, testMethodName, 599 supportedVersionAnnotationOnTestMethod); 600 return; 601 } 602 } 603 604 private void validateUnsupportedVersionTest(Class<?> testClass, String testMethodName, 605 @Nullable UnsupportedVersionTest unsupportedVersionAnnotationOnTestMethod) { 606 // Test class must have a counterpart supportedVersionTest 607 String supportedVersionMethodName = unsupportedVersionAnnotationOnTestMethod 608 .supportedVersionTest(); 609 if (TextUtils.isEmpty(supportedVersionMethodName)) { 610 throw new IllegalArgumentException("missing supportedVersionTest on " 611 + unsupportedVersionAnnotationOnTestMethod); 612 } 613 614 Method supportedVersionMethod = null; 615 Class<?>[] noParams = {}; 616 try { 617 supportedVersionMethod = testClass.getDeclaredMethod(supportedVersionMethodName, 618 noParams); 619 } catch (Exception e) { 620 Log.w(TAG, "Error getting method named " + supportedVersionMethodName 621 + " on class " + testClass, e); 622 throw new IllegalArgumentException("invalid supportedVersionTest on " 623 + unsupportedVersionAnnotationOnTestMethod + ": " + e); 624 } 625 // And it must be annotated with @SupportedVersionTest 626 SupportedVersionTest supportedVersionAnnotationOnUnsupportedMethod = 627 supportedVersionMethod.getAnnotation(SupportedVersionTest.class); 628 if (supportedVersionAnnotationOnUnsupportedMethod == null) { 629 throw new IllegalArgumentException( 630 "invalid supportedVersionTest method (" + supportedVersionMethodName 631 + " on " + unsupportedVersionAnnotationOnTestMethod 632 + ": it's not annotated with @SupportedVersionTest"); 633 } 634 635 // which in turn must point to the UnsupportedVersionTest itself 636 String unsupportedVersionMethodOnSupportedAnnotation = 637 supportedVersionAnnotationOnUnsupportedMethod.unsupportedVersionTest(); 638 if (!testMethodName.equals(unsupportedVersionMethodOnSupportedAnnotation)) { 639 throw new IllegalArgumentException( 640 "invalid unsupportedVersionTest on " 641 + supportedVersionAnnotationOnUnsupportedMethod 642 + " annotation on method " + supportedVersionMethodName 643 + ": it should be " + testMethodName); 644 } 645 } 646 647 private void validateSupportedVersionTest(Class<?> testClass, String testMethodName, 648 @Nullable SupportedVersionTest supportedVersionAnnotationOnTestMethod) { 649 // Test class must have a counterpart unsupportedVersionTest 650 String unsupportedVersionMethodName = supportedVersionAnnotationOnTestMethod 651 .unsupportedVersionTest(); 652 if (TextUtils.isEmpty(unsupportedVersionMethodName)) { 653 throw new IllegalArgumentException("missing unsupportedVersionTest on " 654 + supportedVersionAnnotationOnTestMethod); 655 } 656 657 Method unsupportedVersionMethod = null; 658 Class<?>[] noParams = {}; 659 try { 660 unsupportedVersionMethod = testClass.getDeclaredMethod(unsupportedVersionMethodName, 661 noParams); 662 } catch (Exception e) { 663 Log.w(TAG, "Error getting method named " + unsupportedVersionMethodName 664 + " on class " + testClass, e); 665 throw new IllegalArgumentException("invalid supportedVersionTest on " 666 + supportedVersionAnnotationOnTestMethod + ": " + e); 667 } 668 // And it must be annotated with @UnupportedVersionTest 669 UnsupportedVersionTest unsupportedVersionAnnotationOnUnsupportedMethod = 670 unsupportedVersionMethod.getAnnotation(UnsupportedVersionTest.class); 671 if (unsupportedVersionAnnotationOnUnsupportedMethod == null) { 672 throw new IllegalArgumentException( 673 "invalid supportedVersionTest method (" + unsupportedVersionMethodName 674 + " on " + supportedVersionAnnotationOnTestMethod 675 + ": it's not annotated with @UnsupportedVersionTest"); 676 } 677 678 // which in turn must point to the UnsupportedVersionTest itself 679 String supportedVersionMethodOnSupportedAnnotation = 680 unsupportedVersionAnnotationOnUnsupportedMethod.supportedVersionTest(); 681 if (!testMethodName.equals(supportedVersionMethodOnSupportedAnnotation)) { 682 throw new IllegalArgumentException( 683 "invalid supportedVersionTest on " 684 + unsupportedVersionAnnotationOnUnsupportedMethod 685 + " annotation on method " + unsupportedVersionMethodName 686 + ": it should be " + testMethodName); 687 } 688 } 689 690 private void apply(Statement base, Description description, 691 @Nullable ApiRequirements apiRequirements, 692 @Nullable SupportedVersionTest supportedVersionTest, 693 @Nullable UnsupportedVersionTest unsupportedVersionTest) 694 throws Throwable { 695 if (DBG) { 696 Log.d(TAG, "Applying rule using ApiRequirements=" + apiRequirements); 697 } 698 if (apiRequirements == null) { 699 Log.w(TAG, "No @ApiRequirements on " + description.getDisplayName() 700 + " (most likely it's annotated with @AddedInOrBefore), running it always"); 701 base.evaluate(); 702 return; 703 } 704 if (isSupported(apiRequirements)) { 705 applyOnSupportedVersion(base, description, apiRequirements, unsupportedVersionTest); 706 return; 707 } 708 709 applyOnUnsupportedVersion(base, description, apiRequirements, supportedVersionTest, 710 unsupportedVersionTest); 711 } 712 713 private void applyOnSupportedVersion(Statement base, Description description, 714 ApiRequirements apiRequirements, 715 @Nullable UnsupportedVersionTest unsupportedVersionTest) 716 throws Throwable { 717 if (unsupportedVersionTest == null) { 718 if (DBG) { 719 Log.d(TAG, "Car / Platform combo is supported, running " 720 + description.getDisplayName()); 721 } 722 base.evaluate(); 723 return; 724 } 725 726 Log.i(TAG, "Car / Platform combo IS supported, but ignoring " 727 + description.getDisplayName() + " because it's annotated with " 728 + unsupportedVersionTest); 729 730 throw new ExpectedVersionAssumptionViolationException(unsupportedVersionTest, 731 Car.getCarVersion(), Car.getPlatformVersion(), apiRequirements); 732 } 733 734 private void applyOnUnsupportedVersion(Statement base, Description description, 735 ApiRequirements apiRequirements, @Nullable SupportedVersionTest supportedVersionTest, 736 @Nullable UnsupportedVersionTest unsupportedVersionTest) 737 throws Throwable { 738 Behavior behavior = unsupportedVersionTest == null ? null 739 : unsupportedVersionTest.behavior(); 740 if (supportedVersionTest == null && !Behavior.EXPECT_PASS.equals(behavior)) { 741 Log.i(TAG, "Car / Platform combo is NOT supported, running " 742 + description.getDisplayName() + " but expecting " 743 + "PlatformVersionMismatchException"); 744 try { 745 base.evaluate(); 746 throw new PlatformVersionMismatchExceptionNotThrownException( 747 Car.getCarVersion(), Car.getPlatformVersion(), apiRequirements); 748 } catch (PlatformVersionMismatchException e) { 749 if (DBG) { 750 Log.d(TAG, "Exception thrown as expected: " + e); 751 } 752 } 753 return; 754 } 755 756 if (supportedVersionTest != null) { 757 Log.i(TAG, "Car / Platform combo is NOT supported, but ignoring " 758 + description.getDisplayName() + " because it's annotated with " 759 + supportedVersionTest); 760 761 throw new ExpectedVersionAssumptionViolationException(supportedVersionTest, 762 Car.getCarVersion(), Car.getPlatformVersion(), apiRequirements); 763 } 764 765 // At this point, it's annotated with RUN_ALWAYS 766 Log.i(TAG, "Car / Platform combo is NOT supported but running anyways becaucase test is" 767 + " annotated with " + unsupportedVersionTest); 768 base.evaluate(); 769 } 770 771 /** 772 * Defines the behavior of a test when it's run in an unsupported device (when it's run in a 773 * supported device, the rule will throw a {@link ExpectedVersionAssumptionViolationException} 774 * exception). 775 * 776 * <p>Without this annotation, a test is expected to throw a 777 * {@link PlatformVersionMismatchException} when running in an unsupported version. 778 * 779 * <p><b>Note: </b>a test annotated with this annotation <b>MUST</b> have a counterpart test 780 * annotated with {@link SupportedVersionTest}. 781 */ 782 @Retention(RUNTIME) 783 @Target({TYPE, METHOD}) 784 public @interface UnsupportedVersionTest { 785 786 /** 787 * Name of the counterpart test should be run on supported versions; such test must be 788 * annoted with {@link SupportedVersionTest}, whith its {@code unsupportedVersionTest} 789 * value point to the test being annotated with this annotation. 790 */ 791 String supportedVersionTest(); 792 793 /** 794 * Behavior of the test when it's run on unsupported versions. 795 */ 796 Behavior behavior() default Behavior.EXPECT_THROWS_VERSION_MISMATCH_EXCEPTION; 797 798 @SuppressWarnings("Enum") 799 enum Behavior { 800 /** 801 * Rule will run the test and assert it throws a 802 * {@link PlatformVersionMismatchException}. 803 */ 804 EXPECT_THROWS_VERSION_MISMATCH_EXCEPTION, 805 806 /** Rule will run the test and assume it will pass.*/ 807 EXPECT_PASS 808 } 809 } 810 811 /** 812 * Defines a test to be a counterpart of a test annotated with {@link UnsupportedVersionTest}. 813 * 814 * <p>Such test will be run as usual on supported devices, but will throw a 815 * {@link ExpectedVersionAssumptionViolationException} when running on unsupported devices. 816 * 817 */ 818 @Retention(RUNTIME) 819 @Target({TYPE, METHOD}) 820 public @interface SupportedVersionTest { 821 822 /** 823 * Name of the counterpart test should be run on unsupported versions; such test must be 824 * annoted with {@link UnsupportedVersionTest}, whith its {@code supportedVersionTest} 825 * value point to the test being annotated with this annotation. 826 */ 827 String unsupportedVersionTest(); 828 829 } 830 831 /*** 832 * Tells the rule to ignore an invalid API passed to {@link ApiTest}. 833 * 834 * <p>Should be used in cases where the API is being indirectly tested (for example, through a 835 * shell command) and hence is not available in the test's classpath. 836 * 837 * <p>Should be used in conjunction with {@link ApiRequirements}. 838 * 839 */ 840 @Retention(RUNTIME) 841 @Target({TYPE, METHOD}) 842 public @interface IgnoreInvalidApi { 843 844 /** 845 * Reason why the invalid API should be ignored. 846 */ 847 String reason(); 848 } 849 850 public static final class ExpectedVersionAssumptionViolationException 851 extends AssumptionViolatedException { 852 853 private static final long serialVersionUID = 1L; 854 855 private final CarVersion mCarVersion; 856 private final PlatformVersion mPlatformVersion; 857 private final ApiRequirements mApiRequirements; 858 859 ExpectedVersionAssumptionViolationException(Annotation annotation, CarVersion carVersion, 860 PlatformVersion platformVersion, ApiRequirements apiRequirements) { 861 super("Test annotated with @" + annotation.annotationType().getCanonicalName() 862 + " when running on unsupported platform: CarVersion=" + carVersion 863 + ", PlatformVersion=" + platformVersion 864 + ", ApiRequirements=" + apiRequirements); 865 866 mCarVersion = carVersion; 867 mPlatformVersion = platformVersion; 868 mApiRequirements = apiRequirements; 869 } 870 871 public CarVersion getCarVersion() { 872 return mCarVersion; 873 } 874 875 public PlatformVersion getPlatformVersion() { 876 return mPlatformVersion; 877 } 878 879 public ApiRequirements getApiRequirements() { 880 return mApiRequirements; 881 } 882 } 883 884 public static final class PlatformVersionMismatchExceptionNotThrownException 885 extends IllegalStateException { 886 887 private static final long serialVersionUID = 1L; 888 889 private final CarVersion mCarVersion; 890 private final PlatformVersion mPlatformVersion; 891 private final ApiRequirements mApiRequirements; 892 893 PlatformVersionMismatchExceptionNotThrownException(CarVersion carVersion, 894 PlatformVersion platformVersion, ApiRequirements apiRequirements) { 895 super("Test should throw " + PlatformVersionMismatchException.class.getSimpleName() 896 + " when running on unsupported platform: CarVersion=" + carVersion 897 + ", PlatformVersion=" + platformVersion 898 + ", ApiRequirements=" + apiRequirements); 899 900 mCarVersion = carVersion; 901 mPlatformVersion = platformVersion; 902 mApiRequirements = apiRequirements; 903 } 904 905 public CarVersion getCarVersion() { 906 return mCarVersion; 907 } 908 909 public PlatformVersion getPlatformVersion() { 910 return mPlatformVersion; 911 } 912 913 public ApiRequirements getApiRequirements() { 914 return mApiRequirements; 915 } 916 } 917 918 public static final class IncompatibleApiRequirementsException 919 extends IllegalArgumentException { 920 921 private static final long serialVersionUID = 1L; 922 923 private final List<String> mApis; 924 private final List<ApiRequirements> mApiRequirements; 925 926 IncompatibleApiRequirementsException(List<String> apis, 927 List<ApiRequirements> apiRequirements) { 928 super("Incompatible API requirements (apis=" + apis + ", apiRequirements=" 929 + apiRequirements + ") on test, consider splitting it into multiple methods"); 930 931 mApis = apis; 932 mApiRequirements = apiRequirements; 933 } 934 935 public List<String> getApis() { 936 return mApis; 937 } 938 939 public List<ApiRequirements> getApiRequirements() { 940 return mApiRequirements; 941 } 942 } 943 } 944