blob: 607da9599a0b0072fe21f8946f1e3353df85b9be [file] [log] [blame]
/*
* Copyright (C) 2013 DroidDriver committers
*
* 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 io.appium.droiddriver.helpers;
import android.app.Activity;
import android.content.Context;
import android.os.Debug;
import android.test.FlakyTest;
import android.util.Log;
import java.io.IOException;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import io.appium.droiddriver.DroidDriver;
import io.appium.droiddriver.exceptions.UnrecoverableException;
import io.appium.droiddriver.util.FileUtils;
import io.appium.droiddriver.util.Logs;
/**
* Base class for tests using DroidDriver that reports uncaught exceptions, for * example OOME,
* instead of crash. Also supports other features, including taking screenshot on failure. It is NOT
* required, but provides handy features.
*/
public abstract class BaseDroidDriverTest<T extends Activity> extends
D2ActivityInstrumentationTestCase2<T> {
private static boolean classSetUpDone = false;
// In case of device-wide fatal errors, e.g. OOME, the remaining tests will
// fail and the messages will not help, so skip them.
private static boolean skipRemainingTests = false;
// Store uncaught exception from AUT.
private static volatile Throwable uncaughtException;
static {
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable ex) {
uncaughtException = ex;
// In most cases uncaughtException will be reported by onFailure().
// But if it occurs in InstrumentationTestRunner, it's swallowed.
// Always log it for all cases.
Logs.log(Log.ERROR, uncaughtException, "uncaughtException");
}
});
}
protected DroidDriver driver;
protected BaseDroidDriverTest(Class<T> activityClass) {
super(activityClass);
}
@Override
protected void setUp() throws Exception {
super.setUp();
if (!classSetUpDone) {
classSetUp();
classSetUpDone = true;
}
driver = DroidDrivers.get();
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
driver = null;
}
protected Context getTargetContext() {
return getInstrumentation().getTargetContext();
}
/**
* Initializes test fixture once for all tests extending this class. This may have unexpected
* behavior - if multiple subclasses override this method, only the first override is executed.
* Other overrides are silently ignored. You can either use {@link SingleRun} in {@link #setUp},
* or override this method, which is a simpler alternative with the aforementioned catch.
*/
protected void classSetUp() {
DroidDriversInitializer.get(DroidDrivers.newDriver()).singleRun();
}
protected boolean reportSkippedAsFailed() {
return false;
}
protected void skip() {
if (reportSkippedAsFailed()) {
fail("Skipped due to prior failure");
}
}
/**
* Hook for handling failure, for example, taking a screenshot.
*/
protected void onFailure(Throwable failure) throws Throwable {
// If skipRemainingTests is true, the failure has already been reported.
if (skipRemainingTests) {
return;
}
if (shouldSkipRemainingTests(failure)) {
skipRemainingTests = true;
}
// Give uncaughtException (thrown by AUT instead of tests) high priority
if (uncaughtException != null) {
failure = uncaughtException;
}
try {
if (failure instanceof OutOfMemoryError) {
dumpHprof();
} else if (uncaughtException == null) {
String baseFileName = getBaseFileName();
driver.dumpUiElementTree(baseFileName + ".xml");
driver.getUiDevice().takeScreenshot(baseFileName + ".png");
}
} catch (Throwable e) {
// This method is for troubleshooting. Do not throw new error; we'll
// throw the original failure.
Logs.log(Log.WARN, e);
if (e instanceof OutOfMemoryError && !(failure instanceof OutOfMemoryError)) {
skipRemainingTests = true;
try {
dumpHprof();
} catch (Throwable ignored) {
}
}
}
throw failure;
}
protected boolean shouldSkipRemainingTests(Throwable e) {
return e instanceof UnrecoverableException || e instanceof OutOfMemoryError
|| skipRemainingTests || uncaughtException != null;
}
/**
* Gets the base filename for troubleshooting files. For example, a screenshot
* is saved in the file "basename".png.
*/
protected String getBaseFileName() {
return "dd/" + getClass().getSimpleName() + "." + getName();
}
protected void dumpHprof() throws IOException {
String path = FileUtils.getAbsoluteFile(getBaseFileName() + ".hprof").getPath();
// create an empty readable file
FileUtils.open(path).close();
Debug.dumpHprofData(path);
}
/**
* Fixes JUnit3: always call tearDown even when setUp throws. Also adds the
* {@link #onFailure} hook.
*/
@Override
public void runBare() throws Throwable {
if (skipRemainingTests) {
skip();
return;
}
if (uncaughtException != null) {
onFailure(uncaughtException);
}
Throwable exception = null;
try {
setUp();
runTest();
} catch (Throwable runException) {
exception = runException;
// ActivityInstrumentationTestCase2.tearDown() finishes activity
// created by getActivity(), so call this before tearDown().
onFailure(exception);
} finally {
try {
tearDown();
} catch (Throwable tearDownException) {
if (exception == null) {
exception = tearDownException;
}
}
}
if (exception != null) {
throw exception;
}
}
/**
* Overrides to fail fast when the test is annotated as FlakyTest and we should skip remaining
* tests (the failure is fatal). Most lines are copied from super classes.
* <p>
* When a flaky test is re-run, tearDown() and setUp() are called first in order to reset state.
*/
@Override
protected void runTest() throws Throwable {
String fName = getName();
assertNotNull(fName);
Method method = null;
try {
// use getMethod to get all public inherited
// methods. getDeclaredMethods returns all
// methods of this class but excludes the
// inherited ones.
method = getClass().getMethod(fName, (Class[]) null);
} catch (NoSuchMethodException e) {
fail("Method \"" + fName + "\" not found");
}
if (!Modifier.isPublic(method.getModifiers())) {
fail("Method \"" + fName + "\" should be public");
}
int tolerance = 1;
if (method.isAnnotationPresent(FlakyTest.class)) {
tolerance = method.getAnnotation(FlakyTest.class).tolerance();
}
for (int runCount = 0; runCount < tolerance; runCount++) {
if (runCount > 0) {
Logs.logfmt(Log.INFO, "Running %s round %d of %d attempts", fName, runCount + 1, tolerance);
// We are re-attempting a test, so reset all state.
tearDown();
setUp();
}
try {
method.invoke(this);
return;
} catch (InvocationTargetException e) {
e.fillInStackTrace();
Throwable exception = e.getTargetException();
if (shouldSkipRemainingTests(exception) || runCount >= tolerance - 1) {
throw exception;
}
Logs.log(Log.WARN, exception);
} catch (IllegalAccessException e) {
e.fillInStackTrace();
throw e;
}
}
}
}