diff options
Diffstat (limited to 'src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java')
-rw-r--r-- | src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java | 288 |
1 files changed, 288 insertions, 0 deletions
diff --git a/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java b/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java new file mode 100644 index 0000000..7b29bc0 --- /dev/null +++ b/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java @@ -0,0 +1,288 @@ +/* + * 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.app.Instrumentation; +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> { + /** + * Calls {@link DroidDrivers#init} once and only once. + */ + public static class DroidDriversInitializer extends SingleRun { + private static DroidDriversInitializer instance; + protected final Instrumentation instrumentation; + + protected DroidDriversInitializer(Instrumentation instrumentation) { + this.instrumentation = instrumentation; + } + + @Override + protected void run() { + DroidDrivers.init(DroidDrivers.newDriver(instrumentation)); + } + + public static synchronized DroidDriversInitializer get(Instrumentation instrumentation) { + if (instance == null) { + instance = new DroidDriversInitializer(instrumentation); + } + return instance; + } + } + + 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. + * <p> + * If an InstrumentationDriver is used, this is a good place to call {@link + * io.appium.droiddriver.instrumentation.ViewElement#overrideClassName} + */ + protected void classSetUp() { + DroidDriversInitializer.get(getInstrumentation()).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; + } + } + } +} |