Commit 3ca8ecbc authored by Ben Kirchman's avatar Ben Kirchman Committed by Commit Bot

Create Fake Cronet API: FakeCronetController.

FakeCronetController is used to create fake CronetEngines and control
responses to UrlRequests that the fake CronetEngine makes. Primarily,
the test should instantiate a FakeCronetController and from that
instance generate CronetEngine.Builders which will use their
FakeCronetController for responses once they are built.

The FakeCronetController can also be used statically to retrieve a
FakeCrontroller that is created by the production code, such as via
FakeCronetProvider. To do this, it maintains a static list of all fake
CronetEngines in order of creation. This list can be retrieved and the
desired CronetEngine can be passed back to the FakeCronetController's
static method getControllerForFakeEngine() in order for the test to get a
pointer to the controller for it's production code's CronetEngine.

It should be noted that a CronetEngine.Builder can build multiple fake
CronetEngines which will all point to the same controller. This is also
true for multiple calls of getFakeCronetEngineBuilder: the created
CronetEngines will all use the same controller.

Design Doc: https://docs.google.com/document/d/1IsBwiqR5f6ksvmHPq5skV802C02We8zjK-c552Tp4lE/edit#heading=h.7nki9mck5t64

Bug: 669707
Change-Id: I8955a97e4a4a6dd8040b5b30fea3c5cbeae30221
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1701534
Commit-Queue: Benjamin Kirchman <kirchman@google.com>
Reviewed-by: default avatarPaul Jensen <pauljensen@chromium.org>
Cr-Commit-Position: refs/heads/master@{#679983}
parent 33f17dcb
......@@ -339,6 +339,7 @@ android_library("cronet_impl_platform_base_java") {
android_library("cronet_impl_fake_base_java") {
java_files = [
"fake/java/org/chromium/net/test/FakeUrlResponse.java",
"fake/java/org/chromium/net/test/FakeCronetController.java",
"fake/java/org/chromium/net/test/FakeCronetEngine.java",
"fake/java/org/chromium/net/test/FakeCronetProvider.java",
"fake/java/org/chromium/net/test/ResponseMatcher.java",
......@@ -358,6 +359,7 @@ android_library("cronet_fake_javatests") {
"fake/javatests/org/chromium/net/test/FakeUrlResponseTest.java",
"fake/javatests/org/chromium/net/test/FakeCronetProviderTest.java",
"fake/javatests/org/chromium/net/test/FakeCronetEngineTest.java",
"fake/javatests/org/chromium/net/test/FakeCronetControllerTest.java",
"fake/javatests/org/chromium/net/test/UrlResponseMatcherTest.java",
]
......@@ -365,6 +367,7 @@ android_library("cronet_fake_javatests") {
":cronet_api_java",
":cronet_impl_common_base_java",
":cronet_impl_fake_base_java",
":cronet_impl_platform_base_java",
"//base:base_java_test_support",
"//third_party/android_sdk:android_test_base_java",
"//third_party/android_support_test_runner:runner_java",
......
// Copyright 2019 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.test;
import android.content.Context;
import org.chromium.net.CronetEngine;
import org.chromium.net.ExperimentalCronetEngine;
import org.chromium.net.UrlRequest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Controller for fake Cronet implementation. Allows a test to setup responses for
* {@link UrlRequest}s. If multiple {@link ResponseMatcher}s match a specific request, the first
* {@link ResponseMatcher} added takes precedence.
*/
public final class FakeCronetController {
// List of FakeCronetEngines so that FakeCronetEngine can be accessed when created with
// the {@link FakeCronetProvider}.
private static final List<CronetEngine> sInstances =
Collections.synchronizedList(new ArrayList<>());
// List of ResponseMatchers to be checked for a response to a request in place of a server.
private final List<ResponseMatcher> mResponseMatchers =
Collections.synchronizedList(new ArrayList<>());
/**
* Creates a fake {@link CronetEngine.Builder} that creates {@link CronetEngine}s that return
* fake {@link UrlRequests}. Once built, the {@link CronetEngine}'s {@link UrlRequest}s will
* retrieve responses from this {@link FakeCronetController}.
*
* @param context the Android context to build the fake {@link CronetEngine} from.
* @return a fake CronetEngine.Builder that uses this {@link FakeCronetController} to manage
* responses once it is built.
*/
public CronetEngine.Builder newFakeCronetEngineBuilder(Context context) {
FakeCronetEngine.Builder builder = new FakeCronetEngine.Builder(context);
builder.setController(this);
// FakeCronetEngine.Builder is not actually a CronetEngine.Builder, so construct one with
// the child of CronetEngine.Builder: ExperimentalCronetEngine.Builder.
return new ExperimentalCronetEngine.Builder(builder);
}
/**
* Adds a {@link UrlResponseMatcher} that will respond to the provided URL with the provided
* {@link FakeUrlResponse}. Equivalent to:
* addResponseMatcher(new UrlResponseMatcher(url, response)).
*
* @param response a {@link FakeUrlResponse} to respond with
* @param url a url for which the response should be returned
*/
public void addResponseForUrl(FakeUrlResponse response, String url) {
addResponseMatcher(new UrlResponseMatcher(url, response));
}
/**
* Adds a {@link ResponseMatcher} to the list of {@link ResponseMatcher}s.
*
* @param matcher the {@link ResponseMatcher} that should be matched against a request
*/
public void addResponseMatcher(ResponseMatcher matcher) {
mResponseMatchers.add(matcher);
}
/**
* Removes a specific {@link ResponseMatcher} from the list of {@link ResponseMatcher}s.
*
* @param matcher the {@link ResponseMatcher} to remove
*/
public void removeResponseMatcher(ResponseMatcher matcher) {
mResponseMatchers.remove(matcher);
}
/**
* Removes all {@link ResponseMatcher}s from the list of {@link ResponseMatcher}s.
*/
public void clearResponseMatchers() {
mResponseMatchers.clear();
}
/**
* Adds a {@link FakeUrlResponse} to the list of responses that will redirect a
* {@link UrlRequest} to the specified URL.
*
* @param redirectLocation the URL to redirect the {@link UrlRequest} to
* @param url the URL that will trigger the redirect
*/
public void addRedirectResponse(String redirectLocation, String url) {
FakeUrlResponse redirectResponse = new FakeUrlResponse.Builder()
.setHttpStatusCode(302)
.addHeader("location", redirectLocation)
.build();
addResponseForUrl(redirectResponse, url);
}
/**
* Adds an {@link FakeUrlResponse} that fails with the specified HTTP code for the specified
* URL.
*
* @param statusCode the code for the {@link FakeUrlResponse}
* @param url the URL that should trigger the error response when requested by a
* {@link UrlRequest}
* @throws IllegalArgumentException if the HTTP status code is not an error code (code >= 400)
*/
public void addHttpErrorResponse(int statusCode, String url) {
addResponseForUrl(getFailedResponse(statusCode), url);
}
// TODO(kirchman): Create a function to add a response that takes a CronetException.
/**
* Adds a successful 200 code {@link FakeUrlResponse} that will match the specified
* URL when requested by a {@link UrlRequest}.
*
* @param url the URL that triggers the successful response
*/
public void addSuccessResponse(String url, String body) {
addResponseForUrl(new FakeUrlResponse.Builder().setResponseBody(body).build(), url);
}
/**
* Returns the {@link CronetEngineController} for a specified {@link CronetEngine}. This method
* should be used in conjunction with {@link FakeCronetController.getInstances}.
*
* @param engine the fake {@link CronetEngine} to get the controller for.
* @return the controller for the specified fake {@link CronetEngine}.
*/
public static FakeCronetController getControllerForFakeEngine(CronetEngine engine) {
if (engine instanceof FakeCronetEngine) {
FakeCronetEngine fakeEngine = (FakeCronetEngine) engine;
return fakeEngine.getController();
}
throw new IllegalArgumentException("Provided CronetEngine is not a fake CronetEngine");
}
/**
* Returns all created fake instances of {@link CronetEngine} that have not been shut down with
* {@link CronetEngine.shutdown()} in order of creation. Can be used to retrieve a controller
* in conjunction with {@link FakeCronetController.getControllerForFakeEngine}.
*
* @return a list of all fake {@link CronetEngine}s that have been created
*/
public static List<CronetEngine> getFakeCronetEngines() {
synchronized (sInstances) {
return new ArrayList<>(sInstances);
}
}
/**
* Removes a fake {@link CronetEngine} from the list of {@link CronetEngine} instances.
*
* @param cronetEngine the instance to remove
*/
static void removeFakeCronetEngine(CronetEngine cronetEngine) {
sInstances.remove(cronetEngine);
}
/**
* Add a CronetEngine to the list of CronetEngines.
*
* @param engine the {@link CronetEngine} to add
*/
static void addFakeCronetEngine(FakeCronetEngine engine) {
sInstances.add(engine);
}
/**
* Gets a response for specified request details if there is one, or a "404" failed response
* if there is no {@link ResponseMatcher} with a {@link FakeUrlResponse} for this request.
*
* @param url the URL that the {@link UrlRequest} is connecting to
* @param httpMethod the HTTP method that the {@link UrlRequest} is using to connect with
* @param headers the headers supplied by the {@link UrlRequest}
* @return a {@link FakeUrlResponse} if there is one, or a failed "404" response if none found
*/
FakeUrlResponse getResponse(
String url, String httpMethod, List<Map.Entry<String, String>> headers) {
synchronized (mResponseMatchers) {
for (ResponseMatcher responseMatcher : mResponseMatchers) {
FakeUrlResponse matchedResponse =
responseMatcher.getMatchingResponse(url, httpMethod, headers);
if (matchedResponse != null) {
return matchedResponse;
}
}
}
return getFailedResponse(404);
}
/**
* Creates and returns a failed response with the specified HTTP status code.
*
* @param statusCode the HTTP code that the returned response will have
* @return a {@link FakeUrlResponse} with the specified code
*/
private static FakeUrlResponse getFailedResponse(int statusCode) {
if (statusCode < 400) {
throw new IllegalArgumentException(
"Expected HTTP error code (code >= 400), but was: " + statusCode);
}
return new FakeUrlResponse.Builder().setHttpStatusCode(statusCode).build();
}
}
......@@ -37,6 +37,8 @@ final class FakeCronetEngine extends CronetEngineBase {
* Builds a {@link FakeCronetEngine}. This implements CronetEngine.Builder.
*/
static class Builder extends CronetEngineBuilderImpl {
private FakeCronetController mController;
/**
* Builder for {@link FakeCronetEngine}.
*
......@@ -50,8 +52,14 @@ final class FakeCronetEngine extends CronetEngineBase {
public FakeCronetEngine build() {
return new FakeCronetEngine(this);
}
void setController(FakeCronetController controller) {
mController = controller;
}
}
private final FakeCronetController mController;
private final Object mLock = new Object();
@GuardedBy("mLock")
......@@ -67,8 +75,23 @@ final class FakeCronetEngine extends CronetEngineBase {
* @param builder a {@link CronetEngineBuilderImpl} to build this {@link CronetEngine}
* implementation from.
*/
private FakeCronetEngine(CronetEngineBuilderImpl builder) {
// TODO(kirchman): Retrieve fields from the builder necessary for a URL request.
private FakeCronetEngine(FakeCronetEngine.Builder builder) {
if (builder.mController != null) {
mController = builder.mController;
} else {
mController = new FakeCronetController();
}
FakeCronetController.addFakeCronetEngine(this);
}
/**
* Gets the controller associated with this instance that will be used for responses to
* {@link UrlRequest}s.
*
* @return the {@link FakeCronetCntroller} that controls this {@link FakeCronetEngine}.
*/
FakeCronetController getController() {
return mController;
}
@Override
......@@ -100,6 +123,7 @@ final class FakeCronetEngine extends CronetEngineBase {
mIsShutdown = true;
}
}
FakeCronetController.removeFakeCronetEngine(this);
}
@Override
......
// Copyright 2019 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.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.net.CronetEngine;
import org.chromium.net.impl.JavaCronetEngineBuilderImpl;
import java.util.AbstractMap;
import java.util.List;
import java.util.Map;
/**
* Test functionality of {@link FakeCronetController}.
*/
@RunWith(AndroidJUnit4.class)
public class FakeCronetControllerTest {
Context mContext;
FakeCronetController mFakeCronetController;
@Before
public void setUp() {
mContext = InstrumentationRegistry.getTargetContext();
mFakeCronetController = new FakeCronetController();
}
@Test
@SmallTest
public void testGetFakeCronetEnginesStartsEmpty() {
List<CronetEngine> engines = FakeCronetController.getFakeCronetEngines();
assertEquals(0, engines.size());
}
@Test
@SmallTest
public void testGetFakeCronetEnginesIncludesCreatedEngineInOrder() {
// Create an instance with the controller.
CronetEngine engine = mFakeCronetController.newFakeCronetEngineBuilder(mContext).build();
// Create an instance with the provider.
FakeCronetProvider provider = new FakeCronetProvider(mContext);
CronetEngine providerEngine = provider.createBuilder().build();
List<CronetEngine> engines = FakeCronetController.getFakeCronetEngines();
assertTrue(engines.contains(engine));
assertTrue(engines.contains(providerEngine));
assertEquals(engine, engines.get(0));
assertEquals(providerEngine, engines.get(1));
}
@Test
@SmallTest
public void testGetControllerGetsCorrectController() {
// Create an instance with the controller.
CronetEngine engine = mFakeCronetController.newFakeCronetEngineBuilder(mContext).build();
CronetEngine engine2 = mFakeCronetController.newFakeCronetEngineBuilder(mContext).build();
// Create two engines with a second controller.
FakeCronetController newController = new FakeCronetController();
CronetEngine newControllerEngine =
newController.newFakeCronetEngineBuilder(mContext).build();
CronetEngine newControllerEngine2 =
newController.newFakeCronetEngineBuilder(mContext).build();
// Create an instance with the provider.
FakeCronetProvider provider = new FakeCronetProvider(mContext);
CronetEngine providerEngine = provider.createBuilder().build();
assertEquals(
mFakeCronetController, FakeCronetController.getControllerForFakeEngine(engine));
assertEquals(
mFakeCronetController, FakeCronetController.getControllerForFakeEngine(engine2));
assertEquals(newController,
FakeCronetController.getControllerForFakeEngine(newControllerEngine));
assertEquals(newController,
FakeCronetController.getControllerForFakeEngine(newControllerEngine2));
// TODO(kirchman): Test which controller the provider-created engine uses once the fake
// UrlRequest class has been implemented.
assertNotEquals(mFakeCronetController,
FakeCronetController.getControllerForFakeEngine(providerEngine));
assertNotEquals(
newController, FakeCronetController.getControllerForFakeEngine(providerEngine));
assertNotNull(FakeCronetController.getControllerForFakeEngine(providerEngine));
}
@Test
@SmallTest
public void testAddNonFakeCronetEngineNotAllowed() {
CronetEngine javaEngine = new JavaCronetEngineBuilderImpl(mContext).build();
try {
FakeCronetController.getControllerForFakeEngine(javaEngine);
fail("Should not be able to get a controller for a non-fake CronetEngine.");
} catch (IllegalArgumentException e) {
assertEquals("Provided CronetEngine is not a fake CronetEngine", e.getMessage());
}
}
@Test
@SmallTest
public void testShutdownRemovesCronetEngine() {
CronetEngine engine = mFakeCronetController.newFakeCronetEngineBuilder(mContext).build();
CronetEngine engine2 = mFakeCronetController.newFakeCronetEngineBuilder(mContext).build();
List<CronetEngine> engines = FakeCronetController.getFakeCronetEngines();
assertTrue(engines.contains(engine));
assertTrue(engines.contains(engine2));
engine.shutdown();
engines = FakeCronetController.getFakeCronetEngines();
assertFalse(engines.contains(engine));
assertTrue(engines.contains(engine2));
}
@Test
@SmallTest
public void testResponseMatchersConsultedInOrderOfAddition() {
String url = "url";
FakeUrlResponse response =
new FakeUrlResponse.Builder().setResponseBody("body text").build();
ResponseMatcher matcher = new UrlResponseMatcher(url, response);
mFakeCronetController.addResponseMatcher(matcher);
mFakeCronetController.addSuccessResponse(url, "different text");
FakeUrlResponse foundResponse =
mFakeCronetController.getResponse(new String(url), null, null);
assertEquals(response, foundResponse);
}
@Test
@SmallTest
public void testRemoveResponseMatcher() {
String url = "url";
FakeUrlResponse response =
new FakeUrlResponse.Builder().setResponseBody("body text").build();
ResponseMatcher matcher = new UrlResponseMatcher(url, response);
mFakeCronetController.addResponseMatcher(matcher);
mFakeCronetController.removeResponseMatcher(matcher);
FakeUrlResponse foundResponse = mFakeCronetController.getResponse(url, null, null);
assertEquals(404, foundResponse.getHttpStatusCode());
assertNotEquals(response, foundResponse);
}
@Test
@SmallTest
public void testClearResponseMatchers() {
String url = "url";
FakeUrlResponse response =
new FakeUrlResponse.Builder().setResponseBody("body text").build();
ResponseMatcher matcher = new UrlResponseMatcher(url, response);
mFakeCronetController.addResponseMatcher(matcher);
mFakeCronetController.clearResponseMatchers();
FakeUrlResponse foundResponse = mFakeCronetController.getResponse(url, null, null);
assertEquals(404, foundResponse.getHttpStatusCode());
assertNotEquals(response, foundResponse);
}
@Test
@SmallTest
public void testAddUrlResponseMatcher() {
String url = "url";
FakeUrlResponse response =
new FakeUrlResponse.Builder().setResponseBody("body text").build();
mFakeCronetController.addResponseForUrl(response, url);
FakeUrlResponse foundResponse = mFakeCronetController.getResponse(url, null, null);
assertEquals(foundResponse, response);
}
@Test
@SmallTest
public void testDefaultResponseIs404() {
FakeUrlResponse foundResponse = mFakeCronetController.getResponse("url", null, null);
assertEquals(404, foundResponse.getHttpStatusCode());
}
@Test
@SmallTest
public void testAddRedirectResponse() {
String url = "url";
String location = "/TEST_REDIRECT_LOCATION";
mFakeCronetController.addRedirectResponse(location, url);
FakeUrlResponse foundResponse = mFakeCronetController.getResponse("url", null, null);
Map.Entry<String, String> headerEntry = new AbstractMap.SimpleEntry<>("location", location);
assertTrue(foundResponse.getAllHeadersList().contains(headerEntry));
assertTrue(foundResponse.getHttpStatusCode() >= 300);
assertTrue(foundResponse.getHttpStatusCode() < 400);
}
@Test
@SmallTest
public void testAddErrorResponse() {
String url = "url";
int httpStatusCode = 400;
mFakeCronetController.addHttpErrorResponse(httpStatusCode, url);
FakeUrlResponse foundResponse = mFakeCronetController.getResponse(url, null, null);
assertEquals(foundResponse.getHttpStatusCode(), httpStatusCode);
}
@Test
@SmallTest
public void testAddErrorResponseWithNonErrorCodeThrowsException() {
int nonErrorCode = 200;
try {
mFakeCronetController.addHttpErrorResponse(nonErrorCode, "url");
fail("Should not be able to add an error response with a non-error code.");
} catch (IllegalArgumentException e) {
assertEquals("Expected HTTP error code (code >= 400), but was: " + nonErrorCode,
e.getMessage());
}
}
@Test
@SmallTest
public void testAddSuccessResponse() {
String url = "url";
String body = "TEST_BODY";
mFakeCronetController.addSuccessResponse(url, body);
FakeUrlResponse foundResponse = mFakeCronetController.getResponse(url, null, null);
assertTrue(foundResponse.getHttpStatusCode() >= 200);
assertTrue(foundResponse.getHttpStatusCode() < 300);
assertEquals(body, foundResponse.getResponseBody());
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment