• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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