• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { @Override public void uncaughtException(Thread thread, Throwable ex) { uncaughtException = ex; Logs.log(Log.ERROR, uncaughtException, "uncaughtException"); } })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 
BaseDroidDriverTest(Class<T> activityClass)65   protected BaseDroidDriverTest(Class<T> activityClass) {
66     super(activityClass);
67   }
68 
69   @Override
setUp()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
tearDown()80   protected void tearDown() throws Exception {
81     super.tearDown();
82     driver = null;
83   }
84 
getTargetContext()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    */
classSetUp()95   protected abstract void classSetUp();
96 
97   /**
98    * Takes a screenshot on failure.
99    */
onFailure(Throwable failure)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 
shouldSkipRemainingTests(Throwable e)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    */
getBaseFileName()144   protected String getBaseFileName() {
145     return "dd/" + getClass().getSimpleName() + "." + getName();
146   }
147 
dumpHprof()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
runBare()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
runTest()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