blob: 652b143bbc216253c29735e6ee4834705dfbe27c [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.test.aupt;
import android.app.Instrumentation;
import android.app.Service;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
import android.support.test.uiautomator.UiDevice;
import android.test.AndroidTestRunner;
import android.test.InstrumentationTestCase;
import android.test.InstrumentationTestRunner;
import android.util.Log;
import dalvik.system.BaseDexClassLoader;
import junit.framework.AssertionFailedError;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestListener;
import junit.framework.TestResult;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.concurrent.TimeUnit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
/**
* Test runner to use when running AUPT tests.
* <p>
* Adds support for multiple iterations, randomizing the order of the tests,
* terminating tests after UI errors, detecting when processes are getting killed,
* collecting bugreports and procrank data while the test is running.
*/
public class AuptTestRunner extends InstrumentationTestRunner {
private static final String DEFAULT_JAR_PATH = "/data/local/tmp/";
private static final String LOG_TAG = "AuptTestRunner";
private static final String DEX_OPT_PATH = "aupt-opt";
private static final String PARAM_JARS = "jars";
private Bundle mParams;
private long mIterations;
private Random mRandom;
private boolean mShuffle;
private boolean mGenerateAnr;
private long mTestCaseTimeout = TimeUnit.MINUTES.toMillis(10);
private DataCollector mDataCollector;
private File mResultsDirectory;
private boolean mDeleteOldFiles;
private long mFileRetainCount;
private AuptPrivateTestRunner mRunner = new AuptPrivateTestRunner();
private ClassLoader mLoader = null;
private Context mTargetContextWrapper;
private IProcessStatusTracker mProcessTracker;
private Map<String, List<AuptTestCase.MemHealthRecord>> mMemHealthRecords;
private boolean mTrackJank;
private GraphicsStatsMonitor mGraphicsStatsMonitor;
/**
* {@inheritDoc}
*/
@Override
public void onCreate(Bundle params) {
mParams = params;
mMemHealthRecords = new HashMap<String, List<AuptTestCase.MemHealthRecord>>();
mIterations = parseLongParam("iterations", 1);
mShuffle = parseBooleanParam("shuffle", false);
long seed = parseLongParam("seed", (new Random()).nextLong());
Log.d(LOG_TAG, String.format("Using seed value: %s", seed));
mRandom = new Random(seed);
// set to 'generateANR to 'true' when more info required for debugging on test timeout'
mGenerateAnr = parseBooleanParam("generateANR", false);
if (parseBooleanParam("quitOnError", false)) {
mRunner.addTestListener(new QuitOnErrorListener());
}
if (parseBooleanParam("checkBattery", false)) {
mRunner.addTestListener(new BatteryChecker());
}
mTestCaseTimeout = parseLongParam("testCaseTimeout", mTestCaseTimeout);
// Option: -e detectKill com.pkg1,...,com.pkg8
String processes = parseStringParam("detectKill", null);
if (processes != null) {
mProcessTracker = new ProcessStatusTracker(processes.split(","));
} else {
mProcessTracker = new ProcessStatusTracker(null);
}
// Option: -e jankInterval integer
long interval = parseLongParam("jankInterval", -1L);
if (interval <= 0) {
mTrackJank = true;
mGraphicsStatsMonitor = new GraphicsStatsMonitor();
mGraphicsStatsMonitor.setIntervalRate(interval);
}
mRunner.addTestListener(new PidChecker());
mResultsDirectory = new File(Environment.getExternalStorageDirectory(),
parseStringParam("outputLocation", "aupt_results"));
if (!mResultsDirectory.exists() && !mResultsDirectory.mkdirs()) {
Log.w(LOG_TAG, "Did not create output directory");
}
mFileRetainCount = parseLongParam("fileRetainCount", -1);
if (mFileRetainCount == -1) {
mDeleteOldFiles = false;
} else {
mDeleteOldFiles = true;
}
mDataCollector = new DataCollector(
TimeUnit.MINUTES.toMillis(parseLongParam("bugreportInterval", 0)),
TimeUnit.MINUTES.toMillis(parseLongParam("meminfoInterval", 0)),
TimeUnit.MINUTES.toMillis(parseLongParam("cpuinfoInterval", 0)),
TimeUnit.MINUTES.toMillis(parseLongParam("fragmentationInterval", 0)),
TimeUnit.MINUTES.toMillis(parseLongParam("ionInterval", 0)),
TimeUnit.MINUTES.toMillis(parseLongParam("pagetypeinfoInterval", 0)),
TimeUnit.MINUTES.toMillis(parseLongParam("traceInterval", 0)),
mResultsDirectory, this);
String jars = params.getString(PARAM_JARS);
if (jars != null) {
loadDexJars(jars);
}
mTargetContextWrapper = new ClassLoaderContextWrapper();
super.onCreate(params);
}
private void loadDexJars(String jars) {
// scan provided jar paths, translate relative to absolute paths, and check for existence
String[] jarsArray = jars.split(":");
StringBuilder jarFiles = new StringBuilder();
for (int i = 0; i < jarsArray.length; i++) {
String jar = jarsArray[i];
if (!jar.startsWith("/")) {
jar = DEFAULT_JAR_PATH + jar;
}
File jarFile = new File(jar);
if (!jarFile.exists() || !jarFile.canRead()) {
throw new IllegalArgumentException("Jar file does not exist or not accessible: "
+ jar);
}
if (i != 0) {
jarFiles.append(File.pathSeparator);
}
jarFiles.append(jarFile.getAbsolutePath());
}
// now load them
File optDir = new File(getContext().getCacheDir(), DEX_OPT_PATH);
if (!optDir.exists() && !optDir.mkdirs()) {
throw new RuntimeException(
"Failed to create dex optimize directory: " + optDir.getAbsolutePath());
}
mLoader = new BaseDexClassLoader(jarFiles.toString(), optDir, null,
super.getTargetContext().getClassLoader());
}
private long parseLongParam(String key, long alternative) throws NumberFormatException {
if (mParams.containsKey(key)) {
return Long.parseLong(
mParams.getString(key));
} else {
return alternative;
}
}
private boolean parseBooleanParam(String key, boolean alternative)
throws NumberFormatException {
if (mParams.containsKey(key)) {
return Boolean.parseBoolean(mParams.getString(key));
} else {
return alternative;
}
}
private String parseStringParam(String key, String alternative) {
if (mParams.containsKey(key)) {
return mParams.getString(key);
} else {
return alternative;
}
}
private void writeProgressMessage(String msg) {
writeMessage("progress.txt", msg);
}
private void writeGraphicsMessage(String msg) {
writeMessage("graphics.txt", msg);
}
private void writeMessage(String filename, String msg) {
try {
FileOutputStream fos = new FileOutputStream(
new File(mResultsDirectory, filename));
fos.write(msg.getBytes());
fos.flush();
fos.close();
} catch (IOException ioe) {
Log.e(LOG_TAG, "error saving progress file", ioe);
}
}
/**
* Provide a wrapped context so that we can provide an alternative class loader
* @return
*/
@Override
public Context getTargetContext() {
return mTargetContextWrapper;
}
/**
* {@inheritDoc}
*/
@Override
protected AndroidTestRunner getAndroidTestRunner() {
// AndroidTestRunner is what determines which tests get to run.
// Unfortunately there is no hooks into it, so most of
// the functionality has to be duplicated
return mRunner;
}
/**
* Sets up and starts monitoring jank metrics by clearing the currently existing data.
*/
private void startJankMonitoring () {
if (mTrackJank) {
mGraphicsStatsMonitor.setUiAutomation(getUiAutomation());
mGraphicsStatsMonitor.startMonitoring();
// TODO: Clear graphics.txt file if extant
}
}
/**
* Stops future monitoring of jank metrics, but preserves current metrics intact.
*/
private void stopJankMonitoring () {
if (mTrackJank) {
mGraphicsStatsMonitor.stopMonitoring();
}
}
/**
* Aggregates and merges jank metrics and writes them to the graphics file.
*/
private void writeJankMetrics () {
if (mTrackJank) {
List<JankStat> mergedStats = mGraphicsStatsMonitor.aggregateStatsImages();
String mergedStatsString = JankStat.statsListToString(mergedStats);
Log.d(LOG_TAG, "Writing jank metrics to the graphics file");
writeGraphicsMessage(mergedStatsString);
}
}
/**
* Determines which tests to run, configures the test class and then runs the test.
*/
private class AuptPrivateTestRunner extends AndroidTestRunner {
private List<TestCase> mTestCases;
private List<TestListener> mTestListeners = new ArrayList<>();
private Instrumentation mInstrumentation;
private TestResult mTestResult;
@Override
public List<TestCase> getTestCases() {
if (mTestCases != null) {
return mTestCases;
}
List<TestCase> testCases = new ArrayList<TestCase>(super.getTestCases());
List<TestCase> completeList = new ArrayList<TestCase>();
for (int i = 0; i < mIterations; i++) {
if (mShuffle) {
Collections.shuffle(testCases, mRandom);
}
completeList.addAll(testCases);
}
mTestCases = completeList;
return mTestCases;
}
@Override
public void runTest(TestResult testResult) {
mTestResult = testResult;
((ProcessStatusTracker)mProcessTracker).setUiAutomation(getUiAutomation());
mDataCollector.start();
startJankMonitoring();
for (TestListener testListener : mTestListeners) {
mTestResult.addListener(testListener);
}
Runnable timeBomb = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(mTestCaseTimeout);
} catch (InterruptedException e) {
return;
}
// if we ever wake up, a timeout has occurred, set off the bomb,
// but trigger a service ANR first
if (mGenerateAnr) {
Context ctx = getTargetContext();
Log.d(LOG_TAG, "About to induce artificial ANR for debugging");
ctx.startService(new Intent(ctx, BadService.class));
// intentional delay to allow the service ANR to happen then resolve
try {
Thread.sleep(BadService.DELAY + BadService.DELAY / 4);
} catch (InterruptedException e) {
// ignore
Log.d(LOG_TAG, "interrupted in wait on BadService");
return;
}
} else {
Log.d("THREAD_DUMP", getStackTraces());
}
throw new RuntimeException(String.format("max testcase timeout exceeded: %s ms",
mTestCaseTimeout));
}
};
try {
// Try to run all TestCases, but ensure the finally block is reached
for (TestCase testCase : mTestCases) {
setInstrumentationIfInstrumentationTestCase(testCase, mInstrumentation);
setupAuptIfAuptTestCase(testCase);
// Remove device storage as necessary
removeOldImagesFromDcimCameraFolder();
Thread timeBombThread = null;
if (mTestCaseTimeout > 0) {
timeBombThread = new Thread(timeBomb);
timeBombThread.setName("Boom!");
timeBombThread.setDaemon(true);
timeBombThread.start();
}
try {
testCase.run(mTestResult);
} catch (AuptTerminator ex) {
// Write to progress.txt to pass the exception message to the dashboard
writeProgressMessage("Exception: " + ex);
// Throw the exception, because we want to discontinue running tests
throw ex;
}
if (mTestCaseTimeout > 0) {
timeBombThread.interrupt();
try {
timeBombThread.join();
} catch (InterruptedException e) {
// ignore
}
}
}
} finally {
// Ensure the DataCollector ends all dangling Threads
mDataCollector.stop();
// Ensure the Timer in GraphicsStatsMonitor is canceled
stopJankMonitoring(); // However, it is daemon
// Ensure jank metrics are written to the graphics file
writeJankMetrics();
}
}
/**
* Gets all thread stack traces.
*
* @return string of all thread stack traces
*/
private String getStackTraces() {
StringBuilder sb = new StringBuilder();
Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces();
for (Thread t : stacks.keySet()) {
sb.append(t.toString()).append('\n');
for (StackTraceElement ste : t.getStackTrace()) {
sb.append("\tat ").append(ste.toString()).append('\n');
}
sb.append('\n');
}
return sb.toString();
}
private void setInstrumentationIfInstrumentationTestCase(
Test test, Instrumentation instrumentation) {
if (InstrumentationTestCase.class.isAssignableFrom(test.getClass())) {
((InstrumentationTestCase) test).injectInstrumentation(instrumentation);
}
}
// Aupt specific set up.
private void setupAuptIfAuptTestCase(Test test) {
if (test instanceof AuptTestCase){
((AuptTestCase)test).setProcessStatusTracker(mProcessTracker);
((AuptTestCase)test).setMemHealthRecords(mMemHealthRecords);
((AuptTestCase)test).setDataCollector(mDataCollector);
}
}
private void removeOldImagesFromDcimCameraFolder() {
if (!mDeleteOldFiles) {
return;
}
File dcimFolder = new File(Environment.getExternalStorageDirectory(), "DCIM");
if (dcimFolder != null) {
File cameraFolder = new File(dcimFolder, "Camera");
if (cameraFolder != null) {
File[] allMediaFiles = cameraFolder.listFiles();
Arrays.sort(allMediaFiles, new Comparator<File> () {
public int compare(File f1, File f2) {
return Long.valueOf(f1.lastModified()).compareTo(f2.lastModified());
}
});
for (int i = 0; i < allMediaFiles.length - mFileRetainCount; i++) {
allMediaFiles[i].delete();
}
} else {
Log.w(LOG_TAG, "No Camera folder found to delete from.");
}
} else {
Log.w(LOG_TAG, "No DCIM folder found to delete from.");
}
}
@Override
public void clearTestListeners() {
mTestListeners.clear();
}
@Override
public void addTestListener(TestListener testListener) {
if (testListener != null) {
mTestListeners.add(testListener);
}
}
@Override
public void setInstrumentation(Instrumentation instrumentation) {
mInstrumentation = instrumentation;
}
@Override
public TestResult getTestResult() {
return mTestResult;
}
@Override
protected TestResult createTestResult() {
return new TestResult();
}
}
/**
* Test listener that monitors the AUPT tests for any errors. If the option is set it will
* terminate the whole test run if it encounters an exception.
*/
private class QuitOnErrorListener implements TestListener {
@Override
public void addError(Test test, Throwable t) {
Log.e(LOG_TAG, "Caught exception from a test", t);
if ((t instanceof AuptTerminator)) {
throw (AuptTerminator)t;
} else {
// check that if the UI exception is caused by process getting killed
if (test instanceof AuptTestCase) {
((AuptTestCase)test).getProcessStatusTracker().verifyRunningProcess();
}
// if previous line did not throw an exception, we are interested to know what
// caused the UI exception
Log.v(LOG_TAG, "Dumping UI hierarchy");
try {
UiDevice.getInstance(AuptTestRunner.this).dumpWindowHierarchy(
new File("/data/local/tmp/error_dump.xml"));
} catch (IOException e) {
Log.w(LOG_TAG, "Failed to create UI hierarchy dump for UI error", e);
}
}
throw new AuptTerminator(t.getMessage(), t);
}
@Override
public void addFailure(Test test, AssertionFailedError t) {
throw new AuptTerminator(t.getMessage(), t);
}
@Override
public void endTest(Test test) {
// skip
}
@Override
public void startTest(Test test) {
// skip
}
}
/**
* A listener that checks that none of the monitored processes died during the test.
* If a process dies it will terminate the test early.
*/
private class PidChecker implements TestListener {
@Override
public void addError(Test test, Throwable t) {
// no-op
}
@Override
public void addFailure(Test test, AssertionFailedError t) {
// no-op
}
@Override
public void endTest(Test test) {
mProcessTracker.verifyRunningProcess();
}
@Override
public void startTest(Test test) {
mProcessTracker.verifyRunningProcess();
}
}
private class BatteryChecker implements TestListener {
private static final double BATTERY_THRESHOLD = 0.05;
private void checkBattery() {
Intent batteryIntent = getContext().registerReceiver(null,
new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
int rawLevel = batteryIntent.getIntExtra("level", -1);
int scale = batteryIntent.getIntExtra("scale", -1);
if (rawLevel < 0 || scale <= 0) {
return;
}
double level = (double) rawLevel / (double) scale;
if (level < BATTERY_THRESHOLD) {
throw new AuptTerminator(String.format("Current battery level %f lower than %f",
level,
BATTERY_THRESHOLD));
}
}
@Override
public void addError(Test test, Throwable t) {
// skip
}
@Override
public void addFailure(Test test, AssertionFailedError afe) {
// skip
}
@Override
public void endTest(Test test) {
// skip
}
@Override
public void startTest(Test test) {
checkBattery();
}
}
/**
* A {@link ContextWrapper} that overrides {@link Context#getClassLoader()}
*/
class ClassLoaderContextWrapper extends ContextWrapper {
public ClassLoaderContextWrapper() {
super(AuptTestRunner.super.getTargetContext());
}
/**
* Alternatively returns a custom class loader with classes loaded from additional jars
*/
@Override
public ClassLoader getClassLoader() {
if (mLoader != null) {
return mLoader;
} else {
return super.getClassLoader();
}
}
}
public static class BadService extends Service {
public static final long DELAY = 30000;
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int id) {
Log.i(LOG_TAG, "in service start -- about to hang");
try { Thread.sleep(DELAY); } catch (InterruptedException e) { Log.wtf(LOG_TAG, e); }
Log.i(LOG_TAG, "service hang finished -- stopping and returning");
stopSelf();
return START_NOT_STICKY;
}
}
}