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 android.support.test.aupt;
18 
19 import android.app.Instrumentation;
20 import android.test.AndroidTestRunner;
21 import android.util.Log;
22 
23 import dalvik.system.DexClassLoader;
24 
25 import junit.framework.AssertionFailedError;
26 import junit.framework.Test;
27 import junit.framework.TestCase;
28 import junit.framework.TestListener;
29 import junit.framework.TestResult;
30 import junit.framework.TestSuite;
31 
32 import java.io.File;
33 import java.util.ArrayList;
34 import java.util.Enumeration;
35 import java.util.List;
36 import java.util.concurrent.Callable;
37 import java.util.concurrent.ExecutionException;
38 import java.util.concurrent.ExecutorService;
39 import java.util.concurrent.Executors;
40 import java.util.concurrent.Future;
41 import java.util.concurrent.TimeUnit;
42 import java.util.concurrent.TimeoutException;
43 
44 /**
45  * A DexTestRunner runs tests by name from a given list of JARs,
46  * with the following additional magic:
47  *
48  * - Custom ClassLoading from given dexed Jars
49  * - Custom test scheduling (via Scheduler)
50  *
51  * In addition to the parameters in the constructor, be sure to run setTest or setTestClassName
52  * before attempting to runTest.
53  */
54 class DexTestRunner extends AndroidTestRunner {
55     private static final String LOG_TAG = DexTestRunner.class.getSimpleName();
56 
57     /* Constants */
58     static final String DEFAULT_JAR_PATH = "/data/local/tmp/";
59     static final String DEX_OPT_PATH = "dex-test-opt";
60 
61     /* Private fields */
62     private final List<TestListener> mTestListeners = new ArrayList<>();
63     private final DexClassLoader mLoader;
64     private final long mTestTimeoutMillis;
65     private final long mSuiteTimeoutMillis;
66 
67     /* TestRunner State */
68     protected TestResult mTestResult = new TestResult();
69     protected List<TestCase> mTestCases = new ArrayList<>();
70     protected String mTestClassName;
71     protected Instrumentation mInstrumentation;
72     protected Scheduler mScheduler;
73     protected long mSuiteEndTime;
74 
75     /** A temporary ExecutorService to manage running the current test. */
76     private ExecutorService mExecutorService;
77 
78     /** The current test. */
79     private TestCase mTestCase;
80 
81     /* Field initialization */
DexTestRunner( Instrumentation instrumentation, Scheduler scheduler, List<String> jars, long testTimeoutMillis, long suiteTimeoutMillis)82     DexTestRunner(
83             Instrumentation instrumentation,
84             Scheduler scheduler,
85             List<String> jars,
86             long testTimeoutMillis,
87             long suiteTimeoutMillis) {
88         super();
89 
90         mInstrumentation = instrumentation;
91         mScheduler = scheduler;
92         mLoader = makeLoader(jars);
93         mTestTimeoutMillis = testTimeoutMillis;
94         mSuiteTimeoutMillis = suiteTimeoutMillis;
95     }
96 
97     /* Main methods */
98 
99     @Override
runTest()100     public void runTest() {
101         runTest(newResult());
102     }
103 
104     @Override
runTest(final TestResult testResult)105     public synchronized void runTest(final TestResult testResult) {
106         mTestResult = testResult;
107         mSuiteEndTime = System.currentTimeMillis() + mSuiteTimeoutMillis;
108 
109         for (final TestCase testCase : mScheduler.apply(mTestCases)) {
110             // Timeout the suite if we've passed the end time.
111             if (mSuiteTimeoutMillis != 0 && System.currentTimeMillis() > mSuiteEndTime) {
112                 Log.w(LOG_TAG, String.format("Ending suite after %d mins running.",
113                         TimeUnit.MILLISECONDS.toMinutes(mSuiteTimeoutMillis)));
114                 break;
115             }
116 
117             mExecutorService = Executors.newSingleThreadExecutor();
118             mTestCase = testCase;
119 
120             // A Future that calls testCase::run. The reasoning behind using a thread here
121             // is that AuptTestRunner should be able to interrupt it (via killTest) if it runs
122             // too long; and interrupting the main thread here without actually exiting is tricky.
123             Future<TestResult> result =
124                     mExecutorService.submit(
125                             new Callable<TestResult>() {
126                                 @Override
127                                 public TestResult call() throws Exception {
128                                     testCase.run(testResult);
129                                     return testResult;
130                                 }
131                             });
132 
133             try {
134                 // Run our test-running thread and wait on it.
135                 result.get(mTestTimeoutMillis, TimeUnit.MILLISECONDS);
136             } catch (TimeoutException e) {
137                 killTest(e);
138             } catch (ExecutionException e) {
139                 onError(testCase, e.getCause());
140             } catch (InterruptedException e) {
141                 Thread.currentThread().interrupt();
142             } finally {
143                 mExecutorService.shutdownNow();
144                 mTestCase = null;
145             }
146         }
147     }
148 
149     /** Interrupt the current test with the given exception. */
killTest(Exception e)150     void killTest(Exception e) {
151         if (mTestCase != null) {
152             // First, tell our listeners.
153             onError(mTestCase, e);
154 
155             // Kill the test.
156             mExecutorService.shutdownNow();
157         }
158     }
159 
160     /* TestCase Initialization */
161 
162     @Override
setTestClassName(String className, String methodName)163     public void setTestClassName(String className, String methodName) {
164         mTestCases.clear();
165         addTestClassByName(className, methodName);
166     }
167 
addTestClassByName(final String className, final String methodName)168     void addTestClassByName(final String className, final String methodName) {
169         try {
170             final Class<?> testClass = mLoader.loadClass(className);
171 
172             if (Test.class.isAssignableFrom(testClass)) {
173                 Test test = null;
174 
175                 try {
176                     // Make sure it works
177                     test = (Test) testClass.getConstructor().newInstance();
178                 } catch (Exception e1) { /* If we fail, test will just stay null */ }
179 
180                 try {
181                     test = (Test) testClass.getConstructor(String.class).newInstance(methodName);
182                 } catch (Exception e2) { /* If we fail, test will just stay null */ }
183 
184                 addTest(test);
185             } else {
186                 throw new RuntimeException("Test class not found: " + className);
187             }
188         } catch (ClassNotFoundException ex) {
189             throw new RuntimeException("Class not found: " + ex.getMessage());
190         }
191 
192         if (mTestCases.isEmpty()) {
193             throw new RuntimeException("No tests found in " + className + "#" + methodName);
194         }
195     }
196 
197     @Override
setTest(Test test)198     public void setTest(Test test) {
199         mTestCases.clear();
200         addTest(test);
201 
202         // Update our test class name.
203         if (TestSuite.class.isAssignableFrom(test.getClass())) {
204             mTestClassName = ((TestSuite) test).getName();
205         } else if (TestCase.class.isAssignableFrom(test.getClass())) {
206             mTestClassName = ((TestCase) test).getName();
207         } else {
208             mTestClassName = test.getClass().getSimpleName();
209         }
210     }
211 
addTest(Test test)212     public void addTest(Test test) {
213         if (test instanceof TestCase) {
214 
215             mTestCases.add((TestCase) test);
216 
217         } else if (test instanceof TestSuite) {
218             Enumeration<Test> tests = ((TestSuite) test).tests();
219 
220             while (tests.hasMoreElements()) {
221                 addTest(tests.nextElement());
222             }
223         } else {
224             throw new RuntimeException("Tried to add invalid test: " + test.toString());
225         }
226     }
227 
228     /* State Manipulation Methods */
229 
230     @Override
clearTestListeners()231     public void clearTestListeners() {
232         mTestListeners.clear();
233     }
234 
235     @Override
addTestListener(TestListener testListener)236     public void addTestListener(TestListener testListener) {
237         if (testListener != null) {
238             mTestListeners.add(testListener);
239             mTestResult.addListener(testListener);
240         }
241     }
242 
addTestListenerIf(Boolean cond, TestListener testListener)243     void addTestListenerIf(Boolean cond, TestListener testListener) {
244         if (cond && testListener != null) {
245             mTestListeners.add(testListener);
246         }
247     }
248 
249     @Override
getTestCases()250     public List<TestCase> getTestCases() {
251         return mTestCases;
252     }
253 
254     @Override
setInstrumentation(Instrumentation instrumentation)255     public void setInstrumentation(Instrumentation instrumentation) {
256         mInstrumentation = instrumentation;
257     }
258 
259     @Override
getTestResult()260     public TestResult getTestResult() {
261         return mTestResult;
262     }
263 
264     @Override
createTestResult()265     protected TestResult createTestResult() {
266         return new TestResult();
267     }
268 
269     @Override
getTestClassName()270     public String getTestClassName() {
271         return mTestClassName;
272     }
273 
274     /* Listener Exception Callback. */
275 
onError(Test test, Throwable t)276     void onError(Test test, Throwable t) {
277         if (t instanceof AssertionFailedError) {
278             for (TestListener listener : mTestListeners) {
279                 listener.addFailure(test, (AssertionFailedError) t);
280             }
281         } else {
282             for (TestListener listener : mTestListeners) {
283                 listener.addError(test, t);
284             }
285         }
286     }
287 
288     /* Package-private Utilities */
289 
newResult()290     TestResult newResult() {
291         TestResult result = new TestResult();
292 
293         for (TestListener listener: mTestListeners) {
294             result.addListener(listener);
295         }
296 
297         return result;
298     }
299 
parseDexedJarPaths(String jarString)300     static List<String> parseDexedJarPaths(String jarString) {
301         List<String> jars = new ArrayList<>();
302 
303         for (String jar : jarString.split(":")) {
304             // Check that jar isn't empty, but don't fail because String::split will yield
305             // spurious empty results if, for example, we don't specify any jars, accidentally
306             // start with a leading colon, etc.
307             if (!jar.trim().isEmpty()) {
308                 File jarFile = jar.startsWith("/")
309                         ? new File(jar)
310                         : new File(DEFAULT_JAR_PATH + jar);
311 
312                 if (jarFile.exists()) {
313                     jars.add(jarFile.getAbsolutePath());
314                 } else {
315                     throw new RuntimeException("Can't find jar file " + jarFile);
316                 }
317             }
318         }
319 
320         return jars;
321     }
322 
getDexClassLoader()323     DexClassLoader getDexClassLoader() {
324         return mLoader;
325     }
326 
makeLoader(List<String> jars)327     DexClassLoader makeLoader(List<String> jars) {
328         StringBuilder jarFiles = new StringBuilder();
329 
330         for (String jar : jars) {
331             if (new File(jar).exists() && new File(jar).canRead()) {
332                 if (jarFiles.length() != 0) {
333                     jarFiles.append(File.pathSeparator);
334                 }
335 
336                 jarFiles.append(jar);
337             } else {
338                 throw new IllegalArgumentException(
339                         "Jar file does not exist or not accessible: "  + jar);
340             }
341         }
342 
343         File optDir = new File(mInstrumentation.getTargetContext().getCacheDir(), DEX_OPT_PATH);
344 
345         if (optDir.exists() || optDir.mkdirs()) {
346             return new DexClassLoader(
347                     jarFiles.toString(),
348                     optDir.getAbsolutePath(),
349                     null,
350                     DexTestRunner.class.getClassLoader());
351         } else {
352             throw new RuntimeException(
353                     "Failed to create dex optimization directory: " + optDir.getAbsolutePath());
354         }
355     }
356 }
357