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 package com.android.compatibility.dalvik; 18 19 import com.android.compatibility.common.util.TestSuiteFilter; 20 21 import dalvik.system.DexFile; 22 import dalvik.system.PathClassLoader; 23 24 import junit.framework.AssertionFailedError; 25 import junit.framework.Test; 26 import junit.framework.TestCase; 27 import junit.framework.TestListener; 28 import junit.framework.TestResult; 29 import junit.framework.TestSuite; 30 31 import java.io.File; 32 import java.io.FileNotFoundException; 33 import java.io.IOException; 34 import java.lang.annotation.Annotation; 35 import java.lang.reflect.Method; 36 import java.lang.reflect.Modifier; 37 import java.util.ArrayList; 38 import java.util.Arrays; 39 import java.util.Enumeration; 40 import java.util.HashSet; 41 import java.util.List; 42 import java.util.Scanner; 43 import java.util.Set; 44 45 /** 46 * Runs tests against the Dalvik VM. 47 */ 48 public class DalvikTestRunner { 49 50 private static final String ABI = "--abi="; 51 private static final String INCLUDE = "--include-filter="; 52 private static final String EXCLUDE = "--exclude-filter="; 53 private static final String INCLUDE_FILE = "--include-filter-file="; 54 private static final String EXCLUDE_FILE = "--exclude-filter-file="; 55 private static final String COLLECT_TESTS_ONLY = "--collect-tests-only"; 56 private static final String JUNIT_IGNORE = "org.junit.Ignore"; 57 58 public static void main(String[] args) { 59 String abiName = null; 60 Set<String> includes = new HashSet<>(); 61 Set<String> excludes = new HashSet<>(); 62 boolean collectTestsOnly = false; 63 64 for (String arg : args) { 65 if (arg.startsWith(ABI)) { 66 abiName = arg.substring(ABI.length()); 67 } else if (arg.startsWith(INCLUDE)) { 68 for (String include : arg.substring(INCLUDE.length()).split(",")) { 69 includes.add(include); 70 } 71 } else if (arg.startsWith(EXCLUDE)) { 72 for (String exclude : arg.substring(EXCLUDE.length()).split(",")) { 73 excludes.add(exclude); 74 } 75 } else if (arg.startsWith(INCLUDE_FILE)) { 76 loadFilters(arg.substring(INCLUDE_FILE.length()), includes); 77 } else if (arg.startsWith(EXCLUDE_FILE)) { 78 loadFilters(arg.substring(EXCLUDE_FILE.length()), excludes); 79 } else if (COLLECT_TESTS_ONLY.equals(arg)) { 80 collectTestsOnly = true; 81 } 82 } 83 84 TestListener listener = new DalvikTestListener(); 85 String[] classPathItems = System.getProperty("java.class.path").split(File.pathSeparator); 86 List<Class<?>> classes = getClasses(classPathItems, abiName); 87 TestSuite suite = TestSuiteFilter.createSuite(classes, includes, excludes); 88 int count = suite.countTestCases(); 89 System.out.println(String.format("start-run:%d", count)); 90 long start = System.currentTimeMillis(); 91 92 if (collectTestsOnly) { // only simulate running/passing the tests with the listener 93 collectTests(suite, listener, includes, excludes); 94 } else { // run the tests 95 TestResult result = new TestResult(); 96 result.addListener(listener); 97 suite.run(result); 98 } 99 100 long end = System.currentTimeMillis(); 101 System.out.println(String.format("end-run:%d", end - start)); 102 } 103 104 /* Recursively collect tests, since Test elements of the TestSuite may also be TestSuite 105 * objects containing Tests. */ 106 private static void collectTests(TestSuite suite, TestListener listener, 107 Set<String> includes, Set<String> excludes) { 108 109 Enumeration<Test> tests = suite.tests(); 110 while (tests.hasMoreElements()) { 111 Test test = tests.nextElement(); 112 if (test instanceof TestSuite) { 113 collectTests((TestSuite) test, listener, includes, excludes); 114 } else if (shouldCollect(test, includes, excludes)) { 115 listener.startTest(test); 116 listener.endTest(test); 117 } 118 } 119 } 120 121 /* Copied from FilterableTestSuite.shouldRun(), which is private */ 122 private static boolean shouldCollect(Test test, Set<String> includes, Set<String> excludes) { 123 String fullName = test.toString(); 124 String[] parts = fullName.split("[\\(\\)]"); 125 String className = parts[1]; 126 String methodName = String.format("%s#%s", className, parts[0]); 127 int index = className.lastIndexOf('.'); 128 String packageName = index < 0 ? "" : className.substring(0, index); 129 130 if (excludes.contains(packageName)) { 131 // Skip package because it was excluded 132 return false; 133 } 134 if (excludes.contains(className)) { 135 // Skip class because it was excluded 136 return false; 137 } 138 if (excludes.contains(methodName)) { 139 // Skip method because it was excluded 140 return false; 141 } 142 return includes.isEmpty() 143 || includes.contains(methodName) 144 || includes.contains(className) 145 || includes.contains(packageName); 146 } 147 148 private static void loadFilters(String filename, Set<String> filters) { 149 try { 150 Scanner in = new Scanner(new File(filename)); 151 while (in.hasNextLine()) { 152 filters.add(in.nextLine()); 153 } 154 in.close(); 155 } catch (FileNotFoundException e) { 156 System.out.println(String.format("File %s not found when loading filters", filename)); 157 } 158 } 159 160 private static List<Class<?>> getClasses(String[] jars, String abiName) { 161 List<Class<?>> classes = new ArrayList<>(); 162 for (String jar : jars) { 163 try { 164 ClassLoader loader = createClassLoader(jar, abiName); 165 DexFile file = new DexFile(jar); 166 Enumeration<String> entries = file.entries(); 167 while (entries.hasMoreElements()) { 168 String e = entries.nextElement(); 169 Class<?> cls = loader.loadClass(e); 170 if (isTestClass(cls)) { 171 classes.add(cls); 172 } 173 } 174 } catch (IllegalAccessError | IOException | ClassNotFoundException e) { 175 e.printStackTrace(); 176 } 177 } 178 return classes; 179 } 180 181 private static ClassLoader createClassLoader(String jar, String abiName) { 182 StringBuilder libPath = new StringBuilder(); 183 libPath.append(jar).append("!/lib/").append(abiName); 184 return new PathClassLoader( 185 jar, libPath.toString(), DalvikTestRunner.class.getClassLoader()); 186 } 187 188 private static boolean isTestClass(Class<?> cls) { 189 // FIXME(b/25154702): have to have a null check here because some 190 // classes such as 191 // SQLite.JDBC2z.JDBCPreparedStatement can be found in the classes.dex 192 // by DexFile.entries 193 // but trying to load them with DexFile.loadClass returns null. 194 if (cls == null) { 195 return false; 196 } 197 for (Annotation a : cls.getAnnotations()) { 198 if (a.annotationType().getName().equals(JUNIT_IGNORE)) { 199 return false; 200 } 201 } 202 203 if (!hasPublicTestMethods(cls)) { 204 return false; 205 } 206 207 // TODO: Add junit4 support here 208 int modifiers = cls.getModifiers(); 209 return (Test.class.isAssignableFrom(cls) 210 && Modifier.isPublic(modifiers) 211 && !Modifier.isStatic(modifiers) 212 && !Modifier.isInterface(modifiers) 213 && !Modifier.isAbstract(modifiers)); 214 } 215 216 private static boolean hasPublicTestMethods(Class<?> cls) { 217 for (Method m : cls.getDeclaredMethods()) { 218 if (isPublicTestMethod(m)) { 219 return true; 220 } 221 } 222 return false; 223 } 224 225 private static boolean isPublicTestMethod(Method m) { 226 boolean hasTestName = m.getName().startsWith("test"); 227 boolean takesNoParameters = (m.getParameterTypes().length == 0); 228 boolean returnsVoid = m.getReturnType().equals(Void.TYPE); 229 boolean isPublic = Modifier.isPublic(m.getModifiers()); 230 return hasTestName && takesNoParameters && returnsVoid && isPublic; 231 } 232 233 // TODO: expand this to setup and teardown things needed by Dalvik tests. 234 private static class DalvikTestListener implements TestListener { 235 /** 236 * {@inheritDoc} 237 */ 238 @Override 239 public void startTest(Test test) { 240 System.out.println(String.format("start-test:%s", getId(test))); 241 } 242 243 /** 244 * {@inheritDoc} 245 */ 246 @Override 247 public void endTest(Test test) { 248 System.out.println(String.format("end-test:%s", getId(test))); 249 } 250 251 /** 252 * {@inheritDoc} 253 */ 254 @Override 255 public void addFailure(Test test, AssertionFailedError error) { 256 System.out.println(String.format("failure:%s", stringify(error))); 257 } 258 259 /** 260 * {@inheritDoc} 261 */ 262 @Override 263 public void addError(Test test, Throwable error) { 264 System.out.println(String.format("failure:%s", stringify(error))); 265 } 266 267 private String getId(Test test) { 268 String className = test.getClass().getName(); 269 if (test instanceof TestCase) { 270 return String.format("%s#%s", className, ((TestCase) test).getName()); 271 } 272 return className; 273 } 274 275 private String stringify(Throwable error) { 276 return Arrays.toString(error.getStackTrace()).replaceAll("\n", " "); 277 } 278 } 279 } 280