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