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.net; 6 7 import static com.google.common.truth.Truth.assertThat; 8 import static com.google.common.truth.Truth.assertWithMessage; 9 10 import static org.junit.Assume.assumeFalse; 11 import static org.junit.Assume.assumeTrue; 12 13 import static org.chromium.net.truth.UrlResponseInfoSubject.assertThat; 14 15 import android.content.Context; 16 import android.content.MutableContextWrapper; 17 import android.os.Build; 18 import android.os.StrictMode; 19 20 import androidx.annotation.Nullable; 21 import androidx.test.core.app.ApplicationProvider; 22 23 import org.junit.rules.TestRule; 24 import org.junit.runner.Description; 25 import org.junit.runners.model.Statement; 26 27 import org.chromium.base.ContextUtils; 28 import org.chromium.base.Log; 29 import org.chromium.base.PathUtils; 30 import org.chromium.net.httpflags.Flags; 31 import org.chromium.net.httpflags.HttpFlagsInterceptor; 32 import org.chromium.net.impl.CronetUrlRequestContext; 33 import org.chromium.net.impl.HttpEngineNativeProvider; 34 import org.chromium.net.impl.JavaCronetEngine; 35 import org.chromium.net.impl.JavaCronetProvider; 36 import org.chromium.net.impl.NativeCronetProvider; 37 import org.chromium.net.impl.UserAgent; 38 import org.chromium.net.impl.VersionSafeCallbacks; 39 40 import java.io.File; 41 import java.lang.annotation.Annotation; 42 import java.lang.annotation.ElementType; 43 import java.lang.annotation.Retention; 44 import java.lang.annotation.RetentionPolicy; 45 import java.lang.annotation.Target; 46 import java.util.Arrays; 47 import java.util.EnumSet; 48 import java.util.Set; 49 50 /** Custom TestRule for Cronet instrumentation tests. */ 51 public class CronetTestRule implements TestRule { 52 private static final String PRIVATE_DATA_DIRECTORY_SUFFIX = "cronet_test"; 53 private static final String TAG = "CronetTestRule"; 54 55 private CronetTestFramework mCronetTestFramework; 56 private CronetImplementation mImplementation; 57 58 private final EngineStartupMode mEngineStartupMode; 59 CronetTestRule(EngineStartupMode engineStartupMode)60 private CronetTestRule(EngineStartupMode engineStartupMode) { 61 this.mEngineStartupMode = engineStartupMode; 62 } 63 64 /** 65 * Requires the user to call {@code CronetTestFramework.startEngine()} but allows to customize 66 * the builder parameters. 67 */ withManualEngineStartup()68 public static CronetTestRule withManualEngineStartup() { 69 return new CronetTestRule(EngineStartupMode.MANUAL); 70 } 71 72 /** 73 * Starts the Cronet engine automatically for each test case, but doesn't allow any 74 * customizations to the builder. 75 */ withAutomaticEngineStartup()76 public static CronetTestRule withAutomaticEngineStartup() { 77 return new CronetTestRule(EngineStartupMode.AUTOMATIC); 78 } 79 getTestFramework()80 public CronetTestFramework getTestFramework() { 81 return mCronetTestFramework; 82 } 83 assertResponseEquals(UrlResponseInfo expected, UrlResponseInfo actual)84 public void assertResponseEquals(UrlResponseInfo expected, UrlResponseInfo actual) { 85 assertThat(actual).hasHeadersThat().isEqualTo(expected.getAllHeaders()); 86 assertThat(actual).hasHeadersListThat().isEqualTo(expected.getAllHeadersAsList()); 87 assertThat(actual).hasHttpStatusCodeThat().isEqualTo(expected.getHttpStatusCode()); 88 assertThat(actual).hasHttpStatusTextThat().isEqualTo(expected.getHttpStatusText()); 89 assertThat(actual).hasUrlChainThat().isEqualTo(expected.getUrlChain()); 90 assertThat(actual).hasUrlThat().isEqualTo(expected.getUrl()); 91 // Transferred bytes and proxy server are not supported in pure java 92 if (!testingJavaImpl()) { 93 assertThat(actual) 94 .hasReceivedByteCountThat() 95 .isEqualTo(expected.getReceivedByteCount()); 96 assertThat(actual).hasProxyServerThat().isEqualTo(expected.getProxyServer()); 97 // This is a place where behavior intentionally differs between native and java 98 assertThat(actual) 99 .hasNegotiatedProtocolThat() 100 .isEqualTo(expected.getNegotiatedProtocol()); 101 } 102 } 103 assertCronetInternalErrorCode(NetworkException exception, int expectedErrorCode)104 public void assertCronetInternalErrorCode(NetworkException exception, int expectedErrorCode) { 105 switch (implementationUnderTest()) { 106 case STATICALLY_LINKED: 107 assertThat(exception.getCronetInternalErrorCode()).isEqualTo(expectedErrorCode); 108 break; 109 case AOSP_PLATFORM: 110 case FALLBACK: 111 // Internal error codes aren't supported in the fallback implementation, and 112 // inaccessible in AOSP 113 break; 114 } 115 } 116 117 /** 118 * Returns {@code true} when test is being run against the java implementation of CronetEngine. 119 * 120 * @deprecated use the implementation enum 121 */ 122 @Deprecated testingJavaImpl()123 public boolean testingJavaImpl() { 124 return mImplementation.equals(CronetImplementation.FALLBACK); 125 } 126 implementationUnderTest()127 public CronetImplementation implementationUnderTest() { 128 return mImplementation; 129 } 130 131 @Override apply(final Statement base, final Description desc)132 public Statement apply(final Statement base, final Description desc) { 133 return new Statement() { 134 @Override 135 public void evaluate() throws Throwable { 136 runBase(base, desc); 137 } 138 }; 139 } 140 141 // TODO(yolandyan): refactor this using parameterize framework 142 private void runBase(Statement base, Description desc) throws Throwable { 143 setImplementationUnderTest(CronetImplementation.STATICALLY_LINKED); 144 String packageName = desc.getTestClass().getPackage().getName(); 145 String testName = desc.getTestClass().getName() + "#" + desc.getMethodName(); 146 147 // Find the API version required by the test. 148 int requiredApiVersion = getMaximumAvailableApiLevel(); 149 int requiredAndroidApiVersion = Build.VERSION_CODES.LOLLIPOP; 150 boolean netLogEnabled = true; 151 for (Annotation a : desc.getTestClass().getAnnotations()) { 152 if (a instanceof RequiresMinApi) { 153 requiredApiVersion = ((RequiresMinApi) a).value(); 154 } 155 if (a instanceof RequiresMinAndroidApi) { 156 requiredAndroidApiVersion = ((RequiresMinAndroidApi) a).value(); 157 } 158 if (a instanceof DisableAutomaticNetLog) { 159 netLogEnabled = false; 160 Log.i( 161 TAG, 162 "Disabling automatic NetLog collection due to: " 163 + ((DisableAutomaticNetLog) a).reason()); 164 } 165 } 166 for (Annotation a : desc.getAnnotations()) { 167 // Method scoped requirements take precedence over class scoped 168 // requirements. 169 if (a instanceof RequiresMinApi) { 170 requiredApiVersion = ((RequiresMinApi) a).value(); 171 } 172 if (a instanceof RequiresMinAndroidApi) { 173 requiredAndroidApiVersion = ((RequiresMinAndroidApi) a).value(); 174 } 175 if (a instanceof DisableAutomaticNetLog) { 176 netLogEnabled = false; 177 Log.i( 178 TAG, 179 "Disabling automatic NetLog collection due to: " 180 + ((DisableAutomaticNetLog) a).reason()); 181 } 182 } 183 184 assumeTrue( 185 desc.getMethodName() 186 + " skipped because it requires API " 187 + requiredApiVersion 188 + " but only API " 189 + getMaximumAvailableApiLevel() 190 + " is present.", 191 getMaximumAvailableApiLevel() >= requiredApiVersion); 192 assumeTrue( 193 desc.getMethodName() 194 + " skipped because it Android's API level " 195 + requiredAndroidApiVersion 196 + " but test device supports only API " 197 + Build.VERSION.SDK_INT, 198 Build.VERSION.SDK_INT >= requiredAndroidApiVersion); 199 200 EnumSet<CronetImplementation> excludedImplementations = 201 EnumSet.noneOf(CronetImplementation.class); 202 IgnoreFor ignoreDueToClassAnnotation = getTestClassAnnotation(desc, IgnoreFor.class); 203 if (ignoreDueToClassAnnotation != null) { 204 excludedImplementations.addAll( 205 Arrays.asList(ignoreDueToClassAnnotation.implementations())); 206 } 207 IgnoreFor ignoreDueToMethodAnnotation = getTestMethodAnnotation(desc, IgnoreFor.class); 208 if (ignoreDueToMethodAnnotation != null) { 209 excludedImplementations.addAll( 210 Arrays.asList(ignoreDueToMethodAnnotation.implementations())); 211 } 212 213 Set<CronetImplementation> implementationsUnderTest = 214 EnumSet.complementOf(excludedImplementations); 215 assertWithMessage( 216 "Test should not be skipped via IgnoreFor annotation. " 217 + "Use DisabledTest instead") 218 .that(implementationsUnderTest) 219 .isNotEmpty(); 220 221 if (Build.VERSION.SDK_INT < 34) { 222 implementationsUnderTest.remove(CronetImplementation.AOSP_PLATFORM); 223 assumeFalse( 224 desc.getMethodName() 225 + " skipped because it's supposed to run against only AOSP_PLATFORM but" 226 + " test device is not U+", 227 implementationsUnderTest.isEmpty()); 228 } 229 230 Log.i(TAG, "Implementations to be tested against: %s", implementationsUnderTest); 231 232 if (packageName.startsWith("org.chromium.net")) { 233 for (CronetImplementation implementation : implementationsUnderTest) { 234 if (isRunningInAOSP() && implementation.equals(CronetImplementation.FALLBACK)) { 235 // Skip executing tests for JavaCronetEngine. 236 continue; 237 } 238 Log.i(TAG, "Running test against " + implementation + " implementation."); 239 setImplementationUnderTest(implementation); 240 evaluateWithFramework(base, testName, netLogEnabled); 241 } 242 } else { 243 evaluateWithFramework(base, testName, netLogEnabled); 244 } 245 } 246 247 /** 248 * This method only returns the value of the `is_running_in_aosp` flag which for Chromium can be 249 * found inside components/cronet/android/test/res/values/cronet-test-rule-configuration.xml 250 * for which it should be equal to false. However, on AOSP, we ship a different value 251 * which is equal to true. 252 * 253 * <p>This distinction between where the tests are being executed is crucial because we don't 254 * want to run JavaCronetEngine tests in AOSP. 255 * 256 * @return True if the tests are being executed in AOSP. 257 */ 258 @SuppressWarnings("DiscouragedApi") 259 public boolean isRunningInAOSP() { 260 int resId = 261 ApplicationProvider.getApplicationContext() 262 .getResources() 263 .getIdentifier( 264 "is_running_in_aosp", 265 "bool", 266 ApplicationProvider.getApplicationContext().getPackageName()); 267 if (resId == 0) { 268 throw new IllegalStateException( 269 "Could not find any value for `is_running_in_aosp` boolean entry."); 270 } 271 return ApplicationProvider.getApplicationContext().getResources().getBoolean(resId); 272 } 273 274 private void evaluateWithFramework(Statement statement, String testName, boolean netLogEnabled) 275 throws Throwable { 276 try (CronetTestFramework framework = createCronetTestFramework(testName, netLogEnabled)) { 277 statement.evaluate(); 278 } finally { 279 mCronetTestFramework = null; 280 } 281 } 282 283 private CronetTestFramework createCronetTestFramework(String testName, boolean netLogEnabled) { 284 mCronetTestFramework = new CronetTestFramework(mImplementation, testName, netLogEnabled); 285 if (mEngineStartupMode.equals(EngineStartupMode.AUTOMATIC)) { 286 mCronetTestFramework.startEngine(); 287 } 288 return mCronetTestFramework; 289 } 290 291 static int getMaximumAvailableApiLevel() { 292 return VersionSafeCallbacks.ApiVersion.getMaximumAvailableApiLevel(); 293 } 294 295 /** 296 * Annotation allowing classes or individual tests to be skipped based on the implementation 297 * being currently tested. When this annotation is present the test is only run against the 298 * {@link CronetImplementation} cases not specified in the annotation. If the annotation is 299 * specified both at the class and method levels, the union of IgnoreFor#implementations() will 300 * be skipped. 301 */ 302 @Target({ElementType.TYPE, ElementType.METHOD}) 303 @Retention(RetentionPolicy.RUNTIME) 304 public @interface IgnoreFor { 305 CronetImplementation[] implementations(); 306 307 String reason(); 308 } 309 310 /** 311 * Annotation allowing classes or individual tests to be skipped based on the version of the 312 * Cronet API present. Takes the minimum API version upon which the test should be run. 313 * For example if a test should only be run with API version 2 or greater: 314 * @RequiresMinApi(2) 315 * public void testFoo() {} 316 */ 317 @Target({ElementType.TYPE, ElementType.METHOD}) 318 @Retention(RetentionPolicy.RUNTIME) 319 public @interface RequiresMinApi { 320 int value(); 321 } 322 323 /** 324 * Annotation allowing classes or individual tests to be skipped based on the Android OS version 325 * installed in the deviced used for testing. Takes the minimum API version upon which the test 326 * should be run. For example if a test should only be run with Android Oreo or greater: 327 * @RequiresMinApi(Build.VERSION_CODES.O) 328 * public void testFoo() {} 329 */ 330 @Target({ElementType.TYPE, ElementType.METHOD}) 331 @Retention(RetentionPolicy.RUNTIME) 332 public @interface RequiresMinAndroidApi { 333 int value(); 334 } 335 336 /** Annotation allowing classes or individual tests to disable automatic NetLog collection. */ 337 @Target({ElementType.TYPE, ElementType.METHOD}) 338 @Retention(RetentionPolicy.RUNTIME) 339 public @interface DisableAutomaticNetLog { 340 String reason(); 341 } 342 343 /** Prepares the path for the test storage (http cache, QUIC server info). */ 344 public static void prepareTestStorage(Context context) { 345 File storage = new File(getTestStorageDirectory()); 346 if (storage.exists()) { 347 assertThat(recursiveDelete(storage)).isTrue(); 348 } 349 ensureTestStorageExists(); 350 } 351 352 /** 353 * Returns the path for the test storage (http cache, QUIC server info). 354 * Also ensures it exists. 355 */ 356 public static String getTestStorage(Context context) { 357 ensureTestStorageExists(); 358 return getTestStorageDirectory(); 359 } 360 361 /** 362 * Returns the path for the test storage (http cache, QUIC server info). 363 * NOTE: Does not ensure it exists; tests should use {@link #getTestStorage}. 364 */ 365 private static String getTestStorageDirectory() { 366 return PathUtils.getDataDirectory() + "/test_storage"; 367 } 368 369 /** Ensures test storage directory exists, i.e. creates one if it does not exist. */ 370 private static void ensureTestStorageExists() { 371 File storage = new File(getTestStorageDirectory()); 372 if (!storage.exists()) { 373 assertThat(storage.mkdir()).isTrue(); 374 } 375 } 376 377 private static boolean recursiveDelete(File path) { 378 if (path.isDirectory()) { 379 for (File c : path.listFiles()) { 380 if (!recursiveDelete(c)) { 381 return false; 382 } 383 } 384 } 385 return path.delete(); 386 } 387 388 private void setImplementationUnderTest(CronetImplementation implementation) { 389 mImplementation = implementation; 390 } 391 392 /** Creates and holds pointer to CronetEngine. */ 393 public static class CronetTestFramework implements AutoCloseable { 394 // This is the Context that Cronet will use. The specific Context instance can never change 395 // because that would break ContextUtils.initApplicationContext(). We work around this by 396 // using a static MutableContextWrapper whose identity is constant, but the wrapped 397 // Context isn't. 398 // 399 // TODO: in theory, no code under test should be running in between tests, and we should be 400 // able to enforce that by rejecting all Context calls in between tests (e.g. by resetting 401 // the base context to null while not running a test). Unfortunately, it's not that simple 402 // because the code under test doesn't currently wait for all asynchronous operations to 403 // complete before the test finishes (e.g. ProxyChangeListener can call back into the 404 // CronetInit thread even while a test isn't running), so we have to keep that context 405 // working even in between tests to prevent crashes. This is problematic as that makes tests 406 // non-hermetic/racy/brittle. Ideally, we should ensure that no code under test can run in 407 // between tests. 408 @SuppressWarnings("StaticFieldLeak") 409 private static final MutableContextWrapper sContextWrapper = 410 new MutableContextWrapper(ApplicationProvider.getApplicationContext()) { 411 @Override 412 public Context getApplicationContext() { 413 // Ensure the code under test (in particular, the CronetEngineBuilderImpl 414 // constructor) cannot use this method to "escape" context interception. 415 return this; 416 } 417 }; 418 419 private final CronetImplementation mImplementation; 420 private final ExperimentalCronetEngine.Builder mBuilder; 421 private final MutableContextWrapper mContextWrapperWithoutFlags; 422 private final MutableContextWrapper mContextWrapper; 423 private final StrictMode.VmPolicy mOldVmPolicy; 424 private final String mTestName; 425 private final boolean mNetLogEnabled; 426 427 private HttpFlagsInterceptor mHttpFlagsInterceptor; 428 private ExperimentalCronetEngine mCronetEngine; 429 private boolean mClosed; 430 431 private CronetTestFramework( 432 CronetImplementation implementation, String testName, boolean netLogEnabled) { 433 mContextWrapperWithoutFlags = 434 new MutableContextWrapper(ApplicationProvider.getApplicationContext()); 435 mContextWrapper = new MutableContextWrapper(mContextWrapperWithoutFlags); 436 assert sContextWrapper.getBaseContext() == ApplicationProvider.getApplicationContext(); 437 sContextWrapper.setBaseContext(mContextWrapper); 438 mBuilder = 439 implementation 440 .createBuilder(sContextWrapper) 441 .setUserAgent(UserAgent.getDefault()) 442 .enableQuic(true); 443 mImplementation = implementation; 444 mTestName = testName; 445 mNetLogEnabled = netLogEnabled; 446 447 System.loadLibrary("cronet_tests"); 448 ContextUtils.initApplicationContext(sContextWrapper); 449 PathUtils.setPrivateDataDirectorySuffix(PRIVATE_DATA_DIRECTORY_SUFFIX); 450 prepareTestStorage(getContext()); 451 mOldVmPolicy = StrictMode.getVmPolicy(); 452 // Only enable StrictMode testing after leaks were fixed in crrev.com/475945 453 if (getMaximumAvailableApiLevel() >= 7) { 454 StrictMode.setVmPolicy( 455 new StrictMode.VmPolicy.Builder() 456 .detectLeakedClosableObjects() 457 .penaltyLog() 458 .penaltyDeath() 459 .build()); 460 } 461 462 setHttpFlags(null); 463 } 464 465 /** 466 * Replaces the {@link Context} implementation that the Cronet engine calls into. Useful for 467 * faking/mocking Android context calls. 468 * 469 * @throws IllegalStateException if called after the Cronet engine has already been built. 470 * Intercepting context calls while the code under test is running is racy and runs the risk 471 * that the code under test will not pick up the change. 472 */ 473 public void interceptContext(ContextInterceptor contextInterceptor) { 474 checkNotClosed(); 475 476 if (mCronetEngine != null) { 477 throw new IllegalStateException( 478 "Refusing to intercept context after the Cronet engine has been built"); 479 } 480 481 mContextWrapperWithoutFlags.setBaseContext( 482 contextInterceptor.interceptContext( 483 mContextWrapperWithoutFlags.getBaseContext())); 484 } 485 486 /** 487 * Sets the HTTP flags, if any, that the code under test should run with. This affects the 488 * behavior of the {@link Context} that the code under test sees. 489 * 490 * If this method is never called, the default behavior is to simulate the absence of a 491 * flags file. This ensures that the code under test does not end up accidentally using a 492 * flags file from the host system, which would lead to non-deterministic results. 493 * 494 * @param flagsFileContents the contents of the flags file, or null to simulate a missing 495 * file (default behavior). 496 * 497 * @throws IllegalStateException if called after the engine has already been built. 498 * Modifying flags while the code under test is running is always a mistake, because the 499 * code under test won't notice the changes. 500 * 501 * @see org.chromium.net.impl.HttpFlagsLoader 502 * @see HttpFlagsInterceptor 503 */ 504 public void setHttpFlags(@Nullable Flags flagsFileContents) { 505 checkNotClosed(); 506 507 if (mCronetEngine != null) { 508 throw new IllegalStateException( 509 "Refusing to replace flags file provider after the Cronet engine has been " 510 + "built"); 511 } 512 513 if (mHttpFlagsInterceptor != null) mHttpFlagsInterceptor.close(); 514 mHttpFlagsInterceptor = new HttpFlagsInterceptor(flagsFileContents); 515 mContextWrapper.setBaseContext( 516 mHttpFlagsInterceptor.interceptContext(mContextWrapperWithoutFlags)); 517 } 518 519 /** 520 * @return the context to be used by the Cronet engine 521 * 522 * @see #interceptContext 523 * @see #setFlagsFileContents 524 */ 525 public Context getContext() { 526 checkNotClosed(); 527 return sContextWrapper; 528 } 529 530 public CronetEngine.Builder enableDiskCache(CronetEngine.Builder cronetEngineBuilder) { 531 cronetEngineBuilder.setStoragePath(getTestStorage(getContext())); 532 cronetEngineBuilder.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK, 1000 * 1024); 533 return cronetEngineBuilder; 534 } 535 536 public ExperimentalCronetEngine startEngine() { 537 checkNotClosed(); 538 539 if (mCronetEngine != null) { 540 throw new IllegalStateException("Engine is already started!"); 541 } 542 543 mCronetEngine = mBuilder.build(); 544 mImplementation.verifyCronetEngineInstance(mCronetEngine); 545 546 // Start collecting metrics. 547 mCronetEngine.getGlobalMetricsDeltas(); 548 549 if (mNetLogEnabled) { 550 File dataDir = new File(PathUtils.getDataDirectory()); 551 File netLogDir = new File(dataDir, "NetLog"); 552 netLogDir.mkdir(); 553 String netLogFileName = 554 mTestName + "-" + String.valueOf(System.currentTimeMillis()); 555 File netLogFile = new File(netLogDir, netLogFileName + ".json"); 556 Log.i(TAG, "Enabling netlog to: " + netLogFile.getPath()); 557 mCronetEngine.startNetLogToFile(netLogFile.getPath(), /* logAll= */ true); 558 } 559 560 return mCronetEngine; 561 } 562 563 public ExperimentalCronetEngine getEngine() { 564 checkNotClosed(); 565 566 if (mCronetEngine == null) { 567 throw new IllegalStateException("Engine not started yet!"); 568 } 569 570 return mCronetEngine; 571 } 572 573 /** Applies the given patch to the primary Cronet Engine builder associated with this run. */ 574 public void applyEngineBuilderPatch(CronetBuilderPatch patch) { 575 checkNotClosed(); 576 577 if (mCronetEngine != null) { 578 throw new IllegalStateException("The engine was already built!"); 579 } 580 581 try { 582 patch.apply(mBuilder); 583 } catch (Exception e) { 584 throw new IllegalArgumentException("Cannot apply the given patch!", e); 585 } 586 } 587 588 /** 589 * Returns a new instance of a Cronet builder corresponding to the implementation under 590 * test. 591 * 592 * <p>Some test cases need to create multiple instances of Cronet engines to test 593 * interactions between them, so we provide the capability to do so and reliably obtain 594 * the correct Cronet implementation. 595 * 596 * <p>Note that this builder and derived Cronet engine is not managed by the framework! The 597 * caller is responsible for cleaning up resources (e.g. calling {@code engine.shutdown()} 598 * at the end of the test). 599 * 600 */ 601 public ExperimentalCronetEngine.Builder createNewSecondaryBuilder(Context context) { 602 return mImplementation.createBuilder(context); 603 } 604 605 @Override 606 public void close() { 607 if (mClosed) { 608 return; 609 } 610 shutdownEngine(); 611 assert sContextWrapper.getBaseContext() == mContextWrapper; 612 sContextWrapper.setBaseContext(ApplicationProvider.getApplicationContext()); 613 mClosed = true; 614 615 if (mHttpFlagsInterceptor != null) mHttpFlagsInterceptor.close(); 616 617 try { 618 // Run GC and finalizers a few times to pick up leaked closeables 619 for (int i = 0; i < 10; i++) { 620 System.gc(); 621 System.runFinalization(); 622 } 623 } finally { 624 StrictMode.setVmPolicy(mOldVmPolicy); 625 } 626 } 627 628 private void shutdownEngine() { 629 if (mCronetEngine == null) { 630 return; 631 } 632 try { 633 mCronetEngine.stopNetLog(); 634 mCronetEngine.shutdown(); 635 } catch (IllegalStateException e) { 636 if (e.getMessage().contains("Engine is shut down")) { 637 // We're trying to shut the engine down repeatedly. Make such calls idempotent 638 // instead of failing, as there's no API to query whether an engine is shut down 639 // and some tests shut the engine down deliberately (e.g. to make sure 640 // everything is flushed properly). 641 Log.d(TAG, "Cronet engine already shut down by the test.", e); 642 } else { 643 throw e; 644 } 645 } 646 mCronetEngine = null; 647 } 648 649 private void checkNotClosed() { 650 if (mClosed) { 651 throw new IllegalStateException( 652 "Unable to interact with a closed CronetTestFramework!"); 653 } 654 } 655 } 656 657 /** 658 * A functional interface that allows Cronet tests to modify parameters of the Cronet engine 659 * provided by {@code CronetTestFramework}. 660 * 661 * <p>The builder itself isn't exposed directly as a getter to tests to stress out ownership 662 * and make accidental local access less likely. 663 */ 664 public static interface CronetBuilderPatch { 665 public void apply(ExperimentalCronetEngine.Builder builder) throws Exception; 666 } 667 668 private enum EngineStartupMode { 669 MANUAL, 670 AUTOMATIC, 671 } 672 673 // This is a replacement for java.util.function.Function as Function is only available 674 // starting android API level 24. 675 private interface EngineBuilderSupplier { 676 ExperimentalCronetEngine.Builder getCronetEngineBuilder(Context context); 677 } 678 679 public enum CronetImplementation { 680 STATICALLY_LINKED( 681 context -> 682 (ExperimentalCronetEngine.Builder) 683 new NativeCronetProvider(context).createBuilder()), 684 FALLBACK( 685 (context) -> 686 (ExperimentalCronetEngine.Builder) 687 new JavaCronetProvider(context).createBuilder()), 688 AOSP_PLATFORM( 689 context -> 690 (ExperimentalCronetEngine.Builder) 691 new HttpEngineNativeProvider(context).createBuilder()); 692 693 private final EngineBuilderSupplier mEngineSupplier; 694 695 private CronetImplementation(EngineBuilderSupplier engineSupplier) { 696 this.mEngineSupplier = engineSupplier; 697 } 698 699 ExperimentalCronetEngine.Builder createBuilder(Context context) { 700 return mEngineSupplier.getCronetEngineBuilder(context); 701 } 702 703 private void verifyCronetEngineInstance(CronetEngine engine) { 704 switch (this) { 705 case STATICALLY_LINKED: 706 assertThat(engine).isInstanceOf(CronetUrlRequestContext.class); 707 break; 708 case FALLBACK: 709 assertThat(engine).isInstanceOf(JavaCronetEngine.class); 710 break; 711 case AOSP_PLATFORM: 712 // We cannot reference the impl class for AOSP_PLATFORM. Do a reverse check 713 // instead. 714 assertThat(engine).isNotInstanceOf(CronetUrlRequestContext.class); 715 assertThat(engine).isNotInstanceOf(JavaCronetEngine.class); 716 break; 717 } 718 } 719 720 private void checkImplClass(CronetEngine engine, Class expectedClass) { 721 assertThat(engine).isInstanceOf(expectedClass); 722 } 723 } 724 725 @Nullable 726 private static <T extends Annotation> T getTestMethodAnnotation( 727 Description description, Class<T> clazz) { 728 return description.getAnnotation(clazz); 729 } 730 731 @Nullable 732 private static <T extends Annotation> T getTestClassAnnotation( 733 Description description, Class<T> clazz) { 734 return description.getTestClass().getAnnotation(clazz); 735 } 736 737 private static String safeGetIgnoreReason(IgnoreFor ignoreAnnotation) { 738 if (ignoreAnnotation == null) { 739 return ""; 740 } 741 return ignoreAnnotation.reason(); 742 } 743 } 744