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