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