Home | History | Annotate | Download | only in helpers
      1 /*
      2  * Copyright (C) 2013 DroidDriver committers
      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 com.google.android.droiddriver.helpers;
     18 
     19 import android.app.Activity;
     20 import android.content.Context;
     21 import android.os.Debug;
     22 import android.test.FlakyTest;
     23 import android.util.Log;
     24 
     25 import com.google.android.droiddriver.DroidDriver;
     26 import com.google.android.droiddriver.exceptions.UnrecoverableException;
     27 import com.google.android.droiddriver.util.FileUtils;
     28 import com.google.android.droiddriver.util.Logs;
     29 
     30 import java.io.FileNotFoundException;
     31 import java.io.IOException;
     32 import java.lang.Thread.UncaughtExceptionHandler;
     33 import java.lang.reflect.InvocationTargetException;
     34 import java.lang.reflect.Method;
     35 import java.lang.reflect.Modifier;
     36 
     37 /**
     38  * Base class for tests using DroidDriver that handles uncaught exceptions, for
     39  * example OOME, and takes screenshot on failure. It is NOT required, but
     40  * provides handy utility methods.
     41  */
     42 public abstract class BaseDroidDriverTest<T extends Activity> extends
     43     D2ActivityInstrumentationTestCase2<T> {
     44   private static boolean classSetUpDone = false;
     45   // In case of device-wide fatal errors, e.g. OOME, the remaining tests will
     46   // fail and the messages will not help, so skip them.
     47   private static boolean skipRemainingTests = false;
     48   // Prevent crash by uncaught exception.
     49   private static volatile Throwable uncaughtException;
     50   static {
     51     Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
     52       @Override
     53       public void uncaughtException(Thread thread, Throwable ex) {
     54         uncaughtException = ex;
     55         // In most cases uncaughtException will be reported by onFailure().
     56         // But if it occurs in InstrumentationTestRunner, it's swallowed.
     57         // Always log it for all cases.
     58         Logs.log(Log.ERROR, uncaughtException, "uncaughtException");
     59       }
     60     });
     61   }
     62 
     63   protected DroidDriver driver;
     64 
     65   protected BaseDroidDriverTest(Class<T> activityClass) {
     66     super(activityClass);
     67   }
     68 
     69   @Override
     70   protected void setUp() throws Exception {
     71     super.setUp();
     72     if (!classSetUpDone) {
     73       classSetUp();
     74       classSetUpDone = true;
     75     }
     76     driver = DroidDrivers.get();
     77   }
     78 
     79   @Override
     80   protected void tearDown() throws Exception {
     81     super.tearDown();
     82     driver = null;
     83   }
     84 
     85   protected Context getTargetContext() {
     86     return getInstrumentation().getTargetContext();
     87   }
     88 
     89   /**
     90    * Initializes test fixture once for all tests extending this class. Typically
     91    * you call {@link DroidDrivers#init} with an appropriate instance. If an
     92    * InstrumentationDriver is used, this is a good place to call
     93    * {@link com.google.android.droiddriver.instrumentation.ViewElement#overrideClassName}
     94    */
     95   protected abstract void classSetUp();
     96 
     97   /**
     98    * Takes a screenshot on failure.
     99    */
    100   protected void onFailure(Throwable failure) throws Throwable {
    101     // If skipRemainingTests is true, the failure has already been reported.
    102     if (skipRemainingTests) {
    103       return;
    104     }
    105     if (shouldSkipRemainingTests(failure)) {
    106       skipRemainingTests = true;
    107     }
    108 
    109     // Give uncaughtException (thrown by app instead of tests) high priority
    110     if (uncaughtException != null) {
    111       failure = uncaughtException;
    112     }
    113 
    114     try {
    115       if (failure instanceof OutOfMemoryError) {
    116         dumpHprof();
    117       } else if (uncaughtException == null) {
    118         String baseFileName = getBaseFileName();
    119         driver.dumpUiElementTree(baseFileName + ".xml");
    120         driver.getUiDevice().takeScreenshot(baseFileName + ".png");
    121       }
    122     } catch (Throwable e) {
    123       // This method is for troubleshooting. Do not throw new error; we'll
    124       // throw the original failure.
    125       Logs.log(Log.WARN, e);
    126       if (e instanceof OutOfMemoryError && !(failure instanceof OutOfMemoryError)) {
    127         skipRemainingTests = true;
    128         dumpHprof();
    129       }
    130     }
    131 
    132     throw failure;
    133   }
    134 
    135   protected boolean shouldSkipRemainingTests(Throwable e) {
    136     return e instanceof UnrecoverableException || e instanceof OutOfMemoryError
    137         || skipRemainingTests || uncaughtException != null;
    138   }
    139 
    140   /**
    141    * Gets the base filename for troubleshooting files. For example, a screenshot
    142    * is saved in the file "basename".png.
    143    */
    144   protected String getBaseFileName() {
    145     return "dd/" + getClass().getSimpleName() + "." + getName();
    146   }
    147 
    148   protected void dumpHprof() throws IOException, FileNotFoundException {
    149     String path = FileUtils.getAbsoluteFile(getBaseFileName() + ".hprof").getPath();
    150     // create an empty readable file
    151     FileUtils.open(path).close();
    152     Debug.dumpHprofData(path);
    153   }
    154 
    155   /**
    156    * Fixes JUnit3: always call tearDown even when setUp throws. Also calls
    157    * {@link #onFailure}.
    158    */
    159   @Override
    160   public void runBare() throws Throwable {
    161     if (skipRemainingTests) {
    162       return;
    163     }
    164     if (uncaughtException != null) {
    165       onFailure(uncaughtException);
    166     }
    167 
    168     Throwable exception = null;
    169     try {
    170       setUp();
    171       runTest();
    172     } catch (Throwable runException) {
    173       exception = runException;
    174       // ActivityInstrumentationTestCase2.tearDown() finishes activity
    175       // created by getActivity(), so call this before tearDown().
    176       onFailure(exception);
    177     } finally {
    178       try {
    179         tearDown();
    180       } catch (Throwable tearDownException) {
    181         if (exception == null) {
    182           exception = tearDownException;
    183         }
    184       }
    185     }
    186     if (exception != null) {
    187       throw exception;
    188     }
    189   }
    190 
    191   /**
    192    * Overrides super.runTest() to fail fast when the test is annotated as
    193    * FlakyTest and we should skip remaining tests (the failure is fatal).
    194    * When a flaky test is re-run, tearDown() and setUp() are called first in order
    195    * to reset the test's state.
    196    */
    197   @Override
    198   protected void runTest() throws Throwable {
    199     String fName = getName();
    200     assertNotNull(fName);
    201     Method method = null;
    202     try {
    203       // use getMethod to get all public inherited
    204       // methods. getDeclaredMethods returns all
    205       // methods of this class but excludes the
    206       // inherited ones.
    207       method = getClass().getMethod(fName, (Class[]) null);
    208     } catch (NoSuchMethodException e) {
    209       fail("Method \"" + fName + "\" not found");
    210     }
    211 
    212     if (!Modifier.isPublic(method.getModifiers())) {
    213       fail("Method \"" + fName + "\" should be public");
    214     }
    215 
    216     int tolerance = 1;
    217     if (method.isAnnotationPresent(FlakyTest.class)) {
    218       tolerance = method.getAnnotation(FlakyTest.class).tolerance();
    219     }
    220 
    221     for (int runCount = 0; runCount < tolerance; runCount++) {
    222       if (runCount > 0) {
    223         Logs.logfmt(Log.INFO, "Running %s round %d of %d attempts", fName, runCount + 1, tolerance);
    224         // We are re-attempting a test, so reset all state.
    225         tearDown();
    226         setUp();
    227       }
    228 
    229       try {
    230         method.invoke(this);
    231         return;
    232       } catch (InvocationTargetException e) {
    233         e.fillInStackTrace();
    234         Throwable exception = e.getTargetException();
    235         if (shouldSkipRemainingTests(exception) || runCount >= tolerance - 1) {
    236           throw exception;
    237         }
    238         Logs.log(Log.WARN, exception);
    239       } catch (IllegalAccessException e) {
    240         e.fillInStackTrace();
    241         throw e;
    242       }
    243     }
    244   }
    245 }
    246