• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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_WHITELIST = {
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             android.test.suitebuilder.annotation.SmallTest.class,
72             android.test.suitebuilder.annotation.MediumTest.class,
73             android.test.suitebuilder.annotation.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_WHITELIST) {
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 = new ChainedClassNameFilter();
114 
115         filter.add(new ExternalClassNameFilter());
116         filter.add(s -> s.startsWith("com.android.systemui")
117                 || s.startsWith("com.android.keyguard"));
118 
119         // Screenshots run in an isolated process and should not be run
120         // with the main process dependency graph because it will not exist
121         // at runtime and could lead to incorrect tests which assume
122         // the main SystemUI process. Therefore, exclude this package
123         // from the base class whitelist.
124         filter.add(s -> !s.startsWith("com.android.systemui.screenshot"));
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 
getClsStr()134     private String getClsStr() {
135         return TextUtils.join(",", Arrays.asList(BASE_CLS_WHITELIST)
136                 .stream().map(cls -> cls.getSimpleName()).toArray());
137     }
138 
139     /**
140      * Determines if given class is a valid test class.
141      *
142      * @param loadedClass
143      * @return <code>true</code> if loadedClass is a test
144      */
isTestClass(Class<?> loadedClass)145     private boolean isTestClass(Class<?> loadedClass) {
146         try {
147             if (Modifier.isAbstract(loadedClass.getModifiers())) {
148                 logDebug(String.format("Skipping abstract class %s: not a test",
149                         loadedClass.getName()));
150                 return false;
151             }
152             // TODO: try to find upstream junit calls to replace these checks
153             if (junit.framework.Test.class.isAssignableFrom(loadedClass)) {
154                 // ensure that if a TestCase, it has at least one test method otherwise
155                 // TestSuite will throw error
156                 if (junit.framework.TestCase.class.isAssignableFrom(loadedClass)) {
157                     return hasJUnit3TestMethod(loadedClass);
158                 }
159                 return true;
160             }
161             // TODO: look for a 'suite' method?
162             if (loadedClass.isAnnotationPresent(org.junit.runner.RunWith.class)) {
163                 return true;
164             }
165             for (Method testMethod : loadedClass.getMethods()) {
166                 if (testMethod.isAnnotationPresent(org.junit.Test.class)) {
167                     return true;
168                 }
169             }
170             logDebug(String.format("Skipping class %s: not a test", loadedClass.getName()));
171             return false;
172         } catch (Exception e) {
173             // Defensively catch exceptions - Will throw runtime exception if it cannot load methods.
174             // For earlier versions of Android (Pre-ICS), Dalvik might try to initialize a class
175             // during getMethods(), fail to do so, hide the error and throw a NoSuchMethodException.
176             // Since the java.lang.Class.getMethods does not declare such an exception, resort to a
177             // generic catch all.
178             // For ICS+, Dalvik will throw a NoClassDefFoundException.
179             Log.w(TAG, String.format("%s in isTestClass for %s", e.toString(),
180                     loadedClass.getName()));
181             return false;
182         } catch (Error e) {
183             // defensively catch Errors too
184             Log.w(TAG, String.format("%s in isTestClass for %s", e.toString(),
185                     loadedClass.getName()));
186             return false;
187         }
188     }
189 
hasJUnit3TestMethod(Class<?> loadedClass)190     private boolean hasJUnit3TestMethod(Class<?> loadedClass) {
191         for (Method testMethod : loadedClass.getMethods()) {
192             if (isPublicTestMethod(testMethod)) {
193                 return true;
194             }
195         }
196         return false;
197     }
198 
199     // copied from junit.framework.TestSuite
isPublicTestMethod(Method m)200     private boolean isPublicTestMethod(Method m) {
201         return isTestMethod(m) && Modifier.isPublic(m.getModifiers());
202     }
203 
204     // copied from junit.framework.TestSuite
isTestMethod(Method m)205     private boolean isTestMethod(Method m) {
206         return m.getParameterTypes().length == 0 && m.getName().startsWith("test")
207                 && m.getReturnType().equals(Void.TYPE);
208     }
209 
210     /**
211      * Utility method for logging debug messages. Only actually logs a message if TAG is marked
212      * as loggable to limit log spam during normal use.
213      */
logDebug(String msg)214     private void logDebug(String msg) {
215         if (Log.isLoggable(TAG, Log.DEBUG)) {
216             Log.d(TAG, msg);
217         }
218     }
219 }
220