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