blob: 1cf66cd9920fb04d7b5e1ccec2f054e1b1c02c8c [file] [log] [blame]
/*
* Copyright (C) 2016 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.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.os.SystemClock;
import android.support.test.uiautomator.UiDevice;
import android.test.AndroidTestRunner;
import android.test.InstrumentationTestCase;
import android.test.InstrumentationTestRunner;
import android.util.Log;
import junit.framework.AssertionFailedError;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestListener;
import junit.framework.TestResult;
import junit.framework.TestSuite;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Ultra-fancy TestRunner to use when running AUPT: supports
*
* - Picking up tests from dexed JARs
* - Running tests for multiple iterations or in a custom order
* - Terminating tests after UI errors, timeouts, or when dependent processes die
* - Injecting additional information into custom TestCase subclasses
* - Passing through continuous metric-collection to a DataCollector instance
* - Collecting bugreports and heapdumps
*
*/
public class AuptTestRunner extends InstrumentationTestRunner {
/* Constants */
private static final String LOG_TAG = AuptTestRunner.class.getSimpleName();
private static final Long ANR_DELAY = 30000L;
private static final Long DEFAULT_SUITE_TIMEOUT = 0L;
private static final Long DEFAULT_TEST_TIMEOUT = 10L;
private static final SimpleDateFormat SCREENSHOT_DATE_FORMAT =
new SimpleDateFormat("dd-mm-yy:HH:mm:ss:SSS");
/* Keep a pointer to our argument bundle around for testing */
private Bundle mParams;
/* Primitive Parameters */
private boolean mDeleteOldFiles;
private long mFileRetainCount;
private boolean mGenerateAnr;
private boolean mRecordMeminfo;
private long mIterations;
private long mSeed;
/* Dumpheap Parameters */
private boolean mDumpheapEnabled;
private long mDumpheapInterval;
private long mDumpheapThreshold;
private long mMaxDumpheaps;
/* String Parameters */
private List<String> mJars = new ArrayList<>();
private List<String> mMemoryTrackedProcesses = new ArrayList<>();
private List<String> mFinishCommands;
/* Other Parameters */
private File mResultsDirectory;
/* Helpers */
private Scheduler mScheduler;
private DataCollector mDataCollector;
private DexTestRunner mRunner;
/* Logging */
private ProcessStatusTracker mProcessTracker;
private List<MemHealthRecord> mMemHealthRecords = new ArrayList<>();
private Map<String, Long> mDumpheapCount = new HashMap<>();
private Map<String, Long> mLastDumpheap = new HashMap<>();
/* Test Initialization */
@Override
public void onCreate(Bundle params) {
mParams = params;
// Parse out primitive parameters
mIterations = parseLongParam("iterations", 1);
mRecordMeminfo = parseBoolParam("record_meminfo", false);
mDumpheapEnabled = parseBoolParam("enableDumpheap", false);
mDumpheapThreshold = parseLongParam("dumpheapThreshold", 200 * 1024 * 1024);
mDumpheapInterval = parseLongParam("dumpheapInterval", 60 * 60 * 1000);
mMaxDumpheaps = parseLongParam("maxDumpheaps", 5);
mSeed = parseLongParam("seed", new Random().nextLong());
// Option: -e finishCommand 'a;b;c;d'
String finishCommandArg = parseStringParam("finishCommand", null);
mFinishCommands =
finishCommandArg == null
? Arrays.<String>asList()
: Arrays.asList(finishCommandArg.split("\\s*;\\s*"));
// Option: -e shuffle true
mScheduler = parseBoolParam("shuffle", false)
? Scheduler.shuffled(new Random(mSeed), mIterations)
: Scheduler.sequential(mIterations);
// Option: -e jars aupt-app-tests.jar:...
mJars.addAll(DexTestRunner.parseDexedJarPaths(parseStringParam("jars", "")));
// Option: -e trackMemory com.pkg1,com.pkg2,...
String memoryTrackedProcesses = parseStringParam("trackMemory", null);
if (memoryTrackedProcesses != null) {
mMemoryTrackedProcesses = Arrays.asList(memoryTrackedProcesses.split(","));
} else {
try {
// Deprecated approach: get tracked processes from a file.
String trackMemoryFileName =
Environment.getExternalStorageDirectory() + "/track_memory.txt";
BufferedReader reader = new BufferedReader(new InputStreamReader(
new FileInputStream(new File(trackMemoryFileName))));
mMemoryTrackedProcesses = Arrays.asList(reader.readLine().split(","));
reader.close();
} catch (NullPointerException | IOException ex) {
mMemoryTrackedProcesses = Arrays.asList();
}
}
// Option: -e detectKill com.pkg1,...,com.pkg8
String processes = parseStringParam("detectKill", null);
if (processes != null) {
mProcessTracker = new ProcessStatusTracker(processes.split(","));
} else {
mProcessTracker = new ProcessStatusTracker(new String[] {});
}
// Option: -e outputLocation aupt_results
mResultsDirectory = new File(Environment.getExternalStorageDirectory(),
parseStringParam("outputLocation", "aupt_results"));
if (!mResultsDirectory.exists() && !mResultsDirectory.mkdirs()) {
Log.w(LOG_TAG, "Could not find or create output directory " + mResultsDirectory);
}
// Option: -e fileRetainCount 1
mFileRetainCount = parseLongParam("fileRetainCount", -1);
mDeleteOldFiles = (mFileRetainCount != -1);
// Primary logging infrastructure
mDataCollector = new DataCollector(
TimeUnit.MINUTES.toMillis(parseLongParam("bugreportInterval", 0)),
TimeUnit.MINUTES.toMillis(parseLongParam("jankInterval", 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)),
TimeUnit.MINUTES.toMillis(parseLongParam("bugreportzInterval", 0)),
mResultsDirectory, this);
// Make our TestRunner and make sure we injectInstrumentation.
mRunner = new DexTestRunner(this, mScheduler, mJars,
TimeUnit.MINUTES.toMillis(parseLongParam("testCaseTimeout", DEFAULT_TEST_TIMEOUT)),
TimeUnit.MINUTES.toMillis(parseLongParam("suiteTimeout", DEFAULT_SUITE_TIMEOUT))) {
@Override
public void runTest(TestResult result) {
for (TestCase test: mTestCases) {
injectInstrumentation(test);
}
try {
super.runTest(result);
} finally {
mDataCollector.stop();
}
}
};
// Aupt's TestListeners
mRunner.addTestListener(new PeriodicHeapDumper());
mRunner.addTestListener(new MemHealthRecorder());
mRunner.addTestListener(new DcimCleaner());
mRunner.addTestListener(new PidChecker());
mRunner.addTestListener(new TimeoutStackDumper());
mRunner.addTestListener(new MemInfoDumper());
mRunner.addTestListener(new FinishCommandRunner());
mRunner.addTestListenerIf(parseBoolParam("generateANR", false), new ANRTrigger());
mRunner.addTestListenerIf(parseBoolParam("quitOnError", false), new QuitOnErrorListener());
mRunner.addTestListenerIf(parseBoolParam("checkBattery", false), new BatteryChecker());
mRunner.addTestListenerIf(parseBoolParam("screenshots", false), new Screenshotter());
// Start our loggers
mDataCollector.start();
// Start the test
super.onCreate(params);
}
/* Option-parsing helpers */
private long parseLongParam(String key, long alternative) throws NumberFormatException {
if (mParams.containsKey(key)) {
return Long.parseLong(mParams.getString(key));
} else {
return alternative;
}
}
private boolean parseBoolParam(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;
}
}
/* Utility methods */
/**
* Injects instrumentation into InstrumentationTestCase and AuptTestCase instances
*/
private void injectInstrumentation(Test test) {
if (InstrumentationTestCase.class.isAssignableFrom(test.getClass())) {
InstrumentationTestCase instrTest = (InstrumentationTestCase) test;
instrTest.injectInstrumentation(AuptTestRunner.this);
}
}
/* Passthrough to our DexTestRunner */
@Override
protected AndroidTestRunner getAndroidTestRunner() {
return mRunner;
}
@Override
public Context getTargetContext() {
return new ContextWrapper(super.getTargetContext()) {
@Override
public ClassLoader getClassLoader() {
if(mRunner != null) {
return mRunner.getDexClassLoader();
} else {
throw new RuntimeException("DexTestRunner not initialized!");
}
}
};
}
/**
* A simple abstract instantiation of TestListener
*
* Primarily meant to work around Java 7's lack of interface-default methods.
*/
abstract static class AuptListener implements TestListener {
/** Called when a test throws an exception. */
public void addError(Test test, Throwable t) {}
/** Called when a test fails. */
public void addFailure(Test test, AssertionFailedError t) {}
/** Called whenever a test ends. */
public void endTest(Test test) {}
/** Called whenever a test begins. */
public void startTest(Test test) {}
}
/**
* Periodically Heap-dump to assist with memory-leaks.
*/
private class PeriodicHeapDumper extends AuptListener {
private Thread mHeapDumpThread;
private class InternalHeapDumper implements Runnable {
private void recordDumpheap(String proc, long pss) throws IOException {
if (!mDumpheapEnabled) {
return;
}
Long count = mDumpheapCount.get(proc);
if (count == null) {
count = 0L;
}
Long lastDumpheap = mLastDumpheap.get(proc);
if (lastDumpheap == null) {
lastDumpheap = 0L;
}
long currentTime = SystemClock.uptimeMillis();
if (pss > mDumpheapThreshold && count < mMaxDumpheaps &&
currentTime - lastDumpheap > mDumpheapInterval) {
recordDumpheap(proc);
mDumpheapCount.put(proc, count + 1);
mLastDumpheap.put(proc, currentTime);
}
}
private void recordDumpheap(String proc) throws IOException {
long count = mDumpheapCount.get(proc);
String filename = String.format("dumpheap-%s-%d", proc, count);
String tempFilename = "/data/local/tmp/" + filename;
String finalFilename = mResultsDirectory + "/" + filename;
AuptTestRunner.this.getUiAutomation().executeShellCommand(
String.format("am dumpheap %s %s", proc, tempFilename));
SystemClock.sleep(3000);
AuptTestRunner.this.getUiAutomation().executeShellCommand(
String.format("cp %s %s", tempFilename, finalFilename));
}
public void run() {
try {
while (true) {
Thread.sleep(mDumpheapInterval);
for(String proc : mMemoryTrackedProcesses) {
recordDumpheap(proc);
}
}
} catch (InterruptedException iex) {
} catch (IOException ioex) {
Log.e(LOG_TAG, "Failed to write heap dump!", ioex);
}
}
}
@Override
public void startTest(Test test) {
mHeapDumpThread = new Thread(new InternalHeapDumper());
mHeapDumpThread.start();
}
@Override
public void endTest(Test test) {
try {
mHeapDumpThread.interrupt();
mHeapDumpThread.join();
} catch (InterruptedException iex) { }
}
}
/**
* Dump memory info on test start/stop
*/
private class MemInfoDumper extends AuptListener {
private void dumpMemInfo() {
if (mRecordMeminfo) {
FilesystemUtil.dumpMeminfo(AuptTestRunner.this, "MemInfoDumper");
}
}
@Override
public void startTest(Test test) {
dumpMemInfo();
}
@Override
public void endTest(Test test) {
dumpMemInfo();
}
}
/**
* Record all of our MemHealthRecords
*/
private class MemHealthRecorder extends AuptListener {
@Override
public void startTest(Test test) {
recordMemHealth();
}
@Override
public void endTest(Test test) {
recordMemHealth();
try {
MemHealthRecord.saveVerbose(mMemHealthRecords,
new File(mResultsDirectory, "memory-health.txt").getPath());
MemHealthRecord.saveCsv(mMemHealthRecords,
new File(mResultsDirectory, "memory-health-details.txt").getPath());
mMemHealthRecords.clear();
} catch (IOException ioex) {
Log.e(LOG_TAG, "Error writing MemHealthRecords", ioex);
}
}
private void recordMemHealth() {
try {
mMemHealthRecords.addAll(MemHealthRecord.get(
AuptTestRunner.this,
mMemoryTrackedProcesses,
System.currentTimeMillis(),
getForegroundProcs()));
} catch (IOException ioex) {
Log.e(LOG_TAG, "Error collecting MemHealthRecords", ioex);
}
}
private List<String> getForegroundProcs() {
List<String> foregroundProcs = new ArrayList<String>();
try {
String compactMeminfo = MemHealthRecord.getProcessOutput(AuptTestRunner.this,
"dumpsys meminfo -c");
for (String line : compactMeminfo.split("\\r?\\n")) {
if (line.contains("proc,fore")) {
String proc = line.split(",")[2];
foregroundProcs.add(proc);
}
}
} catch (IOException e) {
Log.e(LOG_TAG, "Error while getting foreground process", e);
} finally {
return foregroundProcs;
}
}
}
/**
* Kills application and dumps UI Hierarchy on test error
*/
private class QuitOnErrorListener extends AuptListener {
@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 if our exception is caused by process dependency
if (test instanceof AuptTestCase) {
mProcessTracker.setUiAutomation(getUiAutomation());
mProcessTracker.verifyRunningProcess();
}
// If that didn't throw, then dump our hierarchy
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);
}
}
// Quit on an error
throw new AuptTerminator(t.getMessage(), t);
}
@Override
public void addFailure(Test test, AssertionFailedError t) {
// Quit on an error
throw new AuptTerminator(t.getMessage(), t);
}
}
/**
* Makes sure the processes this test requires are all alive
*/
private class PidChecker extends AuptListener {
@Override
public void startTest(Test test) {
mProcessTracker.setUiAutomation(getUiAutomation());
mProcessTracker.verifyRunningProcess();
}
@Override
public void endTest(Test test) {
mProcessTracker.verifyRunningProcess();
}
}
/**
* Initialization for tests that touch the camera
*/
private class DcimCleaner extends AuptListener {
@Override
public void startTest(Test test) {
if (!mDeleteOldFiles) {
return;
}
File dcimFolder = new File(Environment.getExternalStorageDirectory(), "DCIM");
File cameraFolder = new File(dcimFolder, "Camera");
if (dcimFolder.exists()) {
if (cameraFolder.exists()) {
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.");
}
}
}
/**
* Makes sure the battery hasn't died before and after each test.
*/
private class BatteryChecker extends AuptListener {
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 startTest(Test test) {
checkBattery();
}
}
/**
* Generates heap dumps when a test times out
*/
private class TimeoutStackDumper extends AuptListener {
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();
}
@Override
public void addError(Test test, Throwable t) {
if (t instanceof TimeoutException) {
Log.d("THREAD_DUMP", getStackTraces());
}
}
}
/** Generates ANRs when a test takes too long. */
private class ANRTrigger extends AuptListener {
@Override
public void addError(Test test, Throwable t) {
if (t instanceof TimeoutException) {
Context ctx = getTargetContext();
Log.d(LOG_TAG, "About to induce artificial ANR for debugging");
ctx.startService(new Intent(ctx, AnrGenerator.class));
try {
Thread.sleep(ANR_DELAY);
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted while waiting for AnrGenerator...");
}
}
}
/** Service that hangs to trigger an ANR. */
private class AnrGenerator extends Service {
@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(ANR_DELAY);
} catch (InterruptedException e) {
Log.wtf(LOG_TAG, e);
}
Log.i(LOG_TAG, "service hang finished -- stopping and returning");
stopSelf();
return START_NOT_STICKY;
}
}
}
/**
* Collect a screenshot on test failure.
*/
private class Screenshotter extends AuptListener {
private void collectScreenshot(Test test, String suffix) {
UiDevice device = UiDevice.getInstance(AuptTestRunner.this);
if (device == null) {
Log.w(LOG_TAG, "Couldn't collect screenshot on test failure");
return;
}
String testName =
test instanceof TestCase
? ((TestCase) test).getName()
: (test instanceof TestSuite ? ((TestSuite) test).getName() : test.toString());
String fileName =
mResultsDirectory.getPath()
+ "/" + testName.replaceAll(".", "_")
+ suffix + ".png";
device.takeScreenshot(new File(fileName));
}
@Override
public void addError(Test test, Throwable t) {
collectScreenshot(test,
"_failure_screenshot_" + SCREENSHOT_DATE_FORMAT.format(new Date()));
}
@Override
public void addFailure(Test test, AssertionFailedError t) {
collectScreenshot(test,
"_failure_screenshot_" + SCREENSHOT_DATE_FORMAT.format(new Date()));
}
}
/** Runs a command when a test finishes. */
private class FinishCommandRunner extends AuptListener {
@Override
public void endTest(Test test) {
for (String command : mFinishCommands) {
AuptTestRunner.this.getUiAutomation().executeShellCommand(command);
}
}
}
}