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