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