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 dalvik.system.DexFile; 20 import dalvik.system.PathClassLoader; 21 22 import junit.framework.AssertionFailedError; 23 import junit.framework.Test; 24 import junit.framework.TestCase; 25 import junit.framework.TestListener; 26 import junit.framework.TestResult; 27 import junit.framework.TestSuite; 28 29 import java.io.File; 30 import java.io.FileNotFoundException; 31 import java.io.IOException; 32 import java.lang.annotation.Annotation; 33 import java.lang.reflect.Method; 34 import java.lang.reflect.Modifier; 35 import java.util.ArrayList; 36 import java.util.Arrays; 37 import java.util.Enumeration; 38 import java.util.HashSet; 39 import java.util.List; 40 import java.util.Scanner; 41 import java.util.Set; 42 43 /** 44 * Runs tests against the Dalvik VM. 45 */ 46 public class DalvikTestRunner { 47 48 private static final String ABI = "--abi="; 49 private static final String INCLUDE = "--include-filter="; 50 private static final String EXCLUDE = "--exclude-filter="; 51 private static final String INCLUDE_FILE = "--include-filter-file="; 52 private static final String EXCLUDE_FILE = "--exclude-filter-file="; 53 private static final String COLLECT_TESTS_ONLY = "--collect-tests-only"; 54 private static final String JUNIT_IGNORE = "org.junit.Ignore"; 55 56 private static final String RUNNER_JAR = "cts-dalvik-device-test-runner.jar"; 57 main(String[] args)58 public static void main(String[] args) { 59 Config config; 60 try { 61 config = createConfig(args); 62 } catch (Throwable t) { 63 // Simulate one failed test. 64 System.out.println("start-run:1"); 65 System.out.println("start-test:FailedConfigCreation"); 66 System.out.println("failure:" + DalvikTestListener.stringify(t)); 67 System.out.println("end-run:1"); 68 throw new RuntimeException(t); 69 } 70 run(config); 71 } 72 createConfig(String[] args)73 private static Config createConfig(String[] args) { 74 String abiName = null; 75 Config config = new Config(); 76 77 for (String arg : args) { 78 if (arg.startsWith(ABI)) { 79 abiName = arg.substring(ABI.length()); 80 } else if (arg.startsWith(INCLUDE)) { 81 for (String include : arg.substring(INCLUDE.length()).split(",")) { 82 config.includes.add(include); 83 } 84 } else if (arg.startsWith(EXCLUDE)) { 85 for (String exclude : arg.substring(EXCLUDE.length()).split(",")) { 86 config.excludes.add(exclude); 87 } 88 } else if (arg.startsWith(INCLUDE_FILE)) { 89 loadFilters(arg.substring(INCLUDE_FILE.length()), config.includes); 90 } else if (arg.startsWith(EXCLUDE_FILE)) { 91 loadFilters(arg.substring(EXCLUDE_FILE.length()), config.excludes); 92 } else if (COLLECT_TESTS_ONLY.equals(arg)) { 93 config.collectTestsOnly = true; 94 } 95 } 96 97 String[] classPathItems = System.getProperty("java.class.path").split(File.pathSeparator); 98 List<Class<?>> classes = getClasses(classPathItems, abiName); 99 config.suite = new FilterableTestSuite(classes, config.includes, config.excludes); 100 101 return config; 102 } 103 run(Config config)104 private static void run(Config config) { 105 TestListener listener = new DalvikTestListener(); 106 107 int count = config.suite.countTestCases(); 108 System.out.println(String.format("start-run:%d", count)); 109 long start = System.currentTimeMillis(); 110 111 if (config.collectTestsOnly) { // only simulate running/passing the tests with the listener 112 collectTests(config.suite, listener, config.includes, config.excludes); 113 } else { // run the tests 114 TestResult result = new TestResult(); 115 result.addListener(listener); 116 config.suite.run(result); 117 } 118 119 long end = System.currentTimeMillis(); 120 System.out.println(String.format("end-run:%d", end - start)); 121 } 122 123 /* Recursively collect tests, since Test elements of the TestSuite may also be TestSuite 124 * objects containing Tests. */ collectTests(TestSuite suite, TestListener listener, Set<String> includes, Set<String> excludes)125 private static void collectTests(TestSuite suite, TestListener listener, 126 Set<String> includes, Set<String> excludes) { 127 128 Enumeration<Test> tests = suite.tests(); 129 while (tests.hasMoreElements()) { 130 Test test = tests.nextElement(); 131 if (test instanceof TestSuite) { 132 collectTests((TestSuite) test, listener, includes, excludes); 133 } else if (shouldCollect(test, includes, excludes)) { 134 listener.startTest(test); 135 listener.endTest(test); 136 } 137 } 138 } 139 140 /* Copied from FilterableTestSuite.shouldRun(), which is private */ shouldCollect(Test test, Set<String> includes, Set<String> excludes)141 private static boolean shouldCollect(Test test, Set<String> includes, Set<String> excludes) { 142 String fullName = test.toString(); 143 String[] parts = fullName.split("[\\(\\)]"); 144 String className = parts[1]; 145 String methodName = String.format("%s#%s", className, parts[0]); 146 int index = className.lastIndexOf('.'); 147 String packageName = index < 0 ? "" : className.substring(0, index); 148 149 if (excludes.contains(packageName)) { 150 // Skip package because it was excluded 151 return false; 152 } 153 if (excludes.contains(className)) { 154 // Skip class because it was excluded 155 return false; 156 } 157 if (excludes.contains(methodName)) { 158 // Skip method because it was excluded 159 return false; 160 } 161 return includes.isEmpty() 162 || includes.contains(methodName) 163 || includes.contains(className) 164 || includes.contains(packageName); 165 } 166 167 private static void loadFilters(String filename, Set<String> filters) { 168 try { 169 Scanner in = new Scanner(new File(filename)); 170 while (in.hasNextLine()) { 171 filters.add(in.nextLine()); 172 } 173 in.close(); 174 } catch (FileNotFoundException e) { 175 System.out.println(String.format("File %s not found when loading filters", filename)); 176 } 177 } 178 179 private static List<Class<?>> getClasses(String[] jars, String abiName) { 180 List<Class<?>> classes = new ArrayList<>(); 181 for (String jar : jars) { 182 if (jar.contains(RUNNER_JAR)) { 183 // The runner jar must be added to the class path to invoke DalvikTestRunner, 184 // but should not be searched for test classes 185 continue; 186 } 187 try { 188 ClassLoader loader = createClassLoader(jar, abiName); 189 DexFile file = new DexFile(jar); 190 Enumeration<String> entries = file.entries(); 191 while (entries.hasMoreElements()) { 192 String e = entries.nextElement(); 193 try { 194 Class<?> cls = loader.loadClass(e); 195 if (isTestClass(cls)) { 196 classes.add(cls); 197 } 198 } catch (ClassNotFoundException ex) { 199 System.out.println(String.format( 200 "Skipping dex entry %s in %s", e, jar)); 201 } 202 } 203 } catch (IllegalAccessError | IOException e) { 204 e.printStackTrace(); 205 } catch (Exception e) { 206 throw new RuntimeException(jar, e); 207 } 208 } 209 return classes; 210 } 211 212 private static ClassLoader createClassLoader(String jar, String abiName) { 213 StringBuilder libPath = new StringBuilder(); 214 libPath.append(jar).append("!/lib/").append(abiName); 215 return new PathClassLoader( 216 jar, libPath.toString(), DalvikTestRunner.class.getClassLoader()); 217 } 218 219 private static boolean isTestClass(Class<?> cls) { 220 // FIXME(b/25154702): have to have a null check here because some 221 // classes such as 222 // SQLite.JDBC2z.JDBCPreparedStatement can be found in the classes.dex 223 // by DexFile.entries 224 // but trying to load them with DexFile.loadClass returns null. 225 if (cls == null) { 226 return false; 227 } 228 for (Annotation a : cls.getAnnotations()) { 229 if (a.annotationType().getName().equals(JUNIT_IGNORE)) { 230 return false; 231 } 232 } 233 234 try { 235 if (!hasPublicTestMethods(cls)) { 236 return false; 237 } 238 } catch (Throwable exc) { 239 throw new RuntimeException(cls.toString(), exc); 240 } 241 242 // TODO: Add junit4 support here 243 int modifiers = cls.getModifiers(); 244 return (Test.class.isAssignableFrom(cls) 245 && Modifier.isPublic(modifiers) 246 && !Modifier.isStatic(modifiers) 247 && !Modifier.isInterface(modifiers) 248 && !Modifier.isAbstract(modifiers)); 249 } 250 251 private static boolean hasPublicTestMethods(Class<?> cls) { 252 for (Method m : cls.getDeclaredMethods()) { 253 if (isPublicTestMethod(m)) { 254 return true; 255 } 256 } 257 return false; 258 } 259 260 private static boolean isPublicTestMethod(Method m) { 261 boolean hasTestName = m.getName().startsWith("test"); 262 boolean takesNoParameters = (m.getParameterTypes().length == 0); 263 boolean returnsVoid = m.getReturnType().equals(Void.TYPE); 264 boolean isPublic = Modifier.isPublic(m.getModifiers()); 265 return hasTestName && takesNoParameters && returnsVoid && isPublic; 266 } 267 268 // TODO: expand this to setup and teardown things needed by Dalvik tests. 269 private static class DalvikTestListener implements TestListener { 270 /** 271 * {@inheritDoc} 272 */ 273 @Override 274 public void startTest(Test test) { 275 System.out.println(String.format("start-test:%s", getId(test))); 276 } 277 278 /** 279 * {@inheritDoc} 280 */ 281 @Override 282 public void endTest(Test test) { 283 System.out.println(String.format("end-test:%s", getId(test))); 284 } 285 286 /** 287 * {@inheritDoc} 288 */ 289 @Override 290 public void addFailure(Test test, AssertionFailedError error) { 291 System.out.println(String.format("failure:%s", stringify(error))); 292 } 293 294 /** 295 * {@inheritDoc} 296 */ 297 @Override 298 public void addError(Test test, Throwable error) { 299 System.out.println(String.format("failure:%s", stringify(error))); 300 } 301 302 private String getId(Test test) { 303 String className = test.getClass().getName(); 304 if (test instanceof TestCase) { 305 return String.format("%s#%s", className, ((TestCase) test).getName()); 306 } 307 return className; 308 } 309 310 public static String stringify(Throwable error) { 311 return Arrays.toString(error.getStackTrace()).replaceAll("\n", " "); 312 } 313 } 314 315 private static class Config { 316 Set<String> includes = new HashSet<>(); 317 Set<String> excludes = new HashSet<>(); 318 boolean collectTestsOnly = false; 319 TestSuite suite; 320 } 321 322 /** 323 * A {@link TestSuite} that can filter which tests run, given the include and exclude filters. 324 * 325 * This had to be private inner class because the test runner would find it and think it was a 326 * suite of tests, but it has no tests in it, causing a crash. 327 */ 328 private static class FilterableTestSuite extends TestSuite { 329 330 private Set<String> mIncludes; 331 private Set<String> mExcludes; 332 333 public FilterableTestSuite(List<Class<?>> classes, Set<String> includes, 334 Set<String> excludes) { 335 super(classes.toArray(new Class<?>[classes.size()])); 336 mIncludes = includes; 337 mExcludes = excludes; 338 } 339 340 /** 341 * {@inheritDoc} 342 */ 343 @Override 344 public int countTestCases() { 345 return countTests(this); 346 } 347 348 private int countTests(Test test) { 349 if (test instanceof TestSuite) { 350 // If the test is a suite it could contain multiple tests, these need to be split 351 // out into separate tests so they can be filtered 352 TestSuite suite = (TestSuite) test; 353 Enumeration<Test> enumerator = suite.tests(); 354 int count = 0; 355 while (enumerator.hasMoreElements()) { 356 count += countTests(enumerator.nextElement()); 357 } 358 return count; 359 } else if (shouldRun(test)) { 360 return 1; 361 } 362 return 0; 363 } 364 365 /** 366 * {@inheritDoc} 367 */ 368 @Override 369 public void runTest(Test test, TestResult result) { 370 runTests(test, result); 371 } 372 373 private void runTests(Test test, TestResult result) { 374 if (test instanceof TestSuite) { 375 // If the test is a suite it could contain multiple tests, these need to be split 376 // out into separate tests so they can be filtered 377 TestSuite suite = (TestSuite) test; 378 Enumeration<Test> enumerator = suite.tests(); 379 while (enumerator.hasMoreElements()) { 380 runTests(enumerator.nextElement(), result); 381 } 382 } else if (shouldRun(test)) { 383 test.run(result); 384 } 385 } 386 387 private boolean shouldRun(Test test) { 388 String fullName = test.toString(); 389 String[] parts = fullName.split("[\\(\\)]"); 390 String className = parts[1]; 391 String methodName = String.format("%s#%s", className, parts[0]); 392 int index = className.lastIndexOf('.'); 393 String packageName = index < 0 ? "" : className.substring(0, index); 394 395 if (mExcludes.contains(packageName)) { 396 // Skip package because it was excluded 397 return false; 398 } 399 if (mExcludes.contains(className)) { 400 // Skip class because it was excluded 401 return false; 402 } 403 if (mExcludes.contains(methodName)) { 404 // Skip method because it was excluded 405 return false; 406 } 407 return mIncludes.isEmpty() 408 || mIncludes.contains(methodName) 409 || mIncludes.contains(className) 410 || mIncludes.contains(packageName); 411 } 412 } 413 } 414