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