• 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 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