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