• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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