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