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