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