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