• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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