xref: /aosp_15_r20/external/cronet/components/cronet/android/test/javatests/src/org/chromium/net/CronetTestRule.java (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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