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