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