• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2015 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 com.google.common.truth.Truth.assertThat;
8 import static com.google.common.truth.TruthJUnit.assume;
9 
10 import static org.junit.Assert.assertThrows;
11 
12 import static org.chromium.net.truth.UrlResponseInfoSubject.assertThat;
13 
14 import android.net.Network;
15 import android.os.Build;
16 import android.os.ConditionVariable;
17 import android.os.Process;
18 
19 import androidx.test.ext.junit.runners.AndroidJUnit4;
20 import androidx.test.filters.SmallTest;
21 
22 import org.junit.After;
23 import org.junit.Before;
24 import org.junit.Rule;
25 import org.junit.Test;
26 import org.junit.runner.RunWith;
27 
28 import org.chromium.base.Log;
29 import org.chromium.base.test.util.DoNotBatch;
30 import org.chromium.net.CronetTestRule.CronetImplementation;
31 import org.chromium.net.CronetTestRule.IgnoreFor;
32 import org.chromium.net.CronetTestRule.RequiresMinAndroidApi;
33 import org.chromium.net.CronetTestRule.RequiresMinApi;
34 import org.chromium.net.NetworkChangeNotifierAutoDetect.ConnectivityManagerDelegate;
35 import org.chromium.net.TestBidirectionalStreamCallback.FailureType;
36 import org.chromium.net.TestBidirectionalStreamCallback.ResponseStep;
37 import org.chromium.net.impl.BidirectionalStreamNetworkException;
38 import org.chromium.net.impl.CronetBidirectionalStream;
39 import org.chromium.net.impl.CronetExceptionImpl;
40 import org.chromium.net.impl.NetworkExceptionImpl;
41 import org.chromium.net.impl.UrlResponseInfoImpl;
42 
43 import java.nio.ByteBuffer;
44 import java.util.AbstractMap;
45 import java.util.ArrayList;
46 import java.util.Arrays;
47 import java.util.Date;
48 import java.util.List;
49 import java.util.Map;
50 import java.util.regex.Matcher;
51 import java.util.regex.Pattern;
52 
53 /** Test functionality of BidirectionalStream interface. */
54 @DoNotBatch(reason = "crbug/1459563")
55 @RunWith(AndroidJUnit4.class)
56 @IgnoreFor(
57         implementations = {CronetImplementation.FALLBACK},
58         reason = "The fallback implementation doesn't support bidirectional streaming")
59 public class BidirectionalStreamTest {
60     private static final String TAG = BidirectionalStreamTest.class.getSimpleName();
61 
62     @Rule public final CronetTestRule mTestRule = CronetTestRule.withManualEngineStartup();
63 
64     private ExperimentalCronetEngine mCronetEngine;
65 
66     @Before
setUp()67     public void setUp() throws Exception {
68         // TODO(crbug/1490552): Fallback to MockCertVerifier when custom CAs are not supported.
69         if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
70             mTestRule
71                     .getTestFramework()
72                     .applyEngineBuilderPatch(
73                             (builder) ->
74                                     CronetTestUtil.setMockCertVerifierForTesting(
75                                             builder, QuicTestServer.createMockCertVerifier()));
76         }
77         mCronetEngine = mTestRule.getTestFramework().startEngine();
78         assertThat(Http2TestServer.startHttp2TestServer(mTestRule.getTestFramework().getContext()))
79                 .isTrue();
80     }
81 
82     @After
tearDown()83     public void tearDown() throws Exception {
84         assertThat(Http2TestServer.shutdownHttp2TestServer()).isTrue();
85     }
86 
checkResponseInfo( UrlResponseInfo responseInfo, String expectedUrl, int expectedHttpStatusCode, String expectedHttpStatusText)87     private static void checkResponseInfo(
88             UrlResponseInfo responseInfo,
89             String expectedUrl,
90             int expectedHttpStatusCode,
91             String expectedHttpStatusText) {
92         assertThat(responseInfo).hasUrlThat().isEqualTo(expectedUrl);
93         assertThat(responseInfo).hasUrlChainThat().containsExactly(expectedUrl);
94         assertThat(responseInfo).hasHttpStatusCodeThat().isEqualTo(expectedHttpStatusCode);
95         assertThat(responseInfo).hasHttpStatusTextThat().isEqualTo(expectedHttpStatusText);
96         assertThat(responseInfo).wasNotCached();
97         assertThat(responseInfo.toString()).isNotEmpty();
98     }
99 
createLongString(String base, int repetition)100     private static String createLongString(String base, int repetition) {
101         StringBuilder builder = new StringBuilder(base.length() * repetition);
102         for (int i = 0; i < repetition; ++i) {
103             builder.append(i);
104             builder.append(base);
105         }
106         return builder.toString();
107     }
108 
createUrlResponseInfo( String[] urls, String message, int statusCode, int receivedBytes, String... headers)109     private static UrlResponseInfo createUrlResponseInfo(
110             String[] urls, String message, int statusCode, int receivedBytes, String... headers) {
111         ArrayList<Map.Entry<String, String>> headersList = new ArrayList<>();
112         for (int i = 0; i < headers.length; i += 2) {
113             headersList.add(
114                     new AbstractMap.SimpleImmutableEntry<String, String>(
115                             headers[i], headers[i + 1]));
116         }
117         UrlResponseInfoImpl urlResponseInfo =
118                 new UrlResponseInfoImpl(
119                         Arrays.asList(urls),
120                         statusCode,
121                         message,
122                         headersList,
123                         false,
124                         "h2",
125                         null,
126                         receivedBytes);
127         return urlResponseInfo;
128     }
129 
runGetWithExpectedReceivedByteCount(int expectedReceivedBytes)130     private void runGetWithExpectedReceivedByteCount(int expectedReceivedBytes) throws Exception {
131         String url = Http2TestServer.getEchoMethodUrl();
132         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
133         TestRequestFinishedListener requestFinishedListener = new TestRequestFinishedListener();
134         mCronetEngine.addRequestFinishedListener(requestFinishedListener);
135         // Create stream.
136         BidirectionalStream stream =
137                 mCronetEngine
138                         .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
139                         .setHttpMethod("GET")
140                         .build();
141         stream.start();
142         callback.blockForDone();
143         assertThat(stream.isDone()).isTrue();
144         requestFinishedListener.blockUntilDone();
145         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
146         // Default method is 'GET'.
147         assertThat(callback.mResponseAsString).isEqualTo("GET");
148         UrlResponseInfo urlResponseInfo =
149                 createUrlResponseInfo(
150                         new String[] {url}, "", 200, expectedReceivedBytes, ":status", "200");
151         mTestRule.assertResponseEquals(urlResponseInfo, callback.getResponseInfoWithChecks());
152         checkResponseInfo(
153                 callback.getResponseInfoWithChecks(), Http2TestServer.getEchoMethodUrl(), 200, "");
154         RequestFinishedInfo finishedInfo = requestFinishedListener.getRequestInfo();
155         assertThat(finishedInfo.getAnnotations()).isEmpty();
156     }
157 
158     @Test
159     @SmallTest
testBuilderCheck()160     public void testBuilderCheck() throws Exception {
161         ExperimentalCronetEngine engine = mTestRule.getTestFramework().getEngine();
162         if (mTestRule.testingJavaImpl()) {
163             runBuilderCheckJavaImpl(engine);
164         } else {
165             runBuilderCheckNativeImpl(engine);
166         }
167     }
168 
runBuilderCheckNativeImpl(ExperimentalCronetEngine engine)169     private static void runBuilderCheckNativeImpl(ExperimentalCronetEngine engine)
170             throws Exception {
171         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
172 
173         NullPointerException e =
174                 assertThrows(
175                         NullPointerException.class,
176                         () ->
177                                 engine.newBidirectionalStreamBuilder(
178                                         null, callback, callback.getExecutor()));
179         assertThat(e).hasMessageThat().isEqualTo("URL is required.");
180 
181         e =
182                 assertThrows(
183                         NullPointerException.class,
184                         () ->
185                                 engine.newBidirectionalStreamBuilder(
186                                         Http2TestServer.getServerUrl(),
187                                         null,
188                                         callback.getExecutor()));
189         assertThat(e).hasMessageThat().isEqualTo("Callback is required.");
190 
191         e =
192                 assertThrows(
193                         NullPointerException.class,
194                         () ->
195                                 engine.newBidirectionalStreamBuilder(
196                                         Http2TestServer.getServerUrl(), callback, null));
197         assertThat(e).hasMessageThat().isEqualTo("Executor is required.");
198 
199         // Verify successful creation doesn't throw.
200         BidirectionalStream.Builder builder =
201                 engine.newBidirectionalStreamBuilder(
202                         Http2TestServer.getServerUrl(), callback, callback.getExecutor());
203 
204         e = assertThrows(NullPointerException.class, () -> builder.addHeader(null, "value"));
205         assertThat(e).hasMessageThat().isEqualTo("Invalid header name.");
206         e = assertThrows(NullPointerException.class, () -> builder.addHeader("name", null));
207         assertThat(e).hasMessageThat().isEqualTo("Invalid header value.");
208         e = assertThrows(NullPointerException.class, () -> builder.setHttpMethod(null));
209         assertThat(e).hasMessageThat().isEqualTo("Method is required.");
210     }
211 
runBuilderCheckJavaImpl(ExperimentalCronetEngine engine)212     private void runBuilderCheckJavaImpl(ExperimentalCronetEngine engine) {
213         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
214         assertThrows(
215                 "JavaCronetEngine doesn't support BidirectionalStream.",
216                 UnsupportedOperationException.class,
217                 () ->
218                         engine.newBidirectionalStreamBuilder(
219                                 Http2TestServer.getServerUrl(), callback, callback.getExecutor()));
220     }
221 
222     @Test
223     @SmallTest
testFailPlainHttp()224     public void testFailPlainHttp() throws Exception {
225         String url = "http://example.com";
226         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
227         // Create stream.
228         BidirectionalStream stream =
229                 mCronetEngine
230                         .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
231                         .build();
232         stream.start();
233         callback.blockForDone();
234         assertThat(stream.isDone()).isTrue();
235         assertThat(callback.mError)
236                 .hasMessageThat()
237                 .contains("Exception in BidirectionalStream: net::ERR_DISALLOWED_URL_SCHEME");
238         mTestRule.assertCronetInternalErrorCode((NetworkException) callback.mError, -301);
239     }
240 
241     @Test
242     @SmallTest
testSimpleGet()243     public void testSimpleGet() throws Exception {
244         // Since this is the first request on the connection, the expected received bytes count
245         // must account for an HPACK dynamic table size update.
246         int expectedReceivedBytes = 31;
247 
248         String url = Http2TestServer.getEchoMethodUrl();
249         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
250         // Create stream.
251         BidirectionalStream stream =
252                 mCronetEngine
253                         .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
254                         .setHttpMethod("GET")
255                         .build();
256         stream.start();
257         callback.blockForDone();
258         assertThat(stream.isDone()).isTrue();
259         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
260         // Default method is 'GET'.
261         assertThat(callback.mResponseAsString).isEqualTo("GET");
262         UrlResponseInfo urlResponseInfo =
263                 createUrlResponseInfo(
264                         new String[] {url}, "", 200, expectedReceivedBytes, ":status", "200");
265         mTestRule.assertResponseEquals(urlResponseInfo, callback.getResponseInfoWithChecks());
266         checkResponseInfo(
267                 callback.getResponseInfoWithChecks(), Http2TestServer.getEchoMethodUrl(), 200, "");
268     }
269 
270     @Test
271     @SmallTest
testSimpleHead()272     public void testSimpleHead() throws Exception {
273         String url = Http2TestServer.getEchoMethodUrl();
274         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
275         // Create stream.
276         BidirectionalStream stream =
277                 mCronetEngine
278                         .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
279                         .setHttpMethod("HEAD")
280                         .build();
281         stream.start();
282         callback.blockForDone();
283         assertThat(stream.isDone()).isTrue();
284         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
285         assertThat(callback.mResponseAsString).isEqualTo("HEAD");
286         UrlResponseInfo urlResponseInfo =
287                 createUrlResponseInfo(new String[] {url}, "", 200, 32, ":status", "200");
288         mTestRule.assertResponseEquals(urlResponseInfo, callback.getResponseInfoWithChecks());
289         checkResponseInfo(
290                 callback.getResponseInfoWithChecks(), Http2TestServer.getEchoMethodUrl(), 200, "");
291     }
292 
293     @Test
294     @SmallTest
testSimplePost()295     public void testSimplePost() throws Exception {
296         String url = Http2TestServer.getEchoStreamUrl();
297         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
298         callback.addWriteData("Test String".getBytes());
299         callback.addWriteData("1234567890".getBytes());
300         callback.addWriteData("woot!".getBytes());
301         // Create stream.
302         BidirectionalStream stream =
303                 mCronetEngine
304                         .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
305                         .addHeader("foo", "bar")
306                         .addHeader("empty", "")
307                         .addHeader("Content-Type", "zebra")
308                         .addRequestAnnotation(this)
309                         .addRequestAnnotation("request annotation")
310                         .build();
311         stream.start();
312         callback.blockForDone();
313         assertThat(stream.isDone()).isTrue();
314         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
315         assertThat(callback.mResponseAsString).isEqualTo("Test String1234567890woot!");
316         assertThat(callback.getResponseInfoWithChecks())
317                 .hasHeadersThat()
318                 .containsEntry("echo-foo", Arrays.asList("bar"));
319         assertThat(callback.getResponseInfoWithChecks())
320                 .hasHeadersThat()
321                 .containsEntry("echo-empty", Arrays.asList(""));
322         assertThat(callback.getResponseInfoWithChecks())
323                 .hasHeadersThat()
324                 .containsEntry("echo-content-type", Arrays.asList("zebra"));
325     }
326 
327     @Test
328     @SmallTest
329     @IgnoreFor(
330             implementations = {CronetImplementation.AOSP_PLATFORM},
331             reason = "RequedFinishedListener is not available in AOSP")
testPostWithFinishedListener()332     public void testPostWithFinishedListener() throws Exception {
333         String url = Http2TestServer.getEchoStreamUrl();
334         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
335         callback.addWriteData("Test String".getBytes());
336         callback.addWriteData("1234567890".getBytes());
337         callback.addWriteData("woot!".getBytes());
338         TestRequestFinishedListener requestFinishedListener = new TestRequestFinishedListener();
339         mCronetEngine.addRequestFinishedListener(requestFinishedListener);
340         // Create stream.
341         BidirectionalStream stream =
342                 mCronetEngine
343                         .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
344                         .addHeader("foo", "bar")
345                         .addHeader("empty", "")
346                         .addHeader("Content-Type", "zebra")
347                         .addRequestAnnotation(this)
348                         .addRequestAnnotation("request annotation")
349                         .build();
350         Date startTime = new Date();
351         stream.start();
352         callback.blockForDone();
353         assertThat(stream.isDone()).isTrue();
354         requestFinishedListener.blockUntilDone();
355         Date endTime = new Date();
356         RequestFinishedInfo finishedInfo = requestFinishedListener.getRequestInfo();
357         MetricsTestUtil.checkRequestFinishedInfo(finishedInfo, url, startTime, endTime);
358         assertThat(finishedInfo.getFinishedReason()).isEqualTo(RequestFinishedInfo.SUCCEEDED);
359         MetricsTestUtil.checkHasConnectTiming(finishedInfo.getMetrics(), startTime, endTime, true);
360         assertThat(finishedInfo.getAnnotations()).containsExactly("request annotation", this);
361         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
362         assertThat(callback.mResponseAsString).isEqualTo("Test String1234567890woot!");
363         assertThat(callback.getResponseInfoWithChecks())
364                 .hasHeadersThat()
365                 .containsEntry("echo-foo", Arrays.asList("bar"));
366         assertThat(callback.getResponseInfoWithChecks())
367                 .hasHeadersThat()
368                 .containsEntry("echo-empty", Arrays.asList(""));
369         assertThat(callback.getResponseInfoWithChecks())
370                 .hasHeadersThat()
371                 .containsEntry("echo-content-type", Arrays.asList("zebra"));
372     }
373 
374     @Test
375     @SmallTest
376     @IgnoreFor(
377             implementations = {CronetImplementation.AOSP_PLATFORM},
378             reason = "ActiveRequestCount is not available in AOSP")
testGetActiveRequestCount()379     public void testGetActiveRequestCount() throws Exception {
380         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
381         callback.addWriteData("Test String".getBytes());
382         callback.setBlockOnTerminalState(true);
383         BidirectionalStream stream =
384                 mCronetEngine
385                         .newBidirectionalStreamBuilder(
386                                 Http2TestServer.getEchoStreamUrl(),
387                                 callback,
388                                 callback.getExecutor())
389                         .build();
390         assertThat(mCronetEngine.getActiveRequestCount()).isEqualTo(0);
391         stream.start();
392         callback.blockForDone();
393         assertThat(mCronetEngine.getActiveRequestCount()).isEqualTo(1);
394         callback.setBlockOnTerminalState(false);
395         waitForActiveRequestCount(0);
396     }
397 
398     @Test
399     @SmallTest
400     @IgnoreFor(
401             implementations = {CronetImplementation.AOSP_PLATFORM},
402             reason = "ActiveRequestCount is not available in AOSP")
testGetActiveRequestCountWithInvalidRequest()403     public void testGetActiveRequestCountWithInvalidRequest() throws Exception {
404         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
405         BidirectionalStream stream =
406                 mCronetEngine
407                         .newBidirectionalStreamBuilder(
408                                 Http2TestServer.getEchoStreamUrl(),
409                                 callback,
410                                 callback.getExecutor())
411                         .addHeader("", "") // Deliberately invalid
412                         .build();
413         assertThat(mCronetEngine.getActiveRequestCount()).isEqualTo(0);
414         assertThrows(IllegalArgumentException.class, stream::start);
415         assertThat(mCronetEngine.getActiveRequestCount()).isEqualTo(0);
416     }
417 
418     @Test
419     @SmallTest
testSimpleGetWithCombinedHeader()420     public void testSimpleGetWithCombinedHeader() throws Exception {
421         String url = Http2TestServer.getCombinedHeadersUrl();
422         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
423         // Create stream.
424         BidirectionalStream stream =
425                 mCronetEngine
426                         .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
427                         .setHttpMethod("GET")
428                         .build();
429         stream.start();
430         callback.blockForDone();
431         assertThat(stream.isDone()).isTrue();
432         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
433         // Default method is 'GET'.
434         assertThat(callback.mResponseAsString).isEqualTo("GET");
435         assertThat(callback.getResponseInfoWithChecks())
436                 .hasHeadersThat()
437                 .containsEntry("foo", Arrays.asList("bar", "bar2"));
438     }
439 
440     @Test
441     @SmallTest
testSimplePostWithFlush()442     public void testSimplePostWithFlush() throws Exception {
443         String url = Http2TestServer.getEchoStreamUrl();
444         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
445         callback.addWriteData("Test String".getBytes(), false);
446         callback.addWriteData("1234567890".getBytes(), false);
447         callback.addWriteData("woot!".getBytes(), true);
448         BidirectionalStream stream =
449                 mCronetEngine
450                         .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
451                         .addHeader("foo", "bar")
452                         .addHeader("empty", "")
453                         .addHeader("Content-Type", "zebra")
454                         .build();
455         // Flush before stream is started should not crash.
456         stream.flush();
457 
458         stream.start();
459         callback.blockForDone();
460         assertThat(stream.isDone()).isTrue();
461 
462         // Flush after stream is completed is no-op. It shouldn't call into the destroyed adapter.
463         stream.flush();
464 
465         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
466         assertThat(callback.mResponseAsString).isEqualTo("Test String1234567890woot!");
467         assertThat(callback.getResponseInfoWithChecks())
468                 .hasHeadersThat()
469                 .containsEntry("echo-foo", Arrays.asList("bar"));
470         assertThat(callback.getResponseInfoWithChecks())
471                 .hasHeadersThat()
472                 .containsEntry("echo-empty", Arrays.asList(""));
473         assertThat(callback.getResponseInfoWithChecks())
474                 .hasHeadersThat()
475                 .containsEntry("echo-content-type", Arrays.asList("zebra"));
476     }
477 
478     @Test
479     @SmallTest
480     @IgnoreFor(
481             implementations = {CronetImplementation.AOSP_PLATFORM},
482             reason = "crbug.com/1494845: Requires access to internals not available in AOSP")
483     // Tests that a delayed flush() only sends buffers that have been written
484     // before it is called, and it doesn't flush buffers in mPendingQueue.
testFlushData()485     public void testFlushData() throws Exception {
486         String url = Http2TestServer.getEchoStreamUrl();
487         final ConditionVariable waitOnStreamReady = new ConditionVariable();
488         TestBidirectionalStreamCallback callback =
489                 new TestBidirectionalStreamCallback() {
490                     // Number of onWriteCompleted callbacks that have been invoked.
491                     private int mNumWriteCompleted;
492 
493                     @Override
494                     public void onStreamReady(BidirectionalStream stream) {
495                         mResponseStep = ResponseStep.ON_STREAM_READY;
496                         waitOnStreamReady.open();
497                     }
498 
499                     @Override
500                     public void onWriteCompleted(
501                             BidirectionalStream stream,
502                             UrlResponseInfo info,
503                             ByteBuffer buffer,
504                             boolean endOfStream) {
505                         super.onWriteCompleted(stream, info, buffer, endOfStream);
506                         mNumWriteCompleted++;
507                         if (mNumWriteCompleted <= 3) {
508                             // "6" is in pending queue.
509                             List<ByteBuffer> pendingData =
510                                     ((CronetBidirectionalStream) stream).getPendingDataForTesting();
511                             assertThat(pendingData).hasSize(1);
512                             ByteBuffer pendingBuffer = pendingData.get(0);
513                             byte[] content = new byte[pendingBuffer.remaining()];
514                             pendingBuffer.get(content);
515                             assertThat(content).isEqualTo("6".getBytes());
516 
517                             // "4" and "5" have been flushed.
518                             assertThat(
519                                             ((CronetBidirectionalStream) stream)
520                                                     .getFlushDataForTesting())
521                                     .isEmpty();
522                         } else if (mNumWriteCompleted == 5) {
523                             // Now flush "6", which is still in pending queue.
524                             List<ByteBuffer> pendingData =
525                                     ((CronetBidirectionalStream) stream).getPendingDataForTesting();
526                             assertThat(pendingData).hasSize(1);
527                             ByteBuffer pendingBuffer = pendingData.get(0);
528                             byte[] content = new byte[pendingBuffer.remaining()];
529                             pendingBuffer.get(content);
530                             assertThat(content).isEqualTo("6".getBytes());
531 
532                             stream.flush();
533 
534                             assertThat(
535                                             ((CronetBidirectionalStream) stream)
536                                                     .getPendingDataForTesting())
537                                     .isEmpty();
538                             assertThat(
539                                             ((CronetBidirectionalStream) stream)
540                                                     .getFlushDataForTesting())
541                                     .isEmpty();
542                         }
543                     }
544                 };
545         callback.addWriteData("1".getBytes(), false);
546         callback.addWriteData("2".getBytes(), false);
547         callback.addWriteData("3".getBytes(), true);
548         callback.addWriteData("4".getBytes(), false);
549         callback.addWriteData("5".getBytes(), true);
550         callback.addWriteData("6".getBytes(), false);
551         CronetBidirectionalStream stream =
552                 (CronetBidirectionalStream)
553                         mCronetEngine
554                                 .newBidirectionalStreamBuilder(
555                                         url, callback, callback.getExecutor())
556                                 .addHeader("foo", "bar")
557                                 .addHeader("empty", "")
558                                 .addHeader("Content-Type", "zebra")
559                                 .build();
560         stream.start();
561         waitOnStreamReady.block();
562 
563         assertThat(stream.getPendingDataForTesting()).isEmpty();
564         assertThat(stream.getFlushDataForTesting()).isEmpty();
565 
566         // Write 1, 2, 3 and flush().
567         callback.startNextWrite(stream);
568         // Write 4, 5 and flush(). 4, 5 will be in flush queue.
569         callback.startNextWrite(stream);
570         // Write 6, but do not flush. 6 will be in pending queue.
571         callback.startNextWrite(stream);
572 
573         callback.blockForDone();
574         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
575         assertThat(callback.mResponseAsString).isEqualTo("123456");
576         assertThat(callback.getResponseInfoWithChecks())
577                 .hasHeadersThat()
578                 .containsEntry("echo-foo", Arrays.asList("bar"));
579         assertThat(callback.getResponseInfoWithChecks())
580                 .hasHeadersThat()
581                 .containsEntry("echo-empty", Arrays.asList(""));
582         assertThat(callback.getResponseInfoWithChecks())
583                 .hasHeadersThat()
584                 .containsEntry("echo-content-type", Arrays.asList("zebra"));
585     }
586 
587     @Test
588     @SmallTest
589     // Regression test for crbug.com/692168.
testCancelWhileWriteDataPending()590     public void testCancelWhileWriteDataPending() throws Exception {
591         String url = Http2TestServer.getEchoStreamUrl();
592         // Use a direct executor to avoid race.
593         TestBidirectionalStreamCallback callback =
594                 new TestBidirectionalStreamCallback(/* useDirectExecutor= */ true) {
595                     @Override
596                     public void onStreamReady(BidirectionalStream stream) {
597                         // Start the first write.
598                         stream.write(getSampleData(), false);
599                         stream.flush();
600                     }
601 
602                     @Override
603                     public void onReadCompleted(
604                             BidirectionalStream stream,
605                             UrlResponseInfo info,
606                             ByteBuffer byteBuffer,
607                             boolean endOfStream) {
608                         super.onReadCompleted(stream, info, byteBuffer, endOfStream);
609                         // Cancel now when the write side is busy.
610                         stream.cancel();
611                     }
612 
613                     @Override
614                     public void onWriteCompleted(
615                             BidirectionalStream stream,
616                             UrlResponseInfo info,
617                             ByteBuffer buffer,
618                             boolean endOfStream) {
619                         // Flush twice to keep the flush queue non-empty.
620                         stream.write(getSampleData(), false);
621                         stream.flush();
622                         stream.write(getSampleData(), false);
623                         stream.flush();
624                     }
625 
626                     // Returns a piece of sample data to send to the server.
627                     private ByteBuffer getSampleData() {
628                         byte[] data = new byte[100];
629                         for (int i = 0; i < data.length; i++) {
630                             data[i] = 'x';
631                         }
632                         ByteBuffer sampleData = ByteBuffer.allocateDirect(data.length);
633                         sampleData.put(data);
634                         sampleData.flip();
635                         return sampleData;
636                     }
637                 };
638         BidirectionalStream stream =
639                 mCronetEngine
640                         .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
641                         .build();
642         stream.start();
643         callback.blockForDone();
644         assertThat(callback.mOnCanceledCalled).isTrue();
645     }
646 
647     @Test
648     @SmallTest
testSimpleGetWithFlush()649     public void testSimpleGetWithFlush() throws Exception {
650         // TODO(xunjieli): Use ParameterizedTest instead of the loop.
651         for (int i = 0; i < 2; i++) {
652             String url = Http2TestServer.getEchoStreamUrl();
653             TestBidirectionalStreamCallback callback =
654                     new TestBidirectionalStreamCallback() {
655                         @Override
656                         public void onStreamReady(BidirectionalStream stream) {
657                             // Attempt to write data for GET request.
658                             assertThrows(
659                                     IllegalArgumentException.class,
660                                     () -> stream.write(ByteBuffer.wrap("sample".getBytes()), true));
661 
662                             // If there are delayed headers, this flush should try to send them.
663                             // If nothing to flush, it should not crash.
664                             stream.flush();
665                             super.onStreamReady(stream);
666 
667                             // Attempt to write data for GET request.
668                             assertThrows(
669                                     IllegalArgumentException.class,
670                                     () -> stream.write(ByteBuffer.wrap("sample".getBytes()), true));
671                         }
672                     };
673             BidirectionalStream stream =
674                     mCronetEngine
675                             .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
676                             .setHttpMethod("GET")
677                             .delayRequestHeadersUntilFirstFlush(i == 0)
678                             .addHeader("foo", "bar")
679                             .addHeader("empty", "")
680                             .build();
681             // Flush before stream is started should not crash.
682             stream.flush();
683 
684             stream.start();
685             callback.blockForDone();
686             assertThat(stream.isDone()).isTrue();
687 
688             // Flush after stream is completed is no-op. It shouldn't call into the destroyed
689             // adapter.
690             stream.flush();
691 
692             assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
693             assertThat(callback.mResponseAsString).isEmpty();
694             assertThat(callback.getResponseInfoWithChecks())
695                     .hasHeadersThat()
696                     .containsEntry("echo-foo", Arrays.asList("bar"));
697             assertThat(callback.getResponseInfoWithChecks())
698                     .hasHeadersThat()
699                     .containsEntry("echo-empty", Arrays.asList(""));
700         }
701     }
702 
703     @Test
704     @SmallTest
testSimplePostWithFlushAfterOneWrite()705     public void testSimplePostWithFlushAfterOneWrite() throws Exception {
706         // TODO(xunjieli): Use ParameterizedTest instead of the loop.
707         for (int i = 0; i < 2; i++) {
708             String url = Http2TestServer.getEchoStreamUrl();
709             TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
710             callback.addWriteData("Test String".getBytes(), true);
711             BidirectionalStream stream =
712                     mCronetEngine
713                             .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
714                             .delayRequestHeadersUntilFirstFlush(i == 0)
715                             .addHeader("foo", "bar")
716                             .addHeader("empty", "")
717                             .addHeader("Content-Type", "zebra")
718                             .build();
719             stream.start();
720             callback.blockForDone();
721             assertThat(stream.isDone()).isTrue();
722 
723             assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
724             assertThat(callback.mResponseAsString).isEqualTo("Test String");
725             assertThat(callback.getResponseInfoWithChecks())
726                     .hasHeadersThat()
727                     .containsEntry("echo-foo", Arrays.asList("bar"));
728             assertThat(callback.getResponseInfoWithChecks())
729                     .hasHeadersThat()
730                     .containsEntry("echo-empty", Arrays.asList(""));
731             assertThat(callback.getResponseInfoWithChecks())
732                     .hasHeadersThat()
733                     .containsEntry("echo-content-type", Arrays.asList("zebra"));
734         }
735     }
736 
737     @Test
738     @SmallTest
testSimplePostWithFlushTwice()739     public void testSimplePostWithFlushTwice() throws Exception {
740         // TODO(xunjieli): Use ParameterizedTest instead of the loop.
741         for (int i = 0; i < 2; i++) {
742             String url = Http2TestServer.getEchoStreamUrl();
743             TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
744             callback.addWriteData("Test String".getBytes(), false);
745             callback.addWriteData("1234567890".getBytes(), false);
746             callback.addWriteData("woot!".getBytes(), true);
747             callback.addWriteData("Test String".getBytes(), false);
748             callback.addWriteData("1234567890".getBytes(), false);
749             callback.addWriteData("woot!".getBytes(), true);
750             BidirectionalStream stream =
751                     mCronetEngine
752                             .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
753                             .delayRequestHeadersUntilFirstFlush(i == 0)
754                             .addHeader("foo", "bar")
755                             .addHeader("empty", "")
756                             .addHeader("Content-Type", "zebra")
757                             .build();
758             stream.start();
759             callback.blockForDone();
760             assertThat(stream.isDone()).isTrue();
761             assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
762             assertThat(callback.mResponseAsString)
763                     .isEqualTo("Test String1234567890woot!Test String1234567890woot!");
764             assertThat(callback.getResponseInfoWithChecks())
765                     .hasHeadersThat()
766                     .containsEntry("echo-foo", Arrays.asList("bar"));
767             assertThat(callback.getResponseInfoWithChecks())
768                     .hasHeadersThat()
769                     .containsEntry("echo-empty", Arrays.asList(""));
770             assertThat(callback.getResponseInfoWithChecks())
771                     .hasHeadersThat()
772                     .containsEntry("echo-content-type", Arrays.asList("zebra"));
773         }
774     }
775 
776     @Test
777     @SmallTest
778     // Tests that it is legal to call read() in onStreamReady().
testReadDuringOnStreamReady()779     public void testReadDuringOnStreamReady() throws Exception {
780         String url = Http2TestServer.getEchoStreamUrl();
781         TestBidirectionalStreamCallback callback =
782                 new TestBidirectionalStreamCallback() {
783                     @Override
784                     public void onStreamReady(BidirectionalStream stream) {
785                         super.onStreamReady(stream);
786                         startNextRead(stream);
787                     }
788 
789                     @Override
790                     public void onResponseHeadersReceived(
791                             BidirectionalStream stream, UrlResponseInfo info) {
792                         // Do nothing. Skip readng.
793                     }
794                 };
795         callback.addWriteData("Test String".getBytes());
796         callback.addWriteData("1234567890".getBytes());
797         callback.addWriteData("woot!".getBytes());
798         BidirectionalStream stream =
799                 mCronetEngine
800                         .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
801                         .addHeader("foo", "bar")
802                         .addHeader("empty", "")
803                         .addHeader("Content-Type", "zebra")
804                         .build();
805         stream.start();
806         callback.blockForDone();
807         assertThat(stream.isDone()).isTrue();
808         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
809         assertThat(callback.mResponseAsString).isEqualTo("Test String1234567890woot!");
810         assertThat(callback.getResponseInfoWithChecks())
811                 .hasHeadersThat()
812                 .containsEntry("echo-foo", Arrays.asList("bar"));
813         assertThat(callback.getResponseInfoWithChecks())
814                 .hasHeadersThat()
815                 .containsEntry("echo-empty", Arrays.asList(""));
816         assertThat(callback.getResponseInfoWithChecks())
817                 .hasHeadersThat()
818                 .containsEntry("echo-content-type", Arrays.asList("zebra"));
819     }
820 
821     @Test
822     @SmallTest
823     // Tests that it is legal to call flush() when previous nativeWritevData has
824     // yet to complete.
testSimplePostWithFlushBeforePreviousWriteCompleted()825     public void testSimplePostWithFlushBeforePreviousWriteCompleted() throws Exception {
826         String url = Http2TestServer.getEchoStreamUrl();
827         TestBidirectionalStreamCallback callback =
828                 new TestBidirectionalStreamCallback() {
829                     @Override
830                     public void onStreamReady(BidirectionalStream stream) {
831                         super.onStreamReady(stream);
832                         // Write a second time before the previous nativeWritevData has completed.
833                         startNextWrite(stream);
834                         assertThat(numPendingWrites()).isEqualTo(0);
835                     }
836                 };
837         callback.addWriteData("Test String".getBytes(), false);
838         callback.addWriteData("1234567890".getBytes(), false);
839         callback.addWriteData("woot!".getBytes(), true);
840         callback.addWriteData("Test String".getBytes(), false);
841         callback.addWriteData("1234567890".getBytes(), false);
842         callback.addWriteData("woot!".getBytes(), true);
843         BidirectionalStream stream =
844                 mCronetEngine
845                         .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
846                         .addHeader("foo", "bar")
847                         .addHeader("empty", "")
848                         .addHeader("Content-Type", "zebra")
849                         .build();
850         stream.start();
851         callback.blockForDone();
852         assertThat(stream.isDone()).isTrue();
853         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
854         assertThat(callback.mResponseAsString)
855                 .isEqualTo("Test String1234567890woot!Test String1234567890woot!");
856         assertThat(callback.getResponseInfoWithChecks())
857                 .hasHeadersThat()
858                 .containsEntry("echo-foo", Arrays.asList("bar"));
859         assertThat(callback.getResponseInfoWithChecks())
860                 .hasHeadersThat()
861                 .containsEntry("echo-empty", Arrays.asList(""));
862         assertThat(callback.getResponseInfoWithChecks())
863                 .hasHeadersThat()
864                 .containsEntry("echo-content-type", Arrays.asList("zebra"));
865     }
866 
867     @Test
868     @SmallTest
testSimplePut()869     public void testSimplePut() throws Exception {
870         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
871         callback.addWriteData("Put This Data!".getBytes());
872         String methodName = "PUT";
873         BidirectionalStream.Builder builder =
874                 mCronetEngine.newBidirectionalStreamBuilder(
875                         Http2TestServer.getServerUrl(), callback, callback.getExecutor());
876         builder.setHttpMethod(methodName);
877         builder.build().start();
878         callback.blockForDone();
879         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
880         assertThat(callback.mResponseAsString).isEqualTo("Put This Data!");
881         assertThat(callback.getResponseInfoWithChecks())
882                 .hasHeadersThat()
883                 .containsEntry("echo-method", Arrays.asList(methodName));
884     }
885 
886     @Test
887     @SmallTest
testBadMethod()888     public void testBadMethod() throws Exception {
889         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
890         BidirectionalStream.Builder builder =
891                 mCronetEngine.newBidirectionalStreamBuilder(
892                         Http2TestServer.getServerUrl(), callback, callback.getExecutor());
893         builder.setHttpMethod("bad:method!");
894         IllegalArgumentException e =
895                 assertThrows(IllegalArgumentException.class, () -> builder.build().start());
896         assertThat(e).hasMessageThat().isEqualTo("Invalid http method bad:method!");
897     }
898 
899     @Test
900     @SmallTest
testBadHeaderName()901     public void testBadHeaderName() throws Exception {
902         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
903         BidirectionalStream.Builder builder =
904                 mCronetEngine.newBidirectionalStreamBuilder(
905                         Http2TestServer.getServerUrl(), callback, callback.getExecutor());
906         builder.addHeader("goodheader1", "headervalue");
907         builder.addHeader("header:name", "headervalue");
908         builder.addHeader("goodheader2", "headervalue");
909         IllegalArgumentException e =
910                 assertThrows(IllegalArgumentException.class, () -> builder.build().start());
911         if (mTestRule.implementationUnderTest() == CronetImplementation.AOSP_PLATFORM &&
912                 !mTestRule.isRunningInAOSP()) {
913             // TODO(b/307234565): Remove check once chromium Android 14 emulator has latest changes.
914             assertThat(e).hasMessageThat().isEqualTo("Invalid header header:name=headervalue");
915         } else {
916             assertThat(e).hasMessageThat().isEqualTo("Invalid header with headername: header:name");
917         }
918     }
919 
920     @Test
921     @SmallTest
testBadHeaderValue()922     public void testBadHeaderValue() throws Exception {
923         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
924         BidirectionalStream.Builder builder =
925                 mCronetEngine.newBidirectionalStreamBuilder(
926                         Http2TestServer.getServerUrl(), callback, callback.getExecutor());
927         builder.addHeader("headername", "bad header\r\nvalue");
928         IllegalArgumentException e =
929                 assertThrows(IllegalArgumentException.class, () -> builder.build().start());
930         if (mTestRule.implementationUnderTest() == CronetImplementation.AOSP_PLATFORM &&
931                 !mTestRule.isRunningInAOSP()) {
932             // TODO(b/307234565): Remove check once chromium Android 14 emulator has latest changes.
933             assertThat(e)
934                     .hasMessageThat()
935                     .isEqualTo("Invalid header headername=bad header\r\nvalue");
936         } else {
937             assertThat(e).hasMessageThat().isEqualTo("Invalid header with headername: headername");
938         }
939     }
940 
941     @Test
942     @SmallTest
testAddHeader()943     public void testAddHeader() throws Exception {
944         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
945         String headerName = "header-name";
946         String headerValue = "header-value";
947         BidirectionalStream.Builder builder =
948                 mCronetEngine.newBidirectionalStreamBuilder(
949                         Http2TestServer.getEchoHeaderUrl(headerName),
950                         callback,
951                         callback.getExecutor());
952         builder.addHeader(headerName, headerValue);
953         builder.setHttpMethod("GET");
954         builder.build().start();
955         callback.blockForDone();
956         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
957         assertThat(callback.mResponseAsString).isEqualTo(headerValue);
958     }
959 
960     @Test
961     @SmallTest
testMultiRequestHeaders()962     public void testMultiRequestHeaders() throws Exception {
963         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
964         String headerName = "header-name";
965         String headerValue1 = "header-value1";
966         String headerValue2 = "header-value2";
967         BidirectionalStream.Builder builder =
968                 mCronetEngine.newBidirectionalStreamBuilder(
969                         Http2TestServer.getEchoAllHeadersUrl(), callback, callback.getExecutor());
970         builder.addHeader(headerName, headerValue1);
971         builder.addHeader(headerName, headerValue2);
972         builder.setHttpMethod("GET");
973         builder.build().start();
974         callback.blockForDone();
975         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
976         String headers = callback.mResponseAsString;
977         Pattern pattern = Pattern.compile(headerName + ":\\s(.*)\\r\\n");
978         Matcher matcher = pattern.matcher(headers);
979         List<String> actualValues = new ArrayList<String>();
980         while (matcher.find()) {
981             actualValues.add(matcher.group(1));
982         }
983 
984         assertThat(actualValues).containsExactly("header-value2");
985     }
986 
987     @Test
988     @SmallTest
testEchoTrailers()989     public void testEchoTrailers() throws Exception {
990         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
991         String headerName = "header-name";
992         String headerValue = "header-value";
993         BidirectionalStream.Builder builder =
994                 mCronetEngine.newBidirectionalStreamBuilder(
995                         Http2TestServer.getEchoTrailersUrl(), callback, callback.getExecutor());
996         builder.addHeader(headerName, headerValue);
997         builder.setHttpMethod("GET");
998         builder.build().start();
999         callback.blockForDone();
1000         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
1001         assertThat(callback.mTrailers).isNotNull();
1002         // Verify that header value is properly echoed in trailers.
1003         assertThat(callback.mTrailers.getAsMap())
1004                 .containsEntry("echo-" + headerName, Arrays.asList(headerValue));
1005     }
1006 
1007     @Test
1008     @SmallTest
testCustomUserAgent()1009     public void testCustomUserAgent() throws Exception {
1010         String userAgentName = "User-Agent";
1011         String userAgentValue = "User-Agent-Value";
1012         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
1013         BidirectionalStream.Builder builder =
1014                 mCronetEngine.newBidirectionalStreamBuilder(
1015                         Http2TestServer.getEchoHeaderUrl(userAgentName),
1016                         callback,
1017                         callback.getExecutor());
1018         builder.setHttpMethod("GET");
1019         builder.addHeader(userAgentName, userAgentValue);
1020         builder.build().start();
1021         callback.blockForDone();
1022         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
1023         assertThat(callback.mResponseAsString).isEqualTo(userAgentValue);
1024     }
1025 
1026     @Test
1027     @SmallTest
testCustomCronetEngineUserAgent()1028     public void testCustomCronetEngineUserAgent() throws Exception {
1029         String userAgentName = "User-Agent";
1030         String userAgentValue = "User-Agent-Value";
1031         ExperimentalCronetEngine.Builder engineBuilder =
1032                 new ExperimentalCronetEngine.Builder(mTestRule.getTestFramework().getContext());
1033         engineBuilder.setUserAgent(userAgentValue);
1034         // TODO(crbug/1490552): Fallback to MockCertVerifier when custom CAs are not supported.
1035         if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
1036             CronetTestUtil.setMockCertVerifierForTesting(
1037                     engineBuilder, QuicTestServer.createMockCertVerifier());
1038         }
1039         ExperimentalCronetEngine engine = engineBuilder.build();
1040         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
1041         BidirectionalStream.Builder builder =
1042                 engine.newBidirectionalStreamBuilder(
1043                         Http2TestServer.getEchoHeaderUrl(userAgentName),
1044                         callback,
1045                         callback.getExecutor());
1046         builder.setHttpMethod("GET");
1047         builder.build().start();
1048         callback.blockForDone();
1049         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
1050         assertThat(callback.mResponseAsString).isEqualTo(userAgentValue);
1051     }
1052 
1053     @Test
1054     @SmallTest
testDefaultUserAgent()1055     public void testDefaultUserAgent() throws Exception {
1056         String userAgentName = "User-Agent";
1057         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
1058         BidirectionalStream.Builder builder =
1059                 mCronetEngine.newBidirectionalStreamBuilder(
1060                         Http2TestServer.getEchoHeaderUrl(userAgentName),
1061                         callback,
1062                         callback.getExecutor());
1063         builder.setHttpMethod("GET");
1064         builder.build().start();
1065         callback.blockForDone();
1066         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
1067         assertThat(callback.mResponseAsString)
1068                 .isEqualTo(
1069                         new CronetEngine.Builder(mTestRule.getTestFramework().getContext())
1070                                 .getDefaultUserAgent());
1071     }
1072 
1073     @Test
1074     @SmallTest
testEchoStream()1075     public void testEchoStream() throws Exception {
1076         String url = Http2TestServer.getEchoStreamUrl();
1077         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
1078         String[] testData = {"Test String", createLongString("1234567890", 50000), "woot!"};
1079         StringBuilder stringData = new StringBuilder();
1080         for (String writeData : testData) {
1081             callback.addWriteData(writeData.getBytes());
1082             stringData.append(writeData);
1083         }
1084         // Create stream.
1085         BidirectionalStream stream =
1086                 mCronetEngine
1087                         .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
1088                         .addHeader("foo", "Value with Spaces")
1089                         .addHeader("Content-Type", "zebra")
1090                         .build();
1091         stream.start();
1092         callback.blockForDone();
1093         assertThat(stream.isDone()).isTrue();
1094         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
1095         assertThat(callback.mResponseAsString).isEqualTo(stringData.toString());
1096         assertThat(callback.getResponseInfoWithChecks())
1097                 .hasHeadersThat()
1098                 .containsEntry("echo-foo", Arrays.asList("Value with Spaces"));
1099         assertThat(callback.getResponseInfoWithChecks())
1100                 .hasHeadersThat()
1101                 .containsEntry("echo-content-type", Arrays.asList("zebra"));
1102     }
1103 
1104     @Test
1105     @SmallTest
testEchoStreamEmptyWrite()1106     public void testEchoStreamEmptyWrite() throws Exception {
1107         String url = Http2TestServer.getEchoStreamUrl();
1108         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
1109         callback.addWriteData(new byte[0]);
1110         // Create stream.
1111         BidirectionalStream stream =
1112                 mCronetEngine
1113                         .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
1114                         .build();
1115         stream.start();
1116         callback.blockForDone();
1117         assertThat(stream.isDone()).isTrue();
1118         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
1119         assertThat(callback.mResponseAsString).isEmpty();
1120     }
1121 
1122     @Test
1123     @SmallTest
testDoubleWrite()1124     public void testDoubleWrite() throws Exception {
1125         String url = Http2TestServer.getEchoStreamUrl();
1126         TestBidirectionalStreamCallback callback =
1127                 new TestBidirectionalStreamCallback() {
1128                     @Override
1129                     public void onStreamReady(BidirectionalStream stream) {
1130                         // super class will call Write() once.
1131                         super.onStreamReady(stream);
1132                         // Call Write() again.
1133                         startNextWrite(stream);
1134                         // Make sure there is no pending write.
1135                         assertThat(numPendingWrites()).isEqualTo(0);
1136                     }
1137                 };
1138         callback.addWriteData("1".getBytes());
1139         callback.addWriteData("2".getBytes());
1140         // Create stream.
1141         BidirectionalStream stream =
1142                 mCronetEngine
1143                         .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
1144                         .build();
1145         stream.start();
1146         callback.blockForDone();
1147         assertThat(stream.isDone()).isTrue();
1148         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
1149         assertThat(callback.mResponseAsString).isEqualTo("12");
1150     }
1151 
1152     @Test
1153     @SmallTest
testDoubleRead()1154     public void testDoubleRead() throws Exception {
1155         String url = Http2TestServer.getEchoStreamUrl();
1156         TestBidirectionalStreamCallback callback =
1157                 new TestBidirectionalStreamCallback() {
1158                     @Override
1159                     public void onResponseHeadersReceived(
1160                             BidirectionalStream stream, UrlResponseInfo info) {
1161                         startNextRead(stream);
1162                         // Second read from callback invoked on single-threaded executor throws an
1163                         // exception because previous read is still pending until its completion is
1164                         // handled on executor.
1165                         Exception e =
1166                                 assertThrows(
1167                                         Exception.class,
1168                                         () -> stream.read(ByteBuffer.allocateDirect(5)));
1169                         assertThat(e).hasMessageThat().isEqualTo("Unexpected read attempt.");
1170                     }
1171                 };
1172         callback.addWriteData("1".getBytes());
1173         callback.addWriteData("2".getBytes());
1174         // Create stream.
1175         BidirectionalStream stream =
1176                 mCronetEngine
1177                         .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
1178                         .build();
1179         stream.start();
1180         callback.blockForDone();
1181         assertThat(stream.isDone()).isTrue();
1182         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
1183         assertThat(callback.mResponseAsString).isEqualTo("12");
1184     }
1185 
1186     @Test
1187     @SmallTest
testReadAndWrite()1188     public void testReadAndWrite() throws Exception {
1189         String url = Http2TestServer.getEchoStreamUrl();
1190         TestBidirectionalStreamCallback callback =
1191                 new TestBidirectionalStreamCallback() {
1192                     @Override
1193                     public void onResponseHeadersReceived(
1194                             BidirectionalStream stream, UrlResponseInfo info) {
1195                         // Start the write, that will not complete until callback completion.
1196                         setAutoAdvance(true);
1197                         startNextWrite(stream);
1198                         // Start the read. It is allowed with write in flight.
1199                         super.onResponseHeadersReceived(stream, info);
1200                     }
1201                 };
1202         callback.setAutoAdvance(false);
1203         callback.addWriteData("1".getBytes());
1204         callback.addWriteData("2".getBytes());
1205         // Create stream.
1206         BidirectionalStream stream =
1207                 mCronetEngine
1208                         .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
1209                         .build();
1210         stream.start();
1211         callback.waitForNextWriteStep();
1212         callback.blockForDone();
1213         assertThat(stream.isDone()).isTrue();
1214         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
1215         assertThat(callback.mResponseAsString).isEqualTo("12");
1216     }
1217 
1218     @Test
1219     @SmallTest
testEchoStreamWriteFirst()1220     public void testEchoStreamWriteFirst() throws Exception {
1221         String url = Http2TestServer.getEchoStreamUrl();
1222         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
1223         callback.setAutoAdvance(false);
1224         String[] testData = {"a", "bb", "ccc", "Test String", "1234567890", "woot!"};
1225         StringBuilder stringData = new StringBuilder();
1226         for (String writeData : testData) {
1227             callback.addWriteData(writeData.getBytes());
1228             stringData.append(writeData);
1229         }
1230         // Create stream.
1231         BidirectionalStream stream =
1232                 mCronetEngine
1233                         .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
1234                         .build();
1235         stream.start();
1236         // Write first.
1237         callback.waitForNextWriteStep(); // onStreamReady
1238         for (int i = 0; i < testData.length; i++) {
1239             // Write next chunk of test data.
1240             callback.startNextWrite(stream);
1241             callback.waitForNextWriteStep(); // onWriteCompleted
1242         }
1243 
1244         // Wait for read step, but don't read yet.
1245         callback.waitForNextReadStep(); // onResponseHeadersReceived
1246         assertThat(callback.mResponseAsString).isEmpty();
1247         // Read back.
1248         callback.startNextRead(stream);
1249         callback.waitForNextReadStep(); // onReadCompleted
1250         // Verify that some part of proper response is read.
1251         assertThat(callback.mResponseAsString).startsWith(testData[0]);
1252         // Read the rest of the response.
1253         callback.setAutoAdvance(true);
1254         callback.startNextRead(stream);
1255         callback.blockForDone();
1256         assertThat(stream.isDone()).isTrue();
1257         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
1258         assertThat(callback.mResponseAsString).isEqualTo(stringData.toString());
1259     }
1260 
1261     @Test
1262     @SmallTest
testEchoStreamStepByStep()1263     public void testEchoStreamStepByStep() throws Exception {
1264         String url = Http2TestServer.getEchoStreamUrl();
1265         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
1266         callback.setAutoAdvance(false);
1267         String[] testData = {"a", "bb", "ccc", "Test String", "1234567890", "woot!"};
1268         StringBuilder stringData = new StringBuilder();
1269         for (String writeData : testData) {
1270             callback.addWriteData(writeData.getBytes());
1271             stringData.append(writeData);
1272         }
1273         // Create stream.
1274         BidirectionalStream stream =
1275                 mCronetEngine
1276                         .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
1277                         .build();
1278         stream.start();
1279         callback.waitForNextWriteStep();
1280         callback.waitForNextReadStep();
1281 
1282         for (String expected : testData) {
1283             // Write next chunk of test data.
1284             callback.startNextWrite(stream);
1285             callback.waitForNextWriteStep();
1286 
1287             // Read next chunk of test data.
1288             ByteBuffer readBuffer = ByteBuffer.allocateDirect(100);
1289             callback.startNextRead(stream, readBuffer);
1290             callback.waitForNextReadStep();
1291             assertThat(readBuffer.position()).isEqualTo(expected.length());
1292             assertThat(stream.isDone()).isFalse();
1293         }
1294 
1295         callback.setAutoAdvance(true);
1296         callback.startNextRead(stream);
1297         callback.blockForDone();
1298         assertThat(stream.isDone()).isTrue();
1299         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
1300         assertThat(callback.mResponseAsString).isEqualTo(stringData.toString());
1301     }
1302 
1303     /** Checks that the buffer is updated correctly, when starting at an offset. */
1304     @Test
1305     @SmallTest
1306     @IgnoreFor(
1307             implementations = {CronetImplementation.AOSP_PLATFORM},
1308             reason =
1309                     "crbug.com/1494845: Relies on finished listener synchronization which isn't"
1310                             + " available in AOSP")
testSimpleGetBufferUpdates()1311     public void testSimpleGetBufferUpdates() throws Exception {
1312         TestRequestFinishedListener requestFinishedListener = new TestRequestFinishedListener();
1313         mCronetEngine.addRequestFinishedListener(requestFinishedListener);
1314         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
1315         callback.setAutoAdvance(false);
1316         // Since the method is "GET", the expected response body is also "GET".
1317         BidirectionalStream.Builder builder =
1318                 mCronetEngine.newBidirectionalStreamBuilder(
1319                         Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor());
1320         BidirectionalStream stream = builder.setHttpMethod("GET").build();
1321         stream.start();
1322         callback.waitForNextReadStep();
1323 
1324         assertThat(callback.mError).isNull();
1325         assertThat(callback.isDone()).isFalse();
1326         assertThat(callback.mResponseStep)
1327                 .isEqualTo(TestBidirectionalStreamCallback.ResponseStep.ON_RESPONSE_STARTED);
1328 
1329         ByteBuffer readBuffer = ByteBuffer.allocateDirect(5);
1330         readBuffer.put("FOR".getBytes());
1331         assertThat(readBuffer.position()).isEqualTo(3);
1332 
1333         // Read first two characters of the response ("GE"). It's theoretically
1334         // possible to need one read per character, though in practice,
1335         // shouldn't happen.
1336         while (callback.mResponseAsString.length() < 2) {
1337             assertThat(callback.isDone()).isFalse();
1338             callback.startNextRead(stream, readBuffer);
1339             callback.waitForNextReadStep();
1340         }
1341 
1342         // Make sure the two characters were read.
1343         assertThat(callback.mResponseAsString).isEqualTo("GE");
1344 
1345         // Check the contents of the entire buffer. The first 3 characters
1346         // should not have been changed, and the last two should be the first
1347         // two characters from the response.
1348         assertThat(bufferContentsToString(readBuffer, 0, 5)).isEqualTo("FORGE");
1349         // The limit and position should be 5.
1350         assertThat(readBuffer.limit()).isEqualTo(5);
1351         assertThat(readBuffer.position()).isEqualTo(5);
1352 
1353         assertThat(callback.mResponseStep).isEqualTo(ResponseStep.ON_READ_COMPLETED);
1354 
1355         // Start reading from position 3. Since the only remaining character
1356         // from the response is a "T", when the read completes, the buffer
1357         // should contain "FORTE", with a position() of 4 and a limit() of 5.
1358         readBuffer.position(3);
1359         callback.startNextRead(stream, readBuffer);
1360         callback.waitForNextReadStep();
1361 
1362         // Make sure all three characters of the response have now been read.
1363         assertThat(callback.mResponseAsString).isEqualTo("GET");
1364 
1365         // Check the entire contents of the buffer. Only the third character
1366         // should have been modified.
1367         assertThat(bufferContentsToString(readBuffer, 0, 5)).isEqualTo("FORTE");
1368 
1369         // Make sure position and limit were updated correctly.
1370         assertThat(readBuffer.position()).isEqualTo(4);
1371         assertThat(readBuffer.limit()).isEqualTo(5);
1372 
1373         assertThat(callback.mResponseStep).isEqualTo(ResponseStep.ON_READ_COMPLETED);
1374 
1375         // One more read attempt. The request should complete.
1376         readBuffer.position(1);
1377         readBuffer.limit(5);
1378         callback.setAutoAdvance(true);
1379         callback.startNextRead(stream, readBuffer);
1380         callback.blockForDone();
1381 
1382         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
1383         assertThat(callback.mResponseAsString).isEqualTo("GET");
1384         checkResponseInfo(
1385                 callback.getResponseInfoWithChecks(), Http2TestServer.getEchoMethodUrl(), 200, "");
1386 
1387         // Check that buffer contents were not modified.
1388         assertThat(bufferContentsToString(readBuffer, 0, 5)).isEqualTo("FORTE");
1389 
1390         // Position should not have been modified, since nothing was read.
1391         assertThat(readBuffer.position()).isEqualTo(1);
1392         // Limit should be unchanged as always.
1393         assertThat(readBuffer.limit()).isEqualTo(5);
1394 
1395         assertThat(callback.mResponseStep).isEqualTo(ResponseStep.ON_SUCCEEDED);
1396 
1397         // TestRequestFinishedListener expects a single call to onRequestFinished. Here we
1398         // explicitly wait for the call to happen to avoid a race condition with the other
1399         // TestRequestFinishedListener created within runGetWithExpectedReceivedByteCount.
1400         requestFinishedListener.blockUntilDone();
1401         mCronetEngine.removeRequestFinishedListener(requestFinishedListener);
1402 
1403         // Make sure there are no other pending messages, which would trigger
1404         // asserts in TestBidirectionalCallback.
1405         // The expected received bytes count is lower than it would be for the first request on the
1406         // connection, because the server includes an HPACK dynamic table size update only in the
1407         // first response HEADERS frame.
1408         runGetWithExpectedReceivedByteCount(27);
1409     }
1410 
1411     @Test
1412     @SmallTest
testBadBuffers()1413     public void testBadBuffers() throws Exception {
1414         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
1415         callback.setAutoAdvance(false);
1416         BidirectionalStream.Builder builder =
1417                 mCronetEngine.newBidirectionalStreamBuilder(
1418                         Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor());
1419         BidirectionalStream stream = builder.setHttpMethod("GET").build();
1420         stream.start();
1421         callback.waitForNextReadStep();
1422 
1423         assertThat(callback.mError).isNull();
1424         assertThat(callback.isDone()).isFalse();
1425         assertThat(callback.mResponseStep)
1426                 .isEqualTo(TestBidirectionalStreamCallback.ResponseStep.ON_RESPONSE_STARTED);
1427 
1428         // Try to read using a full buffer.
1429         ByteBuffer readBuffer = ByteBuffer.allocateDirect(4);
1430         readBuffer.put("full".getBytes());
1431         IllegalArgumentException e =
1432                 assertThrows(IllegalArgumentException.class, () -> stream.read(readBuffer));
1433         assertThat(e).hasMessageThat().isEqualTo("ByteBuffer is already full.");
1434 
1435         // Try to read using a non-direct buffer.
1436         ByteBuffer readBuffer1 = ByteBuffer.allocate(5);
1437         e = assertThrows(IllegalArgumentException.class, () -> stream.read(readBuffer1));
1438         assertThat(e).hasMessageThat().isEqualTo("byteBuffer must be a direct ByteBuffer.");
1439 
1440         // Finish the stream with a direct ByteBuffer.
1441         callback.setAutoAdvance(true);
1442         ByteBuffer readBuffer2 = ByteBuffer.allocateDirect(5);
1443         stream.read(readBuffer2);
1444         callback.blockForDone();
1445         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
1446         assertThat(callback.mResponseAsString).isEqualTo("GET");
1447     }
1448 
throwOrCancel( FailureType failureType, ResponseStep failureStep, boolean expectError)1449     private void throwOrCancel(
1450             FailureType failureType, ResponseStep failureStep, boolean expectError) {
1451         // Use a fresh CronetEngine each time so Http2 session is not reused.
1452         ExperimentalCronetEngine.Builder builder =
1453                 new ExperimentalCronetEngine.Builder(mTestRule.getTestFramework().getContext());
1454         // TODO(crbug/1490552): Fallback to MockCertVerifier when custom CAs are not supported.
1455         if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
1456             CronetTestUtil.setMockCertVerifierForTesting(
1457                     builder, QuicTestServer.createMockCertVerifier());
1458         }
1459         mCronetEngine = builder.build();
1460         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
1461         callback.setFailure(failureType, failureStep);
1462         TestRequestFinishedListener requestFinishedListener = new TestRequestFinishedListener();
1463         mCronetEngine.addRequestFinishedListener(requestFinishedListener);
1464         BidirectionalStream.Builder streamBuilder =
1465                 mCronetEngine.newBidirectionalStreamBuilder(
1466                         Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor());
1467         BidirectionalStream stream = streamBuilder.setHttpMethod("GET").build();
1468         Date startTime = new Date();
1469         stream.start();
1470         callback.blockForDone();
1471         assertThat(stream.isDone()).isTrue();
1472         requestFinishedListener.blockUntilDone();
1473         Date endTime = new Date();
1474         RequestFinishedInfo finishedInfo = requestFinishedListener.getRequestInfo();
1475         RequestFinishedInfo.Metrics metrics = finishedInfo.getMetrics();
1476         assertThat(metrics).isNotNull();
1477         // Cancellation when stream is ready does not guarantee that
1478         // mResponseInfo is null because there might be a
1479         // onResponseHeadersReceived already queued in the executor.
1480         // See crbug.com/594432.
1481         if (failureStep != ResponseStep.ON_STREAM_READY) {
1482             assertThat(callback.getResponseInfo()).isNotNull();
1483         }
1484         // Check metrics information.
1485         if (failureStep == ResponseStep.ON_RESPONSE_STARTED
1486                 || failureStep == ResponseStep.ON_READ_COMPLETED
1487                 || failureStep == ResponseStep.ON_TRAILERS) {
1488             // For steps after response headers are received, there will be
1489             // connect timing metrics.
1490             MetricsTestUtil.checkTimingMetrics(metrics, startTime, endTime);
1491             MetricsTestUtil.checkHasConnectTiming(metrics, startTime, endTime, true);
1492             assertThat(metrics.getSentByteCount()).isGreaterThan(0L);
1493             assertThat(metrics.getReceivedByteCount()).isGreaterThan(0L);
1494         } else if (failureStep == ResponseStep.ON_STREAM_READY) {
1495             assertThat(metrics.getRequestStart()).isNotNull();
1496             MetricsTestUtil.assertAfter(metrics.getRequestStart(), startTime);
1497             assertThat(metrics.getRequestEnd()).isNotNull();
1498             MetricsTestUtil.assertAfter(endTime, metrics.getRequestEnd());
1499             MetricsTestUtil.assertAfter(metrics.getRequestEnd(), metrics.getRequestStart());
1500         }
1501         assertThat(callback.mError != null).isEqualTo(expectError);
1502         assertThat(callback.mOnErrorCalled).isEqualTo(expectError);
1503         if (expectError) {
1504             assertThat(finishedInfo.getException()).isNotNull();
1505             assertThat(finishedInfo.getFinishedReason()).isEqualTo(RequestFinishedInfo.FAILED);
1506         } else {
1507             assertThat(finishedInfo.getException()).isNull();
1508             assertThat(finishedInfo.getFinishedReason()).isEqualTo(RequestFinishedInfo.CANCELED);
1509         }
1510         assertThat(callback.mOnCanceledCalled)
1511                 .isEqualTo(
1512                         failureType == FailureType.CANCEL_SYNC
1513                                 || failureType == FailureType.CANCEL_ASYNC
1514                                 || failureType == FailureType.CANCEL_ASYNC_WITHOUT_PAUSE);
1515         mCronetEngine.removeRequestFinishedListener(requestFinishedListener);
1516     }
1517 
1518     @Test
1519     @SmallTest
testFailures()1520     public void testFailures() throws Exception {
1521         throwOrCancel(FailureType.CANCEL_SYNC, ResponseStep.ON_STREAM_READY, false);
1522         throwOrCancel(FailureType.CANCEL_ASYNC, ResponseStep.ON_STREAM_READY, false);
1523         throwOrCancel(FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, ResponseStep.ON_STREAM_READY, false);
1524         throwOrCancel(FailureType.THROW_SYNC, ResponseStep.ON_STREAM_READY, true);
1525 
1526         throwOrCancel(FailureType.CANCEL_SYNC, ResponseStep.ON_RESPONSE_STARTED, false);
1527         throwOrCancel(FailureType.CANCEL_ASYNC, ResponseStep.ON_RESPONSE_STARTED, false);
1528         throwOrCancel(
1529                 FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, ResponseStep.ON_RESPONSE_STARTED, false);
1530         throwOrCancel(FailureType.THROW_SYNC, ResponseStep.ON_RESPONSE_STARTED, true);
1531 
1532         throwOrCancel(FailureType.CANCEL_SYNC, ResponseStep.ON_READ_COMPLETED, false);
1533         throwOrCancel(FailureType.CANCEL_ASYNC, ResponseStep.ON_READ_COMPLETED, false);
1534         throwOrCancel(
1535                 FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, ResponseStep.ON_READ_COMPLETED, false);
1536         throwOrCancel(FailureType.THROW_SYNC, ResponseStep.ON_READ_COMPLETED, true);
1537     }
1538 
1539     @Test
1540     @SmallTest
testThrowOnSucceeded()1541     public void testThrowOnSucceeded() {
1542         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
1543         callback.setFailure(FailureType.THROW_SYNC, ResponseStep.ON_SUCCEEDED);
1544         BidirectionalStream.Builder builder =
1545                 mCronetEngine.newBidirectionalStreamBuilder(
1546                         Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor());
1547         BidirectionalStream stream = builder.setHttpMethod("GET").build();
1548         stream.start();
1549         callback.blockForDone();
1550         assertThat(ResponseStep.ON_SUCCEEDED).isEqualTo(callback.mResponseStep);
1551         assertThat(stream.isDone()).isTrue();
1552         assertThat(callback.getResponseInfoWithChecks()).isNotNull();
1553         // Check that error thrown from 'onSucceeded' callback is not reported.
1554         assertThat(callback.mError).isNull();
1555         assertThat(callback.mOnErrorCalled).isFalse();
1556     }
1557 
1558     @Test
1559     @SmallTest
1560     @IgnoreFor(
1561             implementations = {CronetImplementation.AOSP_PLATFORM},
1562             reason = "crbug.com/1494845: Requires access to internals not available in AOSP")
testExecutorShutdownBeforeStreamIsDone()1563     public void testExecutorShutdownBeforeStreamIsDone() {
1564         // Test that stream is destroyed even if executor is shut down and rejects posting tasks.
1565         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
1566         callback.setAutoAdvance(false);
1567         BidirectionalStream.Builder builder =
1568                 mCronetEngine.newBidirectionalStreamBuilder(
1569                         Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor());
1570         CronetBidirectionalStream stream =
1571                 (CronetBidirectionalStream) builder.setHttpMethod("GET").build();
1572         stream.start();
1573         callback.waitForNextReadStep();
1574         assertThat(callback.isDone()).isFalse();
1575         assertThat(stream.isDone()).isFalse();
1576 
1577         final ConditionVariable streamDestroyed = new ConditionVariable(false);
1578         stream.setOnDestroyedCallbackForTesting(
1579                 new Runnable() {
1580                     @Override
1581                     public void run() {
1582                         streamDestroyed.open();
1583                     }
1584                 });
1585 
1586         // Shut down the executor, so posting the task will throw an exception.
1587         callback.shutdownExecutor();
1588         ByteBuffer readBuffer = ByteBuffer.allocateDirect(5);
1589         stream.read(readBuffer);
1590         // Callback will never be called again because executor is shut down,
1591         // but stream will be destroyed from network thread.
1592         streamDestroyed.block();
1593 
1594         assertThat(callback.isDone()).isFalse();
1595         assertThat(stream.isDone()).isTrue();
1596     }
1597 
1598     @Test
1599     @SmallTest
1600     @IgnoreFor(
1601             implementations = {CronetImplementation.AOSP_PLATFORM},
1602             reason = "ActiveRequestCount is not available in AOSP")
testCronetEngineShutdown()1603     public void testCronetEngineShutdown() throws Exception {
1604         // Test that CronetEngine cannot be shut down if there are any active streams.
1605         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
1606         // Block callback when response starts to verify that shutdown fails
1607         // if there are active streams.
1608         callback.setAutoAdvance(false);
1609         BidirectionalStream.Builder builder =
1610                 mCronetEngine.newBidirectionalStreamBuilder(
1611                         Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor());
1612         BidirectionalStream stream = builder.setHttpMethod("GET").build();
1613         stream.start();
1614         Exception e = assertThrows(Exception.class, mCronetEngine::shutdown);
1615         assertThat(e).hasMessageThat().matches("Cannot shutdown with (running|active) requests.");
1616 
1617         callback.waitForNextReadStep();
1618         assertThat(callback.mResponseStep).isEqualTo(ResponseStep.ON_RESPONSE_STARTED);
1619         e = assertThrows(Exception.class, mCronetEngine::shutdown);
1620         assertThat(e).hasMessageThat().matches("Cannot shutdown with (running|active) requests.");
1621         callback.startNextRead(stream);
1622 
1623         callback.waitForNextReadStep();
1624         assertThat(callback.mResponseStep).isEqualTo(ResponseStep.ON_READ_COMPLETED);
1625         e = assertThrows(Exception.class, mCronetEngine::shutdown);
1626         assertThat(e).hasMessageThat().matches("Cannot shutdown with (running|active) requests.");
1627 
1628         // May not have read all the data, in theory. Just enable auto-advance
1629         // and finish the request.
1630         callback.setAutoAdvance(true);
1631         callback.startNextRead(stream);
1632         callback.blockForDone();
1633         waitForActiveRequestCount(0);
1634         mCronetEngine.shutdown();
1635     }
1636 
1637     @Test
1638     @SmallTest
1639     @IgnoreFor(
1640             implementations = {CronetImplementation.AOSP_PLATFORM},
1641             reason = "ActiveRequestCount is not available in AOSP")
testCronetEngineShutdownAfterStreamFailure()1642     public void testCronetEngineShutdownAfterStreamFailure() throws Exception {
1643         // Test that CronetEngine can be shut down after stream reports a failure.
1644         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
1645         BidirectionalStream.Builder builder =
1646                 mCronetEngine.newBidirectionalStreamBuilder(
1647                         Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor());
1648         BidirectionalStream stream = builder.setHttpMethod("GET").build();
1649         stream.start();
1650         callback.setFailure(FailureType.THROW_SYNC, ResponseStep.ON_READ_COMPLETED);
1651         callback.blockForDone();
1652         assertThat(callback.mOnErrorCalled).isTrue();
1653         waitForActiveRequestCount(0);
1654         mCronetEngine.shutdown();
1655     }
1656 
1657     @Test
1658     @SmallTest
1659     @IgnoreFor(
1660             implementations = {CronetImplementation.AOSP_PLATFORM},
1661             reason = "ActiveRequestCount is not available in AOSP")
testCronetEngineShutdownAfterStreamCancel()1662     public void testCronetEngineShutdownAfterStreamCancel() throws Exception {
1663         // Test that CronetEngine can be shut down after stream is canceled.
1664         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
1665         BidirectionalStream.Builder builder =
1666                 mCronetEngine.newBidirectionalStreamBuilder(
1667                         Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor());
1668         BidirectionalStream stream = builder.setHttpMethod("GET").build();
1669 
1670         // Block callback when response starts to verify that shutdown fails
1671         // if there are active requests.
1672         callback.setAutoAdvance(false);
1673         stream.start();
1674         Exception e = assertThrows(Exception.class, mCronetEngine::shutdown);
1675         assertThat(e).hasMessageThat().matches("Cannot shutdown with (running|active) requests.");
1676         callback.waitForNextReadStep();
1677         assertThat(callback.mResponseStep).isEqualTo(ResponseStep.ON_RESPONSE_STARTED);
1678         stream.cancel();
1679         callback.blockForDone();
1680         assertThat(callback.mOnCanceledCalled).isTrue();
1681         waitForActiveRequestCount(0);
1682         mCronetEngine.shutdown();
1683     }
1684 
1685     /*
1686      * Verifies NetworkException constructed from specific error codes are retryable.
1687      */
1688     @SmallTest
1689     @Test
testErrorCodes()1690     public void testErrorCodes() throws Exception {
1691         // Non-BidirectionalStream specific error codes.
1692         checkSpecificErrorCode(
1693                 NetError.ERR_NAME_NOT_RESOLVED,
1694                 NetworkException.ERROR_HOSTNAME_NOT_RESOLVED,
1695                 false);
1696         checkSpecificErrorCode(
1697                 NetError.ERR_INTERNET_DISCONNECTED,
1698                 NetworkException.ERROR_INTERNET_DISCONNECTED,
1699                 false);
1700         checkSpecificErrorCode(
1701                 NetError.ERR_NETWORK_CHANGED, NetworkException.ERROR_NETWORK_CHANGED, true);
1702         checkSpecificErrorCode(
1703                 NetError.ERR_CONNECTION_CLOSED, NetworkException.ERROR_CONNECTION_CLOSED, true);
1704         checkSpecificErrorCode(
1705                 NetError.ERR_CONNECTION_REFUSED, NetworkException.ERROR_CONNECTION_REFUSED, false);
1706         checkSpecificErrorCode(
1707                 NetError.ERR_CONNECTION_RESET, NetworkException.ERROR_CONNECTION_RESET, true);
1708         checkSpecificErrorCode(
1709                 NetError.ERR_CONNECTION_TIMED_OUT,
1710                 NetworkException.ERROR_CONNECTION_TIMED_OUT,
1711                 true);
1712         checkSpecificErrorCode(NetError.ERR_TIMED_OUT, NetworkException.ERROR_TIMED_OUT, true);
1713         checkSpecificErrorCode(
1714                 NetError.ERR_ADDRESS_UNREACHABLE,
1715                 NetworkException.ERROR_ADDRESS_UNREACHABLE,
1716                 false);
1717         // BidirectionalStream specific retryable error codes.
1718         checkSpecificErrorCode(NetError.ERR_HTTP2_PING_FAILED, NetworkException.ERROR_OTHER, true);
1719         checkSpecificErrorCode(
1720                 NetError.ERR_QUIC_HANDSHAKE_FAILED, NetworkException.ERROR_OTHER, true);
1721     }
1722 
1723     // Returns the contents of byteBuffer, from its position() to its limit(),
1724     // as a String. Does not modify byteBuffer's position().
bufferContentsToString(ByteBuffer byteBuffer, int start, int end)1725     private static String bufferContentsToString(ByteBuffer byteBuffer, int start, int end) {
1726         // Use a duplicate to avoid modifying byteBuffer.
1727         ByteBuffer duplicate = byteBuffer.duplicate();
1728         duplicate.position(start);
1729         duplicate.limit(end);
1730         byte[] contents = new byte[duplicate.remaining()];
1731         duplicate.get(contents);
1732         return new String(contents);
1733     }
1734 
checkSpecificErrorCode( int netError, int errorCode, boolean immediatelyRetryable)1735     private static void checkSpecificErrorCode(
1736             int netError, int errorCode, boolean immediatelyRetryable) throws Exception {
1737         NetworkException exception =
1738                 new BidirectionalStreamNetworkException("", errorCode, netError);
1739         assertThat(exception.immediatelyRetryable()).isEqualTo(immediatelyRetryable);
1740         assertThat(exception.getCronetInternalErrorCode()).isEqualTo(netError);
1741         assertThat(exception.getErrorCode()).isEqualTo(errorCode);
1742     }
1743 
1744     @Test
1745     @SmallTest
1746     @RequiresMinApi(10) // Tagging support added in API level 10: crrev.com/c/chromium/src/+/937583
1747     @RequiresMinAndroidApi(Build.VERSION_CODES.M) // crbug/1301957
testTagging()1748     public void testTagging() throws Exception {
1749         if (!CronetTestUtil.nativeCanGetTaggedBytes()) {
1750             Log.i(TAG, "Skipping test - GetTaggedBytes unsupported.");
1751             return;
1752         }
1753         String url = Http2TestServer.getEchoStreamUrl();
1754 
1755         // Test untagged requests are given tag 0.
1756         int tag = 0;
1757         long priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag);
1758         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
1759         callback.addWriteData(new byte[] {0});
1760         mCronetEngine
1761                 .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
1762                 .build()
1763                 .start();
1764         callback.blockForDone();
1765         assertThat(CronetTestUtil.nativeGetTaggedBytes(tag)).isGreaterThan(priorBytes);
1766 
1767         // Test explicit tagging.
1768         tag = 0x12345678;
1769         priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag);
1770         callback = new TestBidirectionalStreamCallback();
1771         callback.addWriteData(new byte[] {0});
1772         ExperimentalBidirectionalStream.Builder builder =
1773                 mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor());
1774         assertThat(builder).isEqualTo(builder.setTrafficStatsTag(tag));
1775         builder.build().start();
1776         callback.blockForDone();
1777         assertThat(CronetTestUtil.nativeGetTaggedBytes(tag)).isGreaterThan(priorBytes);
1778 
1779         // Test a different tag value to make sure reused connections are retagged.
1780         tag = 0x87654321;
1781         priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag);
1782         callback = new TestBidirectionalStreamCallback();
1783         callback.addWriteData(new byte[] {0});
1784         builder =
1785                 mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor());
1786         assertThat(builder).isEqualTo(builder.setTrafficStatsTag(tag));
1787         builder.build().start();
1788         callback.blockForDone();
1789         assertThat(CronetTestUtil.nativeGetTaggedBytes(tag)).isGreaterThan(priorBytes);
1790 
1791         // Test tagging with our UID.
1792         tag = 0;
1793         priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag);
1794         callback = new TestBidirectionalStreamCallback();
1795         callback.addWriteData(new byte[] {0});
1796         builder =
1797                 mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor());
1798         assertThat(builder).isEqualTo(builder.setTrafficStatsUid(Process.myUid()));
1799         builder.build().start();
1800         callback.blockForDone();
1801         assertThat(CronetTestUtil.nativeGetTaggedBytes(tag)).isGreaterThan(priorBytes);
1802     }
1803 
1804     @Test
1805     @RequiresMinAndroidApi(Build.VERSION_CODES.M)
testBindToInvalidNetworkFails()1806     public void testBindToInvalidNetworkFails() {
1807         String url = Http2TestServer.getEchoMethodUrl();
1808         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
1809 
1810         BidirectionalStream.Builder builder =
1811                 mCronetEngine
1812                         .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
1813                         .setHttpMethod("GET");
1814 
1815         if (mTestRule.implementationUnderTest() == CronetImplementation.AOSP_PLATFORM) {
1816             // android.net.http.UrlRequestBuilder#bindToNetwork requires an android.net.Network
1817             // object. So, in this case, it will be the wrapper layer that will fail to translate
1818             // that to a Network, not something in net's code. Hence, the failure will manifest
1819             // itself at bind time, not at request execution time.
1820             // Note: this will never happen in prod, as translation failure can only happen if we're
1821             // given a fake networkHandle.
1822             assertThrows(
1823                     IllegalArgumentException.class,
1824                     () -> builder.bindToNetwork(-150 /* invalid network handle */));
1825             return;
1826         }
1827 
1828         builder.bindToNetwork(-150 /* invalid network handle */);
1829         BidirectionalStream stream = builder.build();
1830         stream.start();
1831 
1832         callback.blockForDone();
1833 
1834         assertThat(callback.mError).isNotNull();
1835         if (mTestRule.implementationUnderTest() == CronetImplementation.FALLBACK) {
1836             assertThat(callback.mError).isInstanceOf(CronetExceptionImpl.class);
1837             assertThat(callback.mError).hasCauseThat().isInstanceOf(NetworkExceptionImpl.class);
1838         } else {
1839             assertThat(callback.mError).isInstanceOf(NetworkExceptionImpl.class);
1840         }
1841     }
1842 
1843     @Test
1844     @RequiresMinAndroidApi(Build.VERSION_CODES.M)
testBindToDefaultNetworkSucceeds()1845     public void testBindToDefaultNetworkSucceeds() {
1846         ConnectivityManagerDelegate delegate =
1847                 new ConnectivityManagerDelegate(mTestRule.getTestFramework().getContext());
1848         Network defaultNetwork = delegate.getDefaultNetwork();
1849         assume().that(defaultNetwork).isNotNull();
1850 
1851         String url = Http2TestServer.getEchoMethodUrl();
1852         TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
1853 
1854         BidirectionalStream.Builder builder =
1855                 mCronetEngine
1856                         .newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
1857                         .setHttpMethod("GET");
1858 
1859         builder.bindToNetwork(defaultNetwork.getNetworkHandle());
1860         builder.build().start();
1861         callback.blockForDone();
1862         assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
1863     }
1864 
1865     /**
1866      * Cronet does not currently provide an API to wait for the active request count to change. We
1867      * can't just wait for the terminal callback to fire because Cronet updates the count some time
1868      * *after* we return from the callback. We hack around this by polling the active request count
1869      * in a loop.
1870      */
waitForActiveRequestCount(int expectedCount)1871     private void waitForActiveRequestCount(int expectedCount) throws Exception {
1872         while (mCronetEngine.getActiveRequestCount() != expectedCount) Thread.sleep(100);
1873     }
1874 }
1875