• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2017 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.Activity;
8 import android.app.ActivityManager;
9 import android.app.Application;
10 import android.app.Instrumentation;
11 import android.app.job.JobScheduler;
12 import android.content.Context;
13 import android.content.ContextWrapper;
14 import android.content.SharedPreferences;
15 import android.content.pm.InstrumentationInfo;
16 import android.content.pm.PackageManager;
17 import android.content.pm.PackageManager.NameNotFoundException;
18 import android.os.Build;
19 import android.os.Build.VERSION;
20 import android.os.Bundle;
21 import android.os.Handler;
22 import android.os.Looper;
23 import android.os.SystemClock;
24 import android.system.Os;
25 import android.text.TextUtils;
26 
27 import androidx.core.content.ContextCompat;
28 import androidx.test.InstrumentationRegistry;
29 import androidx.test.internal.runner.ClassPathScanner;
30 import androidx.test.internal.runner.RunnerArgs;
31 import androidx.test.internal.runner.TestExecutor;
32 import androidx.test.internal.runner.TestRequestBuilder;
33 import androidx.test.runner.AndroidJUnitRunner;
34 
35 import dalvik.system.DexFile;
36 
37 import org.junit.runner.Request;
38 import org.junit.runner.RunWith;
39 
40 import org.chromium.base.ActivityState;
41 import org.chromium.base.ApplicationStatus;
42 import org.chromium.base.CommandLineInitUtil;
43 import org.chromium.base.ContextUtils;
44 import org.chromium.base.FileUtils;
45 import org.chromium.base.LifetimeAssert;
46 import org.chromium.base.Log;
47 import org.chromium.base.library_loader.LibraryLoader;
48 import org.chromium.base.metrics.UmaRecorderHolder;
49 import org.chromium.base.test.util.CallbackHelper;
50 import org.chromium.base.test.util.CommandLineFlags;
51 import org.chromium.base.test.util.InMemorySharedPreferences;
52 import org.chromium.base.test.util.InMemorySharedPreferencesContext;
53 import org.chromium.base.test.util.MinAndroidSdkLevel;
54 import org.chromium.base.test.util.ScalableTimeout;
55 import org.chromium.build.BuildConfig;
56 
57 import java.io.File;
58 import java.io.IOException;
59 import java.lang.reflect.Field;
60 import java.lang.reflect.Method;
61 import java.lang.reflect.Modifier;
62 import java.util.ArrayList;
63 import java.util.Arrays;
64 import java.util.Enumeration;
65 import java.util.List;
66 import java.util.concurrent.TimeUnit;
67 import java.util.concurrent.TimeoutException;
68 
69 /**
70  * A custom AndroidJUnitRunner that supports incremental install and custom test listing. Also
71  * customizes various TestRunner and Instrumentation behaviors, like when Activities get finished,
72  * and adds a timeout to waitForIdleSync.
73  *
74  * <p>Please beware that is this not a class runner. It is declared in test apk AndroidManifest.xml
75  * <instrumentation>
76  */
77 public class BaseChromiumAndroidJUnitRunner extends AndroidJUnitRunner {
78     private static final String LIST_ALL_TESTS_FLAG =
79             "org.chromium.base.test.BaseChromiumAndroidJUnitRunner.TestList";
80     private static final String LIST_TESTS_PACKAGE_FLAG =
81             "org.chromium.base.test.BaseChromiumAndroidJUnitRunner.TestListPackage";
82     private static final String IS_UNIT_TEST_FLAG =
83             "org.chromium.base.test.BaseChromiumAndroidJUnitRunner.IsUnitTest";
84     private static final String EXTRA_CLANG_COVERAGE_DEVICE_FILE =
85             "org.chromium.base.test.BaseChromiumAndroidJUnitRunner.ClangCoverageDeviceFile";
86 
87     /**
88      * This flag is supported by AndroidJUnitRunner.
89      *
90      * See the following page for detail
91      * https://developer.android.com/reference/android/support/test/runner/AndroidJUnitRunner.html
92      */
93     private static final String ARGUMENT_TEST_PACKAGE = "package";
94 
95     /**
96      * The following arguments are corresponding to AndroidJUnitRunner command line arguments.
97      * `annotation`: run with only the argument annotation
98      * `notAnnotation`: run all tests except the ones with argument annotation
99      * `log`: run in log only mode, do not execute tests
100      *
101      * For more detail, please check
102      * https://developer.android.com/reference/android/support/test/runner/AndroidJUnitRunner.html
103      */
104     private static final String ARGUMENT_ANNOTATION = "annotation";
105 
106     private static final String ARGUMENT_NOT_ANNOTATION = "notAnnotation";
107     private static final String ARGUMENT_LOG_ONLY = "log";
108 
109     private static final String TAG = "BaseJUnitRunner";
110 
111     private static final int STATUS_CODE_BATCH_FAILURE = 1338;
112 
113     // The ID of the bundle value Instrumentation uses to report the crash stack, if the test
114     // crashed.
115     private static final String BUNDLE_STACK_ID = "stack";
116 
117     private static final long WAIT_FOR_IDLE_TIMEOUT_MS = 10000L;
118 
119     private static final long FINISH_APP_TASKS_TIMEOUT_MS = 3000L;
120     private static final long FINISH_APP_TASKS_POLL_INTERVAL_MS = 100;
121 
122     static InMemorySharedPreferencesContext sInMemorySharedPreferencesContext;
123 
124     static {
CommandLineFlags.getTestCmdLineFile()125         CommandLineInitUtil.setFilenameOverrideForTesting(CommandLineFlags.getTestCmdLineFile());
126     }
127 
128     @Override
newApplication(ClassLoader cl, String className, Context context)129     public Application newApplication(ClassLoader cl, String className, Context context)
130             throws ClassNotFoundException, IllegalAccessException, InstantiationException {
131         Context targetContext = super.getTargetContext();
132         boolean hasUnderTestApk =
133                 !getContext().getPackageName().equals(targetContext.getPackageName());
134 
135         // Wrap |context| here so that calls to getSharedPreferences() from within
136         // attachBaseContext() will hit our InMemorySharedPreferencesContext.
137         sInMemorySharedPreferencesContext = new InMemorySharedPreferencesContext(context);
138         Application ret = super.newApplication(cl, className, sInMemorySharedPreferencesContext);
139         try {
140             // There is framework code that assumes Application.getBaseContext() can be casted to
141             // ContextImpl (on KitKat for broadcast receivers, refer to ActivityThread.java), so
142             // invert the wrapping relationship.
143             Field baseField = ContextWrapper.class.getDeclaredField("mBase");
144             baseField.setAccessible(true);
145             baseField.set(ret, context);
146             baseField.set(sInMemorySharedPreferencesContext, ret);
147         } catch (NoSuchFieldException e) {
148             throw new RuntimeException(e);
149         }
150 
151         // Replace the application with our wrapper here for any code that runs between
152         // Application.attachBaseContext() and our BaseJUnit4TestRule (e.g. Application.onCreate()).
153         ContextUtils.initApplicationContextForTests(sInMemorySharedPreferencesContext);
154         return ret;
155     }
156 
157     @Override
getTargetContext()158     public Context getTargetContext() {
159         // The target context by default points directly at the ContextImpl, which we can't wrap.
160         // Make it instead point at the Application.
161         return sInMemorySharedPreferencesContext;
162     }
163 
164     /**
165      * Add TestListInstrumentationRunListener when argument ask the runner to list tests info.
166      *
167      * The running mechanism when argument has "listAllTests" is equivalent to that of
168      * {@link androidx.test.runner.AndroidJUnitRunner#onStart()} except it adds
169      * only TestListInstrumentationRunListener to monitor the tests.
170      */
171     @Override
onStart()172     public void onStart() {
173         Bundle arguments = InstrumentationRegistry.getArguments();
174         if (arguments.getString(IS_UNIT_TEST_FLAG) != null) {
175             LibraryLoader.setBrowserProcessStartupBlockedForTesting();
176         }
177 
178         if (shouldListTests()) {
179             Log.w(
180                     TAG,
181                     String.format(
182                             "Runner will list out tests info in JSON without running tests. "
183                                     + "Arguments: %s",
184                             arguments.toString()));
185             listTests(); // Intentionally not calling super.onStart() to avoid additional work.
186         } else {
187             if (arguments != null && arguments.getString(ARGUMENT_LOG_ONLY) != null) {
188                 Log.e(
189                         TAG,
190                         String.format(
191                                 "Runner will log the tests without running tests."
192                                         + " If this cause a test run to fail, please report to"
193                                         + " crbug.com/754015. Arguments: %s",
194                                 arguments.toString()));
195             }
196             finishAllAppTasks(getTargetContext());
197             getTargetContext().getSystemService(JobScheduler.class).cancelAll();
198             checkOrDeleteOnDiskSharedPreferences(false);
199             clearDataDirectory(sInMemorySharedPreferencesContext);
200             InstrumentationRegistry.getInstrumentation().setInTouchMode(true);
201             // //third_party/mockito is looking for android.support.test.InstrumentationRegistry.
202             // Manually set target to override. We can remove this once we roll mockito to support
203             // androidx.test.
204             System.setProperty(
205                     "org.mockito.android.target",
206                     InstrumentationRegistry.getTargetContext().getCacheDir().getPath());
207             setClangCoverageEnvIfEnabled();
208             super.onStart();
209         }
210     }
211 
212     // The Instrumentation implementation of waitForIdleSync does not have a timeout and can wait
213     // indefinitely in the case of animations, etc.
214     //
215     // You should never use this function in new code, as waitForIdleSync hides underlying issues.
216     // There's almost always a better condition to wait on.
217     @Override
waitForIdleSync()218     public void waitForIdleSync() {
219         final CallbackHelper idleCallback = new CallbackHelper();
220         runOnMainSync(
221                 () -> {
222                     Looper.myQueue()
223                             .addIdleHandler(
224                                     () -> {
225                                         idleCallback.notifyCalled();
226                                         return false;
227                                     });
228                 });
229 
230         try {
231             idleCallback.waitForFirst((int) WAIT_FOR_IDLE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
232         } catch (TimeoutException ex) {
233             Log.w(TAG, "Timeout while waiting for idle main thread.");
234         }
235     }
236 
237     // TODO(yolandyan): Move this to test harness side once this class gets removed
addTestListPackage(Bundle bundle)238     private void addTestListPackage(Bundle bundle) {
239         PackageManager pm = getContext().getPackageManager();
240         InstrumentationInfo info;
241         try {
242             info = pm.getInstrumentationInfo(getComponentName(), PackageManager.GET_META_DATA);
243         } catch (NameNotFoundException e) {
244             Log.e(TAG, String.format("Could not find component %s", getComponentName()));
245             throw new RuntimeException(e);
246         }
247         Bundle metaDataBundle = info.metaData;
248         if (metaDataBundle != null && metaDataBundle.getString(LIST_TESTS_PACKAGE_FLAG) != null) {
249             bundle.putString(
250                     ARGUMENT_TEST_PACKAGE, metaDataBundle.getString(LIST_TESTS_PACKAGE_FLAG));
251         }
252     }
253 
listTests()254     private void listTests() {
255         Bundle results = new Bundle();
256         TestListInstrumentationRunListener listener = new TestListInstrumentationRunListener();
257         try {
258             TestExecutor.Builder executorBuilder = new TestExecutor.Builder(this);
259             executorBuilder.addRunListener(listener);
260             Bundle junit3Arguments = new Bundle(InstrumentationRegistry.getArguments());
261             junit3Arguments.putString(ARGUMENT_NOT_ANNOTATION, "org.junit.runner.RunWith");
262             addTestListPackage(junit3Arguments);
263             Request listJUnit3TestRequest = createListTestRequest(junit3Arguments);
264             results = executorBuilder.build().execute(listJUnit3TestRequest);
265 
266             Bundle junit4Arguments = new Bundle(InstrumentationRegistry.getArguments());
267             junit4Arguments.putString(ARGUMENT_ANNOTATION, "org.junit.runner.RunWith");
268             addTestListPackage(junit4Arguments);
269 
270             // Do not use Log runner from android test support.
271             //
272             // Test logging and execution skipping is handled by BaseJUnit4ClassRunner,
273             // having ARGUMENT_LOG_ONLY in argument bundle here causes AndroidJUnitRunner
274             // to use its own log-only class runner instead of BaseJUnit4ClassRunner.
275             junit4Arguments.remove(ARGUMENT_LOG_ONLY);
276 
277             Request listJUnit4TestRequest = createListTestRequest(junit4Arguments);
278             results.putAll(executorBuilder.build().execute(listJUnit4TestRequest));
279             listener.saveTestsToJson(
280                     InstrumentationRegistry.getArguments().getString(LIST_ALL_TESTS_FLAG));
281         } catch (IOException | RuntimeException e) {
282             String msg = "Fatal exception when running tests";
283             Log.e(TAG, msg, e);
284             // report the exception to instrumentation out
285             results.putString(
286                     Instrumentation.REPORT_KEY_STREAMRESULT,
287                     msg + "\n" + Log.getStackTraceString(e));
288         }
289         finish(Activity.RESULT_OK, results);
290     }
291 
createListTestRequest(Bundle arguments)292     private Request createListTestRequest(Bundle arguments) {
293         TestRequestBuilder builder;
294         if (BuildConfig.IS_INCREMENTAL_INSTALL) {
295             try {
296                 Class<?> bootstrapClass =
297                         Class.forName("org.chromium.incrementalinstall.BootstrapApplication");
298                 DexFile[] incrementalInstallDexes =
299                         (DexFile[])
300                                 bootstrapClass.getDeclaredField("sIncrementalDexFiles").get(null);
301                 builder =
302                         new DexFileTestRequestBuilder(
303                                 this, arguments, Arrays.asList(incrementalInstallDexes));
304             } catch (Exception e) {
305                 throw new RuntimeException(e);
306             }
307         } else {
308             builder = new TestRequestBuilder(this, arguments);
309         }
310         RunnerArgs runnerArgs =
311                 new RunnerArgs.Builder().fromManifest(this).fromBundle(this, arguments).build();
312         builder.addFromRunnerArgs(runnerArgs);
313         builder.addPathToScan(getContext().getPackageCodePath());
314 
315         // Ignore tests from framework / support library classes.
316         builder.removeTestPackage("android");
317         builder.setClassLoader(new ForgivingClassLoader());
318         return builder.build();
319     }
320 
shouldListTests()321     static boolean shouldListTests() {
322         Bundle arguments = InstrumentationRegistry.getArguments();
323         return arguments != null && arguments.getString(LIST_ALL_TESTS_FLAG) != null;
324     }
325 
326     /**
327      * Wraps TestRequestBuilder to make it work with incremental install.
328      *
329      * <p>TestRequestBuilder does not know to look through the incremental install dex files, and
330      * has no api for telling it to do so. This class checks to see if the list of tests was given
331      * by the runner (mHasClassList), and if not overrides the auto-detection logic in build() to
332      * manually scan all .dex files.
333      */
334     private static class DexFileTestRequestBuilder extends TestRequestBuilder {
335         final List<String> mExcludedPrefixes = new ArrayList<String>();
336         final List<String> mIncludedPrefixes = new ArrayList<String>();
337         final List<DexFile> mDexFiles;
338         boolean mHasClassList;
339         private ClassLoader mClassLoader = DexFileTestRequestBuilder.class.getClassLoader();
340 
DexFileTestRequestBuilder(Instrumentation instr, Bundle bundle, List<DexFile> dexFiles)341         DexFileTestRequestBuilder(Instrumentation instr, Bundle bundle, List<DexFile> dexFiles) {
342             super(instr, bundle);
343             mDexFiles = dexFiles;
344             mExcludedPrefixes.addAll(ClassPathScanner.getDefaultExcludedPackages());
345         }
346 
347         @Override
removeTestPackage(String testPackage)348         public TestRequestBuilder removeTestPackage(String testPackage) {
349             mExcludedPrefixes.add(testPackage);
350             return this;
351         }
352 
353         @Override
addFromRunnerArgs(RunnerArgs runnerArgs)354         public TestRequestBuilder addFromRunnerArgs(RunnerArgs runnerArgs) {
355             mExcludedPrefixes.addAll(runnerArgs.notTestPackages);
356             mIncludedPrefixes.addAll(runnerArgs.testPackages);
357             // Without clearing, You get IllegalArgumentException:
358             // Ambiguous arguments: cannot provide both test package and test class(es) to run
359             runnerArgs.notTestPackages.clear();
360             runnerArgs.testPackages.clear();
361             return super.addFromRunnerArgs(runnerArgs);
362         }
363 
364         @Override
addTestClass(String className)365         public TestRequestBuilder addTestClass(String className) {
366             mHasClassList = true;
367             return super.addTestClass(className);
368         }
369 
370         @Override
addTestMethod(String testClassName, String testMethodName)371         public TestRequestBuilder addTestMethod(String testClassName, String testMethodName) {
372             mHasClassList = true;
373             return super.addTestMethod(testClassName, testMethodName);
374         }
375 
376         @Override
setClassLoader(ClassLoader loader)377         public TestRequestBuilder setClassLoader(ClassLoader loader) {
378             mClassLoader = loader;
379             return super.setClassLoader(loader);
380         }
381 
382         @Override
build()383         public Request build() {
384             // If a test class was requested, then no need to iterate class loader.
385             if (!mHasClassList) {
386                 // builder.addApkToScan uses new DexFile(path) under the hood, which on Dalvik OS's
387                 // assumes that the optimized dex is in the default location (crashes).
388                 // Perform our own dex file scanning instead as a workaround.
389                 scanDexFilesForTestClasses();
390             }
391             return super.build();
392         }
393 
startsWithAny(String str, List<String> prefixes)394         private static boolean startsWithAny(String str, List<String> prefixes) {
395             for (String prefix : prefixes) {
396                 if (str.startsWith(prefix)) {
397                     return true;
398                 }
399             }
400             return false;
401         }
402 
scanDexFilesForTestClasses()403         private void scanDexFilesForTestClasses() {
404             Log.i(TAG, "Scanning loaded dex files for test classes.");
405             // Mirror TestRequestBuilder.getClassNamesFromClassPath().
406             for (DexFile dexFile : mDexFiles) {
407                 Enumeration<String> classNames = dexFile.entries();
408                 while (classNames.hasMoreElements()) {
409                     String className = classNames.nextElement();
410                     if (!mIncludedPrefixes.isEmpty()
411                             && !startsWithAny(className, mIncludedPrefixes)) {
412                         continue;
413                     }
414                     if (startsWithAny(className, mExcludedPrefixes)) {
415                         continue;
416                     }
417                     if (!className.endsWith("Test")) {
418                         // Speeds up test listing to filter by name before
419                         // trying to load the class. We have an ErrorProne
420                         // check that enforces this convention:
421                         // //tools/android/errorprone_plugin/src/org/chromium/tools/errorprone/plugin/TestClassNameCheck.java
422                         // As of Dec 2019, this speeds up test listing on
423                         // android-kitkat-arm-rel from 41s -> 23s.
424                         continue;
425                     }
426                     if (!className.contains("$") && checkIfTest(className, mClassLoader)) {
427                         addTestClass(className);
428                     }
429                 }
430             }
431         }
432     }
433 
getField(Class<?> clazz, Object instance, String name)434     private static Object getField(Class<?> clazz, Object instance, String name)
435             throws ReflectiveOperationException {
436         Field field = clazz.getDeclaredField(name);
437         field.setAccessible(true);
438         return field.get(instance);
439     }
440 
441     /**
442      * ClassLoader that translates NoClassDefFoundError into ClassNotFoundException.
443      *
444      * Required because Android's TestLoader class tries to load all classes, but catches only
445      * ClassNotFoundException.
446      *
447      * One way NoClassDefFoundError is triggered is on Android L when a class extends a non-existent
448      * class. See https://crbug.com/912690.
449      */
450     private static class ForgivingClassLoader extends ClassLoader {
451         private final ClassLoader mDelegateLoader = getClass().getClassLoader();
452 
453         @Override
loadClass(String name)454         public Class<?> loadClass(String name) throws ClassNotFoundException {
455             try {
456                 var ret = mDelegateLoader.loadClass(name);
457                 // Prevent loading classes that should be skipped due to @MinAndroidSdkLevelon.
458                 // Loading them can cause NoClassDefFoundError to be thrown by junit when listing
459                 // methods (if methods contain types from higher sdk version).
460                 // E.g.: https://chromium-review.googlesource.com/c/chromium/src/+/4738415/1
461                 MinAndroidSdkLevel annotation = ret.getAnnotation(MinAndroidSdkLevel.class);
462                 if (annotation != null && annotation.value() > VERSION.SDK_INT) {
463                     throw new ClassNotFoundException();
464                 }
465                 return ret;
466             } catch (NoClassDefFoundError e) {
467                 throw new ClassNotFoundException(name, e);
468             }
469         }
470     }
471 
checkIfTest(String className, ClassLoader classLoader)472     private static boolean checkIfTest(String className, ClassLoader classLoader) {
473         Class<?> loadedClass = tryLoadClass(className, classLoader);
474         if (loadedClass != null && isTestClass(loadedClass)) {
475             return true;
476         }
477         return false;
478     }
479 
tryLoadClass(String className, ClassLoader classLoader)480     private static Class<?> tryLoadClass(String className, ClassLoader classLoader) {
481         try {
482             return Class.forName(className, false, classLoader);
483         } catch (NoClassDefFoundError | ClassNotFoundException e) {
484             return null;
485         }
486     }
487 
488     // Copied from android.support.test.runner code.
isTestClass(Class<?> loadedClass)489     private static boolean isTestClass(Class<?> loadedClass) {
490         try {
491             if (Modifier.isAbstract(loadedClass.getModifiers())) {
492                 Log.d(
493                         TAG,
494                         String.format(
495                                 "Skipping abstract class %s: not a test", loadedClass.getName()));
496                 return false;
497             }
498             if (junit.framework.Test.class.isAssignableFrom(loadedClass)) {
499                 // ensure that if a TestCase, it has at least one test method otherwise
500                 // TestSuite will throw error
501                 if (junit.framework.TestCase.class.isAssignableFrom(loadedClass)) {
502                     return hasJUnit3TestMethod(loadedClass);
503                 }
504                 return true;
505             }
506             if (loadedClass.isAnnotationPresent(RunWith.class)) {
507                 return true;
508             }
509             for (Method testMethod : loadedClass.getMethods()) {
510                 if (testMethod.isAnnotationPresent(org.junit.Test.class)) {
511                     return true;
512                 }
513             }
514             Log.d(TAG, String.format("Skipping class %s: not a test", loadedClass.getName()));
515             return false;
516         } catch (Exception e) {
517             // Defensively catch exceptions - Will throw runtime exception if it cannot load
518             // methods.
519             Log.w(TAG, String.format("%s in isTestClass for %s", e, loadedClass.getName()));
520             return false;
521         } catch (Error e) {
522             // defensively catch Errors too
523             Log.w(TAG, String.format("%s in isTestClass for %s", e, loadedClass.getName()));
524             return false;
525         }
526     }
527 
hasJUnit3TestMethod(Class<?> loadedClass)528     private static boolean hasJUnit3TestMethod(Class<?> loadedClass) {
529         for (Method testMethod : loadedClass.getMethods()) {
530             if (isPublicTestMethod(testMethod)) {
531                 return true;
532             }
533         }
534         return false;
535     }
536 
537     // copied from junit.framework.TestSuite
isPublicTestMethod(Method m)538     private static boolean isPublicTestMethod(Method m) {
539         return isTestMethod(m) && Modifier.isPublic(m.getModifiers());
540     }
541 
542     // copied from junit.framework.TestSuite
isTestMethod(Method m)543     private static boolean isTestMethod(Method m) {
544         return m.getParameterTypes().length == 0
545                 && m.getName().startsWith("test")
546                 && m.getReturnType().equals(Void.TYPE);
547     }
548 
549     @Override
finish(int resultCode, Bundle results)550     public void finish(int resultCode, Bundle results) {
551         if (shouldListTests()) {
552             super.finish(resultCode, results);
553             return;
554         }
555 
556         try {
557             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
558                 finishAllAppTasks(getTargetContext());
559             }
560             finishAllActivities();
561         } catch (Exception e) {
562             // Ignore any errors finishing Activities so that otherwise passing tests don't fail
563             // during tear down due to framework issues. See crbug.com/653731.
564         }
565 
566         try {
567             writeClangCoverageProfileIfEnabled();
568             getTargetContext().getSystemService(JobScheduler.class).cancelAll();
569             checkOrDeleteOnDiskSharedPreferences(true);
570             UmaRecorderHolder.resetForTesting();
571 
572             // There is a bug on L and below that DestroyActivitiesRule does not cause onStop and
573             // onDestroy. On other versions, DestroyActivitiesRule may still fail flakily. Ignore
574             // lifetime asserts if that is the case.
575             if (!ApplicationStatus.isInitialized()
576                     || ApplicationStatus.isEveryActivityDestroyed()) {
577                 LifetimeAssert.assertAllInstancesDestroyedForTesting();
578             } else {
579                 LifetimeAssert.resetForTesting();
580             }
581         } catch (Exception e) {
582             // It's not possible (as far as I know) to update already reported test results, so we
583             // send another status update have the instrumentation test instance parse it.
584             Bundle b = new Bundle();
585             b.putString(BUNDLE_STACK_ID, Log.getStackTraceString(e));
586             InstrumentationRegistry.getInstrumentation().sendStatus(STATUS_CODE_BATCH_FAILURE, b);
587         }
588 
589         // This will end up force stopping the package, so code after this line will not run.
590         super.finish(resultCode, results);
591     }
592 
593     // Since we prevent the default runner's behaviour of finishing Activities between tests, don't
594     // finish Activities, don't have the runner wait for them to finish either (as this will add a 2
595     // second timeout to each test).
596     @Override
waitForActivitiesToComplete()597     protected void waitForActivitiesToComplete() {}
598 
599     // Note that in this class we cannot use ThreadUtils to post tasks as some tests initialize the
600     // browser in ways that cause tasks posted through PostTask to not run. This function should be
601     // used instead.
602     @Override
runOnMainSync(Runnable runner)603     public void runOnMainSync(Runnable runner) {
604         if (runner.getClass() == ActivityFinisher.class) {
605             // This is a gross hack.
606             // Without migrating to the androidx runner, we have no way to prevent
607             // MonitoringInstrumentation from trying to kill our activities, and we rely on
608             // MonitoringInstrumentation for many things like result reporting.
609             // In order to allow batched tests to reuse Activities, drop the ActivityFinisher tasks
610             // without running them.
611             return;
612         }
613         super.runOnMainSync(runner);
614     }
615 
616     /** Finishes all tasks Chrome has listed in Android's Overview. */
finishAllAppTasks(final Context context)617     private void finishAllAppTasks(final Context context) {
618         // Close all of the tasks one by one.
619         ActivityManager activityManager =
620                 (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
621         for (ActivityManager.AppTask task : activityManager.getAppTasks()) {
622             task.finishAndRemoveTask();
623         }
624         long endTime =
625                 SystemClock.uptimeMillis()
626                         + ScalableTimeout.scaleTimeout(FINISH_APP_TASKS_TIMEOUT_MS);
627         while (activityManager.getAppTasks().size() != 0 && SystemClock.uptimeMillis() < endTime) {
628             try {
629                 Thread.sleep(FINISH_APP_TASKS_POLL_INTERVAL_MS);
630             } catch (InterruptedException e) {
631             }
632         }
633     }
634 
finishAllActivities()635     private void finishAllActivities() {
636         // This mirrors the default logic of the test runner for finishing Activities when
637         // ApplicationStatus isn't initialized. However, we keep Chromium's logic for finishing
638         // Activities below both because it's worked historically and we don't want to risk breaking
639         // things, and because the ActivityFinisher does some filtering on which Activities it
640         // chooses to finish which could potentially cause issues.
641         if (!ApplicationStatus.isInitialized()) {
642             runOnMainSync(() -> new ActivityFinisher().run());
643             super.waitForActivitiesToComplete();
644             return;
645         }
646         Handler mainHandler = new Handler(Looper.getMainLooper());
647         CallbackHelper allDestroyedCalledback = new CallbackHelper();
648         ApplicationStatus.ActivityStateListener activityStateListener =
649                 new ApplicationStatus.ActivityStateListener() {
650                     @Override
651                     public void onActivityStateChange(Activity activity, int newState) {
652                         switch (newState) {
653                             case ActivityState.DESTROYED:
654                                 if (ApplicationStatus.isEveryActivityDestroyed()) {
655                                     // Allow onDestroy to finish running before we notify.
656                                     mainHandler.post(
657                                             () -> {
658                                                 allDestroyedCalledback.notifyCalled();
659                                             });
660                                     ApplicationStatus.unregisterActivityStateListener(this);
661                                 }
662                                 break;
663                             case ActivityState.CREATED:
664                                 if (!activity.isFinishing()) {
665                                     // This is required to ensure we finish any activities created
666                                     // after doing the bulk finish operation below.
667                                     activity.finishAndRemoveTask();
668                                 }
669                                 break;
670                         }
671                     }
672                 };
673 
674         mainHandler.post(
675                 () -> {
676                     if (ApplicationStatus.isEveryActivityDestroyed()) {
677                         allDestroyedCalledback.notifyCalled();
678                     } else {
679                         ApplicationStatus.registerStateListenerForAllActivities(
680                                 activityStateListener);
681                     }
682                     for (Activity a : ApplicationStatus.getRunningActivities()) {
683                         if (!a.isFinishing()) a.finishAndRemoveTask();
684                     }
685                 });
686         try {
687             allDestroyedCalledback.waitForFirst();
688         } catch (TimeoutException e) {
689             // There appears to be a framework bug on K and L where onStop and onDestroy are not
690             // called for a handful of tests. We ignore these exceptions.
691             Log.w(TAG, "Activity failed to be destroyed after a test");
692 
693             runOnMainSync(
694                     () -> {
695                         // Make sure subsequent tests don't have these notifications firing.
696                         ApplicationStatus.unregisterActivityStateListener(activityStateListener);
697                     });
698         }
699     }
700 
701     // This method clears the data directory for the test apk, but device_utils.py clears the data
702     // for the apk under test via `pm clear`. Fake module smoke tests in particular requires some
703     // data to be kept for the apk under test: /sdcard/Android/data/package/files/local_testing
clearDataDirectory(Context targetContext)704     private static void clearDataDirectory(Context targetContext) {
705         File dataDir = ContextCompat.getDataDir(targetContext);
706         File[] files = dataDir.listFiles();
707         if (files == null) return;
708         for (File file : files) {
709             // Symlink to app's native libraries.
710             if (file.getName().equals("lib")) {
711                 continue;
712             }
713             if (file.getName().equals("incremental-install-files")) {
714                 continue;
715             }
716             if (file.getName().equals("code_cache")) {
717                 continue;
718             }
719             // SharedPreferences handled by checkOrDeleteOnDiskSharedPreferences().
720             if (file.getName().equals("shared_prefs")) {
721                 continue;
722             }
723             if (file.isDirectory()
724                     && (file.getName().startsWith("app_") || file.getName().equals("cache"))) {
725                 // Directories are lazily created by PathUtils only once, and so can be cleared but
726                 // not removed.
727                 for (File subFile : file.listFiles()) {
728                     if (!FileUtils.recursivelyDeleteFile(subFile, FileUtils.DELETE_ALL)) {
729                         throw new RuntimeException(
730                                 "Could not delete file: " + subFile.getAbsolutePath());
731                     }
732                 }
733             } else if (!FileUtils.recursivelyDeleteFile(file, FileUtils.DELETE_ALL)) {
734                 throw new RuntimeException("Could not delete file: " + file.getAbsolutePath());
735             }
736         }
737     }
738 
isSharedPrefFileAllowed(File f)739     private static boolean isSharedPrefFileAllowed(File f) {
740         // WebView prefs need to stay because webview tests have no (good) way of hooking
741         // SharedPreferences for instantiated WebViews.
742         String[] allowlist =
743                 new String[] {
744                     "WebViewChromiumPrefs.xml",
745                     "org.chromium.android_webview.devui.MainActivity.xml",
746                     "AwComponentUpdateServicePreferences.xml",
747                     "ComponentsProviderServicePreferences.xml",
748                     "org.chromium.webengine.test.instrumentation_test_apk_preferences.xml",
749                     "AwOriginVisitLoggerPrefs.xml",
750                 };
751         for (String name : allowlist) {
752             // SharedPreferences may also access a ".bak" backup file from a previous run. See
753             // https://crbug.com/1462105#c4 and
754             // https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/SharedPreferencesImpl.java;l=213;drc=6f7c5e0914a18e6adafaa319e670363772e51691
755             // for details.
756             String backupName = name + ".bak";
757 
758             if (f.getName().equals(name) || f.getName().equals(backupName)) {
759                 return true;
760             }
761         }
762         return false;
763     }
764 
checkOrDeleteOnDiskSharedPreferences(boolean check)765     private void checkOrDeleteOnDiskSharedPreferences(boolean check) {
766         File dataDir = ContextCompat.getDataDir(InstrumentationRegistry.getTargetContext());
767         File prefsDir = new File(dataDir, "shared_prefs");
768         File[] files = prefsDir.listFiles();
769         if (files == null) {
770             return;
771         }
772         ArrayList<File> badFiles = new ArrayList<>();
773         for (File f : files) {
774             if (isSharedPrefFileAllowed(f)) {
775                 continue;
776             }
777             if (check) {
778                 badFiles.add(f);
779             } else {
780                 f.delete();
781             }
782         }
783         if (!badFiles.isEmpty()) {
784             String errorMsg =
785                     "Found unexpected shared preferences file(s) after test ran.\n"
786                         + "All code should use ContextUtils.getApplicationContext() when accessing"
787                         + " SharedPreferences so that tests are hooked to use"
788                         + " InMemorySharedPreferences. This could also mean needing to override"
789                         + " getSharedPreferences() on custom Context subclasses (e.g."
790                         + " ChromeBaseAppCompatActivity does this to make Preferences screens"
791                         + " work).\n\n";
792 
793             SharedPreferences testPrefs =
794                     ContextUtils.getApplicationContext()
795                             .getSharedPreferences("test", Context.MODE_PRIVATE);
796             if (!(testPrefs instanceof InMemorySharedPreferences)) {
797                 errorMsg +=
798                         String.format(
799                                 "ContextUtils.getApplicationContext() was set to type \"%s\", which"
800                                     + " does not delegate to InMemorySharedPreferencesContext (this"
801                                     + " is likely the issues).\n\n",
802                                 ContextUtils.getApplicationContext().getClass().getName());
803             }
804 
805             errorMsg += "Files:\n * " + TextUtils.join("\n * ", badFiles);
806             throw new AssertionError(errorMsg);
807         }
808     }
809 
810     /** Configure the required environment variable if Clang coverage argument exists. */
setClangCoverageEnvIfEnabled()811     private void setClangCoverageEnvIfEnabled() {
812         String clangProfileFile =
813                 InstrumentationRegistry.getArguments().getString(EXTRA_CLANG_COVERAGE_DEVICE_FILE);
814         if (clangProfileFile != null) {
815             try {
816                 Os.setenv("LLVM_PROFILE_FILE", clangProfileFile, /* override= */ true);
817             } catch (Exception e) {
818                 Log.w(TAG, "failed to set LLVM_PROFILE_FILE", e);
819             }
820         }
821     }
822 
823     /**
824      * Invoke __llvm_profile_dump() to write raw clang coverage profile to device.
825      * Noop if the required build flag is not set.
826      */
writeClangCoverageProfileIfEnabled()827     private void writeClangCoverageProfileIfEnabled() {
828         if (BuildConfig.WRITE_CLANG_PROFILING_DATA && LibraryLoader.getInstance().isInitialized()) {
829             ClangProfiler.writeClangProfilingProfile();
830         }
831     }
832 }
833