1 // Copyright 2014 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 android.system.OsConstants.AF_INET6; 8 import static android.system.OsConstants.SOCK_STREAM; 9 10 import static com.google.common.truth.Truth.assertThat; 11 import static com.google.common.truth.Truth.assertWithMessage; 12 import static com.google.common.truth.TruthJUnit.assume; 13 14 import static org.chromium.net.truth.UrlResponseInfoSubject.assertThat; 15 16 import android.content.Context; 17 import android.net.ConnectivityManager; 18 import android.net.Network; 19 import android.os.ConditionVariable; 20 import android.system.Os; 21 22 import androidx.annotation.OptIn; 23 import androidx.test.ext.junit.runners.AndroidJUnit4; 24 import androidx.test.filters.MediumTest; 25 import androidx.test.filters.SmallTest; 26 27 import org.json.JSONObject; 28 import org.junit.After; 29 import org.junit.Before; 30 import org.junit.Rule; 31 import org.junit.Test; 32 import org.junit.runner.RunWith; 33 34 import org.chromium.base.test.util.DisabledTest; 35 import org.chromium.base.test.util.DoNotBatch; 36 import org.chromium.net.CronetTestRule.CronetImplementation; 37 import org.chromium.net.CronetTestRule.IgnoreFor; 38 import org.chromium.net.impl.CronetLibraryLoader; 39 40 import java.io.FileDescriptor; 41 import java.net.InetAddress; 42 import java.net.InetSocketAddress; 43 import java.util.concurrent.CountDownLatch; 44 45 /** Test Cronet under different network change scenarios. */ 46 @RunWith(AndroidJUnit4.class) 47 @DoNotBatch(reason = "crbug/1459563") 48 @IgnoreFor( 49 implementations = {CronetImplementation.FALLBACK, CronetImplementation.AOSP_PLATFORM}, 50 reason = "Fake network changes are supported only by the native implementation") 51 public class NetworkChangesTest { 52 @Rule public final CronetTestRule mTestRule = CronetTestRule.withManualEngineStartup(); 53 54 private CountDownLatch mHangingUrlLatch; 55 private FileDescriptor mSocket; 56 57 private static class Networks { 58 private Network mDefaultNetwork; 59 private Network mCellular; 60 private Network mWifi; 61 Networks(ConnectivityManager connectivityManager)62 public Networks(ConnectivityManager connectivityManager) { 63 postToInitThreadSync( 64 () -> { 65 NetworkChangeNotifierAutoDetect autoDetector = 66 NetworkChangeNotifier.getAutoDetectorForTest(); 67 assertThat(autoDetector).isNotNull(); 68 69 mDefaultNetwork = autoDetector.getDefaultNetwork(); 70 71 for (Network network : autoDetector.getNetworksForTesting()) { 72 switch (connectivityManager.getNetworkInfo(network).getType()) { 73 case ConnectivityManager.TYPE_MOBILE: 74 mCellular = network; 75 break; 76 case ConnectivityManager.TYPE_WIFI: 77 mWifi = network; 78 break; 79 default: 80 // Ignore 81 } 82 } 83 }); 84 85 // TODO(crbug.com/1486376): Drop assumes once CQ bots have multiple networks. 86 assume().that(mCellular).isNotNull(); 87 assume().that(mWifi).isNotNull(); 88 assume().that(mDefaultNetwork).isNotNull(); 89 assume().that(mDefaultNetwork).isAnyOf(mWifi, mCellular); 90 // Protect us against unexpected Network#equals implementation. 91 assertThat(mCellular).isNotEqualTo(mWifi); 92 } 93 swapDefaultNetwork()94 public void swapDefaultNetwork() { 95 if (isWifiDefault()) { 96 makeCellularDefault(); 97 } else { 98 makeWifiDefault(); 99 } 100 } 101 disconnectNonDefaultNetwork()102 public void disconnectNonDefaultNetwork() { 103 fakeNetworkDisconnected(getNonDefaultNetwork()); 104 } 105 disconnectDefaultNetwork()106 public void disconnectDefaultNetwork() { 107 fakeNetworkDisconnected(mDefaultNetwork); 108 } 109 connectDefaultNetwork()110 public void connectDefaultNetwork() { 111 fakeNetworkConnected(mDefaultNetwork); 112 fakeDefaultNetworkChange(mDefaultNetwork); 113 } 114 fakeDefaultNetworkChange(Network network)115 private void fakeDefaultNetworkChange(Network network) { 116 postToInitThreadSync( 117 () -> { 118 NetworkChangeNotifier.fakeDefaultNetwork( 119 network.getNetworkHandle(), ConnectionType.CONNECTION_4G); 120 }); 121 } 122 fakeNetworkDisconnected(Network network)123 private void fakeNetworkDisconnected(Network network) { 124 postToInitThreadSync( 125 () -> { 126 NetworkChangeNotifier.fakeNetworkDisconnected(network.getNetworkHandle()); 127 }); 128 } 129 fakeNetworkConnected(Network network)130 private void fakeNetworkConnected(Network network) { 131 postToInitThreadSync( 132 () -> { 133 NetworkChangeNotifier.fakeNetworkConnected( 134 network.getNetworkHandle(), ConnectionType.CONNECTION_4G); 135 }); 136 } 137 isWifiDefault()138 private boolean isWifiDefault() { 139 return mDefaultNetwork.equals(mWifi); 140 } 141 getNonDefaultNetwork()142 private Network getNonDefaultNetwork() { 143 return isWifiDefault() ? mCellular : mWifi; 144 } 145 makeWifiDefault()146 private void makeWifiDefault() { 147 fakeDefaultNetworkChange(mWifi); 148 mDefaultNetwork = mWifi; 149 } 150 makeCellularDefault()151 private void makeCellularDefault() { 152 fakeDefaultNetworkChange(mCellular); 153 mDefaultNetwork = mCellular; 154 } 155 } 156 157 @Before setUp()158 public void setUp() throws Exception { 159 // Bind a listening socket to a local port. The socket won't be used to accept any 160 // connections, but rather to get connection stuck waiting to connect. 161 mSocket = Os.socket(AF_INET6, SOCK_STREAM, 0); 162 // Bind to 127.0.0.1 and a random port (indicated by special 0 value). 163 Os.bind(mSocket, InetAddress.getByAddress(null, new byte[] {127, 0, 0, 1}), 0); 164 // Set backlog to 0 so connections end up stuck waiting to connect(). 165 Os.listen(mSocket, 0); 166 167 QuicTestServer.startQuicTestServer(mTestRule.getTestFramework().getContext()); 168 mTestRule 169 .getTestFramework() 170 .applyEngineBuilderPatch( 171 (builder) -> { 172 JSONObject hostResolverParams = 173 CronetTestUtil.generateHostResolverRules(); 174 JSONObject experimentalOptions = 175 new JSONObject().put("HostResolverRules", hostResolverParams); 176 builder.setExperimentalOptions(experimentalOptions.toString()); 177 178 builder.enableQuic(true); 179 builder.addQuicHint( 180 QuicTestServer.getServerHost(), 181 QuicTestServer.getServerPort(), 182 QuicTestServer.getServerPort()); 183 184 CronetTestUtil.setMockCertVerifierForTesting( 185 builder, QuicTestServer.createMockCertVerifier()); 186 }); 187 188 mHangingUrlLatch = new CountDownLatch(1); 189 assertThat( 190 Http2TestServer.startHttp2TestServer( 191 mTestRule.getTestFramework().getContext(), mHangingUrlLatch)) 192 .isTrue(); 193 } 194 195 @OptIn(markerClass = org.chromium.net.QuicOptions.Experimental.class) disableSessionHandling(CronetEngine.Builder engineBuilder)196 private static void disableSessionHandling(CronetEngine.Builder engineBuilder) { 197 QuicOptions.Builder optionBuilder = QuicOptions.builder(); 198 optionBuilder.closeSessionsOnIpChange(false); 199 optionBuilder.goawaySessionsOnIpChange(false); 200 engineBuilder.setQuicOptions(optionBuilder.build()); 201 } 202 203 @OptIn(markerClass = org.chromium.net.ConnectionMigrationOptions.Experimental.class) disableDefaultNetworkMigration(CronetEngine.Builder engineBuilder)204 private static void disableDefaultNetworkMigration(CronetEngine.Builder engineBuilder) { 205 ConnectionMigrationOptions.Builder optionBuilder = ConnectionMigrationOptions.builder(); 206 optionBuilder.enableDefaultNetworkMigration(false); 207 optionBuilder.migrateIdleConnections(false); 208 engineBuilder.setConnectionMigrationOptions(optionBuilder.build()); 209 } 210 211 @OptIn(markerClass = org.chromium.net.QuicOptions.Experimental.class) closeSessionsOnIpChange(CronetEngine.Builder engineBuilder)212 private static void closeSessionsOnIpChange(CronetEngine.Builder engineBuilder) { 213 QuicOptions.Builder optionBuilder = QuicOptions.builder(); 214 optionBuilder.closeSessionsOnIpChange(true); 215 optionBuilder.goawaySessionsOnIpChange(false); 216 engineBuilder.setQuicOptions(optionBuilder.build()); 217 218 disableDefaultNetworkMigration(engineBuilder); 219 } 220 221 @OptIn(markerClass = org.chromium.net.QuicOptions.Experimental.class) goawayOnIpChange(CronetEngine.Builder engineBuilder)222 private static void goawayOnIpChange(CronetEngine.Builder engineBuilder) { 223 QuicOptions.Builder optionBuilder = QuicOptions.builder(); 224 optionBuilder.closeSessionsOnIpChange(false); 225 optionBuilder.goawaySessionsOnIpChange(true); 226 engineBuilder.setQuicOptions(optionBuilder.build()); 227 228 disableDefaultNetworkMigration(engineBuilder); 229 } 230 231 @OptIn(markerClass = org.chromium.net.ConnectionMigrationOptions.Experimental.class) enableDefaultNetworkMigration(CronetEngine.Builder engineBuilder)232 private static void enableDefaultNetworkMigration(CronetEngine.Builder engineBuilder) { 233 ConnectionMigrationOptions.Builder optionBuilder = ConnectionMigrationOptions.builder(); 234 optionBuilder.enableDefaultNetworkMigration(true); 235 optionBuilder.migrateIdleConnections(true); 236 engineBuilder.setConnectionMigrationOptions(optionBuilder.build()); 237 238 disableSessionHandling(engineBuilder); 239 } 240 241 @After tearDown()242 public void tearDown() throws Exception { 243 QuicTestServer.shutdownQuicTestServer(); 244 mHangingUrlLatch.countDown(); 245 assertThat(Http2TestServer.shutdownHttp2TestServer()).isTrue(); 246 } 247 248 @Test 249 @SmallTest testDefaultNetworkChangeBeforeConnect_failsWithErrNetChanged()250 public void testDefaultNetworkChangeBeforeConnect_failsWithErrNetChanged() throws Exception { 251 mTestRule.getTestFramework().startEngine(); 252 // URL pointing at the local socket, where requests will get stuck connecting. 253 String url = "https://127.0.0.1:" + ((InetSocketAddress) Os.getsockname(mSocket)).getPort(); 254 // Launch a few requests at this local port. Four seems to be the magic number where 255 // the last request (and any further request) get stuck connecting. 256 TestUrlRequestCallback callback = null; 257 UrlRequest request = null; 258 for (int i = 0; i < 4; i++) { 259 callback = new TestUrlRequestCallback(); 260 request = 261 mTestRule 262 .getTestFramework() 263 .getEngine() 264 .newUrlRequestBuilder(url, callback, callback.getExecutor()) 265 .build(); 266 request.start(); 267 } 268 269 waitForStatus(request, UrlRequest.Status.CONNECTING); 270 271 // Simulate network change which should abort connect jobs 272 postToInitThreadSync( 273 () -> { 274 NetworkChangeNotifier.fakeDefaultNetwork( 275 NetworkChangeNotifier.getInstance().getCurrentDefaultNetId(), 276 ConnectionType.CONNECTION_4G); 277 }); 278 279 // Wait for ERR_NETWORK_CHANGED 280 callback.blockForDone(); 281 assertThat(callback.mOnErrorCalled).isTrue(); 282 assertThat(callback.mError) 283 .hasMessageThat() 284 .contains("Exception in CronetUrlRequest: net::ERR_NETWORK_CHANGED"); 285 assertThat(((NetworkException) callback.mError).getCronetInternalErrorCode()) 286 .isEqualTo(NetError.ERR_NETWORK_CHANGED); 287 } 288 289 @Test 290 @MediumTest 291 @DisabledTest(message = "crbug.com/1492515") testDefaultNetworkChange_spdyCloseSessionsOnIpChange_failsWithErrNetChanged()292 public void testDefaultNetworkChange_spdyCloseSessionsOnIpChange_failsWithErrNetChanged() 293 throws Exception { 294 mTestRule 295 .getTestFramework() 296 .applyEngineBuilderPatch( 297 (builder) -> { 298 // This ends up throwing away the experimental options set in setUp. 299 // This is fine as those are related to H/3 tests. This is an H/2 so 300 // that's fine. If this assumption stops being true consider merging 301 // them or splitting NetworkChangeTests in two. 302 JSONObject experimentalOptions = 303 new JSONObject().put("spdy_go_away_on_ip_change", false); 304 builder.setExperimentalOptions(experimentalOptions.toString()); 305 }); 306 mTestRule.getTestFramework().startEngine(); 307 String url = Http2TestServer.getHangingRequestUrl(); 308 309 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 310 UrlRequest request = 311 mTestRule 312 .getTestFramework() 313 .getEngine() 314 .newUrlRequestBuilder(url, callback, callback.getExecutor()) 315 .build(); 316 request.start(); 317 318 // Without this synchronization it seems that the default network change can happen before 319 // the underlying SPDY session is created (read: the test would be flaky). 320 waitForStatus(request, UrlRequest.Status.WAITING_FOR_RESPONSE); 321 postToInitThreadSync( 322 () -> { 323 NetworkChangeNotifier.fakeDefaultNetwork( 324 NetworkChangeNotifier.getInstance().getCurrentDefaultNetId(), 325 ConnectionType.CONNECTION_4G); 326 }); 327 328 // Similarly to tests below, 15s should be enough for the NCN notification to propagate. 329 // In case of a bug (i.e., sessions stop being closed on IP change), don't timeout the test, 330 // instead fail here. 331 callback.blockForDone(/* timeoutMs= */ 1500); 332 333 assertThat(callback.mOnErrorCalled).isTrue(); 334 assertThat(callback.mError) 335 .hasMessageThat() 336 .contains("Exception in CronetUrlRequest: net::ERR_NETWORK_CHANGED"); 337 assertThat(((NetworkException) callback.mError).getCronetInternalErrorCode()) 338 .isEqualTo(NetError.ERR_NETWORK_CHANGED); 339 } 340 341 @Test 342 @MediumTest testDefaultNetworkChange_closeSessionsOnIpChange_pendingRequestFails()343 public void testDefaultNetworkChange_closeSessionsOnIpChange_pendingRequestFails() 344 throws Exception { 345 mTestRule 346 .getTestFramework() 347 .applyEngineBuilderPatch( 348 (builder) -> { 349 closeSessionsOnIpChange(builder); 350 }); 351 mTestRule.getTestFramework().startEngine(); 352 ConnectivityManager connectivityManager = 353 (ConnectivityManager) 354 mTestRule 355 .getTestFramework() 356 .getContext() 357 .getSystemService(Context.CONNECTIVITY_SERVICE); 358 Networks networks = new Networks(connectivityManager); 359 360 String url = QuicTestServer.getServerURL() + "/simple.txt"; 361 // Unfortunately we have no choice but to delay as QuicTestServer doesn't provide any 362 // synchronization control to the caller. 363 // Delay is set to an unreasonably high value as this test doesn't expect this request to 364 // succeed 365 QuicTestServer.delayResponse("/simple.txt", /* delayInSeconds= */ 100); 366 367 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 368 UrlRequest request = 369 mTestRule 370 .getTestFramework() 371 .getEngine() 372 .newUrlRequestBuilder(url, callback, callback.getExecutor()) 373 .build(); 374 request.start(); 375 376 // Without this synchronization it seems that the default network change can happen before 377 // the underlying QUIC session is created (read: the test would be flaky). 378 waitForStatus(request, UrlRequest.Status.WAITING_FOR_RESPONSE); 379 networks.swapDefaultNetwork(); 380 381 // Similarly to tests below, 15s should be enough for the NCN notification to propagate. 382 // In case of a bug (i.e., sessions stop being closed on IP change), don't timeout after 383 // 100s, instead fail here. 384 callback.blockForDone(/* timeoutMs= */ 1500); 385 assertThat(callback.mOnErrorCalled).isTrue(); 386 assertThat(callback.mError).isNotNull(); 387 assertThat(callback.mError).isInstanceOf(NetworkException.class); 388 NetworkException networkException = (NetworkException) callback.mError; 389 assertThat(networkException.getErrorCode()) 390 .isEqualTo(NetworkException.ERROR_NETWORK_CHANGED); 391 } 392 393 @Test 394 @MediumTest testDefaultNetworkChange_goAwayonIpChange_pendingRequestSucceeds()395 public void testDefaultNetworkChange_goAwayonIpChange_pendingRequestSucceeds() 396 throws Exception { 397 mTestRule 398 .getTestFramework() 399 .applyEngineBuilderPatch( 400 (builder) -> { 401 goawayOnIpChange(builder); 402 }); 403 mTestRule.getTestFramework().startEngine(); 404 ConnectivityManager connectivityManager = 405 (ConnectivityManager) 406 mTestRule 407 .getTestFramework() 408 .getContext() 409 .getSystemService(Context.CONNECTIVITY_SERVICE); 410 Networks networks = new Networks(connectivityManager); 411 412 String url = QuicTestServer.getServerURL() + "/simple.txt"; 413 // Unfortunately we have no choice but to delay as QuicTestServer doesn't provide any 414 // synchronization control to the caller. 415 // 15 seconds is, hopefully, a good enough tradeoff between test execution speed and 416 // flakiness. 417 QuicTestServer.delayResponse("/simple.txt", /* delayInSeconds= */ 15); 418 419 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 420 UrlRequest request = 421 mTestRule 422 .getTestFramework() 423 .getEngine() 424 .newUrlRequestBuilder(url, callback, callback.getExecutor()) 425 .build(); 426 request.start(); 427 428 // Without this synchronization it seems that the default network change can happen before 429 // the underlying QUIC session is created (read: the test would be flaky). 430 waitForStatus(request, UrlRequest.Status.WAITING_FOR_RESPONSE); 431 networks.swapDefaultNetwork(); 432 433 callback.blockForDone(); 434 assertThat(callback.getResponseInfoWithChecks()) 435 .hasNegotiatedProtocolThat() 436 .isEqualTo("h3"); 437 } 438 439 @Test 440 @MediumTest testDefaultNetworkChange_defaultNetworkMigration_pendingRequestSucceeds()441 public void testDefaultNetworkChange_defaultNetworkMigration_pendingRequestSucceeds() 442 throws Exception { 443 mTestRule 444 .getTestFramework() 445 .applyEngineBuilderPatch( 446 (builder) -> { 447 enableDefaultNetworkMigration(builder); 448 }); 449 mTestRule.getTestFramework().startEngine(); 450 ConnectivityManager connectivityManager = 451 (ConnectivityManager) 452 mTestRule 453 .getTestFramework() 454 .getContext() 455 .getSystemService(Context.CONNECTIVITY_SERVICE); 456 Networks networks = new Networks(connectivityManager); 457 458 String url = QuicTestServer.getServerURL() + "/simple.txt"; 459 // Unfortunately we have no choice but to delay as QuicTestServer doesn't provide any 460 // synchronization control to the caller. 461 // 15 seconds is, hopefully, a good enough tradeoff between test execution speed and 462 // flakiness. 463 QuicTestServer.delayResponse("/simple.txt", /* delayInSeconds= */ 15); 464 465 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 466 UrlRequest request = 467 mTestRule 468 .getTestFramework() 469 .getEngine() 470 .newUrlRequestBuilder(url, callback, callback.getExecutor()) 471 .build(); 472 request.start(); 473 474 // Without this synchronization it seems that the default network change can happen before 475 // the underlying QUIC session is created (read: the test would be flaky). 476 waitForStatus(request, UrlRequest.Status.WAITING_FOR_RESPONSE); 477 networks.swapDefaultNetwork(); 478 479 callback.blockForDone(); 480 assertThat(callback.getResponseInfoWithChecks()) 481 .hasNegotiatedProtocolThat() 482 .isEqualTo("h3"); 483 } 484 485 @Test 486 @MediumTest testDoubleDefaultNetworkChange_defaultNetworkMigration_pendingRequestSucceeds()487 public void testDoubleDefaultNetworkChange_defaultNetworkMigration_pendingRequestSucceeds() 488 throws Exception { 489 mTestRule 490 .getTestFramework() 491 .applyEngineBuilderPatch( 492 (builder) -> { 493 enableDefaultNetworkMigration(builder); 494 }); 495 mTestRule.getTestFramework().startEngine(); 496 ConnectivityManager connectivityManager = 497 (ConnectivityManager) 498 mTestRule 499 .getTestFramework() 500 .getContext() 501 .getSystemService(Context.CONNECTIVITY_SERVICE); 502 Networks networks = new Networks(connectivityManager); 503 504 String url = QuicTestServer.getServerURL() + "/simple.txt"; 505 // Unfortunately we have no choice but to delay as QuicTestServer doesn't provide any 506 // synchronization control to the caller. 507 // 15 seconds is, hopefully, a good enough tradeoff between test execution speed and 508 // flakiness. 509 QuicTestServer.delayResponse("/simple.txt", /* delayInSeconds= */ 15); 510 511 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 512 UrlRequest request = 513 mTestRule 514 .getTestFramework() 515 .getEngine() 516 .newUrlRequestBuilder(url, callback, callback.getExecutor()) 517 .build(); 518 request.start(); 519 520 // Without this synchronization it seems that the default network change can happen before 521 // the underlying QUIC session is created (read: the test would be flaky). 522 waitForStatus(request, UrlRequest.Status.WAITING_FOR_RESPONSE); 523 networks.swapDefaultNetwork(); 524 // Back to previous default network. 525 networks.swapDefaultNetwork(); 526 527 callback.blockForDone(); 528 assertThat(callback.getResponseInfoWithChecks()) 529 .hasNegotiatedProtocolThat() 530 .isEqualTo("h3"); 531 } 532 533 @Test 534 @MediumTest 535 @DisabledTest(message = "crbug.com/1486882") testDefaultNetworkChangeAndDisconnect_goAwayonIpChange_pendingRequestFails()536 public void testDefaultNetworkChangeAndDisconnect_goAwayonIpChange_pendingRequestFails() 537 throws Exception { 538 mTestRule 539 .getTestFramework() 540 .applyEngineBuilderPatch( 541 (builder) -> { 542 goawayOnIpChange(builder); 543 }); 544 mTestRule.getTestFramework().startEngine(); 545 ConnectivityManager connectivityManager = 546 (ConnectivityManager) 547 mTestRule 548 .getTestFramework() 549 .getContext() 550 .getSystemService(Context.CONNECTIVITY_SERVICE); 551 Networks networks = new Networks(connectivityManager); 552 553 String url = QuicTestServer.getServerURL() + "/simple.txt"; 554 // Unfortunately we have no choice but to delay as QuicTestServer doesn't provide any 555 // synchronization control to the caller. 556 // 15 seconds is, hopefully, a good enough tradeoff between test execution speed and 557 // flakiness. 558 QuicTestServer.delayResponse("/simple.txt", /* delayInSeconds= */ 15); 559 560 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 561 UrlRequest request = 562 mTestRule 563 .getTestFramework() 564 .getEngine() 565 .newUrlRequestBuilder(url, callback, callback.getExecutor()) 566 .build(); 567 request.start(); 568 569 // Without this synchronization it seems that the default network change can happen before 570 // the underlying QUIC session is created (read: the test would be flaky). 571 waitForStatus(request, UrlRequest.Status.WAITING_FOR_RESPONSE); 572 networks.swapDefaultNetwork(); 573 networks.disconnectNonDefaultNetwork(); 574 575 callback.blockForDone(); 576 assertThat(callback.mOnErrorCalled).isTrue(); 577 assertThat(callback.mError).isNotNull(); 578 assertThat(callback.mError).isInstanceOf(NetworkException.class); 579 NetworkException networkException = (NetworkException) callback.mError; 580 assertThat(networkException.getErrorCode()) 581 .isEqualTo(NetworkException.ERROR_NETWORK_CHANGED); 582 } 583 584 @Test 585 @MediumTest 586 @DisabledTest(message = "crbug.com/1486878") 587 public void testDefaultNetworkChangeAndDisconnect_defaultNetworkMigration_pendingRequestSucceeds()588 testDefaultNetworkChangeAndDisconnect_defaultNetworkMigration_pendingRequestSucceeds() 589 throws Exception { 590 mTestRule 591 .getTestFramework() 592 .applyEngineBuilderPatch( 593 (builder) -> { 594 enableDefaultNetworkMigration(builder); 595 }); 596 mTestRule.getTestFramework().startEngine(); 597 ConnectivityManager connectivityManager = 598 (ConnectivityManager) 599 mTestRule 600 .getTestFramework() 601 .getContext() 602 .getSystemService(Context.CONNECTIVITY_SERVICE); 603 Networks networks = new Networks(connectivityManager); 604 605 String url = QuicTestServer.getServerURL() + "/simple.txt"; 606 // Unfortunately we have no choice but to delay as QuicTestServer doesn't provide any 607 // synchronization control to the caller. 608 // 15 seconds is, hopefully, a good enough tradeoff between test execution speed and 609 // flakiness. 610 QuicTestServer.delayResponse("/simple.txt", /* delayInSeconds= */ 15); 611 612 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 613 UrlRequest request = 614 mTestRule 615 .getTestFramework() 616 .getEngine() 617 .newUrlRequestBuilder(url, callback, callback.getExecutor()) 618 .build(); 619 request.start(); 620 621 // Without this synchronization it seems that the default network change can happen before 622 // the underlying QUIC session is created (read: the test would be flaky). 623 waitForStatus(request, UrlRequest.Status.WAITING_FOR_RESPONSE); 624 networks.swapDefaultNetwork(); 625 networks.disconnectNonDefaultNetwork(); 626 627 callback.blockForDone(); 628 assertThat(callback.getResponseInfoWithChecks()) 629 .hasNegotiatedProtocolThat() 630 .isEqualTo("h3"); 631 } 632 633 @Test 634 @MediumTest 635 public void testDefaultNetworkDisconnectAndReconnect_defaultNetworkMigration_pendingRequestSucceeds()636 testDefaultNetworkDisconnectAndReconnect_defaultNetworkMigration_pendingRequestSucceeds() 637 throws Exception { 638 mTestRule 639 .getTestFramework() 640 .applyEngineBuilderPatch( 641 (builder) -> { 642 enableDefaultNetworkMigration(builder); 643 }); 644 mTestRule.getTestFramework().startEngine(); 645 ConnectivityManager connectivityManager = 646 (ConnectivityManager) 647 mTestRule 648 .getTestFramework() 649 .getContext() 650 .getSystemService(Context.CONNECTIVITY_SERVICE); 651 Networks networks = new Networks(connectivityManager); 652 653 String url = QuicTestServer.getServerURL() + "/simple.txt"; 654 // Unfortunately we have no choice but to delay as QuicTestServer doesn't provide any 655 // synchronization control to the caller. 656 // 15 seconds is, hopefully, a good enough tradeoff between test execution speed and 657 // flakiness. 658 QuicTestServer.delayResponse("/simple.txt", 15); 659 660 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 661 UrlRequest request = 662 mTestRule 663 .getTestFramework() 664 .getEngine() 665 .newUrlRequestBuilder(url, callback, callback.getExecutor()) 666 .build(); 667 request.start(); 668 669 // Without this synchronization it seems that the default network change can happen before 670 // the underlying QUIC session is created (read: the test would be flaky). 671 waitForStatus(request, UrlRequest.Status.WAITING_FOR_RESPONSE); 672 networks.disconnectDefaultNetwork(); 673 networks.connectDefaultNetwork(); 674 675 callback.blockForDone(); 676 assertThat(callback.getResponseInfoWithChecks()) 677 .hasNegotiatedProtocolThat() 678 .isEqualTo("h3"); 679 } 680 681 @Test 682 @MediumTest testDefaultNetworkDisconnectAndReconnect_goawayOnIpChange_pendingRequestSucceeds()683 public void testDefaultNetworkDisconnectAndReconnect_goawayOnIpChange_pendingRequestSucceeds() 684 throws Exception { 685 mTestRule 686 .getTestFramework() 687 .applyEngineBuilderPatch( 688 (builder) -> { 689 goawayOnIpChange(builder); 690 }); 691 mTestRule.getTestFramework().startEngine(); 692 ConnectivityManager connectivityManager = 693 (ConnectivityManager) 694 mTestRule 695 .getTestFramework() 696 .getContext() 697 .getSystemService(Context.CONNECTIVITY_SERVICE); 698 Networks networks = new Networks(connectivityManager); 699 700 String url = QuicTestServer.getServerURL() + "/simple.txt"; 701 // Unfortunately we have no choice but to delay as QuicTestServer doesn't provide any 702 // synchronization control to the caller. 703 // 15 seconds is, hopefully, a good enough tradeoff between test execution speed and 704 // flakiness. 705 QuicTestServer.delayResponse("/simple.txt", 15); 706 707 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 708 UrlRequest request = 709 mTestRule 710 .getTestFramework() 711 .getEngine() 712 .newUrlRequestBuilder(url, callback, callback.getExecutor()) 713 .build(); 714 request.start(); 715 716 // Without this synchronization it seems that the default network change can happen before 717 // the underlying QUIC session is created (read: the test would be flaky). 718 waitForStatus(request, UrlRequest.Status.WAITING_FOR_RESPONSE); 719 networks.disconnectDefaultNetwork(); 720 networks.connectDefaultNetwork(); 721 722 callback.blockForDone(); 723 assertThat(callback.getResponseInfoWithChecks()) 724 .hasNegotiatedProtocolThat() 725 .isEqualTo("h3"); 726 } 727 728 @Test 729 @SmallTest 730 public void testDefaultNetworkDisconnectAndReconnect_closeSessionsOnIpChange_pendingRequestFails()731 testDefaultNetworkDisconnectAndReconnect_closeSessionsOnIpChange_pendingRequestFails() 732 throws Exception { 733 mTestRule 734 .getTestFramework() 735 .applyEngineBuilderPatch( 736 (builder) -> { 737 closeSessionsOnIpChange(builder); 738 }); 739 mTestRule.getTestFramework().startEngine(); 740 ConnectivityManager connectivityManager = 741 (ConnectivityManager) 742 mTestRule 743 .getTestFramework() 744 .getContext() 745 .getSystemService(Context.CONNECTIVITY_SERVICE); 746 Networks networks = new Networks(connectivityManager); 747 748 String url = QuicTestServer.getServerURL() + "/simple.txt"; 749 // Unfortunately we have no choice but to delay as QuicTestServer doesn't provide any 750 // synchronization control to the caller. 751 // 15 seconds is, hopefully, a good enough tradeoff between test execution speed and 752 // flakiness. 753 QuicTestServer.delayResponse("/simple.txt", 15); 754 755 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 756 UrlRequest request = 757 mTestRule 758 .getTestFramework() 759 .getEngine() 760 .newUrlRequestBuilder(url, callback, callback.getExecutor()) 761 .build(); 762 request.start(); 763 764 // Without this synchronization it seems that the default network change can happen before 765 // the underlying QUIC session is created (read: the test would be flaky). 766 waitForStatus(request, UrlRequest.Status.WAITING_FOR_RESPONSE); 767 networks.disconnectDefaultNetwork(); 768 networks.connectDefaultNetwork(); 769 770 callback.blockForDone(); 771 assertThat(callback.mOnErrorCalled).isTrue(); 772 assertThat(callback.mError).isNotNull(); 773 assertThat(callback.mError) 774 .hasMessageThat() 775 .contains("Exception in CronetUrlRequest: net::ERR_NETWORK_CHANGED"); 776 assertThat(((NetworkException) callback.mError).getCronetInternalErrorCode()) 777 .isEqualTo(NetError.ERR_NETWORK_CHANGED); 778 } 779 780 @Test 781 @SmallTest testDefaultNetworkChangeAndDisconnect_defaultNetworkMigration_sessionIsMigrated()782 public void testDefaultNetworkChangeAndDisconnect_defaultNetworkMigration_sessionIsMigrated() 783 throws Exception { 784 mTestRule 785 .getTestFramework() 786 .applyEngineBuilderPatch( 787 (builder) -> { 788 enableDefaultNetworkMigration(builder); 789 }); 790 mTestRule.getTestFramework().startEngine(); 791 ConnectivityManager connectivityManager = 792 (ConnectivityManager) 793 mTestRule 794 .getTestFramework() 795 .getContext() 796 .getSystemService(Context.CONNECTIVITY_SERVICE); 797 Networks networks = new Networks(connectivityManager); 798 799 TestRequestFinishedListener listener = new TestRequestFinishedListener(); 800 mTestRule.getTestFramework().getEngine().addRequestFinishedListener(listener); 801 String url = QuicTestServer.getServerURL() + "/simple.txt"; 802 803 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 804 UrlRequest request = 805 mTestRule 806 .getTestFramework() 807 .getEngine() 808 .newUrlRequestBuilder(url, callback, callback.getExecutor()) 809 .build(); 810 request.start(); 811 812 callback.blockForDone(); 813 listener.blockUntilDone(); 814 assertThat(callback.getResponseInfoWithChecks()) 815 .hasNegotiatedProtocolThat() 816 .isEqualTo("h3"); 817 // Socket reused is a poorly named API. What it really represent is whether the underlying 818 // session was reused or not (for requests over QUIC, this is populated from 819 // https://source.chromium.org/chromium/chromium/src/+/main:net/quic/quic_http_stream.h;l=205;drc=150d8c7e45daeef094be8ec8852e3486eed8f59d). 820 assertThat(listener.getRequestInfo().getMetrics().getSocketReused()).isFalse(); 821 mTestRule.getTestFramework().getEngine().removeRequestFinishedListener(listener); 822 823 // QUIC session created due to the previous request should migrate to the new default 824 // network. 825 networks.swapDefaultNetwork(); 826 827 callback = new TestUrlRequestCallback(); 828 listener = new TestRequestFinishedListener(); 829 mTestRule.getTestFramework().getEngine().addRequestFinishedListener(listener); 830 request = 831 mTestRule 832 .getTestFramework() 833 .getEngine() 834 .newUrlRequestBuilder(url, callback, callback.getExecutor()) 835 .build(); 836 request.start(); 837 838 callback.blockForDone(); 839 listener.blockUntilDone(); 840 assertThat(callback.getResponseInfoWithChecks()) 841 .hasNegotiatedProtocolThat() 842 .isEqualTo("h3"); 843 // See previous check as to why we're checking for socket being reused. 844 assertThat(listener.getRequestInfo().getMetrics().getSocketReused()).isTrue(); 845 mTestRule.getTestFramework().getEngine().removeRequestFinishedListener(listener); 846 847 // Disconnecting the non-default network should not affect an existing QUIC session. 848 networks.disconnectNonDefaultNetwork(); 849 850 callback = new TestUrlRequestCallback(); 851 listener = new TestRequestFinishedListener(); 852 mTestRule.getTestFramework().getEngine().addRequestFinishedListener(listener); 853 request = 854 mTestRule 855 .getTestFramework() 856 .getEngine() 857 .newUrlRequestBuilder(url, callback, callback.getExecutor()) 858 .build(); 859 request.start(); 860 861 callback.blockForDone(); 862 listener.blockUntilDone(); 863 assertThat(callback.getResponseInfoWithChecks()) 864 .hasNegotiatedProtocolThat() 865 .isEqualTo("h3"); 866 // See previous check as to why we're checking for socket being reused. 867 assertThat(listener.getRequestInfo().getMetrics().getSocketReused()).isTrue(); 868 mTestRule.getTestFramework().getEngine().removeRequestFinishedListener(listener); 869 } 870 871 @Test 872 @SmallTest testDefaultNetworkChange_goawayOnIpChange_sessionIsNotReused()873 public void testDefaultNetworkChange_goawayOnIpChange_sessionIsNotReused() throws Exception { 874 mTestRule 875 .getTestFramework() 876 .applyEngineBuilderPatch( 877 (builder) -> { 878 goawayOnIpChange(builder); 879 }); 880 mTestRule.getTestFramework().startEngine(); 881 ConnectivityManager connectivityManager = 882 (ConnectivityManager) 883 mTestRule 884 .getTestFramework() 885 .getContext() 886 .getSystemService(Context.CONNECTIVITY_SERVICE); 887 Networks networks = new Networks(connectivityManager); 888 889 TestRequestFinishedListener listener = new TestRequestFinishedListener(); 890 mTestRule.getTestFramework().getEngine().addRequestFinishedListener(listener); 891 String url = QuicTestServer.getServerURL() + "/simple.txt"; 892 893 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 894 UrlRequest request = 895 mTestRule 896 .getTestFramework() 897 .getEngine() 898 .newUrlRequestBuilder(url, callback, callback.getExecutor()) 899 .build(); 900 request.start(); 901 902 callback.blockForDone(); 903 listener.blockUntilDone(); 904 assertThat(callback.getResponseInfoWithChecks()) 905 .hasNegotiatedProtocolThat() 906 .isEqualTo("h3"); 907 // Socket reused is a poorly named API. What it really represent is whether the underlying 908 // session was reused or not (for requests over QUIC, this is populated from 909 // https://source.chromium.org/chromium/chromium/src/+/main:net/quic/quic_http_stream.h;l=205;drc=150d8c7e45daeef094be8ec8852e3486eed8f59d). 910 assertThat(listener.getRequestInfo().getMetrics().getSocketReused()).isFalse(); 911 mTestRule.getTestFramework().getEngine().removeRequestFinishedListener(listener); 912 913 // This should cause the QUIC session created due to the previous request to be marked as 914 // going away. 915 networks.swapDefaultNetwork(); 916 917 callback = new TestUrlRequestCallback(); 918 listener = new TestRequestFinishedListener(); 919 mTestRule.getTestFramework().getEngine().addRequestFinishedListener(listener); 920 request = 921 mTestRule 922 .getTestFramework() 923 .getEngine() 924 .newUrlRequestBuilder(url, callback, callback.getExecutor()) 925 .build(); 926 request.start(); 927 928 callback.blockForDone(); 929 listener.blockUntilDone(); 930 assertThat(callback.getResponseInfoWithChecks()) 931 .hasNegotiatedProtocolThat() 932 .isEqualTo("h3"); 933 // See previous check as to why we're checking for socket being reused. 934 assertThat(listener.getRequestInfo().getMetrics().getSocketReused()).isFalse(); 935 mTestRule.getTestFramework().getEngine().removeRequestFinishedListener(listener); 936 } 937 938 @Test 939 @SmallTest testDefaultNetworkChange_closeSessionsOnIpChange_sessionIsNotReused()940 public void testDefaultNetworkChange_closeSessionsOnIpChange_sessionIsNotReused() 941 throws Exception { 942 mTestRule 943 .getTestFramework() 944 .applyEngineBuilderPatch( 945 (builder) -> { 946 closeSessionsOnIpChange(builder); 947 }); 948 mTestRule.getTestFramework().startEngine(); 949 ConnectivityManager connectivityManager = 950 (ConnectivityManager) 951 mTestRule 952 .getTestFramework() 953 .getContext() 954 .getSystemService(Context.CONNECTIVITY_SERVICE); 955 Networks networks = new Networks(connectivityManager); 956 957 TestRequestFinishedListener listener = new TestRequestFinishedListener(); 958 mTestRule.getTestFramework().getEngine().addRequestFinishedListener(listener); 959 String url = QuicTestServer.getServerURL() + "/simple.txt"; 960 961 TestUrlRequestCallback callback = new TestUrlRequestCallback(); 962 UrlRequest request = 963 mTestRule 964 .getTestFramework() 965 .getEngine() 966 .newUrlRequestBuilder(url, callback, callback.getExecutor()) 967 .build(); 968 request.start(); 969 970 callback.blockForDone(); 971 listener.blockUntilDone(); 972 assertThat(callback.getResponseInfoWithChecks()) 973 .hasNegotiatedProtocolThat() 974 .isEqualTo("h3"); 975 // Socket reused is a poorly named API. What it really represent is whether the underlying 976 // session was reused or not (for requests over QUIC, this is populated from 977 // https://source.chromium.org/chromium/chromium/src/+/main:net/quic/quic_http_stream.h;l=205;drc=150d8c7e45daeef094be8ec8852e3486eed8f59d). 978 assertThat(listener.getRequestInfo().getMetrics().getSocketReused()).isFalse(); 979 mTestRule.getTestFramework().getEngine().removeRequestFinishedListener(listener); 980 981 // This should cause the QUIC session created due to the previous request to be closed. 982 networks.swapDefaultNetwork(); 983 984 callback = new TestUrlRequestCallback(); 985 listener = new TestRequestFinishedListener(); 986 mTestRule.getTestFramework().getEngine().addRequestFinishedListener(listener); 987 request = 988 mTestRule 989 .getTestFramework() 990 .getEngine() 991 .newUrlRequestBuilder(url, callback, callback.getExecutor()) 992 .build(); 993 request.start(); 994 995 callback.blockForDone(); 996 listener.blockUntilDone(); 997 assertThat(callback.getResponseInfoWithChecks()) 998 .hasNegotiatedProtocolThat() 999 .isEqualTo("h3"); 1000 // See previous check as to why we're checking for socket being reused. 1001 assertThat(listener.getRequestInfo().getMetrics().getSocketReused()).isFalse(); 1002 mTestRule.getTestFramework().getEngine().removeRequestFinishedListener(listener); 1003 } 1004 waitForStatus(UrlRequest request, int targetStatus)1005 private static void waitForStatus(UrlRequest request, int targetStatus) { 1006 final ConditionVariable cv = new ConditionVariable(/* open= */ false); 1007 UrlRequest.StatusListener listener = 1008 new UrlRequest.StatusListener() { 1009 @Override 1010 public void onStatus(int status) { 1011 // We are not guaranteed to receive every single state update: we might 1012 // register the listener too late, missing what we're looking for. 1013 // Hence, we should unblock also if we see a status that can only happen 1014 // after the one we're waiting for (this works under the assumption that 1015 // "value ordering" of states represent the "happens-after ordering" of 1016 // states, which is true at the moment). 1017 if (status >= targetStatus) { 1018 cv.open(); 1019 } else { 1020 // Very confusingly, UrlRequest.StatusListener#onStatus gets called 1021 // once. 1022 // We want to keep getting called until we reach the target status, so 1023 // re-register the listener. This effectively means that we're 1024 // busy-polling the state, but this is test code, so it's not too bad. 1025 // Sleep a bit to avoid potential locks contention. 1026 try { 1027 Thread.sleep(/* millis= */ 100); 1028 } catch (InterruptedException e) { 1029 Thread.currentThread().interrupt(); 1030 } 1031 request.getStatus(this); 1032 } 1033 } 1034 }; 1035 request.getStatus(listener); 1036 assertWithMessage("Target status wasn't reached within the given timeout") 1037 .that(cv.block(/* timeoutMs= */ 5000)) 1038 .isTrue(); 1039 } 1040 postToInitThreadSync(Runnable r)1041 private static void postToInitThreadSync(Runnable r) { 1042 final ConditionVariable cv = new ConditionVariable(/* open= */ false); 1043 CronetLibraryLoader.postToInitThread( 1044 () -> { 1045 r.run(); 1046 cv.open(); 1047 }); 1048 cv.block(); 1049 } 1050 } 1051