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.Assert.assertEquals; 8 import static org.junit.Assert.assertTrue; 9 import static org.junit.Assert.fail; 10 11 import static org.chromium.net.CronetTestRule.getContext; 12 import static org.chromium.net.CronetTestRule.getTestStorage; 13 14 import android.net.http.ExperimentalHttpEngine; 15 import android.net.http.UrlRequest; 16 import android.os.StrictMode; 17 18 import androidx.test.ext.junit.runners.AndroidJUnit4; 19 import androidx.test.filters.SmallTest; 20 21 import org.json.JSONObject; 22 import org.junit.After; 23 import org.junit.Before; 24 import org.junit.Ignore; 25 import org.junit.Rule; 26 import org.junit.Test; 27 import org.junit.runner.RunWith; 28 29 import org.chromium.base.Log; 30 import org.chromium.base.metrics.UmaRecorderHolder; 31 import org.chromium.base.test.util.DisabledTest; 32 import org.chromium.base.test.util.HistogramWatcher; 33 import org.chromium.net.CronetTestRule.OnlyRunNativeCronet; 34 import org.chromium.net.MetricsTestUtil.TestExecutor; 35 36 import java.io.File; 37 import java.io.FileInputStream; 38 import java.io.FileNotFoundException; 39 import java.io.IOException; 40 import java.util.concurrent.Executor; 41 import java.util.concurrent.Executors; 42 import java.util.concurrent.ThreadFactory; 43 44 /** 45 * Test Network Quality Estimator. 46 */ 47 @RunWith(AndroidJUnit4.class) 48 public class NQETest { 49 private static final String TAG = NQETest.class.getSimpleName(); 50 51 @Rule 52 public final CronetTestRule mTestRule = new CronetTestRule(); 53 54 private String mUrl; 55 56 // Thread on which network quality listeners should be notified. 57 private Thread mNetworkQualityThread; 58 59 @Before setUp()60 public void setUp() throws Exception { 61 assertTrue(NativeTestServer.startNativeTestServer(getContext())); 62 mUrl = NativeTestServer.getSuccessURL(); 63 } 64 65 @After tearDown()66 public void tearDown() throws Exception { 67 NativeTestServer.shutdownNativeTestServer(); 68 } 69 70 private class ExecutorThreadFactory implements ThreadFactory { 71 @Override newThread(final Runnable r)72 public Thread newThread(final Runnable r) { 73 mNetworkQualityThread = new Thread(new Runnable() { 74 @Override 75 public void run() { 76 StrictMode.ThreadPolicy threadPolicy = StrictMode.getThreadPolicy(); 77 try { 78 StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() 79 .detectNetwork() 80 .penaltyLog() 81 .penaltyDeath() 82 .build()); 83 r.run(); 84 } finally { 85 StrictMode.setThreadPolicy(threadPolicy); 86 } 87 } 88 }); 89 return mNetworkQualityThread; 90 } 91 } 92 93 @Test 94 @SmallTest 95 @OnlyRunNativeCronet testNotEnabled()96 public void testNotEnabled() throws Exception { 97 ExperimentalHttpEngine.Builder cronetEngineBuilder = 98 new ExperimentalHttpEngine.Builder(getContext()); 99 final ExperimentalHttpEngine cronetEngine = cronetEngineBuilder.build(); 100 Executor networkQualityExecutor = Executors.newSingleThreadExecutor(); 101 TestNetworkQualityRttListener rttListener = 102 new TestNetworkQualityRttListener(networkQualityExecutor); 103 TestNetworkQualityThroughputListener throughputListener = 104 new TestNetworkQualityThroughputListener(networkQualityExecutor); 105 try { 106 cronetEngine.addRttListener(rttListener); 107 fail("Should throw an exception."); 108 } catch (IllegalStateException e) { 109 } 110 try { 111 cronetEngine.addThroughputListener(throughputListener); 112 fail("Should throw an exception."); 113 } catch (IllegalStateException e) { 114 } 115 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 116 UrlRequest.Builder builder = 117 cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); 118 UrlRequest urlRequest = builder.build(); 119 120 urlRequest.start(); 121 callback.blockForDone(); 122 assertEquals(0, rttListener.rttObservationCount()); 123 assertEquals(0, throughputListener.throughputObservationCount()); 124 cronetEngine.shutdown(); 125 } 126 127 @Test 128 @SmallTest 129 @OnlyRunNativeCronet testListenerRemoved()130 public void testListenerRemoved() throws Exception { 131 ExperimentalHttpEngine.Builder cronetEngineBuilder = 132 new ExperimentalHttpEngine.Builder(getContext()); 133 TestExecutor networkQualityExecutor = new TestExecutor(); 134 TestNetworkQualityRttListener rttListener = 135 new TestNetworkQualityRttListener(networkQualityExecutor); 136 cronetEngineBuilder.enableNetworkQualityEstimator(true); 137 final ExperimentalHttpEngine cronetEngine = cronetEngineBuilder.build(); 138 cronetEngine.configureNetworkQualityEstimatorForTesting(true, true, false); 139 140 cronetEngine.addRttListener(rttListener); 141 cronetEngine.removeRttListener(rttListener); 142 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 143 UrlRequest.Builder builder = 144 cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); 145 UrlRequest urlRequest = builder.build(); 146 urlRequest.start(); 147 callback.blockForDone(); 148 networkQualityExecutor.runAllTasks(); 149 assertEquals(0, rttListener.rttObservationCount()); 150 cronetEngine.shutdown(); 151 } 152 153 // Returns whether a file contains a particular string. prefsFileContainsString(String content)154 private boolean prefsFileContainsString(String content) throws IOException { 155 File file = new File(getTestStorage(getContext()) + "/prefs/local_prefs.json"); 156 FileInputStream fileInputStream = new FileInputStream(file); 157 byte[] data = new byte[(int) file.length()]; 158 fileInputStream.read(data); 159 fileInputStream.close(); 160 return new String(data, "UTF-8").contains(content); 161 } 162 163 @Test 164 @SmallTest 165 @OnlyRunNativeCronet 166 @DisabledTest(message = "crbug.com/796260") 167 @Ignore("crbug.com/796260") testQuicDisabled()168 public void testQuicDisabled() throws Exception { 169 ExperimentalHttpEngine.Builder cronetEngineBuilder = 170 new ExperimentalHttpEngine.Builder(getContext()); 171 assertTrue(RttThroughputValues.INVALID_RTT_THROUGHPUT < 0); 172 Executor listenersExecutor = Executors.newSingleThreadExecutor(new ExecutorThreadFactory()); 173 TestNetworkQualityRttListener rttListener = 174 new TestNetworkQualityRttListener(listenersExecutor); 175 TestNetworkQualityThroughputListener throughputListener = 176 new TestNetworkQualityThroughputListener(listenersExecutor); 177 cronetEngineBuilder.enableNetworkQualityEstimator(true).setEnableHttp2(true) 178 .setEnableQuic(false); 179 180 // The pref may not be written if the computed Effective Connection Type (ECT) matches the 181 // default ECT for the current connection type. Force the ECT to "Slow-2G". Since "Slow-2G" 182 // is not the default ECT for any connection type, this ensures that the pref is written to. 183 JSONObject nqeOptions = new JSONObject().put("force_effective_connection_type", "Slow-2G"); 184 JSONObject experimentalOptions = 185 new JSONObject().put("NetworkQualityEstimator", nqeOptions); 186 187 cronetEngineBuilder.setExperimentalOptions(experimentalOptions.toString()); 188 189 cronetEngineBuilder.setStoragePath(getTestStorage(getContext())); 190 final ExperimentalHttpEngine cronetEngine = cronetEngineBuilder.build(); 191 cronetEngine.configureNetworkQualityEstimatorForTesting(true, true, true); 192 193 cronetEngine.addRttListener(rttListener); 194 cronetEngine.addThroughputListener(throughputListener); 195 196 // Hackish workaround to crbug.com/1338919 197 UmaRecorderHolder.onLibraryLoaded(); 198 var writeCountHistogram = HistogramWatcher.newBuilder() 199 .expectIntRecord("NQE.Prefs.WriteCount", 1) 200 .allowExtraRecordsForHistogramsAbove() 201 .build(); 202 var readCountHistogram = HistogramWatcher.newBuilder() 203 .expectIntRecord("NQE.Prefs.ReadCount", 1) 204 .allowExtraRecordsForHistogramsAbove() 205 .build(); 206 207 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 208 UrlRequest.Builder builder = 209 cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); 210 UrlRequest urlRequest = builder.build(); 211 urlRequest.start(); 212 callback.blockForDone(); 213 214 // Throughput observation is posted to the network quality estimator on the network thread 215 // after the UrlRequest is completed. The observations are then eventually posted to 216 // throughput listeners on the executor provided to network quality. 217 throughputListener.waitUntilFirstThroughputObservationReceived(); 218 219 // Wait for RTT observation (at the URL request layer) to be posted. 220 rttListener.waitUntilFirstUrlRequestRTTReceived(); 221 222 assertTrue(throughputListener.throughputObservationCount() > 0); 223 224 // Prefs must be read at startup. 225 readCountHistogram.assertExpected(); 226 227 // Check RTT observation count after throughput observation has been received. This ensures 228 // that executor has finished posting the RTT observation to the RTT listeners. 229 assertTrue(rttListener.rttObservationCount() > 0); 230 231 // NETWORK_QUALITY_OBSERVATION_SOURCE_URL_REQUEST 232 assertTrue(rttListener.rttObservationCount(0) > 0); 233 234 // NETWORK_QUALITY_OBSERVATION_SOURCE_TCP 235 assertTrue(rttListener.rttObservationCount(1) > 0); 236 237 // NETWORK_QUALITY_OBSERVATION_SOURCE_QUIC 238 assertEquals(0, rttListener.rttObservationCount(2)); 239 240 // Verify that the listeners were notified on the expected thread. 241 assertEquals(mNetworkQualityThread, rttListener.getThread()); 242 assertEquals(mNetworkQualityThread, throughputListener.getThread()); 243 244 // Verify that effective connection type callback is received and 245 // effective connection type is correctly set. 246 assertTrue( 247 cronetEngine.getEffectiveConnectionType() != EffectiveConnectionType.TYPE_UNKNOWN); 248 249 // Verify that the HTTP RTT, transport RTT and downstream throughput 250 // estimates are available. 251 assertTrue(cronetEngine.getHttpRttMs() >= 0); 252 assertTrue(cronetEngine.getTransportRttMs() >= 0); 253 assertTrue(cronetEngine.getDownstreamThroughputKbps() >= 0); 254 255 // Verify that the cached estimates were written to the prefs. 256 while (true) { 257 Log.i(TAG, "Still waiting for pref file update....."); 258 Thread.sleep(12000); 259 try { 260 if (prefsFileContainsString("network_qualities")) { 261 break; 262 } 263 } catch (FileNotFoundException e) { 264 // Ignored this exception since the file will only be created when updates are 265 // flushed to the disk. 266 } 267 } 268 assertTrue(prefsFileContainsString("network_qualities")); 269 270 cronetEngine.shutdown(); 271 writeCountHistogram.assertExpected(); 272 } 273 274 @Test 275 @SmallTest 276 @OnlyRunNativeCronet 277 @Ignore("b/267353182 Permission denied error") testPrefsWriteRead()278 public void testPrefsWriteRead() throws Exception { 279 // When the loop is run for the first time, network quality is written to the disk. The 280 // test verifies that in the next loop, the network quality is read back. 281 for (int i = 0; i <= 1; ++i) { 282 ExperimentalHttpEngine.Builder cronetEngineBuilder = 283 new ExperimentalHttpEngine.Builder(getContext()); 284 assertTrue(RttThroughputValues.INVALID_RTT_THROUGHPUT < 0); 285 Executor listenersExecutor = 286 Executors.newSingleThreadExecutor(new ExecutorThreadFactory()); 287 TestNetworkQualityRttListener rttListener = 288 new TestNetworkQualityRttListener(listenersExecutor); 289 cronetEngineBuilder.enableNetworkQualityEstimator(true).setEnableHttp2(true) 290 .setEnableQuic(false); 291 292 // The pref may not be written if the computed Effective Connection Type (ECT) matches 293 // the default ECT for the current connection type. Force the ECT to "Slow-2G". Since 294 // "Slow-2G" is not the default ECT for any connection type, this ensures that the pref 295 // is written to. 296 JSONObject nqeOptions = 297 new JSONObject().put("force_effective_connection_type", "Slow-2G"); 298 JSONObject experimentalOptions = 299 new JSONObject().put("NetworkQualityEstimator", nqeOptions); 300 301 cronetEngineBuilder.setExperimentalOptions(experimentalOptions.toString()); 302 303 cronetEngineBuilder.setStoragePath(getTestStorage(getContext())); 304 305 final ExperimentalHttpEngine cronetEngine = cronetEngineBuilder.build(); 306 cronetEngine.configureNetworkQualityEstimatorForTesting(true, true, true); 307 cronetEngine.addRttListener(rttListener); 308 309 // Hackish workaround to crbug.com/1338919 310 if (i == 0) UmaRecorderHolder.onLibraryLoaded(); 311 312 HistogramWatcher readCountHistogram = HistogramWatcher.newBuilder() 313 .expectIntRecord("NQE.Prefs.ReadCount", 1) 314 .allowExtraRecordsForHistogramsAbove() 315 .build(); 316 317 // Stored network quality in the pref should be read in the second iteration. 318 HistogramWatcher readPrefsSizeHistogram; 319 if (i == 0) { 320 readPrefsSizeHistogram = HistogramWatcher.newBuilder() 321 .expectIntRecord("NQE.Prefs.ReadSize", 0) 322 .build(); 323 } else { 324 readPrefsSizeHistogram = HistogramWatcher.newBuilder() 325 .expectIntRecord("NQE.Prefs.ReadSize", 1) 326 .allowExtraRecordsForHistogramsAbove() 327 .build(); 328 } 329 330 // NETWORK_QUALITY_OBSERVATION_SOURCE_HTTP_CACHED_ESTIMATE: 3 331 HistogramWatcher cachedRttHistogram = 332 HistogramWatcher.newBuilder() 333 .expectIntRecord("NQE.RTT.ObservationSource", 3) 334 .allowExtraRecordsForHistogramsAbove() 335 .build(); 336 337 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 338 UrlRequest.Builder builder = 339 cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); 340 UrlRequest urlRequest = builder.build(); 341 urlRequest.start(); 342 callback.blockForDone(); 343 344 // Wait for RTT observation (at the URL request layer) to be posted. 345 rttListener.waitUntilFirstUrlRequestRTTReceived(); 346 347 // Prefs must be read at startup. 348 readCountHistogram.assertExpected(); 349 350 // Check RTT observation count after throughput observation has been received. This 351 // ensures 352 // that executor has finished posting the RTT observation to the RTT listeners. 353 assertTrue(rttListener.rttObservationCount() > 0); 354 355 // Verify that effective connection type callback is received and 356 // effective connection type is correctly set. 357 assertTrue(cronetEngine.getEffectiveConnectionType() 358 != EffectiveConnectionType.TYPE_UNKNOWN); 359 360 cronetEngine.shutdown(); 361 362 if (i == 0) { 363 // Verify that the cached estimates were written to the prefs. 364 assertTrue(prefsFileContainsString("network_qualities")); 365 } 366 367 readPrefsSizeHistogram.assertExpected(); 368 if (i > 0) { 369 cachedRttHistogram.assertExpected(); 370 } 371 } 372 } 373 374 @Test 375 @SmallTest 376 @OnlyRunNativeCronet 377 @DisabledTest(message = "crbug.com/796260") 378 @Ignore("crbug.com/796260") testQuicDisabledWithParams()379 public void testQuicDisabledWithParams() throws Exception { 380 ExperimentalHttpEngine.Builder cronetEngineBuilder = 381 new ExperimentalHttpEngine.Builder(getContext()); 382 Executor listenersExecutor = Executors.newSingleThreadExecutor(new ExecutorThreadFactory()); 383 TestNetworkQualityRttListener rttListener = 384 new TestNetworkQualityRttListener(listenersExecutor); 385 TestNetworkQualityThroughputListener throughputListener = 386 new TestNetworkQualityThroughputListener(listenersExecutor); 387 388 // Force the effective connection type to "2G". 389 JSONObject nqeOptions = new JSONObject().put("force_effective_connection_type", "Slow-2G"); 390 // Add one more extra param two times to ensure robustness. 391 nqeOptions.put("some_other_param_1", "value1"); 392 nqeOptions.put("some_other_param_2", "value2"); 393 JSONObject experimentalOptions = 394 new JSONObject().put("NetworkQualityEstimator", nqeOptions); 395 experimentalOptions.put("SomeOtherFieldTrialName", new JSONObject()); 396 397 cronetEngineBuilder.enableNetworkQualityEstimator(true).setEnableHttp2(true) 398 .setEnableQuic(false); 399 cronetEngineBuilder.setExperimentalOptions(experimentalOptions.toString()); 400 final ExperimentalHttpEngine cronetEngine = cronetEngineBuilder.build(); 401 cronetEngine.configureNetworkQualityEstimatorForTesting(true, true, false); 402 403 cronetEngine.addRttListener(rttListener); 404 cronetEngine.addThroughputListener(throughputListener); 405 406 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 407 UrlRequest.Builder builder = 408 cronetEngine.newUrlRequestBuilder(mUrl, callback, callback.getExecutor()); 409 UrlRequest urlRequest = builder.build(); 410 urlRequest.start(); 411 callback.blockForDone(); 412 413 // Throughput observation is posted to the network quality estimator on the network thread 414 // after the UrlRequest is completed. The observations are then eventually posted to 415 // throughput listeners on the executor provided to network quality. 416 throughputListener.waitUntilFirstThroughputObservationReceived(); 417 418 // Wait for RTT observation (at the URL request layer) to be posted. 419 rttListener.waitUntilFirstUrlRequestRTTReceived(); 420 421 assertTrue(throughputListener.throughputObservationCount() > 0); 422 423 // Check RTT observation count after throughput observation has been received. This ensures 424 // that executor has finished posting the RTT observation to the RTT listeners. 425 assertTrue(rttListener.rttObservationCount() > 0); 426 427 // NETWORK_QUALITY_OBSERVATION_SOURCE_URL_REQUEST 428 assertTrue(rttListener.rttObservationCount(0) > 0); 429 430 // NETWORK_QUALITY_OBSERVATION_SOURCE_TCP 431 assertTrue(rttListener.rttObservationCount(1) > 0); 432 433 // NETWORK_QUALITY_OBSERVATION_SOURCE_QUIC 434 assertEquals(0, rttListener.rttObservationCount(2)); 435 436 // Verify that the listeners were notified on the expected thread. 437 assertEquals(mNetworkQualityThread, rttListener.getThread()); 438 assertEquals(mNetworkQualityThread, throughputListener.getThread()); 439 440 // Verify that effective connection type callback is received and effective connection type 441 // is correctly set to the forced value. This also verifies that the configuration params 442 // from Cronet embedders were correctly read by NetworkQualityEstimator. 443 assertEquals( 444 EffectiveConnectionType.TYPE_SLOW_2G, cronetEngine.getEffectiveConnectionType()); 445 446 cronetEngine.shutdown(); 447 } 448 } 449