• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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