1 /* 2 * Copyright (C) 2014 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.app.Service; 21 import android.content.Context; 22 import android.content.ContextWrapper; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.os.Bundle; 26 import android.os.Environment; 27 import android.os.IBinder; 28 import android.support.test.uiautomator.UiDevice; 29 import android.test.AndroidTestRunner; 30 import android.test.InstrumentationTestCase; 31 import android.test.InstrumentationTestRunner; 32 import android.util.Log; 33 34 import dalvik.system.BaseDexClassLoader; 35 36 import junit.framework.AssertionFailedError; 37 import junit.framework.Test; 38 import junit.framework.TestCase; 39 import junit.framework.TestListener; 40 import junit.framework.TestResult; 41 42 import java.io.File; 43 import java.io.FileOutputStream; 44 import java.io.IOException; 45 import java.util.ArrayList; 46 import java.util.Arrays; 47 import java.util.Collections; 48 import java.util.Comparator; 49 import java.util.concurrent.TimeUnit; 50 import java.util.HashMap; 51 import java.util.List; 52 import java.util.Map; 53 import java.util.Random; 54 55 /** 56 * Test runner to use when running AUPT tests. 57 * <p> 58 * Adds support for multiple iterations, randomizing the order of the tests, 59 * terminating tests after UI errors, detecting when processes are getting killed, 60 * collecting bugreports and procrank data while the test is running. 61 */ 62 public class AuptTestRunner extends InstrumentationTestRunner { 63 private static final String DEFAULT_JAR_PATH = "/data/local/tmp/"; 64 65 private static final String LOG_TAG = "AuptTestRunner"; 66 private static final String DEX_OPT_PATH = "aupt-opt"; 67 private static final String PARAM_JARS = "jars"; 68 private Bundle mParams; 69 70 private long mIterations; 71 private Random mRandom; 72 private boolean mShuffle; 73 private boolean mGenerateAnr; 74 private long mTestCaseTimeout = TimeUnit.MINUTES.toMillis(10); 75 private DataCollector mDataCollector; 76 private File mResultsDirectory; 77 78 private boolean mDeleteOldFiles; 79 private long mFileRetainCount; 80 81 private AuptPrivateTestRunner mRunner = new AuptPrivateTestRunner(); 82 private ClassLoader mLoader = null; 83 private Context mTargetContextWrapper; 84 85 private IProcessStatusTracker mProcessTracker; 86 87 private Map<String, List<AuptTestCase.MemHealthRecord>> mMemHealthRecords; 88 89 private boolean mTrackJank; 90 private GraphicsStatsMonitor mGraphicsStatsMonitor; 91 private List<String> mJars; 92 93 /** 94 * {@inheritDoc} 95 */ 96 @Override onCreate(Bundle params)97 public void onCreate(Bundle params) { 98 99 mParams = params; 100 101 mMemHealthRecords = new HashMap<String, List<AuptTestCase.MemHealthRecord>>(); 102 103 mIterations = parseLongParam("iterations", 1); 104 mShuffle = parseBooleanParam("shuffle", false); 105 long seed = parseLongParam("seed", (new Random()).nextLong()); 106 Log.d(LOG_TAG, String.format("Using seed value: %s", seed)); 107 mRandom = new Random(seed); 108 // set to 'generateANR to 'true' when more info required for debugging on test timeout' 109 mGenerateAnr = parseBooleanParam("generateANR", false); 110 if (parseBooleanParam("quitOnError", false)) { 111 mRunner.addTestListener(new QuitOnErrorListener()); 112 } 113 if (parseBooleanParam("checkBattery", false)) { 114 mRunner.addTestListener(new BatteryChecker()); 115 } 116 mTestCaseTimeout = parseLongParam("testCaseTimeout", mTestCaseTimeout); 117 118 // Option: -e detectKill com.pkg1,...,com.pkg8 119 String processes = parseStringParam("detectKill", null); 120 if (processes != null) { 121 mProcessTracker = new ProcessStatusTracker(processes.split(",")); 122 } else { 123 mProcessTracker = new ProcessStatusTracker(null); 124 } 125 126 // Option: -e jankInterval integer 127 long interval = parseLongParam("jankInterval", -1L); 128 if (interval > 0) { 129 mTrackJank = true; 130 mGraphicsStatsMonitor = new GraphicsStatsMonitor(); 131 mGraphicsStatsMonitor.setIntervalRate(interval); 132 } 133 134 mRunner.addTestListener(new PidChecker()); 135 mResultsDirectory = new File(Environment.getExternalStorageDirectory(), 136 parseStringParam("outputLocation", "aupt_results")); 137 if (!mResultsDirectory.exists() && !mResultsDirectory.mkdirs()) { 138 Log.w(LOG_TAG, "Did not create output directory"); 139 } 140 141 mFileRetainCount = parseLongParam("fileRetainCount", -1); 142 if (mFileRetainCount == -1) { 143 mDeleteOldFiles = false; 144 } else { 145 mDeleteOldFiles = true; 146 } 147 148 mDataCollector = new DataCollector( 149 TimeUnit.MINUTES.toMillis(parseLongParam("bugreportInterval", 0)), 150 TimeUnit.MINUTES.toMillis(parseLongParam("meminfoInterval", 0)), 151 TimeUnit.MINUTES.toMillis(parseLongParam("cpuinfoInterval", 0)), 152 TimeUnit.MINUTES.toMillis(parseLongParam("fragmentationInterval", 0)), 153 TimeUnit.MINUTES.toMillis(parseLongParam("ionInterval", 0)), 154 TimeUnit.MINUTES.toMillis(parseLongParam("pagetypeinfoInterval", 0)), 155 TimeUnit.MINUTES.toMillis(parseLongParam("traceInterval", 0)), 156 mResultsDirectory, this); 157 String jars = params.getString(PARAM_JARS); 158 if (jars != null) { 159 loadDexJars(jars); 160 } 161 mTargetContextWrapper = new ClassLoaderContextWrapper(); 162 super.onCreate(params); 163 } 164 loadDexJars(String jars)165 private void loadDexJars(String jars) { 166 // scan provided jar paths, translate relative to absolute paths, and check for existence 167 String[] jarsArray = jars.split(":"); 168 StringBuilder classLoaderPath = new StringBuilder(); 169 mJars = new ArrayList<String>(); 170 for (int i = 0; i < jarsArray.length; i++) { 171 String jar = jarsArray[i]; 172 if (!jar.startsWith("/")) { 173 jar = DEFAULT_JAR_PATH + jar; 174 } 175 File jarFile = new File(jar); 176 if (!jarFile.exists() || !jarFile.canRead()) { 177 throw new IllegalArgumentException("Jar file does not exist or not accessible: " 178 + jar); 179 } 180 if (i != 0) { 181 classLoaderPath.append(File.pathSeparator); 182 } 183 classLoaderPath.append(jarFile.getAbsolutePath()); 184 mJars.add(jarFile.getAbsolutePath()); 185 } 186 // now load them 187 File optDir = new File(getContext().getCacheDir(), DEX_OPT_PATH); 188 if (!optDir.exists() && !optDir.mkdirs()) { 189 throw new RuntimeException( 190 "Failed to create dex optimize directory: " + optDir.getAbsolutePath()); 191 } 192 mLoader = new BaseDexClassLoader(classLoaderPath.toString(), optDir, null, 193 super.getTargetContext().getClassLoader()); 194 195 } 196 parseLongParam(String key, long alternative)197 private long parseLongParam(String key, long alternative) throws NumberFormatException { 198 if (mParams.containsKey(key)) { 199 return Long.parseLong( 200 mParams.getString(key)); 201 } else { 202 return alternative; 203 } 204 } 205 parseBooleanParam(String key, boolean alternative)206 private boolean parseBooleanParam(String key, boolean alternative) 207 throws NumberFormatException { 208 if (mParams.containsKey(key)) { 209 return Boolean.parseBoolean(mParams.getString(key)); 210 } else { 211 return alternative; 212 } 213 } 214 parseStringParam(String key, String alternative)215 private String parseStringParam(String key, String alternative) { 216 if (mParams.containsKey(key)) { 217 return mParams.getString(key); 218 } else { 219 return alternative; 220 } 221 } 222 writeProgressMessage(String msg)223 private void writeProgressMessage(String msg) { 224 writeMessage("progress.txt", msg); 225 } 226 writeGraphicsMessage(String msg)227 private void writeGraphicsMessage(String msg) { 228 writeMessage("graphics.txt", msg); 229 } 230 writeMessage(String filename, String msg)231 private void writeMessage(String filename, String msg) { 232 try { 233 FileOutputStream fos = new FileOutputStream( 234 new File(mResultsDirectory, filename)); 235 fos.write(msg.getBytes()); 236 fos.flush(); 237 fos.close(); 238 } catch (IOException ioe) { 239 Log.e(LOG_TAG, "error saving progress file", ioe); 240 } 241 } 242 243 /** 244 * Provide a wrapped context so that we can provide an alternative class loader 245 * @return 246 */ 247 @Override getTargetContext()248 public Context getTargetContext() { 249 return mTargetContextWrapper; 250 } 251 252 /** 253 * {@inheritDoc} 254 */ 255 @Override getAndroidTestRunner()256 protected AndroidTestRunner getAndroidTestRunner() { 257 // AndroidTestRunner is what determines which tests get to run. 258 // Unfortunately there is no hooks into it, so most of 259 // the functionality has to be duplicated 260 return mRunner; 261 } 262 263 /** 264 * Sets up and starts monitoring jank metrics by clearing the currently existing data. 265 */ startJankMonitoring()266 private void startJankMonitoring () { 267 if (mTrackJank) { 268 mGraphicsStatsMonitor.setUiAutomation(getUiAutomation()); 269 mGraphicsStatsMonitor.startMonitoring(); 270 271 // TODO: Clear graphics.txt file if extant 272 } 273 } 274 275 /** 276 * Stops future monitoring of jank metrics, but preserves current metrics intact. 277 */ stopJankMonitoring()278 private void stopJankMonitoring () { 279 if (mTrackJank) { 280 mGraphicsStatsMonitor.stopMonitoring(); 281 } 282 } 283 284 /** 285 * Aggregates and merges jank metrics and writes them to the graphics file. 286 */ writeJankMetrics()287 private void writeJankMetrics () { 288 if (mTrackJank) { 289 List<JankStat> mergedStats = mGraphicsStatsMonitor.aggregateStatsImages(); 290 String mergedStatsString = JankStat.statsListToString(mergedStats); 291 292 Log.d(LOG_TAG, "Writing jank metrics to the graphics file"); 293 writeGraphicsMessage(mergedStatsString); 294 } 295 } 296 297 /** 298 * Determines which tests to run, configures the test class and then runs the test. 299 */ 300 private class AuptPrivateTestRunner extends AndroidTestRunner { 301 302 private List<TestCase> mTestCases; 303 private List<TestListener> mTestListeners = new ArrayList<>(); 304 private Instrumentation mInstrumentation; 305 private TestResult mTestResult; 306 307 @Override getTestCases()308 public List<TestCase> getTestCases() { 309 if (mTestCases != null) { 310 return mTestCases; 311 } 312 313 List<TestCase> testCases = new ArrayList<TestCase>(super.getTestCases()); 314 List<TestCase> completeList = new ArrayList<TestCase>(); 315 316 for (int i = 0; i < mIterations; i++) { 317 if (mShuffle) { 318 Collections.shuffle(testCases, mRandom); 319 } 320 completeList.addAll(testCases); 321 } 322 323 mTestCases = completeList; 324 return mTestCases; 325 } 326 327 @Override runTest(TestResult testResult)328 public void runTest(TestResult testResult) { 329 mTestResult = testResult; 330 331 ((ProcessStatusTracker)mProcessTracker).setUiAutomation(getUiAutomation()); 332 333 mDataCollector.start(); 334 startJankMonitoring(); 335 336 for (TestListener testListener : mTestListeners) { 337 mTestResult.addListener(testListener); 338 } 339 340 Runnable timeBomb = new Runnable() { 341 @Override 342 public void run() { 343 try { 344 Thread.sleep(mTestCaseTimeout); 345 } catch (InterruptedException e) { 346 return; 347 } 348 // if we ever wake up, a timeout has occurred, set off the bomb, 349 // but trigger a service ANR first 350 if (mGenerateAnr) { 351 Context ctx = getTargetContext(); 352 Log.d(LOG_TAG, "About to induce artificial ANR for debugging"); 353 ctx.startService(new Intent(ctx, BadService.class)); 354 // intentional delay to allow the service ANR to happen then resolve 355 try { 356 Thread.sleep(BadService.DELAY + BadService.DELAY / 4); 357 } catch (InterruptedException e) { 358 // ignore 359 Log.d(LOG_TAG, "interrupted in wait on BadService"); 360 return; 361 } 362 } else { 363 Log.d("THREAD_DUMP", getStackTraces()); 364 } 365 throw new RuntimeException(String.format("max testcase timeout exceeded: %s ms", 366 mTestCaseTimeout)); 367 } 368 }; 369 370 try { 371 // Try to run all TestCases, but ensure the finally block is reached 372 for (TestCase testCase : mTestCases) { 373 setInstrumentationIfInstrumentationTestCase(testCase, mInstrumentation); 374 setupAuptIfAuptTestCase(testCase); 375 376 // Remove device storage as necessary 377 removeOldImagesFromDcimCameraFolder(); 378 379 Thread timeBombThread = null; 380 if (mTestCaseTimeout > 0) { 381 timeBombThread = new Thread(timeBomb); 382 timeBombThread.setName("Boom!"); 383 timeBombThread.setDaemon(true); 384 timeBombThread.start(); 385 } 386 387 try { 388 testCase.run(mTestResult); 389 } catch (AuptTerminator ex) { 390 // Write to progress.txt to pass the exception message to the dashboard 391 writeProgressMessage("Exception: " + ex); 392 // Throw the exception, because we want to discontinue running tests 393 throw ex; 394 } 395 396 if (mTestCaseTimeout > 0) { 397 timeBombThread.interrupt(); 398 try { 399 timeBombThread.join(); 400 } catch (InterruptedException e) { 401 // ignore 402 } 403 } 404 } 405 } finally { 406 // Ensure the DataCollector ends all dangling Threads 407 mDataCollector.stop(); 408 // Ensure the Timer in GraphicsStatsMonitor is canceled 409 stopJankMonitoring(); // However, it is daemon 410 // Ensure jank metrics are written to the graphics file 411 writeJankMetrics(); 412 } 413 } 414 415 /** 416 * Gets all thread stack traces. 417 * 418 * @return string of all thread stack traces 419 */ getStackTraces()420 private String getStackTraces() { 421 StringBuilder sb = new StringBuilder(); 422 Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces(); 423 for (Thread t : stacks.keySet()) { 424 sb.append(t.toString()).append('\n'); 425 for (StackTraceElement ste : t.getStackTrace()) { 426 sb.append("\tat ").append(ste.toString()).append('\n'); 427 } 428 sb.append('\n'); 429 } 430 return sb.toString(); 431 } 432 setInstrumentationIfInstrumentationTestCase( Test test, Instrumentation instrumentation)433 private void setInstrumentationIfInstrumentationTestCase( 434 Test test, Instrumentation instrumentation) { 435 if (InstrumentationTestCase.class.isAssignableFrom(test.getClass())) { 436 ((InstrumentationTestCase) test).injectInstrumentation(instrumentation); 437 } 438 } 439 440 // Aupt specific set up. setupAuptIfAuptTestCase(Test test)441 private void setupAuptIfAuptTestCase(Test test) { 442 if (test instanceof AuptTestCase){ 443 ((AuptTestCase)test).setProcessStatusTracker(mProcessTracker); 444 ((AuptTestCase)test).setMemHealthRecords(mMemHealthRecords); 445 ((AuptTestCase)test).setDataCollector(mDataCollector); 446 ((AuptTestCase)test).setDexedJarPaths(mJars); 447 } 448 } 449 removeOldImagesFromDcimCameraFolder()450 private void removeOldImagesFromDcimCameraFolder() { 451 if (!mDeleteOldFiles) { 452 return; 453 } 454 455 File dcimFolder = new File(Environment.getExternalStorageDirectory(), "DCIM"); 456 if (dcimFolder != null) { 457 File cameraFolder = new File(dcimFolder, "Camera"); 458 if (cameraFolder != null) { 459 File[] allMediaFiles = cameraFolder.listFiles(); 460 Arrays.sort(allMediaFiles, new Comparator<File> () { 461 public int compare(File f1, File f2) { 462 return Long.valueOf(f1.lastModified()).compareTo(f2.lastModified()); 463 } 464 }); 465 for (int i = 0; i < allMediaFiles.length - mFileRetainCount; i++) { 466 allMediaFiles[i].delete(); 467 } 468 } else { 469 Log.w(LOG_TAG, "No Camera folder found to delete from."); 470 } 471 } else { 472 Log.w(LOG_TAG, "No DCIM folder found to delete from."); 473 } 474 } 475 476 @Override clearTestListeners()477 public void clearTestListeners() { 478 mTestListeners.clear(); 479 } 480 481 @Override addTestListener(TestListener testListener)482 public void addTestListener(TestListener testListener) { 483 if (testListener != null) { 484 mTestListeners.add(testListener); 485 } 486 } 487 488 @Override setInstrumentation(Instrumentation instrumentation)489 public void setInstrumentation(Instrumentation instrumentation) { 490 mInstrumentation = instrumentation; 491 } 492 493 @Override getTestResult()494 public TestResult getTestResult() { 495 return mTestResult; 496 } 497 498 @Override createTestResult()499 protected TestResult createTestResult() { 500 return new TestResult(); 501 } 502 } 503 504 /** 505 * Test listener that monitors the AUPT tests for any errors. If the option is set it will 506 * terminate the whole test run if it encounters an exception. 507 */ 508 private class QuitOnErrorListener implements TestListener { 509 510 @Override addError(Test test, Throwable t)511 public void addError(Test test, Throwable t) { 512 Log.e(LOG_TAG, "Caught exception from a test", t); 513 if ((t instanceof AuptTerminator)) { 514 throw (AuptTerminator)t; 515 } else { 516 // check that if the UI exception is caused by process getting killed 517 if (test instanceof AuptTestCase) { 518 ((AuptTestCase)test).getProcessStatusTracker().verifyRunningProcess(); 519 } 520 // if previous line did not throw an exception, we are interested to know what 521 // caused the UI exception 522 Log.v(LOG_TAG, "Dumping UI hierarchy"); 523 try { 524 UiDevice.getInstance(AuptTestRunner.this).dumpWindowHierarchy( 525 new File("/data/local/tmp/error_dump.xml")); 526 } catch (IOException e) { 527 Log.w(LOG_TAG, "Failed to create UI hierarchy dump for UI error", e); 528 } 529 } 530 531 throw new AuptTerminator(t.getMessage(), t); 532 } 533 534 @Override addFailure(Test test, AssertionFailedError t)535 public void addFailure(Test test, AssertionFailedError t) { 536 throw new AuptTerminator(t.getMessage(), t); 537 } 538 539 @Override endTest(Test test)540 public void endTest(Test test) { 541 // skip 542 } 543 544 @Override startTest(Test test)545 public void startTest(Test test) { 546 // skip 547 } 548 } 549 550 /** 551 * A listener that checks that none of the monitored processes died during the test. 552 * If a process dies it will terminate the test early. 553 */ 554 private class PidChecker implements TestListener { 555 556 @Override addError(Test test, Throwable t)557 public void addError(Test test, Throwable t) { 558 // no-op 559 } 560 561 @Override addFailure(Test test, AssertionFailedError t)562 public void addFailure(Test test, AssertionFailedError t) { 563 // no-op 564 } 565 566 @Override endTest(Test test)567 public void endTest(Test test) { 568 mProcessTracker.verifyRunningProcess(); 569 } 570 571 @Override startTest(Test test)572 public void startTest(Test test) { 573 mProcessTracker.verifyRunningProcess(); 574 } 575 } 576 577 private class BatteryChecker implements TestListener { 578 private static final double BATTERY_THRESHOLD = 0.05; 579 checkBattery()580 private void checkBattery() { 581 Intent batteryIntent = getContext().registerReceiver(null, 582 new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); 583 int rawLevel = batteryIntent.getIntExtra("level", -1); 584 int scale = batteryIntent.getIntExtra("scale", -1); 585 586 if (rawLevel < 0 || scale <= 0) { 587 return; 588 } 589 590 double level = (double) rawLevel / (double) scale; 591 if (level < BATTERY_THRESHOLD) { 592 throw new AuptTerminator(String.format("Current battery level %f lower than %f", 593 level, 594 BATTERY_THRESHOLD)); 595 } 596 } 597 598 @Override addError(Test test, Throwable t)599 public void addError(Test test, Throwable t) { 600 // skip 601 } 602 603 @Override addFailure(Test test, AssertionFailedError afe)604 public void addFailure(Test test, AssertionFailedError afe) { 605 // skip 606 } 607 608 @Override endTest(Test test)609 public void endTest(Test test) { 610 // skip 611 } 612 613 @Override startTest(Test test)614 public void startTest(Test test) { 615 checkBattery(); 616 } 617 } 618 619 /** 620 * A {@link ContextWrapper} that overrides {@link Context#getClassLoader()} 621 */ 622 class ClassLoaderContextWrapper extends ContextWrapper { 623 ClassLoaderContextWrapper()624 public ClassLoaderContextWrapper() { 625 super(AuptTestRunner.super.getTargetContext()); 626 } 627 628 /** 629 * Alternatively returns a custom class loader with classes loaded from additional jars 630 */ 631 @Override getClassLoader()632 public ClassLoader getClassLoader() { 633 if (mLoader != null) { 634 return mLoader; 635 } else { 636 return super.getClassLoader(); 637 } 638 } 639 } 640 641 public static class BadService extends Service { 642 public static final long DELAY = 30000; 643 @Override onBind(Intent intent)644 public IBinder onBind(Intent intent) { 645 return null; 646 } 647 648 @Override onStartCommand(Intent intent, int flags, int id)649 public int onStartCommand(Intent intent, int flags, int id) { 650 Log.i(LOG_TAG, "in service start -- about to hang"); 651 try { Thread.sleep(DELAY); } catch (InterruptedException e) { Log.wtf(LOG_TAG, e); } 652 Log.i(LOG_TAG, "service hang finished -- stopping and returning"); 653 stopSelf(); 654 return START_NOT_STICKY; 655 } 656 } 657 } 658