Home | History | Annotate | Download | only in android
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
      5  * except in compliance with the License. You may obtain a copy of the License at
      6  *
      7  *      http://www.apache.org/licenses/LICENSE-2.0
      8  *
      9  * Unless required by applicable law or agreed to in writing, software distributed under the
     10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     11  * KIND, either express or implied. See the License for the specific language governing
     12  * permissions and limitations under the License.
     13  */
     14 
     15 package com.android;
     16 
     17 import static org.hamcrest.Matchers.empty;
     18 import static org.hamcrest.Matchers.is;
     19 import static org.junit.Assert.assertThat;
     20 
     21 import android.content.pm.PackageManager;
     22 import android.testing.AndroidTestingRunner;
     23 import android.text.TextUtils;
     24 import android.util.Log;
     25 
     26 import androidx.test.filters.LargeTest;
     27 import androidx.test.filters.MediumTest;
     28 import androidx.test.filters.SmallTest;
     29 import androidx.test.internal.runner.ClassPathScanner;
     30 import androidx.test.internal.runner.ClassPathScanner.ChainedClassNameFilter;
     31 import androidx.test.internal.runner.ClassPathScanner.ExternalClassNameFilter;
     32 
     33 import com.android.systemui.SysuiBaseFragmentTest;
     34 import com.android.systemui.SysuiTestCase;
     35 
     36 import org.junit.Test;
     37 import org.junit.runner.RunWith;
     38 
     39 import java.io.IOException;
     40 import java.lang.reflect.Method;
     41 import java.lang.reflect.Modifier;
     42 import java.util.ArrayList;
     43 import java.util.Arrays;
     44 import java.util.Collection;
     45 import java.util.Collections;
     46 
     47 /**
     48  * This is named AAAPlusPlusVerifySysuiRequiredTestPropertiesTest for two reasons.
     49  * a) Its so awesome it deserves an AAA++
     50  * b) It should run first to draw attention to itself.
     51  *
     52  * For trues though: this test verifies that all the sysui tests extend the right classes.
     53  * This matters because including tests with different context implementations in the same
     54  * test suite causes errors, such as the incorrect settings provider being cached.
     55  * For an example, see {@link com.android.systemui.DependencyTest}.
     56  */
     57 @RunWith(AndroidTestingRunner.class)
     58 @SmallTest
     59 public class AAAPlusPlusVerifySysuiRequiredTestPropertiesTest extends SysuiTestCase {
     60 
     61     private static final String TAG = "AAA++VerifyTest";
     62 
     63     private static final Class[] BASE_CLS_WHITELIST = {
     64             SysuiTestCase.class,
     65             SysuiBaseFragmentTest.class,
     66     };
     67 
     68     private static final Class[] SUPPORTED_SIZES = {
     69             SmallTest.class,
     70             MediumTest.class,
     71             LargeTest.class,
     72             android.test.suitebuilder.annotation.SmallTest.class,
     73             android.test.suitebuilder.annotation.MediumTest.class,
     74             android.test.suitebuilder.annotation.LargeTest.class,
     75     };
     76 
     77     @Test
     78     public void testAllClassInheritance() throws Throwable {
     79         ArrayList<String> fails = new ArrayList<>();
     80         for (String className : getClassNamesFromClassPath()) {
     81             Class<?> cls = Class.forName(className, false, this.getClass().getClassLoader());
     82             if (!isTestClass(cls)) continue;
     83 
     84             boolean hasParent = false;
     85             for (Class<?> parent : BASE_CLS_WHITELIST) {
     86                 if (parent.isAssignableFrom(cls)) {
     87                     hasParent = true;
     88                     break;
     89                 }
     90             }
     91             boolean hasSize = hasSize(cls);
     92             if (!hasSize) {
     93                 fails.add(cls.getName() + " does not have size annotation, such as @SmallTest");
     94             }
     95             if (!hasParent) {
     96                 fails.add(cls.getName() + " does not extend any of " + getClsStr());
     97             }
     98         }
     99 
    100         assertThat("All sysui test classes must have size and extend one of " + getClsStr(),
    101                 fails, is(empty()));
    102     }
    103 
    104     private boolean hasSize(Class<?> cls) {
    105         for (int i = 0; i < SUPPORTED_SIZES.length; i++) {
    106             if (cls.getDeclaredAnnotation(SUPPORTED_SIZES[i]) != null) return true;
    107         }
    108         return false;
    109     }
    110 
    111     private Collection<String> getClassNamesFromClassPath() {
    112         ClassPathScanner scanner = new ClassPathScanner(mContext.getPackageCodePath());
    113 
    114         ChainedClassNameFilter filter = new ChainedClassNameFilter();
    115 
    116         filter.add(new ExternalClassNameFilter());
    117         filter.add(s -> s.startsWith("com.android.systemui")
    118                 || s.startsWith("com.android.keyguard"));
    119 
    120 
    121         if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
    122             // If it's not automotive target, exclude automotive classes from the test.
    123             excludeAutomotiveClasses(filter);
    124         }
    125 
    126         try {
    127             return scanner.getClassPathEntries(filter);
    128         } catch (IOException e) {
    129             Log.e(TAG, "Failed to scan classes", e);
    130         }
    131         return Collections.emptyList();
    132     }
    133 
    134     private void excludeAutomotiveClasses(ChainedClassNameFilter filter) {
    135         // Modifies the passed in filter.
    136         filter.add(s -> !s.startsWith("com.android.systemui.statusbar.car."));
    137         filter.add(s -> !s.startsWith("com.android.systemui.qs.car."));
    138         filter.add(s -> !s.startsWith("com.android.systemui.car."));
    139     }
    140 
    141     private String getClsStr() {
    142         return TextUtils.join(",", Arrays.asList(BASE_CLS_WHITELIST)
    143                 .stream().map(cls -> cls.getSimpleName()).toArray());
    144     }
    145 
    146     /**
    147      * Determines if given class is a valid test class.
    148      *
    149      * @param loadedClass
    150      * @return <code>true</code> if loadedClass is a test
    151      */
    152     private boolean isTestClass(Class<?> loadedClass) {
    153         try {
    154             if (Modifier.isAbstract(loadedClass.getModifiers())) {
    155                 logDebug(String.format("Skipping abstract class %s: not a test",
    156                         loadedClass.getName()));
    157                 return false;
    158             }
    159             // TODO: try to find upstream junit calls to replace these checks
    160             if (junit.framework.Test.class.isAssignableFrom(loadedClass)) {
    161                 // ensure that if a TestCase, it has at least one test method otherwise
    162                 // TestSuite will throw error
    163                 if (junit.framework.TestCase.class.isAssignableFrom(loadedClass)) {
    164                     return hasJUnit3TestMethod(loadedClass);
    165                 }
    166                 return true;
    167             }
    168             // TODO: look for a 'suite' method?
    169             if (loadedClass.isAnnotationPresent(org.junit.runner.RunWith.class)) {
    170                 return true;
    171             }
    172             for (Method testMethod : loadedClass.getMethods()) {
    173                 if (testMethod.isAnnotationPresent(org.junit.Test.class)) {
    174                     return true;
    175                 }
    176             }
    177             logDebug(String.format("Skipping class %s: not a test", loadedClass.getName()));
    178             return false;
    179         } catch (Exception e) {
    180             // Defensively catch exceptions - Will throw runtime exception if it cannot load methods.
    181             // For earlier versions of Android (Pre-ICS), Dalvik might try to initialize a class
    182             // during getMethods(), fail to do so, hide the error and throw a NoSuchMethodException.
    183             // Since the java.lang.Class.getMethods does not declare such an exception, resort to a
    184             // generic catch all.
    185             // For ICS+, Dalvik will throw a NoClassDefFoundException.
    186             Log.w(TAG, String.format("%s in isTestClass for %s", e.toString(),
    187                     loadedClass.getName()));
    188             return false;
    189         } catch (Error e) {
    190             // defensively catch Errors too
    191             Log.w(TAG, String.format("%s in isTestClass for %s", e.toString(),
    192                     loadedClass.getName()));
    193             return false;
    194         }
    195     }
    196 
    197     private boolean hasJUnit3TestMethod(Class<?> loadedClass) {
    198         for (Method testMethod : loadedClass.getMethods()) {
    199             if (isPublicTestMethod(testMethod)) {
    200                 return true;
    201             }
    202         }
    203         return false;
    204     }
    205 
    206     // copied from junit.framework.TestSuite
    207     private boolean isPublicTestMethod(Method m) {
    208         return isTestMethod(m) && Modifier.isPublic(m.getModifiers());
    209     }
    210 
    211     // copied from junit.framework.TestSuite
    212     private boolean isTestMethod(Method m) {
    213         return m.getParameterTypes().length == 0 && m.getName().startsWith("test")
    214                 && m.getReturnType().equals(Void.TYPE);
    215     }
    216 
    217     /**
    218      * Utility method for logging debug messages. Only actually logs a message if TAG is marked
    219      * as loggable to limit log spam during normal use.
    220      */
    221     private void logDebug(String msg) {
    222         if (Log.isLoggable(TAG, Log.DEBUG)) {
    223             Log.d(TAG, msg);
    224         }
    225     }
    226 }
    227