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