• 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 org.junit.Assume.assumeTrue;
8 
9 import android.net.http.ApiVersion;
10 import android.net.http.HttpEngine;
11 import android.net.http.ExperimentalHttpEngine;
12 import android.net.http.UrlResponseInfo;
13 import android.content.Context;
14 import android.os.Build;
15 import android.os.StrictMode;
16 
17 import androidx.test.core.app.ApplicationProvider;
18 
19 import org.junit.Assert;
20 import org.junit.rules.TestRule;
21 import org.junit.runner.Description;
22 import org.junit.runners.model.Statement;
23 
24 import org.chromium.base.ContextUtils;
25 import org.chromium.base.Log;
26 import org.chromium.base.PathUtils;
27 
28 import java.io.File;
29 import java.lang.annotation.Annotation;
30 import java.lang.annotation.ElementType;
31 import java.lang.annotation.Retention;
32 import java.lang.annotation.RetentionPolicy;
33 import java.lang.annotation.Target;
34 import java.lang.reflect.Field;
35 import java.net.URL;
36 import java.net.URLStreamHandlerFactory;
37 
38 /**
39  * Custom TestRule for Cronet instrumentation tests.
40  */
41 public class CronetTestRule implements TestRule {
42     private static final String PRIVATE_DATA_DIRECTORY_SUFFIX = "cronet_test";
43 
44     /**
45      * Name of the file that contains the test server certificate in PEM format.
46      */
47     public static final String SERVER_CERT_PEM = "quic-chain.pem";
48 
49     /**
50      * Name of the file that contains the test server private key in PKCS8 PEM format.
51      */
52     public static final String SERVER_KEY_PKCS8_PEM = "quic-leaf-cert.key.pkcs8.pem";
53 
54     private static final String TAG = "CronetTestRule";
55 
56     private CronetTestFramework mCronetTestFramework;
57 
58     private boolean mTestingSystemHttpURLConnection;
59     private StrictMode.VmPolicy mOldVmPolicy;
60 
61     private Field factoryField;
62 
63     /**
64      * Creates and holds pointer to CronetEngine.
65      */
66     public static class CronetTestFramework {
67         public ExperimentalHttpEngine mCronetEngine;
68         public ExperimentalHttpEngine.Builder mBuilder;
69 
70         private Context mContext;
71 
CronetTestFramework(Context context)72         private CronetTestFramework(Context context) {
73             mContext = context;
74             mBuilder = createNativeEngineBuilder();
75         }
76 
createUsingNativeImpl(Context context)77         private static CronetTestFramework createUsingNativeImpl(Context context) {
78             return new CronetTestFramework(context);
79         }
80 
startEngine()81         public ExperimentalHttpEngine startEngine() {
82             assert mCronetEngine == null;
83 
84             mCronetEngine = mBuilder.build();
85 
86             // Start collecting metrics.
87             mCronetEngine.getGlobalMetricsDeltas();
88 
89             return mCronetEngine;
90         }
91 
shutdownEngine()92         public void shutdownEngine() {
93             if (mCronetEngine == null) return;
94             mCronetEngine.shutdown();
95             mCronetEngine = null;
96         }
97 
createNativeEngineBuilder()98         private ExperimentalHttpEngine.Builder createNativeEngineBuilder() {
99             return CronetTestRule.createNativeEngineBuilder(mContext).setEnableQuic(true);
100         }
101     }
102 
getContext()103     public static Context getContext() {
104         return ApplicationProvider.getApplicationContext();
105     }
106 
getMaximumAvailableApiLevel()107     int getMaximumAvailableApiLevel() {
108         // Prior to M59 the ApiVersion.getMaximumAvailableApiLevel API didn't exist
109         int cronetMajorVersion = Integer.parseInt(ApiVersion.getCronetVersion().split("\\.")[0]);
110         if (cronetMajorVersion < 59) {
111             return 3;
112         }
113         return ApiVersion.getMaximumAvailableApiLevel();
114     }
115 
116     @Override
apply(final Statement base, final Description desc)117     public Statement apply(final Statement base, final Description desc) {
118         return new Statement() {
119             @Override
120             public void evaluate() throws Throwable {
121                 setUp();
122                 try {
123                     runBase(base, desc);
124                 } finally {
125                     tearDown();
126                 }
127             }
128         };
129     }
130 
131     /**
132      * Returns {@code true} when test is being run against system HttpURLConnection implementation.
133      */
134     public boolean testingSystemHttpURLConnection() {
135         return mTestingSystemHttpURLConnection;
136     }
137 
138     /**
139      * Returns {@code true} when test is being run against the java implementation of CronetEngine.
140      */
141     public boolean testingJavaImpl() {
142         return false;
143     }
144 
145     // TODO(yolandyan): refactor this using parameterize framework
146     private void runBase(Statement base, Description desc) throws Throwable {
147         setTestingSystemHttpURLConnection(false);
148         String packageName = desc.getTestClass().getPackage().getName();
149 
150         boolean onlyRunTestForNative = desc.getAnnotation(OnlyRunNativeCronet.class) != null;
151         boolean onlyRunTestForJava = desc.getAnnotation(OnlyRunJavaCronet.class) != null;
152         if (onlyRunTestForNative && onlyRunTestForJava) {
153             throw new IllegalArgumentException(desc.getMethodName()
154                     + " skipped because it specified both "
155                     + "OnlyRunNativeCronet and OnlyRunJavaCronet annotations");
156         }
157         boolean doRunTestForNative = onlyRunTestForNative || !onlyRunTestForJava;
158 
159         // Find the API version required by the test.
160         int requiredApiVersion = getMaximumAvailableApiLevel();
161         int requiredAndroidApiVersion = Build.VERSION_CODES.KITKAT;
162         for (Annotation a : desc.getTestClass().getAnnotations()) {
163             if (a instanceof RequiresMinApi) {
164                 requiredApiVersion = ((RequiresMinApi) a).value();
165             }
166             if (a instanceof RequiresMinAndroidApi) {
167                 requiredAndroidApiVersion = ((RequiresMinAndroidApi) a).value();
168             }
169         }
170         for (Annotation a : desc.getAnnotations()) {
171             // Method scoped requirements take precedence over class scoped
172             // requirements.
173             if (a instanceof RequiresMinApi) {
174                 requiredApiVersion = ((RequiresMinApi) a).value();
175             }
176             if (a instanceof RequiresMinAndroidApi) {
177                 requiredAndroidApiVersion = ((RequiresMinAndroidApi) a).value();
178             }
179         }
180         assumeTrue(desc.getMethodName() + " skipped because it requires API " + requiredApiVersion
181                         + " but only API " + getMaximumAvailableApiLevel() + " is present.",
182                 getMaximumAvailableApiLevel() >= requiredApiVersion);
183         assumeTrue(desc.getMethodName() + " skipped because it Android's API level "
184                         + requiredAndroidApiVersion + " but test device supports only API "
185                         + Build.VERSION.SDK_INT,
186                 Build.VERSION.SDK_INT >= requiredAndroidApiVersion);
187 
188         if (packageName.equals("org.chromium.net.urlconnection")) {
189             // TODO(b/275044376) Switch cronetEngine instead of resetting factory using reflection
190             // Clear the factory field so that the next test can set reset it
191             resetUrlStreamHandlerFactoryField();
192             if (desc.getAnnotation(CompareDefaultWithCronet.class) != null) {
193                 try {
194                     // Run with the default HttpURLConnection implementation first.
195                     setTestingSystemHttpURLConnection(true);
196                     base.evaluate();
197                     // Use Cronet's implementation, and run the same test.
198                     setTestingSystemHttpURLConnection(false);
199                     base.evaluate();
200                 } catch (Throwable e) {
201                     Log.e(TAG, "CronetTestBase#runTest failed for %s implementation.",
202                             testingSystemHttpURLConnection() ? "System" : "Cronet");
203                     throw e;
204                 }
205             } else {
206                 // For all other tests.
207                 base.evaluate();
208             }
209         } else if (packageName.startsWith("org.chromium.net")) {
210             try {
211                 if (doRunTestForNative) {
212                     Log.i(TAG, "Running test against Native implementation.");
213                     base.evaluate();
214                 }
215             } catch (Throwable e) {
216                 Log.e(TAG, "CronetTestBase#runTest failed for %s implementation.",
217                         testingJavaImpl() ? "Java" : "Native");
218                 throw e;
219             }
220         } else {
221             base.evaluate();
222         }
223     }
224 
225     private void setUp() throws Exception {
226         System.loadLibrary("cronet_tests");
227         ContextUtils.initApplicationContext(getContext().getApplicationContext());
228         PathUtils.setPrivateDataDirectorySuffix(PRIVATE_DATA_DIRECTORY_SUFFIX);
229         prepareTestStorage(getContext());
230         mOldVmPolicy = StrictMode.getVmPolicy();
231         // Only enable StrictMode testing after leaks were fixed in crrev.com/475945
232         if (getMaximumAvailableApiLevel() >= 7) {
233             StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
234                                            .detectLeakedClosableObjects()
235                                            .penaltyLog()
236                                            .penaltyDeath()
237                                            .build());
238         }
239     }
240 
241     private void tearDown() throws Exception {
242         try {
243             // Run GC and finalizers a few times to pick up leaked closeables
244             for (int i = 0; i < 10; i++) {
245                 System.gc();
246                 System.runFinalization();
247             }
248             System.gc();
249             System.runFinalization();
250         } finally {
251             StrictMode.setVmPolicy(mOldVmPolicy);
252         }
253     }
254 
255     private CronetTestFramework createCronetTestFramework() {
256         mCronetTestFramework = CronetTestFramework.createUsingNativeImpl(getContext());
257         return mCronetTestFramework;
258     }
259 
260     /**
261      * Builds and starts the CronetTest framework.
262      */
263     public CronetTestFramework startCronetTestFramework() {
264         createCronetTestFramework();
265         mCronetTestFramework.startEngine();
266         return mCronetTestFramework;
267     }
268 
269     /**
270      * Builds the CronetTest framework.
271      */
272     public CronetTestFramework buildCronetTestFramework() {
273         return createCronetTestFramework();
274     }
275 
276     /**
277      * Creates and returns {@link ExperimentalHttpEngine.Builder} that creates
278      * Chromium (native) based {@link HttpEngine.Builder}.
279      *
280      * @return the {@code CronetEngine.Builder} that builds Chromium-based {@code Cronet engine}.
281      */
282     public static ExperimentalHttpEngine.Builder createNativeEngineBuilder(Context context) {
283         return new ExperimentalHttpEngine.Builder(context);
284     }
285 
286     public void assertResponseEquals(UrlResponseInfo expected, UrlResponseInfo actual) {
287         Assert.assertEquals(expected.getHeaders().getAsMap(), actual.getHeaders().getAsMap());
288         Assert.assertEquals(expected.getHeaders().getAsList(), actual.getHeaders().getAsList());
289         Assert.assertEquals(expected.getHttpStatusCode(), actual.getHttpStatusCode());
290         Assert.assertEquals(expected.getHttpStatusText(), actual.getHttpStatusText());
291         Assert.assertEquals(expected.getUrlChain(), actual.getUrlChain());
292         Assert.assertEquals(expected.getUrl(), actual.getUrl());
293         // Transferred bytes and proxy server are not supported in pure java
294         if (!testingJavaImpl()) {
295             Assert.assertEquals(expected.getReceivedByteCount(), actual.getReceivedByteCount());
296             Assert.assertEquals(expected.getProxyServer(), actual.getProxyServer());
297             // This is a place where behavior intentionally differs between native and java
298             Assert.assertEquals(expected.getNegotiatedProtocol(), actual.getNegotiatedProtocol());
299         }
300     }
301 
302     public static void assertContains(String expectedSubstring, String actualString) {
303         Assert.assertNotNull(actualString);
304         if (!actualString.contains(expectedSubstring)) {
305             Assert.fail("String [" + actualString + "] doesn't contain substring ["
306                     + expectedSubstring + "]");
307         }
308     }
309 
310     public HttpEngine.Builder enableDiskCache(HttpEngine.Builder cronetEngineBuilder) {
311         cronetEngineBuilder.setStoragePath(getTestStorage(getContext()));
312         cronetEngineBuilder.setEnableHttpCache(HttpEngine.Builder.HTTP_CACHE_DISK, 1000 * 1024);
313         return cronetEngineBuilder;
314     }
315 
316     /**
317      * Sets the {@link URLStreamHandlerFactory} from {@code cronetEngine}.  This should be called
318      * during setUp() and is installed by {@link runTest()} as the default when Cronet is tested.
319      */
320     public void setStreamHandlerFactory(HttpEngine cronetEngine) {
321         // This clears the cached URL handlers
322         if (testingSystemHttpURLConnection()) {
323             URL.setURLStreamHandlerFactory(null);
324         } else {
325             URL.setURLStreamHandlerFactory(cronetEngine.createUrlStreamHandlerFactory());
326         }
327     }
328 
329     /**
330      * Store and clear URL's StreamHandlerFactory field to a global variable.
331      * {@link URL}'s {@code factory} field cannot be reassigned in a JVM instance so we need
332      * to reflectively clear it in order to switch factory's for the tests.
333      */
334     private void resetUrlStreamHandlerFactoryField() throws IllegalAccessException {
335         try {
336             if (factoryField != null) {
337                 // Clear the factory field so the next test run can set it.
338                 factoryField.set(null, null);
339                 return;
340             }
341             for (Field field : URL.class.getDeclaredFields()) {
342                 if (URLStreamHandlerFactory.class.equals(field.getType())) {
343                     factoryField = field;
344                     factoryField.setAccessible(true);
345                     // Clear the factoryField as the first test might have set it.
346                     factoryField.set(null, null);
347                     return;
348                 }
349             }
350         } catch (IllegalAccessException e) {
351             Log.e(TAG, "CronetTestBase#runTest: factory could not be reset");
352             throw e;
353         }
354     }
355 
356     /**
357      * Annotation for test methods in org.chromium.net.urlconnection pacakage that runs them
358      * against both Cronet's HttpURLConnection implementation, and against the system's
359      * HttpURLConnection implementation.
360      */
361     @Target(ElementType.METHOD)
362     @Retention(RetentionPolicy.RUNTIME)
363     public @interface CompareDefaultWithCronet {}
364 
365     /**
366      * Annotation for test methods in org.chromium.net.urlconnection pacakage that runs them
367      * only against Cronet's HttpURLConnection implementation, and not against the system's
368      * HttpURLConnection implementation.
369      */
370     @Target(ElementType.METHOD)
371     @Retention(RetentionPolicy.RUNTIME)
372     public @interface OnlyRunCronetHttpURLConnection {}
373 
374     /**
375      * Annotation for test methods in org.chromium.net package that disables rerunning the test
376      * against the Java-only implementation. When this annotation is present the test is only run
377      * against the native implementation.
378      */
379     @Target(ElementType.METHOD)
380     @Retention(RetentionPolicy.RUNTIME)
381     public @interface OnlyRunNativeCronet {}
382 
383     /**
384      * Annotation for test methods in org.chromium.net package that disables rerunning the test
385      * against the Native/Chromium implementation. When this annotation is present the test is only
386      * run against the Java implementation.
387      */
388     @Target(ElementType.METHOD)
389     @Retention(RetentionPolicy.RUNTIME)
390     public @interface OnlyRunJavaCronet {}
391 
392     /**
393      * Annotation allowing classes or individual tests to be skipped based on the version of the
394      * Cronet API present. Takes the minimum API version upon which the test should be run.
395      * For example if a test should only be run with API version 2 or greater:
396      *   @RequiresMinApi(2)
397      *   public void testFoo() {}
398      */
399     @Target({ElementType.TYPE, ElementType.METHOD})
400     @Retention(RetentionPolicy.RUNTIME)
401     public @interface RequiresMinApi {
402         int value();
403     }
404 
405     /**
406      * Annotation allowing classes or individual tests to be skipped based on the Android OS version
407      * installed in the deviced used for testing. Takes the minimum API version upon which the test
408      * should be run. For example if a test should only be run with Android Oreo or greater:
409      *   @RequiresMinApi(Build.VERSION_CODES.O)
410      *   public void testFoo() {}
411      */
412     @Target({ElementType.TYPE, ElementType.METHOD})
413     @Retention(RetentionPolicy.RUNTIME)
414     public @interface RequiresMinAndroidApi {
415         int value();
416     }
417 
418     /**
419      * Prepares the path for the test storage (http cache, QUIC server info).
420      */
421     public static void prepareTestStorage(Context context) {
422         File storage = new File(getTestStorageDirectory());
423         if (storage.exists()) {
424             Assert.assertTrue(recursiveDelete(storage));
425         }
426         ensureTestStorageExists();
427     }
428 
429     /**
430      * Returns the path for the test storage (http cache, QUIC server info).
431      * Also ensures it exists.
432      */
433     public static String getTestStorage(Context context) {
434         ensureTestStorageExists();
435         return getTestStorageDirectory();
436     }
437 
438     /**
439      * Returns the path for the test storage (http cache, QUIC server info).
440      * NOTE: Does not ensure it exists; tests should use {@link #getTestStorage}.
441      */
442     private static String getTestStorageDirectory() {
443         return PathUtils.getDataDirectory() + "/test_storage";
444     }
445 
446     /**
447      * Ensures test storage directory exists, i.e. creates one if it does not exist.
448      */
449     private static void ensureTestStorageExists() {
450         File storage = new File(getTestStorageDirectory());
451         if (!storage.exists()) {
452             Assert.assertTrue(storage.mkdir());
453         }
454     }
455 
456     private static boolean recursiveDelete(File path) {
457         if (path.isDirectory()) {
458             for (File c : path.listFiles()) {
459                 if (!recursiveDelete(c)) {
460                     return false;
461                 }
462             }
463         }
464         return path.delete();
465     }
466 
467     private void setTestingSystemHttpURLConnection(boolean value) {
468         mTestingSystemHttpURLConnection = value;
469     }
470 
471     private void setTestingJavaImpl(boolean value) {
472     }
473 }
474