1 /*
2  * Copyright (C) 2016 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 /**
18  * This file is a copy of https://cs.corp.google.com/android/frameworks/testing/runner/src/main/java/android/support/test/internal/runner/TestLoader.java
19  * The only changes that have been made starts with // Libcore-specific
20  */
21 
22 package com.android.cts.core.internal.runner;
23 
24 import android.util.Log;
25 
26 import org.junit.runner.Description;
27 import org.junit.runner.notification.Failure;
28 
29 import java.lang.annotation.Annotation;
30 import java.lang.reflect.Method;
31 import java.lang.reflect.Modifier;
32 import java.util.Collection;
33 import java.util.LinkedHashMap;
34 import java.util.List;
35 import java.util.Map;
36 
37 /**
38  * A class for loading JUnit3 and JUnit4 test classes given a set of potential class names.
39  */
40 public final class TestLoader {
41 
42     private static final String LOG_TAG = "TestLoader";
43     // Libcore-specific change: Fully qualified name of TestNG annotation class.
44     private static final String TESTNG_TEST = "org.testng.annotations.Test";
45 
46     private Map<String, Class<?>> mLoadedClassesMap = new LinkedHashMap<String, Class<?>>();
47     private Map<String, Failure> mLoadFailuresMap = new LinkedHashMap<String, Failure>();
48 
49     private ClassLoader mClassLoader;
50 
51     /**
52      * Set the {@link ClassLoader} to be used to load test cases.
53      *
54      * @param loader {@link ClassLoader} to load test cases with.
55      */
setClassLoader(ClassLoader loader)56     public void setClassLoader(ClassLoader loader) {
57         mClassLoader = loader;
58     }
59 
60     /**
61      * Loads the test class from a given class name if its not already loaded.
62      * <p/>
63      * Will store the result internally. Successfully loaded classes can be retrieved via
64      * {@link #getLoadedClasses()}, failures via {@link #getLoadFailures()}.
65      *
66      * @param className the class name to attempt to load
67      * @return the loaded class or null.
68      */
loadClass(String className)69     public Class<?> loadClass(String className) {
70         Class<?> loadedClass = doLoadClass(className);
71         if (loadedClass != null) {
72             mLoadedClassesMap.put(className, loadedClass);
73         }
74         return loadedClass;
75     }
76 
getClassLoader()77     protected ClassLoader getClassLoader() {
78         if (mClassLoader != null) {
79             return mClassLoader;
80         }
81 
82         // TODO: InstrumentationTestRunner uses
83         // Class.forName(className, false, getTargetContext().getClassLoader());
84         // Evaluate if that is needed. Initial testing indicates
85         // getTargetContext().getClassLoader() == this.getClass().getClassLoader()
86         return this.getClass().getClassLoader();
87     }
88 
doLoadClass(String className)89     private Class<?> doLoadClass(String className) {
90         if (mLoadFailuresMap.containsKey(className)) {
91             // Don't load classes that already failed to load
92             return null;
93         } else if (mLoadedClassesMap.containsKey(className)) {
94             // Class with the same name was already loaded, return it
95             return mLoadedClassesMap.get(className);
96         }
97 
98         try {
99             ClassLoader myClassLoader = getClassLoader();
100             return Class.forName(className, false, myClassLoader);
101         } catch (ClassNotFoundException e) {
102             String errMsg = String.format("Could not find class: %s", className);
103             Log.e(LOG_TAG, errMsg);
104             Description description = Description.createSuiteDescription(className);
105             Failure failure = new Failure(description, e);
106             mLoadFailuresMap.put(className, failure);
107         }
108         return null;
109     }
110 
111     /**
112      * Loads the test class from the given class name.
113      * <p/>
114      * Similar to {@link #loadClass(String)}, but will ignore classes that are
115      * not tests.
116      *
117      * @param className the class name to attempt to load
118      * @return the loaded class or null.
119      */
loadIfTest(String className)120     public Class<?> loadIfTest(String className) {
121         Class<?> loadedClass = doLoadClass(className);
122         if (loadedClass != null && isTestClass(loadedClass)) {
123             mLoadedClassesMap.put(className, loadedClass);
124             return loadedClass;
125         }
126         return null;
127     }
128 
129     /**
130      * @return whether this {@link TestLoader} contains any loaded classes or load failures.
131      */
isEmpty()132     public boolean isEmpty() {
133         return mLoadedClassesMap.isEmpty() && mLoadFailuresMap.isEmpty();
134     }
135 
136     /**
137      * Get the {@link Collection) of classes successfully loaded via
138      * {@link #loadIfTest(String)} calls.
139      */
getLoadedClasses()140     public Collection<Class<?>> getLoadedClasses() {
141         return mLoadedClassesMap.values();
142     }
143 
144     /**
145      * Get the {@link List) of {@link Failure} that occurred during
146      * {@link #loadIfTest(String)} calls.
147      */
getLoadFailures()148     public Collection<Failure> getLoadFailures() {
149         return mLoadFailuresMap.values();
150     }
151 
152     /**
153      * Determines if given class is a valid test class.
154      *
155      * @param loadedClass
156      * @return <code>true</code> if loadedClass is a test
157      */
isTestClass(Class<?> loadedClass)158     private boolean isTestClass(Class<?> loadedClass) {
159         try {
160             if (Modifier.isAbstract(loadedClass.getModifiers())) {
161                 logDebug(String.format("Skipping abstract class %s: not a test",
162                         loadedClass.getName()));
163                 return false;
164             }
165             // Libcore-specific change: Also consider TestNG annotated classes.
166             if (isTestNgTestClass(loadedClass)) {
167               return true;
168             }
169             // TODO: try to find upstream junit calls to replace these checks
170             if (junit.framework.Test.class.isAssignableFrom(loadedClass)) {
171                 // ensure that if a TestCase, it has at least one test method otherwise
172                 // TestSuite will throw error
173                 if (junit.framework.TestCase.class.isAssignableFrom(loadedClass)) {
174                     return hasJUnit3TestMethod(loadedClass);
175                 }
176                 return true;
177             }
178             // TODO: look for a 'suite' method?
179             if (loadedClass.isAnnotationPresent(org.junit.runner.RunWith.class)) {
180                 return true;
181             }
182             for (Method testMethod : loadedClass.getMethods()) {
183                 if (testMethod.isAnnotationPresent(org.junit.Test.class)) {
184                     return true;
185                 }
186             }
187             logDebug(String.format("Skipping class %s: not a test", loadedClass.getName()));
188             return false;
189         } catch (Exception e) {
190             // Defensively catch exceptions - Will throw runtime exception if it cannot load methods.
191             // For earlier versions of Android (Pre-ICS), Dalvik might try to initialize a class
192             // during getMethods(), fail to do so, hide the error and throw a NoSuchMethodException.
193             // Since the java.lang.Class.getMethods does not declare such an exception, resort to a
194             // generic catch all.
195             // For ICS+, Dalvik will throw a NoClassDefFoundException.
196             Log.w(LOG_TAG, String.format("%s in isTestClass for %s", e.toString(),
197                     loadedClass.getName()));
198             return false;
199         } catch (Error e) {
200             // defensively catch Errors too
201             Log.w(LOG_TAG, String.format("%s in isTestClass for %s", e.toString(),
202                     loadedClass.getName()));
203             return false;
204         }
205     }
206 
hasJUnit3TestMethod(Class<?> loadedClass)207     private boolean hasJUnit3TestMethod(Class<?> loadedClass) {
208         for (Method testMethod : loadedClass.getMethods()) {
209             if (isPublicTestMethod(testMethod)) {
210                 return true;
211             }
212         }
213         return false;
214     }
215 
216     // copied from junit.framework.TestSuite
isPublicTestMethod(Method m)217     private boolean isPublicTestMethod(Method m) {
218         return isTestMethod(m) && Modifier.isPublic(m.getModifiers());
219     }
220 
221     // copied from junit.framework.TestSuite
isTestMethod(Method m)222     private boolean isTestMethod(Method m) {
223         return m.getParameterTypes().length == 0 && m.getName().startsWith("test")
224                 && m.getReturnType().equals(Void.TYPE);
225     }
226 
227     // Libcore-specific change: Add method for checking TestNG-annotated classes.
isTestNgTestClass(Class<?> cls)228     private static boolean isTestNgTestClass(Class<?> cls) {
229       // TestNG test is either marked @Test at the class
230       for (Annotation a : cls.getAnnotations()) {
231           if (a.annotationType().getName().equals(TESTNG_TEST)) {
232               return true;
233           }
234       }
235 
236       // Or It's marked @Test at the method level
237       for (Method m : cls.getDeclaredMethods()) {
238         for (Annotation a : m.getAnnotations()) {
239           if (a.annotationType().getName().equals(TESTNG_TEST)) {
240               return true;
241           }
242         }
243       }
244 
245       return false;
246     }
247 
248 
249     /**
250      * Utility method for logging debug messages. Only actually logs a message if LOG_TAG is marked
251      * as loggable to limit log spam during normal use.
252      */
logDebug(String msg)253     private void logDebug(String msg) {
254         if (Log.isLoggable(LOG_TAG, Log.DEBUG)) {
255             Log.d(LOG_TAG, msg);
256         }
257     }
258 }
259