First pass at a functional test for the Download Manager.
Steve Howard [Thu, 10 Jun 2010 00:50:07 +0000 (17:50 -0700)]
This "Large" test sets up an HTTP server on the device using
MockWebServer and then initiates downloads from that server through
the download manager.  It uses ServiceTestCase to control the
construction and execution of the DownloadService, and it uses some
logic from ProviderTestCase2 to construct a DownloadProvider and a
ContentResolver that uses it.

This setup gives us some ability to mock dependencies.  This commit
includes use of a fake ConnectivityManager to test responses to
connectivity changes, and use of some customizations to MockWebServer
to test resuming an interrupted download.  This test is disabled,
though, since it requires a very long sleep.

Avoiding that, and achieving certain other things, will require
changes to the Download Manager code itself to introduce new seams.  I
wanted to check this in before I started such changes.

Change-Id: Iefb13b3c3cccdc13fabe5cc18703e13244805539

tests/Android.mk
tests/AndroidManifest.xml [new file with mode: 0644]
tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java [new file with mode: 0644]
tests/src/com/android/providers/downloads/FakeIConnectivityManager.java [new file with mode: 0644]
tests/src/tests/http/MockResponse.java
tests/src/tests/http/MockWebServer.java
tests/src/tests/http/RecordedRequest.java

index e9e3a87..80a1c76 100644 (file)
@@ -1,10 +1,17 @@
 LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
 
-########################
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
 
-include $(CLEAR_VARS)
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_INSTRUMENTATION_FOR := DownloadProvider
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_PACKAGE_NAME := DownloadProviderTests
+LOCAL_CERTIFICATE := media
 
-# no tests to build for now
+include $(BUILD_PACKAGE)
 
 # additionally, build sub-tests in a separate .apk
-include $(call all-makefiles-under,$(LOCAL_PATH))
\ No newline at end of file
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
new file mode 100644 (file)
index 0000000..4d971db
--- /dev/null
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2009 Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.providers.downloads.tests"
+          android:sharedUserId="android.media">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <!--
+    The test declared in this instrumentation can be run via this command
+    "adb shell am instrument -w com.android.providers.downloads/android.test.InstrumentationTestRunner"
+    -->
+    <instrumentation android:name="android.test.InstrumentationTestRunner"
+                     android:targetPackage="com.android.providers.downloads"
+                     android:label="Tests for Download Manager"/>
+
+</manifest>
diff --git a/tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java b/tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java
new file mode 100644 (file)
index 0000000..76b3d58
--- /dev/null
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.downloads;
+
+import android.content.ComponentName;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.provider.Downloads;
+import android.test.RenamingDelegatingContext;
+import android.test.ServiceTestCase;
+import android.test.mock.MockContentResolver;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import tests.http.MockResponse;
+import tests.http.MockWebServer;
+import tests.http.RecordedRequest;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.MalformedURLException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * This test exercises the entire download manager working together -- it requests downloads through
+ * the {@link DownloadProvider}, just like a normal client would, and runs the
+ * {@link DownloadService} with start intents.  It sets up a {@link MockWebServer} running on the
+ * device to serve downloads.
+ */
+@LargeTest
+public class DownloadManagerFunctionalTest extends ServiceTestCase<DownloadService> {
+    private static final int HTTP_PARTIAL_CONTENT = 206;
+    private static final String PROVIDER_AUTHORITY = "downloads";
+    private static final int HTTP_OK = 200;
+    private static final String LOG_TAG = "DownloadManagerFunctionalTest";
+    private static final int HTTP_NOT_FOUND = 404;
+    private static final String FILE_CONTENT = "hello world";
+    private static final long REQUEST_TIMEOUT_MILLIS = 5000;
+
+    private MockWebServer mServer;
+    // resolves requests to the DownloadProvider we set up
+    private MockContentResolver mResolver;
+    private TestContext mTestContext;
+
+    /**
+     * Context passed to the provider and the service.  Allows most methods to pass through to the
+     * real Context (this is a LargeTest), with a few exceptions, including renaming file operations
+     * to avoid file and DB conflicts (via RenamingDelegatingContext).
+     */
+    private static class TestContext extends RenamingDelegatingContext {
+        private static final String FILENAME_PREFIX = "test.";
+
+        private Context mRealContext;
+        private Set<String> mAllowedSystemServices;
+        private ContentResolver mResolver;
+
+        boolean mHasServiceBeenStarted = false;
+        FakeIConnectivityManager mFakeIConnectivityManager;
+
+        public TestContext(Context realContext) {
+            super(realContext, FILENAME_PREFIX);
+            mRealContext = realContext;
+            mAllowedSystemServices = new HashSet<String>(Arrays.asList(new String[] {
+                    Context.NOTIFICATION_SERVICE,
+                    Context.POWER_SERVICE,
+            }));
+            mFakeIConnectivityManager = new FakeIConnectivityManager();
+        }
+
+        public void setResolver(ContentResolver resolver) {
+            mResolver = resolver;
+        }
+
+        /**
+         * Direct DownloadService to our test instance of DownloadProvider.
+         */
+        @Override
+        public ContentResolver getContentResolver() {
+            assert mResolver != null;
+            return mResolver;
+        }
+
+        /**
+         * Stub some system services, allow access to others, and block the rest.
+         */
+        @Override
+        public Object getSystemService(String name) {
+            if (name.equals(Context.CONNECTIVITY_SERVICE)) {
+                return new ConnectivityManager(mFakeIConnectivityManager);
+            }
+            if (mAllowedSystemServices.contains(name)) {
+                return mRealContext.getSystemService(name);
+            }
+            return super.getSystemService(name);
+        }
+
+        /**
+         * Record when DownloadProvider starts DownloadService.
+         */
+        @Override
+        public ComponentName startService(Intent service) {
+            if (service.getComponent().getClassName().equals(DownloadService.class.getName())) {
+                mHasServiceBeenStarted = true;
+                return service.getComponent();
+            }
+            throw new UnsupportedOperationException("Unexpected service: " + service);
+        }
+    }
+
+    public DownloadManagerFunctionalTest() {
+        super(DownloadService.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        Context realContext = getContext();
+        mTestContext = new TestContext(realContext);
+        setupProviderAndResolver();
+        assert isDatabaseEmpty(); // ensure we're not messing with real data
+
+        mTestContext.setResolver(mResolver);
+        setContext(mTestContext);
+
+        mServer = new MockWebServer();
+        mServer.play();
+    }
+
+    private void setupProviderAndResolver() {
+        ContentProvider provider = new DownloadProvider();
+        provider.attachInfo(mTestContext, null);
+        mResolver = new MockContentResolver();
+        mResolver.addProvider(PROVIDER_AUTHORITY, provider);
+    }
+
+    private boolean isDatabaseEmpty() {
+        Cursor cursor = mResolver.query(Downloads.CONTENT_URI, null, null, null, null);
+        try {
+            return cursor.getCount() == 0;
+        } finally {
+            cursor.close();
+        }
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        cleanUpDownloads();
+        super.tearDown();
+    }
+
+    /**
+     * Remove any downloaded files and delete any lingering downloads.
+     */
+    private void cleanUpDownloads() {
+        if (mResolver == null) {
+            return;
+        }
+        String[] columns = new String[] {Downloads._DATA};
+        Cursor cursor = mResolver.query(Downloads.CONTENT_URI, columns, null, null, null);
+        try {
+            for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
+                String filePath = cursor.getString(0);
+                if (filePath == null) continue;
+                Log.d(LOG_TAG, "Deleting " + filePath);
+                new File(filePath).delete();
+            }
+        } finally {
+            cursor.close();
+        }
+        mResolver.delete(Downloads.CONTENT_URI, null, null);
+    }
+
+    public void testBasicRequest() throws Exception {
+        enqueueResponse(HTTP_OK, FILE_CONTENT);
+
+        String path = "/download_manager_test_path";
+        Uri downloadUri = requestDownload(path);
+        assertEquals(Downloads.STATUS_PENDING, getDownloadStatus(downloadUri));
+        assertTrue(mTestContext.mHasServiceBeenStarted);
+
+        startService(null);
+
+        RecordedRequest request = takeRequest();
+        assertEquals("GET", request.getMethod());
+        assertEquals(path, request.getPath());
+
+        waitForDownloadToStop(downloadUri, Downloads.STATUS_SUCCESS);
+        assertEquals(FILE_CONTENT, getDownloadContents(downloadUri));
+        checkForUnexpectedRequests();
+    }
+
+    public void testFileNotFound() throws Exception {
+        enqueueEmptyResponse(HTTP_NOT_FOUND);
+        Uri downloadUri = requestDownload("/nonexistent_path");
+        assertEquals(Downloads.STATUS_PENDING, getDownloadStatus(downloadUri));
+
+        startService(null);
+        takeRequest();
+        waitForDownloadToStop(downloadUri, HTTP_NOT_FOUND);
+        checkForUnexpectedRequests();
+    }
+
+    public void testBasicConnectivityChanges() throws Exception {
+        enqueueResponse(HTTP_OK, FILE_CONTENT);
+        Uri downloadUri = requestDownload("/path");
+
+        // without connectivity, download immediately pauses
+        mTestContext.mFakeIConnectivityManager.setNetworkState(NetworkInfo.State.DISCONNECTED);
+        startService(null);
+        waitForDownloadToStop(downloadUri, Downloads.STATUS_RUNNING_PAUSED);
+        checkForUnexpectedRequests();
+
+        // connecting should start the download
+        mTestContext.mFakeIConnectivityManager.setNetworkState(NetworkInfo.State.CONNECTED);
+        startService(null); // normally done by DownloadReceiver
+        takeRequest();
+        waitForDownloadToStop(downloadUri, Downloads.STATUS_SUCCESS);
+        checkForUnexpectedRequests();
+    }
+
+    // disabled due to excessive sleep
+    public void disabledTestInterruptedDownload() throws Exception {
+        int initialLength = 5;
+        String etag = "my_etag";
+        int totalLength = FILE_CONTENT.length();
+        // the first response has normal headers but unexpectedly closes after initialLength bytes
+        enqueueResponse(HTTP_OK, FILE_CONTENT.substring(0, initialLength))
+                .addHeader("Content-length", totalLength)
+                .addHeader("Etag", etag)
+                .setCloseConnectionAfter(true);
+        Uri downloadUri = requestDownload("/path");
+
+        startService(null);
+        takeRequest();
+        waitForDownloadToStop(downloadUri, Downloads.STATUS_RUNNING_PAUSED);
+
+        Thread.sleep(61 * 1000); // TODO: avoid this by stubbing the system clock
+        mServer.drainRequests();
+        // the second response returns partial content for the rest of the data
+        enqueueResponse(HTTP_PARTIAL_CONTENT, FILE_CONTENT.substring(initialLength))
+                .addHeader("Content-range",
+                           "bytes " + initialLength + "-" + totalLength + "/" + totalLength)
+                .addHeader("Etag", etag);
+        // TODO: ideally we wouldn't need to call startService again, but there's a bug where the
+        // service won't retry a download until an intent comes in
+        startService(null);
+        waitForDownloadToStop(downloadUri, Downloads.STATUS_SUCCESS);
+
+        RecordedRequest request = takeRequest();
+        List<String> headers = request.getHeaders();
+        assertTrue("No Range header: " + headers,
+                   headers.contains("Range: bytes=" + initialLength + "-"));
+        assertTrue("No ETag header: " + headers, headers.contains("If-Match: " + etag));
+
+        assertEquals(FILE_CONTENT, getDownloadContents(downloadUri));
+        checkForUnexpectedRequests();
+    }
+
+    /**
+     * Enqueue a response from the MockWebServer.
+     */
+    private MockResponse enqueueResponse(int status, String body) {
+        MockResponse response = new MockResponse()
+                        .setResponseCode(status)
+                        .setBody(body)
+                        .addHeader("Content-type", "text/plain");
+        mServer.enqueue(response);
+        return response;
+    }
+
+    private void enqueueEmptyResponse(int status) {
+        enqueueResponse(status, "");
+    }
+
+    /**
+     * Wait for a request to come to the MockWebServer and return it.
+     */
+    private RecordedRequest takeRequest() throws InterruptedException {
+        RecordedRequest request = mServer.takeRequestWithTimeout(REQUEST_TIMEOUT_MILLIS);
+        assertNotNull("Timed out waiting for request", request);
+        return request;
+    }
+
+    /**
+     * Read a downloaded file from disk.
+     */
+    private String getDownloadContents(Uri downloadUri) throws Exception {
+        InputStream inputStream = mResolver.openInputStream(downloadUri);
+        try {
+            return readStream(inputStream);
+        } finally {
+            inputStream.close();
+        }
+    }
+
+    /**
+     * Wait for a download to given a given status, with a timeout.  Fails if the download reaches
+     * any other final status.
+     */
+    private void waitForDownloadToStop(Uri downloadUri, int expectedStatus) {
+        // TODO(showard): find a better way to accomplish this
+        long startTimeMillis = System.currentTimeMillis();
+        int status = getDownloadStatus(downloadUri);
+        while (status != expectedStatus) {
+            if (!Downloads.isStatusInformational(status)) {
+                fail("Download completed with unexpected status: " + status);
+            }
+            if (System.currentTimeMillis() > startTimeMillis + REQUEST_TIMEOUT_MILLIS) {
+                fail("Download timed out with status " + status);
+            }
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException exc) {
+                // no problem
+            }
+            status = getDownloadStatus(downloadUri);
+        }
+
+        long delta = System.currentTimeMillis() - startTimeMillis;
+        Log.d(LOG_TAG, "Status " + status + " reached after " + delta + "ms");
+    }
+
+    private int getDownloadStatus(Uri downloadUri) {
+        final String[] columns = new String[] {Downloads.COLUMN_STATUS};
+        Cursor cursor = mResolver.query(downloadUri, columns, null, null, null);
+        try {
+            assertEquals(1, cursor.getCount());
+            cursor.moveToFirst();
+            return cursor.getInt(0);
+        } finally {
+            cursor.close();
+        }
+    }
+
+    /**
+     * Request a download from the Download Manager.
+     */
+    private Uri requestDownload(String path) throws MalformedURLException {
+        ContentValues values = new ContentValues();
+        values.put(Downloads.COLUMN_URI, mServer.getUrl(path).toString());
+        values.put(Downloads.COLUMN_DESTINATION, Downloads.DESTINATION_EXTERNAL);
+        return mResolver.insert(Downloads.CONTENT_URI, values);
+    }
+
+    private String readStream(InputStream inputStream) throws IOException {
+        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
+        try {
+            char[] buffer = new char[1024];
+            int length = reader.read(buffer);
+            assertTrue("Failed to read anything from input stream", length > -1);
+            return String.valueOf(buffer, 0, length);
+        } finally {
+            reader.close();
+        }
+    }
+
+    /**
+     * Check for any extra requests made to the MockWebServer that weren't consumed with
+     * {@link #takeRequest()}.
+     */
+    private void checkForUnexpectedRequests() {
+        if (mServer == null) {
+            return;
+        }
+        List<RecordedRequest> extraRequests = mServer.drainRequests();
+        assertEquals("Invalid requests: " + extraRequests.toString(), 0, extraRequests.size());
+    }
+}
diff --git a/tests/src/com/android/providers/downloads/FakeIConnectivityManager.java b/tests/src/com/android/providers/downloads/FakeIConnectivityManager.java
new file mode 100644 (file)
index 0000000..0e92bd5
--- /dev/null
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.downloads;
+
+import android.net.ConnectivityManager;
+import android.net.IConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.State;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+class FakeIConnectivityManager implements IConnectivityManager {
+    private static class MockNetworkInfo extends NetworkInfo {
+        private State mState;
+
+        @SuppressWarnings("deprecation")
+        public MockNetworkInfo(State state) {
+            super(0);
+            mState = state;
+        }
+
+        @Override
+        public State getState() {
+            return mState;
+        }
+
+        @Override
+        public int getType() {
+            return ConnectivityManager.TYPE_MOBILE;
+        }
+    }
+
+    private State mCurrentState = State.CONNECTED;
+
+    public void setNetworkState(State state) {
+        mCurrentState = state;
+    }
+
+    public IBinder asBinder() {
+        throw new UnsupportedOperationException();
+    }
+
+    public NetworkInfo getActiveNetworkInfo() throws RemoteException {
+        return new MockNetworkInfo(mCurrentState);
+    }
+
+    public NetworkInfo[] getAllNetworkInfo() throws RemoteException {
+        return new NetworkInfo[] {getActiveNetworkInfo()};
+    }
+
+    public boolean getBackgroundDataSetting() throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+
+    public int getLastTetherError(String iface) throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean getMobileDataEnabled() throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+
+    public NetworkInfo getNetworkInfo(int networkType) throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+
+    public int getNetworkPreference() throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+
+    public String[] getTetherableIfaces() throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+
+    public String[] getTetherableUsbRegexs() throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+
+    public String[] getTetherableWifiRegexs() throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+
+    public String[] getTetheredIfaces() throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+
+    public String[] getTetheringErroredIfaces() throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean isTetheringSupported() throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean requestRouteToHost(int networkType, int hostAddress) throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+
+    public void setBackgroundDataSetting(boolean allowBackgroundData) throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+
+    public void setMobileDataEnabled(boolean enabled) throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+
+    public void setNetworkPreference(int pref) throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean setRadio(int networkType, boolean turnOn) throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean setRadios(boolean onOff) throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+
+    public int startUsingNetworkFeature(int networkType, String feature, IBinder binder)
+            throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+
+    public int stopUsingNetworkFeature(int networkType, String feature) throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+
+    public int tether(String iface) throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+
+    public int untether(String iface) throws RemoteException {
+        throw new UnsupportedOperationException();
+    }
+}
index 9893e2f..2139701 100644 (file)
 
 package tests.http;
 
+import static tests.http.MockWebServer.ASCII;
+
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
-
-import static tests.http.MockWebServer.ASCII;
+import java.util.Map;
 
 /**
  * A scripted response to be replayed by the mock web server.
  */
 public class MockResponse {
-    private static final String EMPTY_BODY_HEADER = "Content-Length: 0";
-    private static final String CHUNKED_BODY_HEADER = "Transfer-encoding: chunked";
     private static final byte[] EMPTY_BODY = new byte[0];
 
     private String status = "HTTP/1.1 200 OK";
-    private List<String> headers = new ArrayList<String>();
+    private Map<String, String> headers = new HashMap<String, String>();
     private byte[] body = EMPTY_BODY;
+    private boolean closeConnectionAfter = false;
 
     public MockResponse() {
-        headers.add(EMPTY_BODY_HEADER);
+        addHeader("Content-Length", 0);
     }
 
     /**
@@ -56,14 +57,22 @@ public class MockResponse {
      * Returns the HTTP headers, such as "Content-Length: 0".
      */
     public List<String> getHeaders() {
-        return headers;
+        List<String> headerStrings = new ArrayList<String>();
+        for (String header : headers.keySet()) {
+            headerStrings.add(header + ": " + headers.get(header));
+        }
+        return headerStrings;
     }
 
-    public MockResponse addHeader(String header) {
-        headers.add(header);
+    public MockResponse addHeader(String header, String value) {
+        headers.put(header.toLowerCase(), value);
         return this;
     }
 
+    public MockResponse addHeader(String header, int value) {
+        return addHeader(header, Integer.toString(value));
+    }
+
     /**
      * Returns an input stream containing the raw HTTP payload.
      */
@@ -72,10 +81,7 @@ public class MockResponse {
     }
 
     public MockResponse setBody(byte[] body) {
-        if (this.body == EMPTY_BODY) {
-            headers.remove(EMPTY_BODY_HEADER);
-        }
-        this.headers.add("Content-Length: " + body.length);
+        addHeader("Content-Length", body.length);
         this.body = body;
         return this;
     }
@@ -89,8 +95,7 @@ public class MockResponse {
     }
 
     public MockResponse setChunkedBody(byte[] body, int maxChunkSize) throws IOException {
-        headers.remove(EMPTY_BODY_HEADER);
-        headers.add(CHUNKED_BODY_HEADER);
+        addHeader("Transfer-encoding", "chunked");
 
         ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
         int pos = 0;
@@ -114,4 +119,13 @@ public class MockResponse {
     @Override public String toString() {
         return status;
     }
+
+    public boolean shouldCloseConnectionAfter() {
+        return closeConnectionAfter;
+    }
+
+    public MockResponse setCloseConnectionAfter(boolean closeConnectionAfter) {
+        this.closeConnectionAfter = closeConnectionAfter;
+        return this;
+    }
 }
index e3df2e8..b2cb8d7 100644 (file)
@@ -32,21 +32,22 @@ import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.concurrent.LinkedBlockingDeque;
 import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
 
 /**
  * A scriptable web server. Callers supply canned responses and the server
  * replays them upon request in sequence.
+ *
+ * TODO: merge with the version from libcore/support/src/tests/java once it's in.
  */
 public final class MockWebServer {
-
     static final String ASCII = "US-ASCII";
 
     private final BlockingQueue<RecordedRequest> requestQueue
             = new LinkedBlockingQueue<RecordedRequest>();
     private final BlockingQueue<MockResponse> responseQueue
-            = new LinkedBlockingDeque<MockResponse>();
+            = new LinkedBlockingQueue<MockResponse>();
     private int bodyLimit = Integer.MAX_VALUE;
     private final ExecutorService executor = Executors.newCachedThreadPool();
 
@@ -88,6 +89,16 @@ public final class MockWebServer {
         return requestQueue.take();
     }
 
+    public RecordedRequest takeRequestWithTimeout(long timeoutMillis) throws InterruptedException {
+        return requestQueue.poll(timeoutMillis, TimeUnit.MILLISECONDS);
+    }
+
+    public List<RecordedRequest> drainRequests() {
+        List<RecordedRequest> requests = new ArrayList<RecordedRequest>();
+        requestQueue.drainTo(requests);
+        return requests;
+    }
+
     /**
      * Starts the server, serves all enqueued requests, and shuts the server
      * down.
@@ -130,7 +141,11 @@ public final class MockWebServer {
                         }
                     }
                     requestQueue.add(request);
-                    writeResponse(out, computeResponse(request));
+                    MockResponse response = computeResponse(request);
+                    writeResponse(out, response);
+                    if (response.shouldCloseConnectionAfter()) {
+                        break;
+                    }
                     sequenceNumber++;
                 }
 
@@ -146,7 +161,7 @@ public final class MockWebServer {
      */
     private RecordedRequest readRequest(InputStream in, int sequenceNumber) throws IOException {
         String request = readAsciiUntilCrlf(in);
-        if (request.isEmpty()) {
+        if (request.equals("")) {
             return null; // end of data; no more requests
         }
 
@@ -154,7 +169,7 @@ public final class MockWebServer {
         int contentLength = -1;
         boolean chunked = false;
         String header;
-        while (!(header = readAsciiUntilCrlf(in)).isEmpty()) {
+        while (!(header = readAsciiUntilCrlf(in)).equals("")) {
             headers.add(header);
             String lowercaseHeader = header.toLowerCase();
             if (contentLength == -1 && lowercaseHeader.startsWith("content-length:")) {
@@ -219,7 +234,6 @@ public final class MockWebServer {
         }
         out.write(("\r\n").getBytes(ASCII));
         out.write(response.getBody());
-        out.write(("\r\n").getBytes(ASCII));
         out.flush();
     }
 
@@ -260,7 +274,7 @@ public final class MockWebServer {
 
     private void readEmptyLine(InputStream in) throws IOException {
         String line = readAsciiUntilCrlf(in);
-        if (!line.isEmpty()) {
+        if (!line.equals("")) {
             throw new IllegalStateException("Expected empty but was: " + line);
         }
     }
index c805006..6b67af2 100644 (file)
@@ -82,4 +82,12 @@ public final class RecordedRequest {
     @Override public String toString() {
         return requestLine;
     }
+
+    public String getMethod() {
+        return getRequestLine().split(" ")[0];
+    }
+
+    public String getPath() {
+        return getRequestLine().split(" ")[1];
+    }
 }