1 // Copyright 2016 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.base.test; 6 7 import android.app.Application; 8 import android.content.Context; 9 import android.os.Bundle; 10 import android.os.SystemClock; 11 12 import androidx.annotation.CallSuper; 13 import androidx.test.InstrumentationRegistry; 14 import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner; 15 import androidx.test.internal.util.AndroidRunnerParams; 16 17 import org.junit.rules.MethodRule; 18 import org.junit.rules.TestRule; 19 import org.junit.runner.Description; 20 import org.junit.runner.notification.RunNotifier; 21 import org.junit.runners.model.FrameworkMethod; 22 import org.junit.runners.model.InitializationError; 23 import org.junit.runners.model.Statement; 24 25 import org.chromium.base.CommandLine; 26 import org.chromium.base.Log; 27 import org.chromium.base.ResettersForTesting; 28 import org.chromium.base.test.params.MethodParamAnnotationRule; 29 import org.chromium.base.test.util.AndroidSdkLevelSkipCheck; 30 import org.chromium.base.test.util.CommandLineFlags; 31 import org.chromium.base.test.util.DisableIfSkipCheck; 32 import org.chromium.base.test.util.EspressoIdleTimeoutRule; 33 import org.chromium.base.test.util.RestrictionSkipCheck; 34 import org.chromium.base.test.util.SkipCheck; 35 36 import java.io.File; 37 import java.util.ArrayList; 38 import java.util.Arrays; 39 import java.util.Collections; 40 import java.util.List; 41 import java.util.concurrent.TimeUnit; 42 43 /** 44 * A custom runner for JUnit4 tests that checks requirements to conditionally ignore tests. 45 * 46 * This ClassRunner imports from AndroidJUnit4ClassRunner which is a hidden but accessible 47 * class. The reason is that default JUnit4 runner for Android is a final class, 48 * AndroidJUnit4. We need to extends an inheritable class to change {@link #runChild} 49 * and {@link #isIgnored} to add SkipChecks and PreTesthook. 50 */ 51 public class BaseJUnit4ClassRunner extends AndroidJUnit4ClassRunner { 52 private static final String TAG = "BaseJUnit4ClassRunnr"; 53 54 private static final String EXTRA_TRACE_FILE = 55 "org.chromium.base.test.BaseJUnit4ClassRunner.TraceFile"; 56 57 // Arbirary int that must not overlap with status codes defined by 58 // https://developer.android.com/reference/android/test/InstrumentationTestRunner.html#REPORT_VALUE_ID 59 private static final int STATUS_CODE_TEST_DURATION = 1337; 60 private static final String DURATION_BUNDLE_ID = "duration_ms"; 61 62 /** 63 * An interface for classes that have some code to run before (or after) the class is 64 * instantiated. They run after {@Link BeforeClass} (or before @AfterClass) methods are called. 65 * Provides access to the test class (and the annotations defined for it) and the 66 * instrumentation context. 67 * 68 * The only reason to use a ClassHook instead of a TestRule is because @BeforeClass/@AfterClass 69 * run during test listing, or multiple times for parameterized tests. See 70 * https://crbug.com/1090043. 71 * 72 * TODO(https://crbug.com/1092646): Migrate all Class/Test Hooks to TestRules. 73 */ 74 public interface ClassHook { 75 /** 76 * @param targetContext the instrumentation context that will be used during the test. 77 * @param testMethod the test method to be run. 78 */ run(Context targetContext, Class<?> testClass)79 public void run(Context targetContext, Class<?> testClass); 80 } 81 82 /** 83 * An interface for classes that have some code to run before a test. They run after 84 * {@link SkipCheck}s and before {@Link Before} (or after @After). Provides access to the test 85 * method (and the annotations defined for it) and the instrumentation context. 86 * 87 * Do not use TestHooks unless you also require ClassHooks. Otherwise, you should use TestRules 88 * and {@link #getDefaultTestRules}. 89 */ 90 public interface TestHook { 91 /** 92 * @param targetContext the instrumentation context that will be used during the test. 93 * @param testMethod the test method to be run. 94 */ run(Context targetContext, FrameworkMethod testMethod)95 public void run(Context targetContext, FrameworkMethod testMethod); 96 } 97 98 /** 99 * Create a BaseJUnit4ClassRunner to run {@code klass} and initialize values. 100 * 101 * @throws InitializationError if the test class malformed 102 */ BaseJUnit4ClassRunner(final Class<?> klass)103 public BaseJUnit4ClassRunner(final Class<?> klass) throws InitializationError { 104 super( 105 klass, 106 new AndroidRunnerParams( 107 InstrumentationRegistry.getInstrumentation(), 108 InstrumentationRegistry.getArguments(), 109 false, 110 0L, 111 false)); 112 113 assert InstrumentationRegistry.getInstrumentation() 114 instanceof BaseChromiumAndroidJUnitRunner 115 : "Must use BaseChromiumAndroidJUnitRunner instrumentation with " 116 + "BaseJUnit4ClassRunner, but found: " 117 + InstrumentationRegistry.getInstrumentation().getClass(); 118 String traceOutput = InstrumentationRegistry.getArguments().getString(EXTRA_TRACE_FILE); 119 120 if (traceOutput != null) { 121 File traceOutputFile = new File(traceOutput); 122 File traceOutputDir = traceOutputFile.getParentFile(); 123 124 if (traceOutputDir != null) { 125 if (traceOutputDir.exists() || traceOutputDir.mkdirs()) { 126 TestTraceEvent.enable(traceOutputFile); 127 } 128 } 129 } 130 } 131 132 /** Returns the singleton Application instance. */ getApplication()133 public static Application getApplication() { 134 return (Application) 135 BaseChromiumAndroidJUnitRunner.sInMemorySharedPreferencesContext.getBaseContext(); 136 } 137 138 /** 139 * Merge two List into a new ArrayList. 140 * 141 * Used to merge the default SkipChecks/PreTestHooks with the subclasses's 142 * SkipChecks/PreTestHooks. 143 */ mergeList(List<T> listA, List<T> listB)144 private static <T> List<T> mergeList(List<T> listA, List<T> listB) { 145 List<T> l = new ArrayList<>(listA); 146 l.addAll(listB); 147 return l; 148 } 149 150 @SafeVarargs addToList(List<T> list, T... additionalEntries)151 protected static <T> List<T> addToList(List<T> list, T... additionalEntries) { 152 return mergeList(list, Arrays.asList(additionalEntries)); 153 } 154 155 @Override collectInitializationErrors(List<Throwable> errors)156 protected void collectInitializationErrors(List<Throwable> errors) { 157 super.collectInitializationErrors(errors); 158 // Log any initialization errors to help debugging, as the host-side test runner can get 159 // confused by the thrown exception. 160 if (!errors.isEmpty()) { 161 Log.e(TAG, "Initialization errors in %s: %s", getTestClass().getName(), errors); 162 } 163 } 164 165 /** 166 * Override this method to return a list of {@link SkipCheck}s}. 167 * 168 * Additional hooks can be added to the list using {@link #addToList}: 169 * {@code return addToList(super.getSkipChecks(), check1, check2);} 170 */ 171 @CallSuper getSkipChecks()172 protected List<SkipCheck> getSkipChecks() { 173 return Arrays.asList( 174 new RestrictionSkipCheck(InstrumentationRegistry.getTargetContext()), 175 new AndroidSdkLevelSkipCheck(), 176 new DisableIfSkipCheck()); 177 } 178 179 /** 180 * See {@link ClassHook}. Prefer to use TestRules over this. 181 * 182 * Additional hooks can be added to the list by overriding this method and using {@link 183 * #addToList}: 184 * {@code return addToList(super.getPreClassHooks(), hook1, hook2);} 185 */ 186 @CallSuper getPreClassHooks()187 protected List<ClassHook> getPreClassHooks() { 188 return Arrays.asList(CommandLineFlags.getPreClassHook()); 189 } 190 191 /** 192 * See {@link ClassHook}. Prefer to use TestRules over this. 193 * 194 * Additional hooks can be added to the list by overriding this method and using {@link 195 * #addToList}: 196 * {@code return addToList(super.getPostClassHooks(), hook1, hook2);} 197 */ 198 @CallSuper getPostClassHooks()199 protected List<ClassHook> getPostClassHooks() { 200 return Arrays.asList(CommandLineFlags.getPostClassHook()); 201 } 202 203 /** 204 * See {@link TestHook}. Prefer to use TestRules over this. 205 * 206 * Additional hooks can be added to the list by overriding this method and using {@link 207 * #addToList}: 208 * {@code return addToList(super.getPreTestHooks(), hook1, hook2);} 209 */ 210 @CallSuper getPreTestHooks()211 protected List<TestHook> getPreTestHooks() { 212 return Arrays.asList( 213 CommandLineFlags.getPreTestHook(), 214 new UnitTestNoBrowserProcessHook(), 215 new ResetCachedFlagValuesTestHook()); 216 } 217 218 /** 219 * See {@link TestHook}. Prefer to use TestRules over this. 220 * 221 * Additional hooks can be added to the list by overriding this method and using {@link 222 * #addToList}: 223 * {@code return addToList(super.getPostTestHooks(), hook1, hook2);} 224 */ 225 @CallSuper getPostTestHooks()226 protected List<TestHook> getPostTestHooks() { 227 return Arrays.asList(CommandLineFlags.getPostTestHook()); 228 } 229 230 /** 231 * Override this method to return a list of method rules that should be applied to all tests 232 * run with this test runner. 233 * 234 * Additional rules can be added to the list using {@link #addToList}: 235 * {@code return addToList(super.getDefaultMethodRules(), rule1, rule2);} 236 */ 237 @CallSuper getDefaultMethodRules()238 protected List<MethodRule> getDefaultMethodRules() { 239 return Collections.singletonList(new MethodParamAnnotationRule()); 240 } 241 242 /** 243 * Override this method to return a list of rules that should be applied to all tests run with 244 * this test runner. 245 * 246 * Additional rules can be added to the list using {@link #addToList}: 247 * {@code return addToList(super.getDefaultTestRules(), rule1, rule2);} 248 */ 249 @CallSuper getDefaultTestRules()250 protected List<TestRule> getDefaultTestRules() { 251 return Arrays.asList( 252 new BaseJUnit4TestRule(), 253 new MockitoErrorHandler(), 254 new UnitTestLifetimeAssertRule(), 255 new EspressoIdleTimeoutRule(20, TimeUnit.SECONDS)); 256 } 257 258 /** Evaluate whether a FrameworkMethod is ignored based on {@code SkipCheck}s. */ 259 @Override isIgnored(FrameworkMethod method)260 protected boolean isIgnored(FrameworkMethod method) { 261 return super.isIgnored(method) || shouldSkip(method); 262 } 263 264 @Override rules(Object target)265 protected List<MethodRule> rules(Object target) { 266 List<MethodRule> declaredRules = super.rules(target); 267 List<MethodRule> defaultRules = getDefaultMethodRules(); 268 return mergeList(defaultRules, declaredRules); 269 } 270 271 @Override getTestRules(Object target)272 protected final List<TestRule> getTestRules(Object target) { 273 List<TestRule> declaredRules = super.getTestRules(target); 274 List<TestRule> defaultRules = getDefaultTestRules(); 275 return mergeList(declaredRules, defaultRules); 276 } 277 278 /** Run test with or without execution based on bundle arguments. */ 279 @Override run(RunNotifier notifier)280 public void run(RunNotifier notifier) { 281 if (BaseChromiumAndroidJUnitRunner.shouldListTests()) { 282 for (Description child : getDescription().getChildren()) { 283 notifier.fireTestFinished(child); 284 } 285 return; 286 } 287 288 runPreClassHooks(getDescription().getTestClass()); 289 assert CommandLine.isInitialized(); 290 291 super.run(notifier); 292 293 try { 294 runPostClassHooks(getDescription().getTestClass()); 295 } finally { 296 ResettersForTesting.onAfterClass(); 297 } 298 } 299 300 @Override runChild(FrameworkMethod method, RunNotifier notifier)301 protected void runChild(FrameworkMethod method, RunNotifier notifier) { 302 String testName = method.getName(); 303 TestTraceEvent.begin(testName); 304 305 long start = SystemClock.uptimeMillis(); 306 307 ResettersForTesting.setMethodMode(); 308 runPreTestHooks(method); 309 310 super.runChild(method, notifier); 311 ResettersForTesting.onAfterMethod(); 312 313 runPostTestHooks(method); 314 315 Bundle b = new Bundle(); 316 b.putLong(DURATION_BUNDLE_ID, SystemClock.uptimeMillis() - start); 317 InstrumentationRegistry.getInstrumentation().sendStatus(STATUS_CODE_TEST_DURATION, b); 318 319 TestTraceEvent.end(testName); 320 321 // A new instance of BaseJUnit4ClassRunner is created on the device 322 // for each new method, so runChild will only be called once. Thus, we 323 // can disable tracing, and dump the output, once we get here. 324 TestTraceEvent.disable(); 325 } 326 327 /** Loop through all the {@code PreTestHook}s to run them */ runPreTestHooks(FrameworkMethod frameworkMethod)328 private void runPreTestHooks(FrameworkMethod frameworkMethod) { 329 Context targetContext = InstrumentationRegistry.getTargetContext(); 330 for (TestHook hook : getPreTestHooks()) { 331 hook.run(targetContext, frameworkMethod); 332 } 333 } 334 runPreClassHooks(Class<?> klass)335 private void runPreClassHooks(Class<?> klass) { 336 Context targetContext = InstrumentationRegistry.getTargetContext(); 337 for (ClassHook hook : getPreClassHooks()) { 338 hook.run(targetContext, klass); 339 } 340 } 341 runPostTestHooks(FrameworkMethod frameworkMethod)342 private void runPostTestHooks(FrameworkMethod frameworkMethod) { 343 Context targetContext = InstrumentationRegistry.getTargetContext(); 344 for (TestHook hook : getPostTestHooks()) { 345 hook.run(targetContext, frameworkMethod); 346 } 347 } 348 runPostClassHooks(Class<?> klass)349 private void runPostClassHooks(Class<?> klass) { 350 Context targetContext = InstrumentationRegistry.getTargetContext(); 351 for (ClassHook hook : getPostClassHooks()) { 352 hook.run(targetContext, klass); 353 } 354 } 355 356 /** Loop through all the {@code SkipCheck}s to confirm whether a test should be ignored */ shouldSkip(FrameworkMethod method)357 private boolean shouldSkip(FrameworkMethod method) { 358 for (SkipCheck s : getSkipChecks()) { 359 if (s.shouldSkip(method)) { 360 return true; 361 } 362 } 363 return false; 364 } 365 366 /** Overriding this method to take screenshot of failure before tear down functions are run. */ 367 @Override withAfters(FrameworkMethod method, Object test, Statement base)368 protected Statement withAfters(FrameworkMethod method, Object test, Statement base) { 369 return super.withAfters(method, test, new ScreenshotOnFailureStatement(base)); 370 } 371 372 /** 373 * This function replicates the androidx AndroidJUnit4ClassRunner version of this function. 374 * We can delete this override when we migrate to androidx. 375 */ 376 @Override methodInvoker(FrameworkMethod method, Object test)377 protected Statement methodInvoker(FrameworkMethod method, Object test) { 378 if (UiThreadStatement.shouldRunOnUiThread(method)) { 379 return new UiThreadStatement(super.methodInvoker(method, test)); 380 } 381 return super.methodInvoker(method, test); 382 } 383 } 384