blob: 14dc571f01abe1d1885f3a29e8b2f3d569eba869 [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.net;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static org.junit.Assert.assertThrows;
import static org.chromium.net.truth.UrlResponseInfoSubject.assertThat;
import android.net.Network;
import android.os.Build;
import android.os.ConditionVariable;
import android.os.Process;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.Log;
import org.chromium.base.test.util.DoNotBatch;
import org.chromium.net.CronetTestRule.CronetImplementation;
import org.chromium.net.CronetTestRule.IgnoreFor;
import org.chromium.net.CronetTestRule.RequiresMinAndroidApi;
import org.chromium.net.CronetTestRule.RequiresMinApi;
import org.chromium.net.NetworkChangeNotifierAutoDetect.ConnectivityManagerDelegate;
import org.chromium.net.TestBidirectionalStreamCallback.FailureType;
import org.chromium.net.TestBidirectionalStreamCallback.ResponseStep;
import org.chromium.net.impl.BidirectionalStreamNetworkException;
import org.chromium.net.impl.CronetBidirectionalStream;
import org.chromium.net.impl.CronetExceptionImpl;
import org.chromium.net.impl.NetworkExceptionImpl;
import org.chromium.net.impl.UrlResponseInfoImpl;
import java.nio.ByteBuffer;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Test functionality of BidirectionalStream interface. */
@DoNotBatch(reason = "crbug/1459563")
@RunWith(AndroidJUnit4.class)
@IgnoreFor(
implementations = {CronetImplementation.FALLBACK},
reason = "The fallback implementation doesn't support bidirectional streaming")
public class BidirectionalStreamTest {
private static final String TAG = BidirectionalStreamTest.class.getSimpleName();
@Rule public final CronetTestRule mTestRule = CronetTestRule.withManualEngineStartup();
private ExperimentalCronetEngine mCronetEngine;
@Before
public void setUp() throws Exception {
// TODO(crbug/1490552): Fallback to MockCertVerifier when custom CAs are not supported.
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
mTestRule
.getTestFramework()
.applyEngineBuilderPatch(
(builder) ->
CronetTestUtil.setMockCertVerifierForTesting(
builder, QuicTestServer.createMockCertVerifier()));
}
mCronetEngine = mTestRule.getTestFramework().startEngine();
assertThat(Http2TestServer.startHttp2TestServer(mTestRule.getTestFramework().getContext()))
.isTrue();
}
@After
public void tearDown() throws Exception {
assertThat(Http2TestServer.shutdownHttp2TestServer()).isTrue();
}
private static void checkResponseInfo(
UrlResponseInfo responseInfo,
String expectedUrl,
int expectedHttpStatusCode,
String expectedHttpStatusText) {
assertThat(responseInfo).hasUrlThat().isEqualTo(expectedUrl);
assertThat(responseInfo).hasUrlChainThat().containsExactly(expectedUrl);
assertThat(responseInfo).hasHttpStatusCodeThat().isEqualTo(expectedHttpStatusCode);
assertThat(responseInfo).hasHttpStatusTextThat().isEqualTo(expectedHttpStatusText);
assertThat(responseInfo).wasNotCached();
assertThat(responseInfo.toString()).isNotEmpty();
}
private static String createLongString(String base, int repetition) {
StringBuilder builder = new StringBuilder(base.length() * repetition);
for (int i = 0; i < repetition; ++i) {
builder.append(i);
builder.append(base);
}
return builder.toString();
}
private static UrlResponseInfo createUrlResponseInfo(
String[] urls, String message, int statusCode, int receivedBytes, String... headers) {
ArrayList<Map.Entry<String, String>> headersList = new ArrayList<>();
for (int i = 0; i < headers.length; i += 2) {
headersList.add(
new AbstractMap.SimpleImmutableEntry<String, String>(
headers[i], headers[i + 1]));
}
UrlResponseInfoImpl urlResponseInfo =
new UrlResponseInfoImpl(
Arrays.asList(urls),
statusCode,
message,
headersList,
false,
"h2",
null,
receivedBytes);
return urlResponseInfo;
}
private void runGetWithExpectedReceivedByteCount(int expectedReceivedBytes) throws Exception {
String url = Http2TestServer.getEchoMethodUrl();
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
TestRequestFinishedListener requestFinishedListener = new TestRequestFinishedListener();
mCronetEngine.addRequestFinishedListener(requestFinishedListener);
// Create stream.
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.setHttpMethod("GET")
.build();
stream.start();
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
requestFinishedListener.blockUntilDone();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
// Default method is 'GET'.
assertThat(callback.mResponseAsString).isEqualTo("GET");
UrlResponseInfo urlResponseInfo =
createUrlResponseInfo(
new String[] {url}, "", 200, expectedReceivedBytes, ":status", "200");
mTestRule.assertResponseEquals(urlResponseInfo, callback.getResponseInfoWithChecks());
checkResponseInfo(
callback.getResponseInfoWithChecks(), Http2TestServer.getEchoMethodUrl(), 200, "");
RequestFinishedInfo finishedInfo = requestFinishedListener.getRequestInfo();
assertThat(finishedInfo.getAnnotations()).isEmpty();
}
@Test
@SmallTest
public void testBuilderCheck() throws Exception {
ExperimentalCronetEngine engine = mTestRule.getTestFramework().getEngine();
if (mTestRule.testingJavaImpl()) {
runBuilderCheckJavaImpl(engine);
} else {
runBuilderCheckNativeImpl(engine);
}
}
private static void runBuilderCheckNativeImpl(ExperimentalCronetEngine engine)
throws Exception {
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
NullPointerException e =
assertThrows(
NullPointerException.class,
() ->
engine.newBidirectionalStreamBuilder(
null, callback, callback.getExecutor()));
assertThat(e).hasMessageThat().isEqualTo("URL is required.");
e =
assertThrows(
NullPointerException.class,
() ->
engine.newBidirectionalStreamBuilder(
Http2TestServer.getServerUrl(),
null,
callback.getExecutor()));
assertThat(e).hasMessageThat().isEqualTo("Callback is required.");
e =
assertThrows(
NullPointerException.class,
() ->
engine.newBidirectionalStreamBuilder(
Http2TestServer.getServerUrl(), callback, null));
assertThat(e).hasMessageThat().isEqualTo("Executor is required.");
// Verify successful creation doesn't throw.
BidirectionalStream.Builder builder =
engine.newBidirectionalStreamBuilder(
Http2TestServer.getServerUrl(), callback, callback.getExecutor());
e = assertThrows(NullPointerException.class, () -> builder.addHeader(null, "value"));
assertThat(e).hasMessageThat().isEqualTo("Invalid header name.");
e = assertThrows(NullPointerException.class, () -> builder.addHeader("name", null));
assertThat(e).hasMessageThat().isEqualTo("Invalid header value.");
e = assertThrows(NullPointerException.class, () -> builder.setHttpMethod(null));
assertThat(e).hasMessageThat().isEqualTo("Method is required.");
}
private void runBuilderCheckJavaImpl(ExperimentalCronetEngine engine) {
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
assertThrows(
"JavaCronetEngine doesn't support BidirectionalStream.",
UnsupportedOperationException.class,
() ->
engine.newBidirectionalStreamBuilder(
Http2TestServer.getServerUrl(), callback, callback.getExecutor()));
}
@Test
@SmallTest
public void testFailPlainHttp() throws Exception {
String url = "http://example.com";
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
// Create stream.
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.build();
stream.start();
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
assertThat(callback.mError)
.hasMessageThat()
.contains("Exception in BidirectionalStream: net::ERR_DISALLOWED_URL_SCHEME");
mTestRule.assertCronetInternalErrorCode((NetworkException) callback.mError, -301);
}
@Test
@SmallTest
public void testSimpleGet() throws Exception {
// Since this is the first request on the connection, the expected received bytes count
// must account for an HPACK dynamic table size update.
int expectedReceivedBytes = 31;
String url = Http2TestServer.getEchoMethodUrl();
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
// Create stream.
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.setHttpMethod("GET")
.build();
stream.start();
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
// Default method is 'GET'.
assertThat(callback.mResponseAsString).isEqualTo("GET");
UrlResponseInfo urlResponseInfo =
createUrlResponseInfo(
new String[] {url}, "", 200, expectedReceivedBytes, ":status", "200");
mTestRule.assertResponseEquals(urlResponseInfo, callback.getResponseInfoWithChecks());
checkResponseInfo(
callback.getResponseInfoWithChecks(), Http2TestServer.getEchoMethodUrl(), 200, "");
}
@Test
@SmallTest
public void testSimpleHead() throws Exception {
String url = Http2TestServer.getEchoMethodUrl();
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
// Create stream.
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.setHttpMethod("HEAD")
.build();
stream.start();
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEqualTo("HEAD");
UrlResponseInfo urlResponseInfo =
createUrlResponseInfo(new String[] {url}, "", 200, 32, ":status", "200");
mTestRule.assertResponseEquals(urlResponseInfo, callback.getResponseInfoWithChecks());
checkResponseInfo(
callback.getResponseInfoWithChecks(), Http2TestServer.getEchoMethodUrl(), 200, "");
}
@Test
@SmallTest
public void testSimplePost() throws Exception {
String url = Http2TestServer.getEchoStreamUrl();
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
callback.addWriteData("Test String".getBytes());
callback.addWriteData("1234567890".getBytes());
callback.addWriteData("woot!".getBytes());
// Create stream.
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.addHeader("foo", "bar")
.addHeader("empty", "")
.addHeader("Content-Type", "zebra")
.addRequestAnnotation(this)
.addRequestAnnotation("request annotation")
.build();
stream.start();
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEqualTo("Test String1234567890woot!");
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-foo", Arrays.asList("bar"));
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-empty", Arrays.asList(""));
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-content-type", Arrays.asList("zebra"));
}
@Test
@SmallTest
@IgnoreFor(
implementations = {CronetImplementation.AOSP_PLATFORM},
reason = "RequedFinishedListener is not available in AOSP")
public void testPostWithFinishedListener() throws Exception {
String url = Http2TestServer.getEchoStreamUrl();
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
callback.addWriteData("Test String".getBytes());
callback.addWriteData("1234567890".getBytes());
callback.addWriteData("woot!".getBytes());
TestRequestFinishedListener requestFinishedListener = new TestRequestFinishedListener();
mCronetEngine.addRequestFinishedListener(requestFinishedListener);
// Create stream.
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.addHeader("foo", "bar")
.addHeader("empty", "")
.addHeader("Content-Type", "zebra")
.addRequestAnnotation(this)
.addRequestAnnotation("request annotation")
.build();
Date startTime = new Date();
stream.start();
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
requestFinishedListener.blockUntilDone();
Date endTime = new Date();
RequestFinishedInfo finishedInfo = requestFinishedListener.getRequestInfo();
MetricsTestUtil.checkRequestFinishedInfo(finishedInfo, url, startTime, endTime);
assertThat(finishedInfo.getFinishedReason()).isEqualTo(RequestFinishedInfo.SUCCEEDED);
MetricsTestUtil.checkHasConnectTiming(finishedInfo.getMetrics(), startTime, endTime, true);
assertThat(finishedInfo.getAnnotations()).containsExactly("request annotation", this);
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEqualTo("Test String1234567890woot!");
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-foo", Arrays.asList("bar"));
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-empty", Arrays.asList(""));
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-content-type", Arrays.asList("zebra"));
}
@Test
@SmallTest
@IgnoreFor(
implementations = {CronetImplementation.AOSP_PLATFORM},
reason = "ActiveRequestCount is not available in AOSP")
public void testGetActiveRequestCount() throws Exception {
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
callback.addWriteData("Test String".getBytes());
callback.setBlockOnTerminalState(true);
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(
Http2TestServer.getEchoStreamUrl(),
callback,
callback.getExecutor())
.build();
assertThat(mCronetEngine.getActiveRequestCount()).isEqualTo(0);
stream.start();
callback.blockForDone();
assertThat(mCronetEngine.getActiveRequestCount()).isEqualTo(1);
callback.setBlockOnTerminalState(false);
waitForActiveRequestCount(0);
}
@Test
@SmallTest
@IgnoreFor(
implementations = {CronetImplementation.AOSP_PLATFORM},
reason = "ActiveRequestCount is not available in AOSP")
public void testGetActiveRequestCountWithInvalidRequest() throws Exception {
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(
Http2TestServer.getEchoStreamUrl(),
callback,
callback.getExecutor())
.addHeader("", "") // Deliberately invalid
.build();
assertThat(mCronetEngine.getActiveRequestCount()).isEqualTo(0);
assertThrows(IllegalArgumentException.class, stream::start);
assertThat(mCronetEngine.getActiveRequestCount()).isEqualTo(0);
}
@Test
@SmallTest
public void testSimpleGetWithCombinedHeader() throws Exception {
String url = Http2TestServer.getCombinedHeadersUrl();
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
// Create stream.
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.setHttpMethod("GET")
.build();
stream.start();
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
// Default method is 'GET'.
assertThat(callback.mResponseAsString).isEqualTo("GET");
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("foo", Arrays.asList("bar", "bar2"));
}
@Test
@SmallTest
public void testSimplePostWithFlush() throws Exception {
String url = Http2TestServer.getEchoStreamUrl();
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
callback.addWriteData("Test String".getBytes(), false);
callback.addWriteData("1234567890".getBytes(), false);
callback.addWriteData("woot!".getBytes(), true);
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.addHeader("foo", "bar")
.addHeader("empty", "")
.addHeader("Content-Type", "zebra")
.build();
// Flush before stream is started should not crash.
stream.flush();
stream.start();
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
// Flush after stream is completed is no-op. It shouldn't call into the destroyed adapter.
stream.flush();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEqualTo("Test String1234567890woot!");
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-foo", Arrays.asList("bar"));
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-empty", Arrays.asList(""));
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-content-type", Arrays.asList("zebra"));
}
@Test
@SmallTest
@IgnoreFor(
implementations = {CronetImplementation.AOSP_PLATFORM},
reason = "crbug.com/1494845: Requires access to internals not available in AOSP")
// Tests that a delayed flush() only sends buffers that have been written
// before it is called, and it doesn't flush buffers in mPendingQueue.
public void testFlushData() throws Exception {
String url = Http2TestServer.getEchoStreamUrl();
final ConditionVariable waitOnStreamReady = new ConditionVariable();
TestBidirectionalStreamCallback callback =
new TestBidirectionalStreamCallback() {
// Number of onWriteCompleted callbacks that have been invoked.
private int mNumWriteCompleted;
@Override
public void onStreamReady(BidirectionalStream stream) {
mResponseStep = ResponseStep.ON_STREAM_READY;
waitOnStreamReady.open();
}
@Override
public void onWriteCompleted(
BidirectionalStream stream,
UrlResponseInfo info,
ByteBuffer buffer,
boolean endOfStream) {
super.onWriteCompleted(stream, info, buffer, endOfStream);
mNumWriteCompleted++;
if (mNumWriteCompleted <= 3) {
// "6" is in pending queue.
List<ByteBuffer> pendingData =
((CronetBidirectionalStream) stream).getPendingDataForTesting();
assertThat(pendingData).hasSize(1);
ByteBuffer pendingBuffer = pendingData.get(0);
byte[] content = new byte[pendingBuffer.remaining()];
pendingBuffer.get(content);
assertThat(content).isEqualTo("6".getBytes());
// "4" and "5" have been flushed.
assertThat(
((CronetBidirectionalStream) stream)
.getFlushDataForTesting())
.isEmpty();
} else if (mNumWriteCompleted == 5) {
// Now flush "6", which is still in pending queue.
List<ByteBuffer> pendingData =
((CronetBidirectionalStream) stream).getPendingDataForTesting();
assertThat(pendingData).hasSize(1);
ByteBuffer pendingBuffer = pendingData.get(0);
byte[] content = new byte[pendingBuffer.remaining()];
pendingBuffer.get(content);
assertThat(content).isEqualTo("6".getBytes());
stream.flush();
assertThat(
((CronetBidirectionalStream) stream)
.getPendingDataForTesting())
.isEmpty();
assertThat(
((CronetBidirectionalStream) stream)
.getFlushDataForTesting())
.isEmpty();
}
}
};
callback.addWriteData("1".getBytes(), false);
callback.addWriteData("2".getBytes(), false);
callback.addWriteData("3".getBytes(), true);
callback.addWriteData("4".getBytes(), false);
callback.addWriteData("5".getBytes(), true);
callback.addWriteData("6".getBytes(), false);
CronetBidirectionalStream stream =
(CronetBidirectionalStream)
mCronetEngine
.newBidirectionalStreamBuilder(
url, callback, callback.getExecutor())
.addHeader("foo", "bar")
.addHeader("empty", "")
.addHeader("Content-Type", "zebra")
.build();
stream.start();
waitOnStreamReady.block();
assertThat(stream.getPendingDataForTesting()).isEmpty();
assertThat(stream.getFlushDataForTesting()).isEmpty();
// Write 1, 2, 3 and flush().
callback.startNextWrite(stream);
// Write 4, 5 and flush(). 4, 5 will be in flush queue.
callback.startNextWrite(stream);
// Write 6, but do not flush. 6 will be in pending queue.
callback.startNextWrite(stream);
callback.blockForDone();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEqualTo("123456");
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-foo", Arrays.asList("bar"));
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-empty", Arrays.asList(""));
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-content-type", Arrays.asList("zebra"));
}
@Test
@SmallTest
// Regression test for crbug.com/692168.
public void testCancelWhileWriteDataPending() throws Exception {
String url = Http2TestServer.getEchoStreamUrl();
// Use a direct executor to avoid race.
TestBidirectionalStreamCallback callback =
new TestBidirectionalStreamCallback(/* useDirectExecutor= */ true) {
@Override
public void onStreamReady(BidirectionalStream stream) {
// Start the first write.
stream.write(getSampleData(), false);
stream.flush();
}
@Override
public void onReadCompleted(
BidirectionalStream stream,
UrlResponseInfo info,
ByteBuffer byteBuffer,
boolean endOfStream) {
super.onReadCompleted(stream, info, byteBuffer, endOfStream);
// Cancel now when the write side is busy.
stream.cancel();
}
@Override
public void onWriteCompleted(
BidirectionalStream stream,
UrlResponseInfo info,
ByteBuffer buffer,
boolean endOfStream) {
// Flush twice to keep the flush queue non-empty.
stream.write(getSampleData(), false);
stream.flush();
stream.write(getSampleData(), false);
stream.flush();
}
// Returns a piece of sample data to send to the server.
private ByteBuffer getSampleData() {
byte[] data = new byte[100];
for (int i = 0; i < data.length; i++) {
data[i] = 'x';
}
ByteBuffer sampleData = ByteBuffer.allocateDirect(data.length);
sampleData.put(data);
sampleData.flip();
return sampleData;
}
};
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.build();
stream.start();
callback.blockForDone();
assertThat(callback.mOnCanceledCalled).isTrue();
}
@Test
@SmallTest
public void testSimpleGetWithFlush() throws Exception {
// TODO(xunjieli): Use ParameterizedTest instead of the loop.
for (int i = 0; i < 2; i++) {
String url = Http2TestServer.getEchoStreamUrl();
TestBidirectionalStreamCallback callback =
new TestBidirectionalStreamCallback() {
@Override
public void onStreamReady(BidirectionalStream stream) {
// Attempt to write data for GET request.
assertThrows(
IllegalArgumentException.class,
() -> stream.write(ByteBuffer.wrap("sample".getBytes()), true));
// If there are delayed headers, this flush should try to send them.
// If nothing to flush, it should not crash.
stream.flush();
super.onStreamReady(stream);
// Attempt to write data for GET request.
assertThrows(
IllegalArgumentException.class,
() -> stream.write(ByteBuffer.wrap("sample".getBytes()), true));
}
};
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.setHttpMethod("GET")
.delayRequestHeadersUntilFirstFlush(i == 0)
.addHeader("foo", "bar")
.addHeader("empty", "")
.build();
// Flush before stream is started should not crash.
stream.flush();
stream.start();
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
// Flush after stream is completed is no-op. It shouldn't call into the destroyed
// adapter.
stream.flush();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEmpty();
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-foo", Arrays.asList("bar"));
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-empty", Arrays.asList(""));
}
}
@Test
@SmallTest
public void testSimplePostWithFlushAfterOneWrite() throws Exception {
// TODO(xunjieli): Use ParameterizedTest instead of the loop.
for (int i = 0; i < 2; i++) {
String url = Http2TestServer.getEchoStreamUrl();
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
callback.addWriteData("Test String".getBytes(), true);
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.delayRequestHeadersUntilFirstFlush(i == 0)
.addHeader("foo", "bar")
.addHeader("empty", "")
.addHeader("Content-Type", "zebra")
.build();
stream.start();
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEqualTo("Test String");
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-foo", Arrays.asList("bar"));
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-empty", Arrays.asList(""));
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-content-type", Arrays.asList("zebra"));
}
}
@Test
@SmallTest
public void testSimplePostWithFlushTwice() throws Exception {
// TODO(xunjieli): Use ParameterizedTest instead of the loop.
for (int i = 0; i < 2; i++) {
String url = Http2TestServer.getEchoStreamUrl();
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
callback.addWriteData("Test String".getBytes(), false);
callback.addWriteData("1234567890".getBytes(), false);
callback.addWriteData("woot!".getBytes(), true);
callback.addWriteData("Test String".getBytes(), false);
callback.addWriteData("1234567890".getBytes(), false);
callback.addWriteData("woot!".getBytes(), true);
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.delayRequestHeadersUntilFirstFlush(i == 0)
.addHeader("foo", "bar")
.addHeader("empty", "")
.addHeader("Content-Type", "zebra")
.build();
stream.start();
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString)
.isEqualTo("Test String1234567890woot!Test String1234567890woot!");
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-foo", Arrays.asList("bar"));
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-empty", Arrays.asList(""));
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-content-type", Arrays.asList("zebra"));
}
}
@Test
@SmallTest
// Tests that it is legal to call read() in onStreamReady().
public void testReadDuringOnStreamReady() throws Exception {
String url = Http2TestServer.getEchoStreamUrl();
TestBidirectionalStreamCallback callback =
new TestBidirectionalStreamCallback() {
@Override
public void onStreamReady(BidirectionalStream stream) {
super.onStreamReady(stream);
startNextRead(stream);
}
@Override
public void onResponseHeadersReceived(
BidirectionalStream stream, UrlResponseInfo info) {
// Do nothing. Skip readng.
}
};
callback.addWriteData("Test String".getBytes());
callback.addWriteData("1234567890".getBytes());
callback.addWriteData("woot!".getBytes());
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.addHeader("foo", "bar")
.addHeader("empty", "")
.addHeader("Content-Type", "zebra")
.build();
stream.start();
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEqualTo("Test String1234567890woot!");
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-foo", Arrays.asList("bar"));
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-empty", Arrays.asList(""));
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-content-type", Arrays.asList("zebra"));
}
@Test
@SmallTest
// Tests that it is legal to call flush() when previous nativeWritevData has
// yet to complete.
public void testSimplePostWithFlushBeforePreviousWriteCompleted() throws Exception {
String url = Http2TestServer.getEchoStreamUrl();
TestBidirectionalStreamCallback callback =
new TestBidirectionalStreamCallback() {
@Override
public void onStreamReady(BidirectionalStream stream) {
super.onStreamReady(stream);
// Write a second time before the previous nativeWritevData has completed.
startNextWrite(stream);
assertThat(numPendingWrites()).isEqualTo(0);
}
};
callback.addWriteData("Test String".getBytes(), false);
callback.addWriteData("1234567890".getBytes(), false);
callback.addWriteData("woot!".getBytes(), true);
callback.addWriteData("Test String".getBytes(), false);
callback.addWriteData("1234567890".getBytes(), false);
callback.addWriteData("woot!".getBytes(), true);
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.addHeader("foo", "bar")
.addHeader("empty", "")
.addHeader("Content-Type", "zebra")
.build();
stream.start();
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString)
.isEqualTo("Test String1234567890woot!Test String1234567890woot!");
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-foo", Arrays.asList("bar"));
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-empty", Arrays.asList(""));
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-content-type", Arrays.asList("zebra"));
}
@Test
@SmallTest
public void testSimplePut() throws Exception {
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
callback.addWriteData("Put This Data!".getBytes());
String methodName = "PUT";
BidirectionalStream.Builder builder =
mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getServerUrl(), callback, callback.getExecutor());
builder.setHttpMethod(methodName);
builder.build().start();
callback.blockForDone();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEqualTo("Put This Data!");
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-method", Arrays.asList(methodName));
}
@Test
@SmallTest
public void testBadMethod() throws Exception {
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
BidirectionalStream.Builder builder =
mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getServerUrl(), callback, callback.getExecutor());
builder.setHttpMethod("bad:method!");
IllegalArgumentException e =
assertThrows(IllegalArgumentException.class, () -> builder.build().start());
assertThat(e).hasMessageThat().isEqualTo("Invalid http method bad:method!");
}
@Test
@SmallTest
public void testBadHeaderName() throws Exception {
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
BidirectionalStream.Builder builder =
mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getServerUrl(), callback, callback.getExecutor());
builder.addHeader("goodheader1", "headervalue");
builder.addHeader("header:name", "headervalue");
builder.addHeader("goodheader2", "headervalue");
IllegalArgumentException e =
assertThrows(IllegalArgumentException.class, () -> builder.build().start());
if (mTestRule.implementationUnderTest() == CronetImplementation.AOSP_PLATFORM &&
!mTestRule.isRunningInAOSP()) {
// TODO(b/307234565): Remove check once chromium Android 14 emulator has latest changes.
assertThat(e).hasMessageThat().isEqualTo("Invalid header header:name=headervalue");
} else {
assertThat(e).hasMessageThat().isEqualTo("Invalid header with headername: header:name");
}
}
@Test
@SmallTest
public void testBadHeaderValue() throws Exception {
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
BidirectionalStream.Builder builder =
mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getServerUrl(), callback, callback.getExecutor());
builder.addHeader("headername", "bad header\r\nvalue");
IllegalArgumentException e =
assertThrows(IllegalArgumentException.class, () -> builder.build().start());
if (mTestRule.implementationUnderTest() == CronetImplementation.AOSP_PLATFORM &&
!mTestRule.isRunningInAOSP()) {
// TODO(b/307234565): Remove check once chromium Android 14 emulator has latest changes.
assertThat(e)
.hasMessageThat()
.isEqualTo("Invalid header headername=bad header\r\nvalue");
} else {
assertThat(e).hasMessageThat().isEqualTo("Invalid header with headername: headername");
}
}
@Test
@SmallTest
public void testAddHeader() throws Exception {
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
String headerName = "header-name";
String headerValue = "header-value";
BidirectionalStream.Builder builder =
mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getEchoHeaderUrl(headerName),
callback,
callback.getExecutor());
builder.addHeader(headerName, headerValue);
builder.setHttpMethod("GET");
builder.build().start();
callback.blockForDone();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEqualTo(headerValue);
}
@Test
@SmallTest
public void testMultiRequestHeaders() throws Exception {
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
String headerName = "header-name";
String headerValue1 = "header-value1";
String headerValue2 = "header-value2";
BidirectionalStream.Builder builder =
mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getEchoAllHeadersUrl(), callback, callback.getExecutor());
builder.addHeader(headerName, headerValue1);
builder.addHeader(headerName, headerValue2);
builder.setHttpMethod("GET");
builder.build().start();
callback.blockForDone();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
String headers = callback.mResponseAsString;
Pattern pattern = Pattern.compile(headerName + ":\\s(.*)\\r\\n");
Matcher matcher = pattern.matcher(headers);
List<String> actualValues = new ArrayList<String>();
while (matcher.find()) {
actualValues.add(matcher.group(1));
}
assertThat(actualValues).containsExactly("header-value2");
}
@Test
@SmallTest
public void testEchoTrailers() throws Exception {
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
String headerName = "header-name";
String headerValue = "header-value";
BidirectionalStream.Builder builder =
mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getEchoTrailersUrl(), callback, callback.getExecutor());
builder.addHeader(headerName, headerValue);
builder.setHttpMethod("GET");
builder.build().start();
callback.blockForDone();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mTrailers).isNotNull();
// Verify that header value is properly echoed in trailers.
assertThat(callback.mTrailers.getAsMap())
.containsEntry("echo-" + headerName, Arrays.asList(headerValue));
}
@Test
@SmallTest
public void testCustomUserAgent() throws Exception {
String userAgentName = "User-Agent";
String userAgentValue = "User-Agent-Value";
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
BidirectionalStream.Builder builder =
mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getEchoHeaderUrl(userAgentName),
callback,
callback.getExecutor());
builder.setHttpMethod("GET");
builder.addHeader(userAgentName, userAgentValue);
builder.build().start();
callback.blockForDone();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEqualTo(userAgentValue);
}
@Test
@SmallTest
public void testCustomCronetEngineUserAgent() throws Exception {
String userAgentName = "User-Agent";
String userAgentValue = "User-Agent-Value";
ExperimentalCronetEngine.Builder engineBuilder =
new ExperimentalCronetEngine.Builder(mTestRule.getTestFramework().getContext());
engineBuilder.setUserAgent(userAgentValue);
// TODO(crbug/1490552): Fallback to MockCertVerifier when custom CAs are not supported.
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
CronetTestUtil.setMockCertVerifierForTesting(
engineBuilder, QuicTestServer.createMockCertVerifier());
}
ExperimentalCronetEngine engine = engineBuilder.build();
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
BidirectionalStream.Builder builder =
engine.newBidirectionalStreamBuilder(
Http2TestServer.getEchoHeaderUrl(userAgentName),
callback,
callback.getExecutor());
builder.setHttpMethod("GET");
builder.build().start();
callback.blockForDone();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEqualTo(userAgentValue);
}
@Test
@SmallTest
public void testDefaultUserAgent() throws Exception {
String userAgentName = "User-Agent";
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
BidirectionalStream.Builder builder =
mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getEchoHeaderUrl(userAgentName),
callback,
callback.getExecutor());
builder.setHttpMethod("GET");
builder.build().start();
callback.blockForDone();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString)
.isEqualTo(
new CronetEngine.Builder(mTestRule.getTestFramework().getContext())
.getDefaultUserAgent());
}
@Test
@SmallTest
public void testEchoStream() throws Exception {
String url = Http2TestServer.getEchoStreamUrl();
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
String[] testData = {"Test String", createLongString("1234567890", 50000), "woot!"};
StringBuilder stringData = new StringBuilder();
for (String writeData : testData) {
callback.addWriteData(writeData.getBytes());
stringData.append(writeData);
}
// Create stream.
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.addHeader("foo", "Value with Spaces")
.addHeader("Content-Type", "zebra")
.build();
stream.start();
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEqualTo(stringData.toString());
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-foo", Arrays.asList("Value with Spaces"));
assertThat(callback.getResponseInfoWithChecks())
.hasHeadersThat()
.containsEntry("echo-content-type", Arrays.asList("zebra"));
}
@Test
@SmallTest
public void testEchoStreamEmptyWrite() throws Exception {
String url = Http2TestServer.getEchoStreamUrl();
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
callback.addWriteData(new byte[0]);
// Create stream.
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.build();
stream.start();
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEmpty();
}
@Test
@SmallTest
public void testDoubleWrite() throws Exception {
String url = Http2TestServer.getEchoStreamUrl();
TestBidirectionalStreamCallback callback =
new TestBidirectionalStreamCallback() {
@Override
public void onStreamReady(BidirectionalStream stream) {
// super class will call Write() once.
super.onStreamReady(stream);
// Call Write() again.
startNextWrite(stream);
// Make sure there is no pending write.
assertThat(numPendingWrites()).isEqualTo(0);
}
};
callback.addWriteData("1".getBytes());
callback.addWriteData("2".getBytes());
// Create stream.
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.build();
stream.start();
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEqualTo("12");
}
@Test
@SmallTest
public void testDoubleRead() throws Exception {
String url = Http2TestServer.getEchoStreamUrl();
TestBidirectionalStreamCallback callback =
new TestBidirectionalStreamCallback() {
@Override
public void onResponseHeadersReceived(
BidirectionalStream stream, UrlResponseInfo info) {
startNextRead(stream);
// Second read from callback invoked on single-threaded executor throws an
// exception because previous read is still pending until its completion is
// handled on executor.
Exception e =
assertThrows(
Exception.class,
() -> stream.read(ByteBuffer.allocateDirect(5)));
assertThat(e).hasMessageThat().isEqualTo("Unexpected read attempt.");
}
};
callback.addWriteData("1".getBytes());
callback.addWriteData("2".getBytes());
// Create stream.
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.build();
stream.start();
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEqualTo("12");
}
@Test
@SmallTest
public void testReadAndWrite() throws Exception {
String url = Http2TestServer.getEchoStreamUrl();
TestBidirectionalStreamCallback callback =
new TestBidirectionalStreamCallback() {
@Override
public void onResponseHeadersReceived(
BidirectionalStream stream, UrlResponseInfo info) {
// Start the write, that will not complete until callback completion.
setAutoAdvance(true);
startNextWrite(stream);
// Start the read. It is allowed with write in flight.
super.onResponseHeadersReceived(stream, info);
}
};
callback.setAutoAdvance(false);
callback.addWriteData("1".getBytes());
callback.addWriteData("2".getBytes());
// Create stream.
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.build();
stream.start();
callback.waitForNextWriteStep();
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEqualTo("12");
}
@Test
@SmallTest
public void testEchoStreamWriteFirst() throws Exception {
String url = Http2TestServer.getEchoStreamUrl();
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
callback.setAutoAdvance(false);
String[] testData = {"a", "bb", "ccc", "Test String", "1234567890", "woot!"};
StringBuilder stringData = new StringBuilder();
for (String writeData : testData) {
callback.addWriteData(writeData.getBytes());
stringData.append(writeData);
}
// Create stream.
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.build();
stream.start();
// Write first.
callback.waitForNextWriteStep(); // onStreamReady
for (int i = 0; i < testData.length; i++) {
// Write next chunk of test data.
callback.startNextWrite(stream);
callback.waitForNextWriteStep(); // onWriteCompleted
}
// Wait for read step, but don't read yet.
callback.waitForNextReadStep(); // onResponseHeadersReceived
assertThat(callback.mResponseAsString).isEmpty();
// Read back.
callback.startNextRead(stream);
callback.waitForNextReadStep(); // onReadCompleted
// Verify that some part of proper response is read.
assertThat(callback.mResponseAsString).startsWith(testData[0]);
// Read the rest of the response.
callback.setAutoAdvance(true);
callback.startNextRead(stream);
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEqualTo(stringData.toString());
}
@Test
@SmallTest
public void testEchoStreamStepByStep() throws Exception {
String url = Http2TestServer.getEchoStreamUrl();
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
callback.setAutoAdvance(false);
String[] testData = {"a", "bb", "ccc", "Test String", "1234567890", "woot!"};
StringBuilder stringData = new StringBuilder();
for (String writeData : testData) {
callback.addWriteData(writeData.getBytes());
stringData.append(writeData);
}
// Create stream.
BidirectionalStream stream =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.build();
stream.start();
callback.waitForNextWriteStep();
callback.waitForNextReadStep();
for (String expected : testData) {
// Write next chunk of test data.
callback.startNextWrite(stream);
callback.waitForNextWriteStep();
// Read next chunk of test data.
ByteBuffer readBuffer = ByteBuffer.allocateDirect(100);
callback.startNextRead(stream, readBuffer);
callback.waitForNextReadStep();
assertThat(readBuffer.position()).isEqualTo(expected.length());
assertThat(stream.isDone()).isFalse();
}
callback.setAutoAdvance(true);
callback.startNextRead(stream);
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEqualTo(stringData.toString());
}
/** Checks that the buffer is updated correctly, when starting at an offset. */
@Test
@SmallTest
@IgnoreFor(
implementations = {CronetImplementation.AOSP_PLATFORM},
reason =
"crbug.com/1494845: Relies on finished listener synchronization which isn't"
+ " available in AOSP")
public void testSimpleGetBufferUpdates() throws Exception {
TestRequestFinishedListener requestFinishedListener = new TestRequestFinishedListener();
mCronetEngine.addRequestFinishedListener(requestFinishedListener);
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
callback.setAutoAdvance(false);
// Since the method is "GET", the expected response body is also "GET".
BidirectionalStream.Builder builder =
mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor());
BidirectionalStream stream = builder.setHttpMethod("GET").build();
stream.start();
callback.waitForNextReadStep();
assertThat(callback.mError).isNull();
assertThat(callback.isDone()).isFalse();
assertThat(callback.mResponseStep)
.isEqualTo(TestBidirectionalStreamCallback.ResponseStep.ON_RESPONSE_STARTED);
ByteBuffer readBuffer = ByteBuffer.allocateDirect(5);
readBuffer.put("FOR".getBytes());
assertThat(readBuffer.position()).isEqualTo(3);
// Read first two characters of the response ("GE"). It's theoretically
// possible to need one read per character, though in practice,
// shouldn't happen.
while (callback.mResponseAsString.length() < 2) {
assertThat(callback.isDone()).isFalse();
callback.startNextRead(stream, readBuffer);
callback.waitForNextReadStep();
}
// Make sure the two characters were read.
assertThat(callback.mResponseAsString).isEqualTo("GE");
// Check the contents of the entire buffer. The first 3 characters
// should not have been changed, and the last two should be the first
// two characters from the response.
assertThat(bufferContentsToString(readBuffer, 0, 5)).isEqualTo("FORGE");
// The limit and position should be 5.
assertThat(readBuffer.limit()).isEqualTo(5);
assertThat(readBuffer.position()).isEqualTo(5);
assertThat(callback.mResponseStep).isEqualTo(ResponseStep.ON_READ_COMPLETED);
// Start reading from position 3. Since the only remaining character
// from the response is a "T", when the read completes, the buffer
// should contain "FORTE", with a position() of 4 and a limit() of 5.
readBuffer.position(3);
callback.startNextRead(stream, readBuffer);
callback.waitForNextReadStep();
// Make sure all three characters of the response have now been read.
assertThat(callback.mResponseAsString).isEqualTo("GET");
// Check the entire contents of the buffer. Only the third character
// should have been modified.
assertThat(bufferContentsToString(readBuffer, 0, 5)).isEqualTo("FORTE");
// Make sure position and limit were updated correctly.
assertThat(readBuffer.position()).isEqualTo(4);
assertThat(readBuffer.limit()).isEqualTo(5);
assertThat(callback.mResponseStep).isEqualTo(ResponseStep.ON_READ_COMPLETED);
// One more read attempt. The request should complete.
readBuffer.position(1);
readBuffer.limit(5);
callback.setAutoAdvance(true);
callback.startNextRead(stream, readBuffer);
callback.blockForDone();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEqualTo("GET");
checkResponseInfo(
callback.getResponseInfoWithChecks(), Http2TestServer.getEchoMethodUrl(), 200, "");
// Check that buffer contents were not modified.
assertThat(bufferContentsToString(readBuffer, 0, 5)).isEqualTo("FORTE");
// Position should not have been modified, since nothing was read.
assertThat(readBuffer.position()).isEqualTo(1);
// Limit should be unchanged as always.
assertThat(readBuffer.limit()).isEqualTo(5);
assertThat(callback.mResponseStep).isEqualTo(ResponseStep.ON_SUCCEEDED);
// TestRequestFinishedListener expects a single call to onRequestFinished. Here we
// explicitly wait for the call to happen to avoid a race condition with the other
// TestRequestFinishedListener created within runGetWithExpectedReceivedByteCount.
requestFinishedListener.blockUntilDone();
mCronetEngine.removeRequestFinishedListener(requestFinishedListener);
// Make sure there are no other pending messages, which would trigger
// asserts in TestBidirectionalCallback.
// The expected received bytes count is lower than it would be for the first request on the
// connection, because the server includes an HPACK dynamic table size update only in the
// first response HEADERS frame.
runGetWithExpectedReceivedByteCount(27);
}
@Test
@SmallTest
public void testBadBuffers() throws Exception {
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
callback.setAutoAdvance(false);
BidirectionalStream.Builder builder =
mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor());
BidirectionalStream stream = builder.setHttpMethod("GET").build();
stream.start();
callback.waitForNextReadStep();
assertThat(callback.mError).isNull();
assertThat(callback.isDone()).isFalse();
assertThat(callback.mResponseStep)
.isEqualTo(TestBidirectionalStreamCallback.ResponseStep.ON_RESPONSE_STARTED);
// Try to read using a full buffer.
ByteBuffer readBuffer = ByteBuffer.allocateDirect(4);
readBuffer.put("full".getBytes());
IllegalArgumentException e =
assertThrows(IllegalArgumentException.class, () -> stream.read(readBuffer));
assertThat(e).hasMessageThat().isEqualTo("ByteBuffer is already full.");
// Try to read using a non-direct buffer.
ByteBuffer readBuffer1 = ByteBuffer.allocate(5);
e = assertThrows(IllegalArgumentException.class, () -> stream.read(readBuffer1));
assertThat(e).hasMessageThat().isEqualTo("byteBuffer must be a direct ByteBuffer.");
// Finish the stream with a direct ByteBuffer.
callback.setAutoAdvance(true);
ByteBuffer readBuffer2 = ByteBuffer.allocateDirect(5);
stream.read(readBuffer2);
callback.blockForDone();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
assertThat(callback.mResponseAsString).isEqualTo("GET");
}
private void throwOrCancel(
FailureType failureType, ResponseStep failureStep, boolean expectError) {
// Use a fresh CronetEngine each time so Http2 session is not reused.
ExperimentalCronetEngine.Builder builder =
new ExperimentalCronetEngine.Builder(mTestRule.getTestFramework().getContext());
// TODO(crbug/1490552): Fallback to MockCertVerifier when custom CAs are not supported.
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
CronetTestUtil.setMockCertVerifierForTesting(
builder, QuicTestServer.createMockCertVerifier());
}
mCronetEngine = builder.build();
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
callback.setFailure(failureType, failureStep);
TestRequestFinishedListener requestFinishedListener = new TestRequestFinishedListener();
mCronetEngine.addRequestFinishedListener(requestFinishedListener);
BidirectionalStream.Builder streamBuilder =
mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor());
BidirectionalStream stream = streamBuilder.setHttpMethod("GET").build();
Date startTime = new Date();
stream.start();
callback.blockForDone();
assertThat(stream.isDone()).isTrue();
requestFinishedListener.blockUntilDone();
Date endTime = new Date();
RequestFinishedInfo finishedInfo = requestFinishedListener.getRequestInfo();
RequestFinishedInfo.Metrics metrics = finishedInfo.getMetrics();
assertThat(metrics).isNotNull();
// Cancellation when stream is ready does not guarantee that
// mResponseInfo is null because there might be a
// onResponseHeadersReceived already queued in the executor.
// See crbug.com/594432.
if (failureStep != ResponseStep.ON_STREAM_READY) {
assertThat(callback.getResponseInfo()).isNotNull();
}
// Check metrics information.
if (failureStep == ResponseStep.ON_RESPONSE_STARTED
|| failureStep == ResponseStep.ON_READ_COMPLETED
|| failureStep == ResponseStep.ON_TRAILERS) {
// For steps after response headers are received, there will be
// connect timing metrics.
MetricsTestUtil.checkTimingMetrics(metrics, startTime, endTime);
MetricsTestUtil.checkHasConnectTiming(metrics, startTime, endTime, true);
assertThat(metrics.getSentByteCount()).isGreaterThan(0L);
assertThat(metrics.getReceivedByteCount()).isGreaterThan(0L);
} else if (failureStep == ResponseStep.ON_STREAM_READY) {
assertThat(metrics.getRequestStart()).isNotNull();
MetricsTestUtil.assertAfter(metrics.getRequestStart(), startTime);
assertThat(metrics.getRequestEnd()).isNotNull();
MetricsTestUtil.assertAfter(endTime, metrics.getRequestEnd());
MetricsTestUtil.assertAfter(metrics.getRequestEnd(), metrics.getRequestStart());
}
assertThat(callback.mError != null).isEqualTo(expectError);
assertThat(callback.mOnErrorCalled).isEqualTo(expectError);
if (expectError) {
assertThat(finishedInfo.getException()).isNotNull();
assertThat(finishedInfo.getFinishedReason()).isEqualTo(RequestFinishedInfo.FAILED);
} else {
assertThat(finishedInfo.getException()).isNull();
assertThat(finishedInfo.getFinishedReason()).isEqualTo(RequestFinishedInfo.CANCELED);
}
assertThat(callback.mOnCanceledCalled)
.isEqualTo(
failureType == FailureType.CANCEL_SYNC
|| failureType == FailureType.CANCEL_ASYNC
|| failureType == FailureType.CANCEL_ASYNC_WITHOUT_PAUSE);
mCronetEngine.removeRequestFinishedListener(requestFinishedListener);
}
@Test
@SmallTest
public void testFailures() throws Exception {
throwOrCancel(FailureType.CANCEL_SYNC, ResponseStep.ON_STREAM_READY, false);
throwOrCancel(FailureType.CANCEL_ASYNC, ResponseStep.ON_STREAM_READY, false);
throwOrCancel(FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, ResponseStep.ON_STREAM_READY, false);
throwOrCancel(FailureType.THROW_SYNC, ResponseStep.ON_STREAM_READY, true);
throwOrCancel(FailureType.CANCEL_SYNC, ResponseStep.ON_RESPONSE_STARTED, false);
throwOrCancel(FailureType.CANCEL_ASYNC, ResponseStep.ON_RESPONSE_STARTED, false);
throwOrCancel(
FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, ResponseStep.ON_RESPONSE_STARTED, false);
throwOrCancel(FailureType.THROW_SYNC, ResponseStep.ON_RESPONSE_STARTED, true);
throwOrCancel(FailureType.CANCEL_SYNC, ResponseStep.ON_READ_COMPLETED, false);
throwOrCancel(FailureType.CANCEL_ASYNC, ResponseStep.ON_READ_COMPLETED, false);
throwOrCancel(
FailureType.CANCEL_ASYNC_WITHOUT_PAUSE, ResponseStep.ON_READ_COMPLETED, false);
throwOrCancel(FailureType.THROW_SYNC, ResponseStep.ON_READ_COMPLETED, true);
}
@Test
@SmallTest
public void testThrowOnSucceeded() {
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
callback.setFailure(FailureType.THROW_SYNC, ResponseStep.ON_SUCCEEDED);
BidirectionalStream.Builder builder =
mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor());
BidirectionalStream stream = builder.setHttpMethod("GET").build();
stream.start();
callback.blockForDone();
assertThat(ResponseStep.ON_SUCCEEDED).isEqualTo(callback.mResponseStep);
assertThat(stream.isDone()).isTrue();
assertThat(callback.getResponseInfoWithChecks()).isNotNull();
// Check that error thrown from 'onSucceeded' callback is not reported.
assertThat(callback.mError).isNull();
assertThat(callback.mOnErrorCalled).isFalse();
}
@Test
@SmallTest
@IgnoreFor(
implementations = {CronetImplementation.AOSP_PLATFORM},
reason = "crbug.com/1494845: Requires access to internals not available in AOSP")
public void testExecutorShutdownBeforeStreamIsDone() {
// Test that stream is destroyed even if executor is shut down and rejects posting tasks.
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
callback.setAutoAdvance(false);
BidirectionalStream.Builder builder =
mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor());
CronetBidirectionalStream stream =
(CronetBidirectionalStream) builder.setHttpMethod("GET").build();
stream.start();
callback.waitForNextReadStep();
assertThat(callback.isDone()).isFalse();
assertThat(stream.isDone()).isFalse();
final ConditionVariable streamDestroyed = new ConditionVariable(false);
stream.setOnDestroyedCallbackForTesting(
new Runnable() {
@Override
public void run() {
streamDestroyed.open();
}
});
// Shut down the executor, so posting the task will throw an exception.
callback.shutdownExecutor();
ByteBuffer readBuffer = ByteBuffer.allocateDirect(5);
stream.read(readBuffer);
// Callback will never be called again because executor is shut down,
// but stream will be destroyed from network thread.
streamDestroyed.block();
assertThat(callback.isDone()).isFalse();
assertThat(stream.isDone()).isTrue();
}
@Test
@SmallTest
@IgnoreFor(
implementations = {CronetImplementation.AOSP_PLATFORM},
reason = "ActiveRequestCount is not available in AOSP")
public void testCronetEngineShutdown() throws Exception {
// Test that CronetEngine cannot be shut down if there are any active streams.
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
// Block callback when response starts to verify that shutdown fails
// if there are active streams.
callback.setAutoAdvance(false);
BidirectionalStream.Builder builder =
mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor());
BidirectionalStream stream = builder.setHttpMethod("GET").build();
stream.start();
Exception e = assertThrows(Exception.class, mCronetEngine::shutdown);
assertThat(e).hasMessageThat().matches("Cannot shutdown with (running|active) requests.");
callback.waitForNextReadStep();
assertThat(callback.mResponseStep).isEqualTo(ResponseStep.ON_RESPONSE_STARTED);
e = assertThrows(Exception.class, mCronetEngine::shutdown);
assertThat(e).hasMessageThat().matches("Cannot shutdown with (running|active) requests.");
callback.startNextRead(stream);
callback.waitForNextReadStep();
assertThat(callback.mResponseStep).isEqualTo(ResponseStep.ON_READ_COMPLETED);
e = assertThrows(Exception.class, mCronetEngine::shutdown);
assertThat(e).hasMessageThat().matches("Cannot shutdown with (running|active) requests.");
// May not have read all the data, in theory. Just enable auto-advance
// and finish the request.
callback.setAutoAdvance(true);
callback.startNextRead(stream);
callback.blockForDone();
waitForActiveRequestCount(0);
mCronetEngine.shutdown();
}
@Test
@SmallTest
@IgnoreFor(
implementations = {CronetImplementation.AOSP_PLATFORM},
reason = "ActiveRequestCount is not available in AOSP")
public void testCronetEngineShutdownAfterStreamFailure() throws Exception {
// Test that CronetEngine can be shut down after stream reports a failure.
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
BidirectionalStream.Builder builder =
mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor());
BidirectionalStream stream = builder.setHttpMethod("GET").build();
stream.start();
callback.setFailure(FailureType.THROW_SYNC, ResponseStep.ON_READ_COMPLETED);
callback.blockForDone();
assertThat(callback.mOnErrorCalled).isTrue();
waitForActiveRequestCount(0);
mCronetEngine.shutdown();
}
@Test
@SmallTest
@IgnoreFor(
implementations = {CronetImplementation.AOSP_PLATFORM},
reason = "ActiveRequestCount is not available in AOSP")
public void testCronetEngineShutdownAfterStreamCancel() throws Exception {
// Test that CronetEngine can be shut down after stream is canceled.
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
BidirectionalStream.Builder builder =
mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor());
BidirectionalStream stream = builder.setHttpMethod("GET").build();
// Block callback when response starts to verify that shutdown fails
// if there are active requests.
callback.setAutoAdvance(false);
stream.start();
Exception e = assertThrows(Exception.class, mCronetEngine::shutdown);
assertThat(e).hasMessageThat().matches("Cannot shutdown with (running|active) requests.");
callback.waitForNextReadStep();
assertThat(callback.mResponseStep).isEqualTo(ResponseStep.ON_RESPONSE_STARTED);
stream.cancel();
callback.blockForDone();
assertThat(callback.mOnCanceledCalled).isTrue();
waitForActiveRequestCount(0);
mCronetEngine.shutdown();
}
/*
* Verifies NetworkException constructed from specific error codes are retryable.
*/
@SmallTest
@Test
public void testErrorCodes() throws Exception {
// Non-BidirectionalStream specific error codes.
checkSpecificErrorCode(
NetError.ERR_NAME_NOT_RESOLVED,
NetworkException.ERROR_HOSTNAME_NOT_RESOLVED,
false);
checkSpecificErrorCode(
NetError.ERR_INTERNET_DISCONNECTED,
NetworkException.ERROR_INTERNET_DISCONNECTED,
false);
checkSpecificErrorCode(
NetError.ERR_NETWORK_CHANGED, NetworkException.ERROR_NETWORK_CHANGED, true);
checkSpecificErrorCode(
NetError.ERR_CONNECTION_CLOSED, NetworkException.ERROR_CONNECTION_CLOSED, true);
checkSpecificErrorCode(
NetError.ERR_CONNECTION_REFUSED, NetworkException.ERROR_CONNECTION_REFUSED, false);
checkSpecificErrorCode(
NetError.ERR_CONNECTION_RESET, NetworkException.ERROR_CONNECTION_RESET, true);
checkSpecificErrorCode(
NetError.ERR_CONNECTION_TIMED_OUT,
NetworkException.ERROR_CONNECTION_TIMED_OUT,
true);
checkSpecificErrorCode(NetError.ERR_TIMED_OUT, NetworkException.ERROR_TIMED_OUT, true);
checkSpecificErrorCode(
NetError.ERR_ADDRESS_UNREACHABLE,
NetworkException.ERROR_ADDRESS_UNREACHABLE,
false);
// BidirectionalStream specific retryable error codes.
checkSpecificErrorCode(NetError.ERR_HTTP2_PING_FAILED, NetworkException.ERROR_OTHER, true);
checkSpecificErrorCode(
NetError.ERR_QUIC_HANDSHAKE_FAILED, NetworkException.ERROR_OTHER, true);
}
// Returns the contents of byteBuffer, from its position() to its limit(),
// as a String. Does not modify byteBuffer's position().
private static String bufferContentsToString(ByteBuffer byteBuffer, int start, int end) {
// Use a duplicate to avoid modifying byteBuffer.
ByteBuffer duplicate = byteBuffer.duplicate();
duplicate.position(start);
duplicate.limit(end);
byte[] contents = new byte[duplicate.remaining()];
duplicate.get(contents);
return new String(contents);
}
private static void checkSpecificErrorCode(
int netError, int errorCode, boolean immediatelyRetryable) throws Exception {
NetworkException exception =
new BidirectionalStreamNetworkException("", errorCode, netError);
assertThat(exception.immediatelyRetryable()).isEqualTo(immediatelyRetryable);
assertThat(exception.getCronetInternalErrorCode()).isEqualTo(netError);
assertThat(exception.getErrorCode()).isEqualTo(errorCode);
}
@Test
@SmallTest
@RequiresMinApi(10) // Tagging support added in API level 10: crrev.com/c/chromium/src/+/937583
@RequiresMinAndroidApi(Build.VERSION_CODES.M) // crbug/1301957
public void testTagging() throws Exception {
if (!CronetTestUtil.nativeCanGetTaggedBytes()) {
Log.i(TAG, "Skipping test - GetTaggedBytes unsupported.");
return;
}
String url = Http2TestServer.getEchoStreamUrl();
// Test untagged requests are given tag 0.
int tag = 0;
long priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag);
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
callback.addWriteData(new byte[] {0});
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.build()
.start();
callback.blockForDone();
assertThat(CronetTestUtil.nativeGetTaggedBytes(tag)).isGreaterThan(priorBytes);
// Test explicit tagging.
tag = 0x12345678;
priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag);
callback = new TestBidirectionalStreamCallback();
callback.addWriteData(new byte[] {0});
ExperimentalBidirectionalStream.Builder builder =
mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor());
assertThat(builder).isEqualTo(builder.setTrafficStatsTag(tag));
builder.build().start();
callback.blockForDone();
assertThat(CronetTestUtil.nativeGetTaggedBytes(tag)).isGreaterThan(priorBytes);
// Test a different tag value to make sure reused connections are retagged.
tag = 0x87654321;
priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag);
callback = new TestBidirectionalStreamCallback();
callback.addWriteData(new byte[] {0});
builder =
mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor());
assertThat(builder).isEqualTo(builder.setTrafficStatsTag(tag));
builder.build().start();
callback.blockForDone();
assertThat(CronetTestUtil.nativeGetTaggedBytes(tag)).isGreaterThan(priorBytes);
// Test tagging with our UID.
tag = 0;
priorBytes = CronetTestUtil.nativeGetTaggedBytes(tag);
callback = new TestBidirectionalStreamCallback();
callback.addWriteData(new byte[] {0});
builder =
mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor());
assertThat(builder).isEqualTo(builder.setTrafficStatsUid(Process.myUid()));
builder.build().start();
callback.blockForDone();
assertThat(CronetTestUtil.nativeGetTaggedBytes(tag)).isGreaterThan(priorBytes);
}
@Test
@RequiresMinAndroidApi(Build.VERSION_CODES.M)
public void testBindToInvalidNetworkFails() {
String url = Http2TestServer.getEchoMethodUrl();
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
BidirectionalStream.Builder builder =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.setHttpMethod("GET");
if (mTestRule.implementationUnderTest() == CronetImplementation.AOSP_PLATFORM) {
// android.net.http.UrlRequestBuilder#bindToNetwork requires an android.net.Network
// object. So, in this case, it will be the wrapper layer that will fail to translate
// that to a Network, not something in net's code. Hence, the failure will manifest
// itself at bind time, not at request execution time.
// Note: this will never happen in prod, as translation failure can only happen if we're
// given a fake networkHandle.
assertThrows(
IllegalArgumentException.class,
() -> builder.bindToNetwork(-150 /* invalid network handle */));
return;
}
builder.bindToNetwork(-150 /* invalid network handle */);
BidirectionalStream stream = builder.build();
stream.start();
callback.blockForDone();
assertThat(callback.mError).isNotNull();
if (mTestRule.implementationUnderTest() == CronetImplementation.FALLBACK) {
assertThat(callback.mError).isInstanceOf(CronetExceptionImpl.class);
assertThat(callback.mError).hasCauseThat().isInstanceOf(NetworkExceptionImpl.class);
} else {
assertThat(callback.mError).isInstanceOf(NetworkExceptionImpl.class);
}
}
@Test
@RequiresMinAndroidApi(Build.VERSION_CODES.M)
public void testBindToDefaultNetworkSucceeds() {
ConnectivityManagerDelegate delegate =
new ConnectivityManagerDelegate(mTestRule.getTestFramework().getContext());
Network defaultNetwork = delegate.getDefaultNetwork();
assume().that(defaultNetwork).isNotNull();
String url = Http2TestServer.getEchoMethodUrl();
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
BidirectionalStream.Builder builder =
mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.setHttpMethod("GET");
builder.bindToNetwork(defaultNetwork.getNetworkHandle());
builder.build().start();
callback.blockForDone();
assertThat(callback.getResponseInfoWithChecks()).hasHttpStatusCodeThat().isEqualTo(200);
}
/**
* Cronet does not currently provide an API to wait for the active request count to change. We
* can't just wait for the terminal callback to fire because Cronet updates the count some time
* *after* we return from the callback. We hack around this by polling the active request count
* in a loop.
*/
private void waitForActiveRequestCount(int expectedCount) throws Exception {
while (mCronetEngine.getActiveRequestCount() != expectedCount) Thread.sleep(100);
}
}