• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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