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