| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.base.test; |
| |
| import android.app.Activity; |
| import android.app.ActivityManager; |
| import android.app.Application; |
| import android.app.Instrumentation; |
| import android.app.job.JobScheduler; |
| import android.content.Context; |
| import android.content.ContextWrapper; |
| import android.content.SharedPreferences; |
| import android.content.pm.InstrumentationInfo; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageManager.NameNotFoundException; |
| import android.os.Build; |
| import android.os.Build.VERSION; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.SystemClock; |
| import android.system.Os; |
| import android.text.TextUtils; |
| |
| import androidx.core.content.ContextCompat; |
| import androidx.test.InstrumentationRegistry; |
| import androidx.test.internal.runner.ClassPathScanner; |
| import androidx.test.internal.runner.RunnerArgs; |
| import androidx.test.internal.runner.TestExecutor; |
| import androidx.test.internal.runner.TestRequestBuilder; |
| import androidx.test.runner.AndroidJUnitRunner; |
| |
| import dalvik.system.DexFile; |
| |
| import org.junit.runner.Request; |
| import org.junit.runner.RunWith; |
| |
| import org.chromium.base.ActivityState; |
| import org.chromium.base.ApplicationStatus; |
| import org.chromium.base.CommandLineInitUtil; |
| import org.chromium.base.ContextUtils; |
| import org.chromium.base.FileUtils; |
| import org.chromium.base.LifetimeAssert; |
| import org.chromium.base.Log; |
| import org.chromium.base.library_loader.LibraryLoader; |
| import org.chromium.base.metrics.UmaRecorderHolder; |
| import org.chromium.base.test.util.CallbackHelper; |
| import org.chromium.base.test.util.CommandLineFlags; |
| import org.chromium.base.test.util.InMemorySharedPreferences; |
| import org.chromium.base.test.util.InMemorySharedPreferencesContext; |
| import org.chromium.base.test.util.MinAndroidSdkLevel; |
| import org.chromium.base.test.util.ScalableTimeout; |
| import org.chromium.build.BuildConfig; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Method; |
| import java.lang.reflect.Modifier; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Enumeration; |
| import java.util.List; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| |
| /** |
| * A custom AndroidJUnitRunner that supports incremental install and custom test listing. Also |
| * customizes various TestRunner and Instrumentation behaviors, like when Activities get finished, |
| * and adds a timeout to waitForIdleSync. |
| * |
| * <p>Please beware that is this not a class runner. It is declared in test apk AndroidManifest.xml |
| * <instrumentation> |
| */ |
| public class BaseChromiumAndroidJUnitRunner extends AndroidJUnitRunner { |
| private static final String LIST_ALL_TESTS_FLAG = |
| "org.chromium.base.test.BaseChromiumAndroidJUnitRunner.TestList"; |
| private static final String LIST_TESTS_PACKAGE_FLAG = |
| "org.chromium.base.test.BaseChromiumAndroidJUnitRunner.TestListPackage"; |
| private static final String IS_UNIT_TEST_FLAG = |
| "org.chromium.base.test.BaseChromiumAndroidJUnitRunner.IsUnitTest"; |
| private static final String EXTRA_CLANG_COVERAGE_DEVICE_FILE = |
| "org.chromium.base.test.BaseChromiumAndroidJUnitRunner.ClangCoverageDeviceFile"; |
| |
| /** |
| * This flag is supported by AndroidJUnitRunner. |
| * |
| * See the following page for detail |
| * https://developer.android.com/reference/android/support/test/runner/AndroidJUnitRunner.html |
| */ |
| private static final String ARGUMENT_TEST_PACKAGE = "package"; |
| |
| /** |
| * The following arguments are corresponding to AndroidJUnitRunner command line arguments. |
| * `annotation`: run with only the argument annotation |
| * `notAnnotation`: run all tests except the ones with argument annotation |
| * `log`: run in log only mode, do not execute tests |
| * |
| * For more detail, please check |
| * https://developer.android.com/reference/android/support/test/runner/AndroidJUnitRunner.html |
| */ |
| private static final String ARGUMENT_ANNOTATION = "annotation"; |
| |
| private static final String ARGUMENT_NOT_ANNOTATION = "notAnnotation"; |
| private static final String ARGUMENT_LOG_ONLY = "log"; |
| |
| private static final String TAG = "BaseJUnitRunner"; |
| |
| private static final int STATUS_CODE_BATCH_FAILURE = 1338; |
| |
| // The ID of the bundle value Instrumentation uses to report the crash stack, if the test |
| // crashed. |
| private static final String BUNDLE_STACK_ID = "stack"; |
| |
| private static final long WAIT_FOR_IDLE_TIMEOUT_MS = 10000L; |
| |
| private static final long FINISH_APP_TASKS_TIMEOUT_MS = 3000L; |
| private static final long FINISH_APP_TASKS_POLL_INTERVAL_MS = 100; |
| |
| static InMemorySharedPreferencesContext sInMemorySharedPreferencesContext; |
| |
| static { |
| CommandLineInitUtil.setFilenameOverrideForTesting(CommandLineFlags.getTestCmdLineFile()); |
| } |
| |
| @Override |
| public Application newApplication(ClassLoader cl, String className, Context context) |
| throws ClassNotFoundException, IllegalAccessException, InstantiationException { |
| Context targetContext = super.getTargetContext(); |
| boolean hasUnderTestApk = |
| !getContext().getPackageName().equals(targetContext.getPackageName()); |
| |
| // Wrap |context| here so that calls to getSharedPreferences() from within |
| // attachBaseContext() will hit our InMemorySharedPreferencesContext. |
| sInMemorySharedPreferencesContext = new InMemorySharedPreferencesContext(context); |
| Application ret = super.newApplication(cl, className, sInMemorySharedPreferencesContext); |
| try { |
| // There is framework code that assumes Application.getBaseContext() can be casted to |
| // ContextImpl (on KitKat for broadcast receivers, refer to ActivityThread.java), so |
| // invert the wrapping relationship. |
| Field baseField = ContextWrapper.class.getDeclaredField("mBase"); |
| baseField.setAccessible(true); |
| baseField.set(ret, context); |
| baseField.set(sInMemorySharedPreferencesContext, ret); |
| } catch (NoSuchFieldException e) { |
| throw new RuntimeException(e); |
| } |
| |
| // Replace the application with our wrapper here for any code that runs between |
| // Application.attachBaseContext() and our BaseJUnit4TestRule (e.g. Application.onCreate()). |
| ContextUtils.initApplicationContextForTests(sInMemorySharedPreferencesContext); |
| return ret; |
| } |
| |
| @Override |
| public Context getTargetContext() { |
| // The target context by default points directly at the ContextImpl, which we can't wrap. |
| // Make it instead point at the Application. |
| return sInMemorySharedPreferencesContext; |
| } |
| |
| /** |
| * Add TestListInstrumentationRunListener when argument ask the runner to list tests info. |
| * |
| * The running mechanism when argument has "listAllTests" is equivalent to that of |
| * {@link androidx.test.runner.AndroidJUnitRunner#onStart()} except it adds |
| * only TestListInstrumentationRunListener to monitor the tests. |
| */ |
| @Override |
| public void onStart() { |
| Bundle arguments = InstrumentationRegistry.getArguments(); |
| if (arguments.getString(IS_UNIT_TEST_FLAG) != null) { |
| LibraryLoader.setBrowserProcessStartupBlockedForTesting(); |
| } |
| |
| if (shouldListTests()) { |
| Log.w( |
| TAG, |
| String.format( |
| "Runner will list out tests info in JSON without running tests. " |
| + "Arguments: %s", |
| arguments.toString())); |
| listTests(); // Intentionally not calling super.onStart() to avoid additional work. |
| } else { |
| if (arguments != null && arguments.getString(ARGUMENT_LOG_ONLY) != null) { |
| Log.e( |
| TAG, |
| String.format( |
| "Runner will log the tests without running tests." |
| + " If this cause a test run to fail, please report to" |
| + " crbug.com/754015. Arguments: %s", |
| arguments.toString())); |
| } |
| finishAllAppTasks(getTargetContext()); |
| getTargetContext().getSystemService(JobScheduler.class).cancelAll(); |
| checkOrDeleteOnDiskSharedPreferences(false); |
| clearDataDirectory(sInMemorySharedPreferencesContext); |
| InstrumentationRegistry.getInstrumentation().setInTouchMode(true); |
| // //third_party/mockito is looking for android.support.test.InstrumentationRegistry. |
| // Manually set target to override. We can remove this once we roll mockito to support |
| // androidx.test. |
| System.setProperty( |
| "org.mockito.android.target", |
| InstrumentationRegistry.getTargetContext().getCacheDir().getPath()); |
| setClangCoverageEnvIfEnabled(); |
| super.onStart(); |
| } |
| } |
| |
| // The Instrumentation implementation of waitForIdleSync does not have a timeout and can wait |
| // indefinitely in the case of animations, etc. |
| // |
| // You should never use this function in new code, as waitForIdleSync hides underlying issues. |
| // There's almost always a better condition to wait on. |
| @Override |
| public void waitForIdleSync() { |
| final CallbackHelper idleCallback = new CallbackHelper(); |
| runOnMainSync( |
| () -> { |
| Looper.myQueue() |
| .addIdleHandler( |
| () -> { |
| idleCallback.notifyCalled(); |
| return false; |
| }); |
| }); |
| |
| try { |
| idleCallback.waitForFirst((int) WAIT_FOR_IDLE_TIMEOUT_MS, TimeUnit.MILLISECONDS); |
| } catch (TimeoutException ex) { |
| Log.w(TAG, "Timeout while waiting for idle main thread."); |
| } |
| } |
| |
| // TODO(yolandyan): Move this to test harness side once this class gets removed |
| private void addTestListPackage(Bundle bundle) { |
| PackageManager pm = getContext().getPackageManager(); |
| InstrumentationInfo info; |
| try { |
| info = pm.getInstrumentationInfo(getComponentName(), PackageManager.GET_META_DATA); |
| } catch (NameNotFoundException e) { |
| Log.e(TAG, String.format("Could not find component %s", getComponentName())); |
| throw new RuntimeException(e); |
| } |
| Bundle metaDataBundle = info.metaData; |
| if (metaDataBundle != null && metaDataBundle.getString(LIST_TESTS_PACKAGE_FLAG) != null) { |
| bundle.putString( |
| ARGUMENT_TEST_PACKAGE, metaDataBundle.getString(LIST_TESTS_PACKAGE_FLAG)); |
| } |
| } |
| |
| private void listTests() { |
| Bundle results = new Bundle(); |
| TestListInstrumentationRunListener listener = new TestListInstrumentationRunListener(); |
| try { |
| TestExecutor.Builder executorBuilder = new TestExecutor.Builder(this); |
| executorBuilder.addRunListener(listener); |
| Bundle junit3Arguments = new Bundle(InstrumentationRegistry.getArguments()); |
| junit3Arguments.putString(ARGUMENT_NOT_ANNOTATION, "org.junit.runner.RunWith"); |
| addTestListPackage(junit3Arguments); |
| Request listJUnit3TestRequest = createListTestRequest(junit3Arguments); |
| results = executorBuilder.build().execute(listJUnit3TestRequest); |
| |
| Bundle junit4Arguments = new Bundle(InstrumentationRegistry.getArguments()); |
| junit4Arguments.putString(ARGUMENT_ANNOTATION, "org.junit.runner.RunWith"); |
| addTestListPackage(junit4Arguments); |
| |
| // Do not use Log runner from android test support. |
| // |
| // Test logging and execution skipping is handled by BaseJUnit4ClassRunner, |
| // having ARGUMENT_LOG_ONLY in argument bundle here causes AndroidJUnitRunner |
| // to use its own log-only class runner instead of BaseJUnit4ClassRunner. |
| junit4Arguments.remove(ARGUMENT_LOG_ONLY); |
| |
| Request listJUnit4TestRequest = createListTestRequest(junit4Arguments); |
| results.putAll(executorBuilder.build().execute(listJUnit4TestRequest)); |
| listener.saveTestsToJson( |
| InstrumentationRegistry.getArguments().getString(LIST_ALL_TESTS_FLAG)); |
| } catch (IOException | RuntimeException e) { |
| String msg = "Fatal exception when running tests"; |
| Log.e(TAG, msg, e); |
| // report the exception to instrumentation out |
| results.putString( |
| Instrumentation.REPORT_KEY_STREAMRESULT, |
| msg + "\n" + Log.getStackTraceString(e)); |
| } |
| finish(Activity.RESULT_OK, results); |
| } |
| |
| private Request createListTestRequest(Bundle arguments) { |
| TestRequestBuilder builder; |
| if (BuildConfig.IS_INCREMENTAL_INSTALL) { |
| try { |
| Class<?> bootstrapClass = |
| Class.forName("org.chromium.incrementalinstall.BootstrapApplication"); |
| DexFile[] incrementalInstallDexes = |
| (DexFile[]) |
| bootstrapClass.getDeclaredField("sIncrementalDexFiles").get(null); |
| builder = |
| new DexFileTestRequestBuilder( |
| this, arguments, Arrays.asList(incrementalInstallDexes)); |
| } catch (Exception e) { |
| throw new RuntimeException(e); |
| } |
| } else { |
| builder = new TestRequestBuilder(this, arguments); |
| } |
| RunnerArgs runnerArgs = |
| new RunnerArgs.Builder().fromManifest(this).fromBundle(this, arguments).build(); |
| builder.addFromRunnerArgs(runnerArgs); |
| builder.addPathToScan(getContext().getPackageCodePath()); |
| |
| // Ignore tests from framework / support library classes. |
| builder.removeTestPackage("android"); |
| builder.setClassLoader(new ForgivingClassLoader()); |
| return builder.build(); |
| } |
| |
| static boolean shouldListTests() { |
| Bundle arguments = InstrumentationRegistry.getArguments(); |
| return arguments != null && arguments.getString(LIST_ALL_TESTS_FLAG) != null; |
| } |
| |
| /** |
| * Wraps TestRequestBuilder to make it work with incremental install. |
| * |
| * <p>TestRequestBuilder does not know to look through the incremental install dex files, and |
| * has no api for telling it to do so. This class checks to see if the list of tests was given |
| * by the runner (mHasClassList), and if not overrides the auto-detection logic in build() to |
| * manually scan all .dex files. |
| */ |
| private static class DexFileTestRequestBuilder extends TestRequestBuilder { |
| final List<String> mExcludedPrefixes = new ArrayList<String>(); |
| final List<String> mIncludedPrefixes = new ArrayList<String>(); |
| final List<DexFile> mDexFiles; |
| boolean mHasClassList; |
| private ClassLoader mClassLoader = DexFileTestRequestBuilder.class.getClassLoader(); |
| |
| DexFileTestRequestBuilder(Instrumentation instr, Bundle bundle, List<DexFile> dexFiles) { |
| super(instr, bundle); |
| mDexFiles = dexFiles; |
| mExcludedPrefixes.addAll(ClassPathScanner.getDefaultExcludedPackages()); |
| } |
| |
| @Override |
| public TestRequestBuilder removeTestPackage(String testPackage) { |
| mExcludedPrefixes.add(testPackage); |
| return this; |
| } |
| |
| @Override |
| public TestRequestBuilder addFromRunnerArgs(RunnerArgs runnerArgs) { |
| mExcludedPrefixes.addAll(runnerArgs.notTestPackages); |
| mIncludedPrefixes.addAll(runnerArgs.testPackages); |
| // Without clearing, You get IllegalArgumentException: |
| // Ambiguous arguments: cannot provide both test package and test class(es) to run |
| runnerArgs.notTestPackages.clear(); |
| runnerArgs.testPackages.clear(); |
| return super.addFromRunnerArgs(runnerArgs); |
| } |
| |
| @Override |
| public TestRequestBuilder addTestClass(String className) { |
| mHasClassList = true; |
| return super.addTestClass(className); |
| } |
| |
| @Override |
| public TestRequestBuilder addTestMethod(String testClassName, String testMethodName) { |
| mHasClassList = true; |
| return super.addTestMethod(testClassName, testMethodName); |
| } |
| |
| @Override |
| public TestRequestBuilder setClassLoader(ClassLoader loader) { |
| mClassLoader = loader; |
| return super.setClassLoader(loader); |
| } |
| |
| @Override |
| public Request build() { |
| // If a test class was requested, then no need to iterate class loader. |
| if (!mHasClassList) { |
| // builder.addApkToScan uses new DexFile(path) under the hood, which on Dalvik OS's |
| // assumes that the optimized dex is in the default location (crashes). |
| // Perform our own dex file scanning instead as a workaround. |
| scanDexFilesForTestClasses(); |
| } |
| return super.build(); |
| } |
| |
| private static boolean startsWithAny(String str, List<String> prefixes) { |
| for (String prefix : prefixes) { |
| if (str.startsWith(prefix)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private void scanDexFilesForTestClasses() { |
| Log.i(TAG, "Scanning loaded dex files for test classes."); |
| // Mirror TestRequestBuilder.getClassNamesFromClassPath(). |
| for (DexFile dexFile : mDexFiles) { |
| Enumeration<String> classNames = dexFile.entries(); |
| while (classNames.hasMoreElements()) { |
| String className = classNames.nextElement(); |
| if (!mIncludedPrefixes.isEmpty() |
| && !startsWithAny(className, mIncludedPrefixes)) { |
| continue; |
| } |
| if (startsWithAny(className, mExcludedPrefixes)) { |
| continue; |
| } |
| if (!className.endsWith("Test")) { |
| // Speeds up test listing to filter by name before |
| // trying to load the class. We have an ErrorProne |
| // check that enforces this convention: |
| // //tools/android/errorprone_plugin/src/org/chromium/tools/errorprone/plugin/TestClassNameCheck.java |
| // As of Dec 2019, this speeds up test listing on |
| // android-kitkat-arm-rel from 41s -> 23s. |
| continue; |
| } |
| if (!className.contains("$") && checkIfTest(className, mClassLoader)) { |
| addTestClass(className); |
| } |
| } |
| } |
| } |
| } |
| |
| private static Object getField(Class<?> clazz, Object instance, String name) |
| throws ReflectiveOperationException { |
| Field field = clazz.getDeclaredField(name); |
| field.setAccessible(true); |
| return field.get(instance); |
| } |
| |
| /** |
| * ClassLoader that translates NoClassDefFoundError into ClassNotFoundException. |
| * |
| * Required because Android's TestLoader class tries to load all classes, but catches only |
| * ClassNotFoundException. |
| * |
| * One way NoClassDefFoundError is triggered is on Android L when a class extends a non-existent |
| * class. See https://crbug.com/912690. |
| */ |
| private static class ForgivingClassLoader extends ClassLoader { |
| private final ClassLoader mDelegateLoader = getClass().getClassLoader(); |
| |
| @Override |
| public Class<?> loadClass(String name) throws ClassNotFoundException { |
| try { |
| var ret = mDelegateLoader.loadClass(name); |
| // Prevent loading classes that should be skipped due to @MinAndroidSdkLevelon. |
| // Loading them can cause NoClassDefFoundError to be thrown by junit when listing |
| // methods (if methods contain types from higher sdk version). |
| // E.g.: https://chromium-review.googlesource.com/c/chromium/src/+/4738415/1 |
| MinAndroidSdkLevel annotation = ret.getAnnotation(MinAndroidSdkLevel.class); |
| if (annotation != null && annotation.value() > VERSION.SDK_INT) { |
| throw new ClassNotFoundException(); |
| } |
| return ret; |
| } catch (NoClassDefFoundError e) { |
| throw new ClassNotFoundException(name, e); |
| } |
| } |
| } |
| |
| private static boolean checkIfTest(String className, ClassLoader classLoader) { |
| Class<?> loadedClass = tryLoadClass(className, classLoader); |
| if (loadedClass != null && isTestClass(loadedClass)) { |
| return true; |
| } |
| return false; |
| } |
| |
| private static Class<?> tryLoadClass(String className, ClassLoader classLoader) { |
| try { |
| return Class.forName(className, false, classLoader); |
| } catch (NoClassDefFoundError | ClassNotFoundException e) { |
| return null; |
| } |
| } |
| |
| // Copied from android.support.test.runner code. |
| private static boolean isTestClass(Class<?> loadedClass) { |
| try { |
| if (Modifier.isAbstract(loadedClass.getModifiers())) { |
| Log.d( |
| TAG, |
| String.format( |
| "Skipping abstract class %s: not a test", loadedClass.getName())); |
| return false; |
| } |
| if (junit.framework.Test.class.isAssignableFrom(loadedClass)) { |
| // ensure that if a TestCase, it has at least one test method otherwise |
| // TestSuite will throw error |
| if (junit.framework.TestCase.class.isAssignableFrom(loadedClass)) { |
| return hasJUnit3TestMethod(loadedClass); |
| } |
| return true; |
| } |
| if (loadedClass.isAnnotationPresent(RunWith.class)) { |
| return true; |
| } |
| for (Method testMethod : loadedClass.getMethods()) { |
| if (testMethod.isAnnotationPresent(org.junit.Test.class)) { |
| return true; |
| } |
| } |
| Log.d(TAG, String.format("Skipping class %s: not a test", loadedClass.getName())); |
| return false; |
| } catch (Exception e) { |
| // Defensively catch exceptions - Will throw runtime exception if it cannot load |
| // methods. |
| Log.w(TAG, String.format("%s in isTestClass for %s", e, loadedClass.getName())); |
| return false; |
| } catch (Error e) { |
| // defensively catch Errors too |
| Log.w(TAG, String.format("%s in isTestClass for %s", e, loadedClass.getName())); |
| return false; |
| } |
| } |
| |
| private static boolean hasJUnit3TestMethod(Class<?> loadedClass) { |
| for (Method testMethod : loadedClass.getMethods()) { |
| if (isPublicTestMethod(testMethod)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| // copied from junit.framework.TestSuite |
| private static boolean isPublicTestMethod(Method m) { |
| return isTestMethod(m) && Modifier.isPublic(m.getModifiers()); |
| } |
| |
| // copied from junit.framework.TestSuite |
| private static boolean isTestMethod(Method m) { |
| return m.getParameterTypes().length == 0 |
| && m.getName().startsWith("test") |
| && m.getReturnType().equals(Void.TYPE); |
| } |
| |
| @Override |
| public void finish(int resultCode, Bundle results) { |
| if (shouldListTests()) { |
| super.finish(resultCode, results); |
| return; |
| } |
| |
| try { |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { |
| finishAllAppTasks(getTargetContext()); |
| } |
| finishAllActivities(); |
| } catch (Exception e) { |
| // Ignore any errors finishing Activities so that otherwise passing tests don't fail |
| // during tear down due to framework issues. See crbug.com/653731. |
| } |
| |
| try { |
| writeClangCoverageProfileIfEnabled(); |
| getTargetContext().getSystemService(JobScheduler.class).cancelAll(); |
| checkOrDeleteOnDiskSharedPreferences(true); |
| UmaRecorderHolder.resetForTesting(); |
| |
| // There is a bug on L and below that DestroyActivitiesRule does not cause onStop and |
| // onDestroy. On other versions, DestroyActivitiesRule may still fail flakily. Ignore |
| // lifetime asserts if that is the case. |
| if (!ApplicationStatus.isInitialized() |
| || ApplicationStatus.isEveryActivityDestroyed()) { |
| LifetimeAssert.assertAllInstancesDestroyedForTesting(); |
| } else { |
| LifetimeAssert.resetForTesting(); |
| } |
| } catch (Exception e) { |
| // It's not possible (as far as I know) to update already reported test results, so we |
| // send another status update have the instrumentation test instance parse it. |
| Bundle b = new Bundle(); |
| b.putString(BUNDLE_STACK_ID, Log.getStackTraceString(e)); |
| InstrumentationRegistry.getInstrumentation().sendStatus(STATUS_CODE_BATCH_FAILURE, b); |
| } |
| |
| // This will end up force stopping the package, so code after this line will not run. |
| super.finish(resultCode, results); |
| } |
| |
| // Since we prevent the default runner's behaviour of finishing Activities between tests, don't |
| // finish Activities, don't have the runner wait for them to finish either (as this will add a 2 |
| // second timeout to each test). |
| @Override |
| protected void waitForActivitiesToComplete() {} |
| |
| // Note that in this class we cannot use ThreadUtils to post tasks as some tests initialize the |
| // browser in ways that cause tasks posted through PostTask to not run. This function should be |
| // used instead. |
| @Override |
| public void runOnMainSync(Runnable runner) { |
| if (runner.getClass() == ActivityFinisher.class) { |
| // This is a gross hack. |
| // Without migrating to the androidx runner, we have no way to prevent |
| // MonitoringInstrumentation from trying to kill our activities, and we rely on |
| // MonitoringInstrumentation for many things like result reporting. |
| // In order to allow batched tests to reuse Activities, drop the ActivityFinisher tasks |
| // without running them. |
| return; |
| } |
| super.runOnMainSync(runner); |
| } |
| |
| /** Finishes all tasks Chrome has listed in Android's Overview. */ |
| private void finishAllAppTasks(final Context context) { |
| // Close all of the tasks one by one. |
| ActivityManager activityManager = |
| (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); |
| for (ActivityManager.AppTask task : activityManager.getAppTasks()) { |
| task.finishAndRemoveTask(); |
| } |
| long endTime = |
| SystemClock.uptimeMillis() |
| + ScalableTimeout.scaleTimeout(FINISH_APP_TASKS_TIMEOUT_MS); |
| while (activityManager.getAppTasks().size() != 0 && SystemClock.uptimeMillis() < endTime) { |
| try { |
| Thread.sleep(FINISH_APP_TASKS_POLL_INTERVAL_MS); |
| } catch (InterruptedException e) { |
| } |
| } |
| } |
| |
| private void finishAllActivities() { |
| // This mirrors the default logic of the test runner for finishing Activities when |
| // ApplicationStatus isn't initialized. However, we keep Chromium's logic for finishing |
| // Activities below both because it's worked historically and we don't want to risk breaking |
| // things, and because the ActivityFinisher does some filtering on which Activities it |
| // chooses to finish which could potentially cause issues. |
| if (!ApplicationStatus.isInitialized()) { |
| runOnMainSync(() -> new ActivityFinisher().run()); |
| super.waitForActivitiesToComplete(); |
| return; |
| } |
| Handler mainHandler = new Handler(Looper.getMainLooper()); |
| CallbackHelper allDestroyedCalledback = new CallbackHelper(); |
| ApplicationStatus.ActivityStateListener activityStateListener = |
| new ApplicationStatus.ActivityStateListener() { |
| @Override |
| public void onActivityStateChange(Activity activity, int newState) { |
| switch (newState) { |
| case ActivityState.DESTROYED: |
| if (ApplicationStatus.isEveryActivityDestroyed()) { |
| // Allow onDestroy to finish running before we notify. |
| mainHandler.post( |
| () -> { |
| allDestroyedCalledback.notifyCalled(); |
| }); |
| ApplicationStatus.unregisterActivityStateListener(this); |
| } |
| break; |
| case ActivityState.CREATED: |
| if (!activity.isFinishing()) { |
| // This is required to ensure we finish any activities created |
| // after doing the bulk finish operation below. |
| activity.finishAndRemoveTask(); |
| } |
| break; |
| } |
| } |
| }; |
| |
| mainHandler.post( |
| () -> { |
| if (ApplicationStatus.isEveryActivityDestroyed()) { |
| allDestroyedCalledback.notifyCalled(); |
| } else { |
| ApplicationStatus.registerStateListenerForAllActivities( |
| activityStateListener); |
| } |
| for (Activity a : ApplicationStatus.getRunningActivities()) { |
| if (!a.isFinishing()) a.finishAndRemoveTask(); |
| } |
| }); |
| try { |
| allDestroyedCalledback.waitForFirst(); |
| } catch (TimeoutException e) { |
| // There appears to be a framework bug on K and L where onStop and onDestroy are not |
| // called for a handful of tests. We ignore these exceptions. |
| Log.w(TAG, "Activity failed to be destroyed after a test"); |
| |
| runOnMainSync( |
| () -> { |
| // Make sure subsequent tests don't have these notifications firing. |
| ApplicationStatus.unregisterActivityStateListener(activityStateListener); |
| }); |
| } |
| } |
| |
| // This method clears the data directory for the test apk, but device_utils.py clears the data |
| // for the apk under test via `pm clear`. Fake module smoke tests in particular requires some |
| // data to be kept for the apk under test: /sdcard/Android/data/package/files/local_testing |
| private static void clearDataDirectory(Context targetContext) { |
| File dataDir = ContextCompat.getDataDir(targetContext); |
| File[] files = dataDir.listFiles(); |
| if (files == null) return; |
| for (File file : files) { |
| // Symlink to app's native libraries. |
| if (file.getName().equals("lib")) { |
| continue; |
| } |
| if (file.getName().equals("incremental-install-files")) { |
| continue; |
| } |
| if (file.getName().equals("code_cache")) { |
| continue; |
| } |
| // SharedPreferences handled by checkOrDeleteOnDiskSharedPreferences(). |
| if (file.getName().equals("shared_prefs")) { |
| continue; |
| } |
| if (file.isDirectory() |
| && (file.getName().startsWith("app_") || file.getName().equals("cache"))) { |
| // Directories are lazily created by PathUtils only once, and so can be cleared but |
| // not removed. |
| for (File subFile : file.listFiles()) { |
| if (!FileUtils.recursivelyDeleteFile(subFile, FileUtils.DELETE_ALL)) { |
| throw new RuntimeException( |
| "Could not delete file: " + subFile.getAbsolutePath()); |
| } |
| } |
| } else if (!FileUtils.recursivelyDeleteFile(file, FileUtils.DELETE_ALL)) { |
| throw new RuntimeException("Could not delete file: " + file.getAbsolutePath()); |
| } |
| } |
| } |
| |
| private static boolean isSharedPrefFileAllowed(File f) { |
| // WebView prefs need to stay because webview tests have no (good) way of hooking |
| // SharedPreferences for instantiated WebViews. |
| String[] allowlist = |
| new String[] { |
| "WebViewChromiumPrefs.xml", |
| "org.chromium.android_webview.devui.MainActivity.xml", |
| "AwComponentUpdateServicePreferences.xml", |
| "ComponentsProviderServicePreferences.xml", |
| "org.chromium.webengine.test.instrumentation_test_apk_preferences.xml", |
| "AwOriginVisitLoggerPrefs.xml", |
| }; |
| for (String name : allowlist) { |
| // SharedPreferences may also access a ".bak" backup file from a previous run. See |
| // https://crbug.com/1462105#c4 and |
| // https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/SharedPreferencesImpl.java;l=213;drc=6f7c5e0914a18e6adafaa319e670363772e51691 |
| // for details. |
| String backupName = name + ".bak"; |
| |
| if (f.getName().equals(name) || f.getName().equals(backupName)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private void checkOrDeleteOnDiskSharedPreferences(boolean check) { |
| File dataDir = ContextCompat.getDataDir(InstrumentationRegistry.getTargetContext()); |
| File prefsDir = new File(dataDir, "shared_prefs"); |
| File[] files = prefsDir.listFiles(); |
| if (files == null) { |
| return; |
| } |
| ArrayList<File> badFiles = new ArrayList<>(); |
| for (File f : files) { |
| if (isSharedPrefFileAllowed(f)) { |
| continue; |
| } |
| if (check) { |
| badFiles.add(f); |
| } else { |
| f.delete(); |
| } |
| } |
| if (!badFiles.isEmpty()) { |
| String errorMsg = |
| "Found unexpected shared preferences file(s) after test ran.\n" |
| + "All code should use ContextUtils.getApplicationContext() when accessing" |
| + " SharedPreferences so that tests are hooked to use" |
| + " InMemorySharedPreferences. This could also mean needing to override" |
| + " getSharedPreferences() on custom Context subclasses (e.g." |
| + " ChromeBaseAppCompatActivity does this to make Preferences screens" |
| + " work).\n\n"; |
| |
| SharedPreferences testPrefs = |
| ContextUtils.getApplicationContext() |
| .getSharedPreferences("test", Context.MODE_PRIVATE); |
| if (!(testPrefs instanceof InMemorySharedPreferences)) { |
| errorMsg += |
| String.format( |
| "ContextUtils.getApplicationContext() was set to type \"%s\", which" |
| + " does not delegate to InMemorySharedPreferencesContext (this" |
| + " is likely the issues).\n\n", |
| ContextUtils.getApplicationContext().getClass().getName()); |
| } |
| |
| errorMsg += "Files:\n * " + TextUtils.join("\n * ", badFiles); |
| throw new AssertionError(errorMsg); |
| } |
| } |
| |
| /** Configure the required environment variable if Clang coverage argument exists. */ |
| private void setClangCoverageEnvIfEnabled() { |
| String clangProfileFile = |
| InstrumentationRegistry.getArguments().getString(EXTRA_CLANG_COVERAGE_DEVICE_FILE); |
| if (clangProfileFile != null) { |
| try { |
| Os.setenv("LLVM_PROFILE_FILE", clangProfileFile, /* override= */ true); |
| } catch (Exception e) { |
| Log.w(TAG, "failed to set LLVM_PROFILE_FILE", e); |
| } |
| } |
| } |
| |
| /** |
| * Invoke __llvm_profile_dump() to write raw clang coverage profile to device. |
| * Noop if the required build flag is not set. |
| */ |
| private void writeClangCoverageProfileIfEnabled() { |
| if (BuildConfig.WRITE_CLANG_PROFILING_DATA && LibraryLoader.getInstance().isInitialized()) { |
| ClangProfiler.writeClangProfilingProfile(); |
| } |
| } |
| } |