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