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