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