blob: 98654e548cd1590fe966fec8b57848b430e8a509 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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 org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.chromium.base.CollectionUtil.newHashSet;
import static org.chromium.net.CronetTestRule.SERVER_CERT_PEM;
import static org.chromium.net.CronetTestRule.SERVER_KEY_PKCS8_PEM;
import static org.chromium.net.CronetTestRule.assertContains;
import static org.chromium.net.CronetTestRule.getContext;
import android.os.ConditionVariable;
import android.os.Process;
import android.support.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.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Feature;
import org.chromium.net.CronetTestRule.OnlyRunNativeCronet;
import org.chromium.net.CronetTestRule.RequiresMinApi;
import org.chromium.net.MetricsTestUtil.TestRequestFinishedListener;
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.UrlResponseInfoImpl;
import java.nio.ByteBuffer;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Test functionality of BidirectionalStream interface.
*/
@RunWith(BaseJUnit4ClassRunner.class)
public class BidirectionalStreamTest {
@Rule
public final CronetTestRule mTestRule = new CronetTestRule();
private ExperimentalCronetEngine mCronetEngine;
@Before
public void setUp() throws Exception {
// Load library first to create MockCertVerifier.
System.loadLibrary("cronet_tests");
ExperimentalCronetEngine.Builder builder =
new ExperimentalCronetEngine.Builder(getContext());
CronetTestUtil.setMockCertVerifierForTesting(
builder, QuicTestServer.createMockCertVerifier());
mCronetEngine = builder.build();
assertTrue(Http2TestServer.startHttp2TestServer(
getContext(), SERVER_CERT_PEM, SERVER_KEY_PKCS8_PEM));
}
@After
public void tearDown() throws Exception {
assertTrue(Http2TestServer.shutdownHttp2TestServer());
if (mCronetEngine != null) {
mCronetEngine.shutdown();
}
}
private static void checkResponseInfo(UrlResponseInfo responseInfo, String expectedUrl,
int expectedHttpStatusCode, String expectedHttpStatusText) {
assertEquals(expectedUrl, responseInfo.getUrl());
assertEquals(
expectedUrl, responseInfo.getUrlChain().get(responseInfo.getUrlChain().size() - 1));
assertEquals(expectedHttpStatusCode, responseInfo.getHttpStatusCode());
assertEquals(expectedHttpStatusText, responseInfo.getHttpStatusText());
assertFalse(responseInfo.wasCached());
assertTrue(responseInfo.toString().length() > 0);
}
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 runSimpleGetWithExpectedReceivedByteCount(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();
assertTrue(stream.isDone());
requestFinishedListener.blockUntilDone();
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
// Default method is 'GET'.
assertEquals("GET", callback.mResponseAsString);
UrlResponseInfo urlResponseInfo = createUrlResponseInfo(
new String[] {url}, "", 200, expectedReceivedBytes, ":status", "200");
mTestRule.assertResponseEquals(urlResponseInfo, callback.mResponseInfo);
checkResponseInfo(callback.mResponseInfo, Http2TestServer.getEchoMethodUrl(), 200, "");
RequestFinishedInfo finishedInfo = requestFinishedListener.getRequestInfo();
assertTrue(finishedInfo.getAnnotations().isEmpty());
}
@Test
@SmallTest
@Feature({"Cronet"})
public void testBuilderCheck() throws Exception {
if (mTestRule.testingJavaImpl()) {
runBuilderCheckJavaImpl();
} else {
runBuilderCheckNativeImpl();
}
}
private void runBuilderCheckNativeImpl() throws Exception {
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
try {
mCronetEngine.newBidirectionalStreamBuilder(null, callback, callback.getExecutor());
fail("URL not null-checked");
} catch (NullPointerException e) {
assertEquals("URL is required.", e.getMessage());
}
try {
mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getServerUrl(), null, callback.getExecutor());
fail("Callback not null-checked");
} catch (NullPointerException e) {
assertEquals("Callback is required.", e.getMessage());
}
try {
mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getServerUrl(), callback, null);
fail("Executor not null-checked");
} catch (NullPointerException e) {
assertEquals("Executor is required.", e.getMessage());
}
// Verify successful creation doesn't throw.
BidirectionalStream.Builder builder = mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getServerUrl(), callback, callback.getExecutor());
try {
builder.addHeader(null, "value");
fail("Header name is not null-checked");
} catch (NullPointerException e) {
assertEquals("Invalid header name.", e.getMessage());
}
try {
builder.addHeader("name", null);
fail("Header value is not null-checked");
} catch (NullPointerException e) {
assertEquals("Invalid header value.", e.getMessage());
}
try {
builder.setHttpMethod(null);
fail("Method name is not null-checked");
} catch (NullPointerException e) {
assertEquals("Method is required.", e.getMessage());
}
}
private void runBuilderCheckJavaImpl() {
try {
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
mTestRule.createJavaEngineBuilder().build().newBidirectionalStreamBuilder(
Http2TestServer.getServerUrl(), callback, callback.getExecutor());
fail("JavaCronetEngine doesn't support BidirectionalStream."
+ " Expected UnsupportedOperationException");
} catch (UnsupportedOperationException e) {
// Expected.
}
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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();
assertTrue(stream.isDone());
assertContains("Exception in BidirectionalStream: net::ERR_DISALLOWED_URL_SCHEME",
callback.mError.getMessage());
assertEquals(-301, ((NetworkException) callback.mError).getCronetInternalErrorCode());
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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.
runSimpleGetWithExpectedReceivedByteCount(31);
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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();
assertTrue(stream.isDone());
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals("HEAD", callback.mResponseAsString);
UrlResponseInfo urlResponseInfo =
createUrlResponseInfo(new String[] {url}, "", 200, 32, ":status", "200");
mTestRule.assertResponseEquals(urlResponseInfo, callback.mResponseInfo);
checkResponseInfo(callback.mResponseInfo, Http2TestServer.getEchoMethodUrl(), 200, "");
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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());
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();
assertTrue(stream.isDone());
requestFinishedListener.blockUntilDone();
Date endTime = new Date();
RequestFinishedInfo finishedInfo = requestFinishedListener.getRequestInfo();
MetricsTestUtil.checkRequestFinishedInfo(finishedInfo, url, startTime, endTime);
assertEquals(RequestFinishedInfo.SUCCEEDED, finishedInfo.getFinishedReason());
MetricsTestUtil.checkHasConnectTiming(finishedInfo.getMetrics(), startTime, endTime, true);
assertEquals(newHashSet("request annotation", this),
new HashSet<Object>(finishedInfo.getAnnotations()));
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals("Test String1234567890woot!", callback.mResponseAsString);
assertEquals("bar", callback.mResponseInfo.getAllHeaders().get("echo-foo").get(0));
assertEquals("", callback.mResponseInfo.getAllHeaders().get("echo-empty").get(0));
assertEquals(
"zebra", callback.mResponseInfo.getAllHeaders().get("echo-content-type").get(0));
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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();
assertTrue(stream.isDone());
// Flush after stream is completed is no-op. It shouldn't call into the destroyed adapter.
stream.flush();
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals("Test String1234567890woot!", callback.mResponseAsString);
assertEquals("bar", callback.mResponseInfo.getAllHeaders().get("echo-foo").get(0));
assertEquals("", callback.mResponseInfo.getAllHeaders().get("echo-empty").get(0));
assertEquals(
"zebra", callback.mResponseInfo.getAllHeaders().get("echo-content-type").get(0));
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
// 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();
assertEquals(1, pendingData.size());
ByteBuffer pendingBuffer = pendingData.get(0);
byte[] content = new byte[pendingBuffer.remaining()];
pendingBuffer.get(content);
assertTrue(Arrays.equals("6".getBytes(), content));
// "4" and "5" have been flushed.
assertEquals(0,
((CronetBidirectionalStream) stream).getFlushDataForTesting().size());
} else if (mNumWriteCompleted == 5) {
// Now flush "6", which is still in pending queue.
List<ByteBuffer> pendingData =
((CronetBidirectionalStream) stream).getPendingDataForTesting();
assertEquals(1, pendingData.size());
ByteBuffer pendingBuffer = pendingData.get(0);
byte[] content = new byte[pendingBuffer.remaining()];
pendingBuffer.get(content);
assertTrue(Arrays.equals("6".getBytes(), content));
stream.flush();
assertEquals(0,
((CronetBidirectionalStream) stream).getPendingDataForTesting().size());
assertEquals(0,
((CronetBidirectionalStream) stream).getFlushDataForTesting().size());
}
}
};
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();
assertEquals(0, stream.getPendingDataForTesting().size());
assertEquals(0, stream.getFlushDataForTesting().size());
// 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();
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals("123456", callback.mResponseAsString);
assertEquals("bar", callback.mResponseInfo.getAllHeaders().get("echo-foo").get(0));
assertEquals("", callback.mResponseInfo.getAllHeaders().get("echo-empty").get(0));
assertEquals(
"zebra", callback.mResponseInfo.getAllHeaders().get("echo-content-type").get(0));
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
// 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(getDummyData(), 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(getDummyData(), false);
stream.flush();
stream.write(getDummyData(), false);
stream.flush();
}
// Returns a piece of dummy data to send to the server.
private ByteBuffer getDummyData() {
byte[] data = new byte[100];
for (int i = 0; i < data.length; i++) {
data[i] = 'x';
}
ByteBuffer dummyData = ByteBuffer.allocateDirect(data.length);
dummyData.put(data);
dummyData.flip();
return dummyData;
}
};
CronetBidirectionalStream stream =
(CronetBidirectionalStream) mCronetEngine
.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.build();
stream.start();
callback.blockForDone();
assertTrue(callback.mOnCanceledCalled);
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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) {
try {
// Attempt to write data for GET request.
stream.write(ByteBuffer.wrap("dummy".getBytes()), true);
} catch (IllegalArgumentException e) {
// Expected.
}
// 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);
try {
// Attempt to write data for GET request.
stream.write(ByteBuffer.wrap("dummy".getBytes()), true);
} catch (IllegalArgumentException e) {
// Expected.
}
}
};
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();
assertTrue(stream.isDone());
// Flush after stream is completed is no-op. It shouldn't call into the destroyed
// adapter.
stream.flush();
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals("", callback.mResponseAsString);
assertEquals("bar", callback.mResponseInfo.getAllHeaders().get("echo-foo").get(0));
assertEquals("", callback.mResponseInfo.getAllHeaders().get("echo-empty").get(0));
}
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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();
assertTrue(stream.isDone());
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals("Test String", callback.mResponseAsString);
assertEquals("bar", callback.mResponseInfo.getAllHeaders().get("echo-foo").get(0));
assertEquals("", callback.mResponseInfo.getAllHeaders().get("echo-empty").get(0));
assertEquals("zebra",
callback.mResponseInfo.getAllHeaders().get("echo-content-type").get(0));
}
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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();
assertTrue(stream.isDone());
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals("Test String1234567890woot!Test String1234567890woot!",
callback.mResponseAsString);
assertEquals("bar", callback.mResponseInfo.getAllHeaders().get("echo-foo").get(0));
assertEquals("", callback.mResponseInfo.getAllHeaders().get("echo-empty").get(0));
assertEquals("zebra",
callback.mResponseInfo.getAllHeaders().get("echo-content-type").get(0));
}
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
// 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();
assertTrue(stream.isDone());
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals("Test String1234567890woot!", callback.mResponseAsString);
assertEquals("bar", callback.mResponseInfo.getAllHeaders().get("echo-foo").get(0));
assertEquals("", callback.mResponseInfo.getAllHeaders().get("echo-empty").get(0));
assertEquals(
"zebra", callback.mResponseInfo.getAllHeaders().get("echo-content-type").get(0));
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
// 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);
assertEquals(0, numPendingWrites());
}
};
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();
assertTrue(stream.isDone());
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals(
"Test String1234567890woot!Test String1234567890woot!", callback.mResponseAsString);
assertEquals("bar", callback.mResponseInfo.getAllHeaders().get("echo-foo").get(0));
assertEquals("", callback.mResponseInfo.getAllHeaders().get("echo-empty").get(0));
assertEquals(
"zebra", callback.mResponseInfo.getAllHeaders().get("echo-content-type").get(0));
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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();
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals("Put This Data!", callback.mResponseAsString);
assertEquals(methodName, callback.mResponseInfo.getAllHeaders().get("echo-method").get(0));
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
public void testBadMethod() throws Exception {
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
BidirectionalStream.Builder builder = mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getServerUrl(), callback, callback.getExecutor());
try {
builder.setHttpMethod("bad:method!");
builder.build().start();
fail("IllegalArgumentException not thrown.");
} catch (IllegalArgumentException e) {
assertEquals("Invalid http method bad:method!", e.getMessage());
}
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
public void testBadHeaderName() throws Exception {
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
BidirectionalStream.Builder builder = mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getServerUrl(), callback, callback.getExecutor());
try {
builder.addHeader("goodheader1", "headervalue");
builder.addHeader("header:name", "headervalue");
builder.addHeader("goodheader2", "headervalue");
builder.build().start();
fail("IllegalArgumentException not thrown.");
} catch (IllegalArgumentException e) {
assertEquals("Invalid header header:name=headervalue", e.getMessage());
}
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
public void testBadHeaderValue() throws Exception {
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback();
BidirectionalStream.Builder builder = mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getServerUrl(), callback, callback.getExecutor());
try {
builder.addHeader("headername", "bad header\r\nvalue");
builder.build().start();
fail("IllegalArgumentException not thrown.");
} catch (IllegalArgumentException e) {
assertEquals("Invalid header headername=bad header\r\nvalue", e.getMessage());
}
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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();
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals(headerValue, callback.mResponseAsString);
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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();
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
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));
}
assertEquals(1, actualValues.size());
assertEquals("header-value2", actualValues.get(0));
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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();
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertNotNull(callback.mTrailers);
// Verify that header value is properly echoed in trailers.
assertEquals(headerValue, callback.mTrailers.getAsMap().get("echo-" + headerName).get(0));
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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();
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals(userAgentValue, callback.mResponseAsString);
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
public void testCustomCronetEngineUserAgent() throws Exception {
String userAgentName = "User-Agent";
String userAgentValue = "User-Agent-Value";
ExperimentalCronetEngine.Builder engineBuilder =
new ExperimentalCronetEngine.Builder(getContext());
engineBuilder.setUserAgent(userAgentValue);
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();
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals(userAgentValue, callback.mResponseAsString);
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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();
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals(new CronetEngine.Builder(getContext()).getDefaultUserAgent(),
callback.mResponseAsString);
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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();
assertTrue(stream.isDone());
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals(stringData.toString(), callback.mResponseAsString);
assertEquals(
"Value with Spaces", callback.mResponseInfo.getAllHeaders().get("echo-foo").get(0));
assertEquals(
"zebra", callback.mResponseInfo.getAllHeaders().get("echo-content-type").get(0));
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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();
assertTrue(stream.isDone());
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals("", callback.mResponseAsString);
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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.
assertEquals(0, numPendingWrites());
}
};
callback.addWriteData("1".getBytes());
callback.addWriteData("2".getBytes());
// Create stream.
BidirectionalStream stream =
mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.build();
stream.start();
callback.blockForDone();
assertTrue(stream.isDone());
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals("12", callback.mResponseAsString);
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
public void testDoubleRead() throws Exception {
String url = Http2TestServer.getEchoStreamUrl();
TestBidirectionalStreamCallback callback = new TestBidirectionalStreamCallback() {
@Override
public void onResponseHeadersReceived(
BidirectionalStream stream, UrlResponseInfo info) {
startNextRead(stream);
try {
// 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.
stream.read(ByteBuffer.allocateDirect(5));
fail("Exception is not thrown.");
} catch (Exception e) {
assertEquals("Unexpected read attempt.", e.getMessage());
}
}
};
callback.addWriteData("1".getBytes());
callback.addWriteData("2".getBytes());
// Create stream.
BidirectionalStream stream =
mCronetEngine.newBidirectionalStreamBuilder(url, callback, callback.getExecutor())
.build();
stream.start();
callback.blockForDone();
assertTrue(stream.isDone());
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals("12", callback.mResponseAsString);
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
@DisabledTest(message = "Disabled due to timeout. See crbug.com/591112")
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.
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.waitForNextReadStep();
callback.startNextRead(stream);
callback.setAutoAdvance(true);
callback.blockForDone();
assertTrue(stream.isDone());
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals("12", callback.mResponseAsString);
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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 (String expected : testData) {
// Write next chunk of test data.
callback.startNextWrite(stream);
callback.waitForNextWriteStep(); // onWriteCompleted
}
// Wait for read step, but don't read yet.
callback.waitForNextReadStep(); // onResponseHeadersReceived
assertEquals("", callback.mResponseAsString);
// Read back.
callback.startNextRead(stream);
callback.waitForNextReadStep(); // onReadCompleted
// Verify that some part of proper response is read.
assertTrue(callback.mResponseAsString.startsWith(testData[0]));
assertTrue(stringData.toString().startsWith(callback.mResponseAsString));
// Read the rest of the response.
callback.setAutoAdvance(true);
callback.startNextRead(stream);
callback.blockForDone();
assertTrue(stream.isDone());
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals(stringData.toString(), callback.mResponseAsString);
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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();
assertEquals(expected.length(), readBuffer.position());
assertFalse(stream.isDone());
}
callback.setAutoAdvance(true);
callback.startNextRead(stream);
callback.blockForDone();
assertTrue(stream.isDone());
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals(stringData.toString(), callback.mResponseAsString);
}
/**
* Checks that the buffer is updated correctly, when starting at an offset.
*/
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
public void testSimpleGetBufferUpdates() throws Exception {
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();
assertEquals(null, callback.mError);
assertFalse(callback.isDone());
assertEquals(TestBidirectionalStreamCallback.ResponseStep.ON_RESPONSE_STARTED,
callback.mResponseStep);
ByteBuffer readBuffer = ByteBuffer.allocateDirect(5);
readBuffer.put("FOR".getBytes());
assertEquals(3, readBuffer.position());
// 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) {
assertFalse(callback.isDone());
callback.startNextRead(stream, readBuffer);
callback.waitForNextReadStep();
}
// Make sure the two characters were read.
assertEquals("GE", callback.mResponseAsString);
// 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.
assertEquals("FORGE", bufferContentsToString(readBuffer, 0, 5));
// The limit and position should be 5.
assertEquals(5, readBuffer.limit());
assertEquals(5, readBuffer.position());
assertEquals(ResponseStep.ON_READ_COMPLETED, callback.mResponseStep);
// 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.
assertEquals("GET", callback.mResponseAsString);
// Check the entire contents of the buffer. Only the third character
// should have been modified.
assertEquals("FORTE", bufferContentsToString(readBuffer, 0, 5));
// Make sure position and limit were updated correctly.
assertEquals(4, readBuffer.position());
assertEquals(5, readBuffer.limit());
assertEquals(ResponseStep.ON_READ_COMPLETED, callback.mResponseStep);
// One more read attempt. The request should complete.
readBuffer.position(1);
readBuffer.limit(5);
callback.setAutoAdvance(true);
callback.startNextRead(stream, readBuffer);
callback.blockForDone();
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals("GET", callback.mResponseAsString);
checkResponseInfo(callback.mResponseInfo, Http2TestServer.getEchoMethodUrl(), 200, "");
// Check that buffer contents were not modified.
assertEquals("FORTE", bufferContentsToString(readBuffer, 0, 5));
// Position should not have been modified, since nothing was read.
assertEquals(1, readBuffer.position());
// Limit should be unchanged as always.
assertEquals(5, readBuffer.limit());
assertEquals(ResponseStep.ON_SUCCEEDED, callback.mResponseStep);
// 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.
runSimpleGetWithExpectedReceivedByteCount(27);
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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();
assertEquals(null, callback.mError);
assertFalse(callback.isDone());
assertEquals(TestBidirectionalStreamCallback.ResponseStep.ON_RESPONSE_STARTED,
callback.mResponseStep);
// Try to read using a full buffer.
try {
ByteBuffer readBuffer = ByteBuffer.allocateDirect(4);
readBuffer.put("full".getBytes());
stream.read(readBuffer);
fail("Exception not thrown");
} catch (IllegalArgumentException e) {
assertEquals("ByteBuffer is already full.", e.getMessage());
}
// Try to read using a non-direct buffer.
try {
ByteBuffer readBuffer = ByteBuffer.allocate(5);
stream.read(readBuffer);
fail("Exception not thrown");
} catch (Exception e) {
assertEquals("byteBuffer must be a direct ByteBuffer.", e.getMessage());
}
// Finish the stream with a direct ByteBuffer.
callback.setAutoAdvance(true);
ByteBuffer readBuffer = ByteBuffer.allocateDirect(5);
stream.read(readBuffer);
callback.blockForDone();
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals("GET", callback.mResponseAsString);
}
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(getContext());
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();
assertTrue(stream.isDone());
requestFinishedListener.blockUntilDone();
Date endTime = new Date();
RequestFinishedInfo finishedInfo = requestFinishedListener.getRequestInfo();
RequestFinishedInfo.Metrics metrics = finishedInfo.getMetrics();
assertNotNull(metrics);
// 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) {
assertNotNull(callback.mResponseInfo);
}
// 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);
assertTrue(metrics.getSentByteCount() > 0);
assertTrue(metrics.getReceivedByteCount() > 0);
} else if (failureStep == ResponseStep.ON_STREAM_READY) {
assertNotNull(metrics.getRequestStart());
MetricsTestUtil.assertAfter(metrics.getRequestStart(), startTime);
assertNotNull(metrics.getRequestEnd());
MetricsTestUtil.assertAfter(endTime, metrics.getRequestEnd());
MetricsTestUtil.assertAfter(metrics.getRequestEnd(), metrics.getRequestStart());
}
assertEquals(expectError, callback.mError != null);
assertEquals(expectError, callback.mOnErrorCalled);
if (expectError) {
assertNotNull(finishedInfo.getException());
assertEquals(RequestFinishedInfo.FAILED, finishedInfo.getFinishedReason());
} else {
assertNull(finishedInfo.getException());
assertEquals(RequestFinishedInfo.CANCELED, finishedInfo.getFinishedReason());
}
assertEquals(failureType == FailureType.CANCEL_SYNC
|| failureType == FailureType.CANCEL_ASYNC
|| failureType == FailureType.CANCEL_ASYNC_WITHOUT_PAUSE,
callback.mOnCanceledCalled);
mCronetEngine.removeRequestFinishedListener(requestFinishedListener);
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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
@Feature({"Cronet"})
@OnlyRunNativeCronet
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();
assertEquals(callback.mResponseStep, ResponseStep.ON_SUCCEEDED);
assertTrue(stream.isDone());
assertNotNull(callback.mResponseInfo);
// Check that error thrown from 'onSucceeded' callback is not reported.
assertNull(callback.mError);
assertFalse(callback.mOnErrorCalled);
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
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();
assertFalse(callback.isDone());
assertFalse(stream.isDone());
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();
assertFalse(callback.isDone());
assertTrue(stream.isDone());
}
/**
* Callback that shuts down the engine when the stream has succeeded
* or failed.
*/
private class ShutdownTestBidirectionalStreamCallback extends TestBidirectionalStreamCallback {
@Override
public void onSucceeded(BidirectionalStream stream, UrlResponseInfo info) {
mCronetEngine.shutdown();
// Clear mCronetEngine so it doesn't get shut down second time in tearDown().
mCronetEngine = null;
super.onSucceeded(stream, info);
}
@Override
public void onFailed(
BidirectionalStream stream, UrlResponseInfo info, CronetException error) {
mCronetEngine.shutdown();
// Clear mCronetEngine so it doesn't get shut down second time in tearDown().
mCronetEngine = null;
super.onFailed(stream, info, error);
}
@Override
public void onCanceled(BidirectionalStream stream, UrlResponseInfo info) {
mCronetEngine.shutdown();
// Clear mCronetEngine so it doesn't get shut down second time in tearDown().
mCronetEngine = null;
super.onCanceled(stream, info);
}
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
public void testCronetEngineShutdown() throws Exception {
// Test that CronetEngine cannot be shut down if there are any active streams.
TestBidirectionalStreamCallback callback = new ShutdownTestBidirectionalStreamCallback();
// 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());
CronetBidirectionalStream stream =
(CronetBidirectionalStream) builder.setHttpMethod("GET").build();
stream.start();
try {
mCronetEngine.shutdown();
fail("Should throw an exception");
} catch (Exception e) {
assertEquals("Cannot shutdown with active requests.", e.getMessage());
}
callback.waitForNextReadStep();
assertEquals(ResponseStep.ON_RESPONSE_STARTED, callback.mResponseStep);
try {
mCronetEngine.shutdown();
fail("Should throw an exception");
} catch (Exception e) {
assertEquals("Cannot shutdown with active requests.", e.getMessage());
}
callback.startNextRead(stream);
callback.waitForNextReadStep();
assertEquals(ResponseStep.ON_READ_COMPLETED, callback.mResponseStep);
try {
mCronetEngine.shutdown();
fail("Should throw an exception");
} catch (Exception e) {
assertEquals("Cannot shutdown with active requests.", e.getMessage());
}
// 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();
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
public void testCronetEngineShutdownAfterStreamFailure() throws Exception {
// Test that CronetEngine can be shut down after stream reports a failure.
TestBidirectionalStreamCallback callback = new ShutdownTestBidirectionalStreamCallback();
BidirectionalStream.Builder builder = mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor());
CronetBidirectionalStream stream =
(CronetBidirectionalStream) builder.setHttpMethod("GET").build();
stream.start();
callback.setFailure(FailureType.THROW_SYNC, ResponseStep.ON_READ_COMPLETED);
callback.blockForDone();
assertTrue(callback.mOnErrorCalled);
assertNull(mCronetEngine);
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
public void testCronetEngineShutdownAfterStreamCancel() throws Exception {
// Test that CronetEngine can be shut down after stream is canceled.
TestBidirectionalStreamCallback callback = new ShutdownTestBidirectionalStreamCallback();
BidirectionalStream.Builder builder = mCronetEngine.newBidirectionalStreamBuilder(
Http2TestServer.getEchoMethodUrl(), callback, callback.getExecutor());
CronetBidirectionalStream stream =
(CronetBidirectionalStream) builder.setHttpMethod("GET").build();
// Block callback when response starts to verify that shutdown fails
// if there are active requests.
callback.setAutoAdvance(false);
stream.start();
try {
mCronetEngine.shutdown();
fail("Should throw an exception");
} catch (Exception e) {
assertEquals("Cannot shutdown with active requests.", e.getMessage());
}
callback.waitForNextReadStep();
assertEquals(ResponseStep.ON_RESPONSE_STARTED, callback.mResponseStep);
stream.cancel();
callback.blockForDone();
assertTrue(callback.mOnCanceledCalled);
assertNull(mCronetEngine);
}
/*
* Verifies NetworkException constructed from specific error codes are retryable.
*/
@SmallTest
@Feature({"Cronet"})
@Test
@OnlyRunNativeCronet
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_SPDY_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);
assertEquals(immediatelyRetryable, exception.immediatelyRetryable());
assertEquals(netError, exception.getCronetInternalErrorCode());
assertEquals(errorCode, exception.getErrorCode());
}
@Test
@SmallTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
@RequiresMinApi(10) // Tagging support added in API level 10: crrev.com/c/chromium/src/+/937583
public void testTagging() throws Exception {
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();
assertTrue(CronetTestUtil.nativeGetTaggedBytes(tag) > 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());
assertEquals(builder.setTrafficStatsTag(tag), builder);
builder.build().start();
callback.blockForDone();
assertTrue(CronetTestUtil.nativeGetTaggedBytes(tag) > 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());
assertEquals(builder.setTrafficStatsTag(tag), builder);
builder.build().start();
callback.blockForDone();
assertTrue(CronetTestUtil.nativeGetTaggedBytes(tag) > 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());
assertEquals(builder.setTrafficStatsUid(Process.myUid()), builder);
builder.build().start();
callback.blockForDone();
assertTrue(CronetTestUtil.nativeGetTaggedBytes(tag) > priorBytes);
}
}