summaryrefslogtreecommitdiffstats
path: root/tests/src/tests
diff options
context:
space:
mode:
authorSteve Howard <showard@google.com>2010-06-17 14:56:48 -0700
committerSteve Howard <showard@google.com>2010-06-21 14:26:11 -0700
commit4c54154c83a5b3516d81903140b31349f196cbe2 (patch)
tree1b2b5d01da3e6a17377b5aba3f5b8ff4bb56d69b /tests/src/tests
parent055286d603745cb3281ddb04b0aa41a660624e65 (diff)
downloadandroid_packages_providers_DownloadProvider-4c54154c83a5b3516d81903140b31349f196cbe2.tar.gz
android_packages_providers_DownloadProvider-4c54154c83a5b3516d81903140b31349f196cbe2.tar.bz2
android_packages_providers_DownloadProvider-4c54154c83a5b3516d81903140b31349f196cbe2.zip
Check in copies of MockWebServer + friends.
This commit includes unmodified copies of MockWebServer and its supporting classes, taken from dalvik-dev:libcore/support/src/tests/java. My test will require some changes, but I'm committing unchanged versions first so I can more easily track my changes and eventually merge them back over. Change-Id: Ic4b79c7a2cbd3bd439864ed94c43fe985eae8e78
Diffstat (limited to 'tests/src/tests')
-rw-r--r--tests/src/tests/http/MockResponse.java117
-rw-r--r--tests/src/tests/http/MockWebServer.java284
-rw-r--r--tests/src/tests/http/RecordedRequest.java85
3 files changed, 486 insertions, 0 deletions
diff --git a/tests/src/tests/http/MockResponse.java b/tests/src/tests/http/MockResponse.java
new file mode 100644
index 00000000..9893e2fc
--- /dev/null
+++ b/tests/src/tests/http/MockResponse.java
@@ -0,0 +1,117 @@
+/*
+ * 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 tests.http;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static tests.http.MockWebServer.ASCII;
+
+/**
+ * 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 byte[] body = EMPTY_BODY;
+
+ public MockResponse() {
+ headers.add(EMPTY_BODY_HEADER);
+ }
+
+ /**
+ * Returns the HTTP response line, such as "HTTP/1.1 200 OK".
+ */
+ public String getStatus() {
+ return status;
+ }
+
+ public MockResponse setResponseCode(int code) {
+ this.status = "HTTP/1.1 " + code + " OK";
+ return this;
+ }
+
+ /**
+ * Returns the HTTP headers, such as "Content-Length: 0".
+ */
+ public List<String> getHeaders() {
+ return headers;
+ }
+
+ public MockResponse addHeader(String header) {
+ headers.add(header);
+ return this;
+ }
+
+ /**
+ * Returns an input stream containing the raw HTTP payload.
+ */
+ public byte[] getBody() {
+ return body;
+ }
+
+ public MockResponse setBody(byte[] body) {
+ if (this.body == EMPTY_BODY) {
+ headers.remove(EMPTY_BODY_HEADER);
+ }
+ this.headers.add("Content-Length: " + body.length);
+ this.body = body;
+ return this;
+ }
+
+ public MockResponse setBody(String body) {
+ try {
+ return setBody(body.getBytes(ASCII));
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError();
+ }
+ }
+
+ public MockResponse setChunkedBody(byte[] body, int maxChunkSize) throws IOException {
+ headers.remove(EMPTY_BODY_HEADER);
+ headers.add(CHUNKED_BODY_HEADER);
+
+ ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+ int pos = 0;
+ while (pos < body.length) {
+ int chunkSize = Math.min(body.length - pos, maxChunkSize);
+ bytesOut.write(Integer.toHexString(chunkSize).getBytes(ASCII));
+ bytesOut.write("\r\n".getBytes(ASCII));
+ bytesOut.write(body, pos, chunkSize);
+ bytesOut.write("\r\n".getBytes(ASCII));
+ pos += chunkSize;
+ }
+ bytesOut.write("0\r\n".getBytes(ASCII));
+ this.body = bytesOut.toByteArray();
+ return this;
+ }
+
+ public MockResponse setChunkedBody(String body, int maxChunkSize) throws IOException {
+ return setChunkedBody(body.getBytes(ASCII), maxChunkSize);
+ }
+
+ @Override public String toString() {
+ return status;
+ }
+}
diff --git a/tests/src/tests/http/MockWebServer.java b/tests/src/tests/http/MockWebServer.java
new file mode 100644
index 00000000..e3df2e83
--- /dev/null
+++ b/tests/src/tests/http/MockWebServer.java
@@ -0,0 +1,284 @@
+/*
+ * 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 tests.http;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+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;
+
+/**
+ * A scriptable web server. Callers supply canned responses and the server
+ * replays them upon request in sequence.
+ */
+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>();
+ private int bodyLimit = Integer.MAX_VALUE;
+ private final ExecutorService executor = Executors.newCachedThreadPool();
+
+ private int port = -1;
+
+ public int getPort() {
+ if (port == -1) {
+ throw new IllegalStateException("Cannot retrieve port before calling play()");
+ }
+ return port;
+ }
+
+ /**
+ * Returns a URL for connecting to this server.
+ *
+ * @param path the request path, such as "/".
+ */
+ public URL getUrl(String path) throws MalformedURLException {
+ return new URL("http://localhost:" + getPort() + path);
+ }
+
+ /**
+ * Sets the number of bytes of the POST body to keep in memory to the given
+ * limit.
+ */
+ public void setBodyLimit(int maxBodyLength) {
+ this.bodyLimit = maxBodyLength;
+ }
+
+ public void enqueue(MockResponse response) {
+ responseQueue.add(response);
+ }
+
+ /**
+ * Awaits the next HTTP request, removes it, and returns it. Callers should
+ * use this to verify the request sent was as intended.
+ */
+ public RecordedRequest takeRequest() throws InterruptedException {
+ return requestQueue.take();
+ }
+
+ /**
+ * Starts the server, serves all enqueued requests, and shuts the server
+ * down.
+ */
+ public void play() throws IOException {
+ final ServerSocket ss = new ServerSocket(0);
+ ss.setReuseAddress(true);
+ port = ss.getLocalPort();
+ executor.submit(new Callable<Void>() {
+ public Void call() throws Exception {
+ int count = 0;
+ while (true) {
+ if (count > 0 && responseQueue.isEmpty()) {
+ ss.close();
+ executor.shutdown();
+ return null;
+ }
+
+ serveConnection(ss.accept());
+ count++;
+ }
+ }
+ });
+ }
+
+ private void serveConnection(final Socket s) {
+ executor.submit(new Callable<Void>() {
+ public Void call() throws Exception {
+ InputStream in = new BufferedInputStream(s.getInputStream());
+ OutputStream out = new BufferedOutputStream(s.getOutputStream());
+
+ int sequenceNumber = 0;
+ while (true) {
+ RecordedRequest request = readRequest(in, sequenceNumber);
+ if (request == null) {
+ if (sequenceNumber == 0) {
+ throw new IllegalStateException("Connection without any request!");
+ } else {
+ break;
+ }
+ }
+ requestQueue.add(request);
+ writeResponse(out, computeResponse(request));
+ sequenceNumber++;
+ }
+
+ in.close();
+ out.close();
+ return null;
+ }
+ });
+ }
+
+ /**
+ * @param sequenceNumber the index of this request on this connection.
+ */
+ private RecordedRequest readRequest(InputStream in, int sequenceNumber) throws IOException {
+ String request = readAsciiUntilCrlf(in);
+ if (request.isEmpty()) {
+ return null; // end of data; no more requests
+ }
+
+ List<String> headers = new ArrayList<String>();
+ int contentLength = -1;
+ boolean chunked = false;
+ String header;
+ while (!(header = readAsciiUntilCrlf(in)).isEmpty()) {
+ headers.add(header);
+ String lowercaseHeader = header.toLowerCase();
+ if (contentLength == -1 && lowercaseHeader.startsWith("content-length:")) {
+ contentLength = Integer.parseInt(header.substring(15).trim());
+ }
+ if (lowercaseHeader.startsWith("transfer-encoding:") &&
+ lowercaseHeader.substring(18).trim().equals("chunked")) {
+ chunked = true;
+ }
+ }
+
+ boolean hasBody = false;
+ TruncatingOutputStream requestBody = new TruncatingOutputStream();
+ List<Integer> chunkSizes = new ArrayList<Integer>();
+ if (contentLength != -1) {
+ hasBody = true;
+ transfer(contentLength, in, requestBody);
+ } else if (chunked) {
+ hasBody = true;
+ while (true) {
+ int chunkSize = Integer.parseInt(readAsciiUntilCrlf(in).trim(), 16);
+ if (chunkSize == 0) {
+ readEmptyLine(in);
+ break;
+ }
+ chunkSizes.add(chunkSize);
+ transfer(chunkSize, in, requestBody);
+ readEmptyLine(in);
+ }
+ }
+
+ if (request.startsWith("GET ")) {
+ if (hasBody) {
+ throw new IllegalArgumentException("GET requests should not have a body!");
+ }
+ } else if (request.startsWith("POST ")) {
+ if (!hasBody) {
+ throw new IllegalArgumentException("POST requests must have a body!");
+ }
+ } else {
+ throw new UnsupportedOperationException("Unexpected method: " + request);
+ }
+
+ return new RecordedRequest(request, headers, chunkSizes,
+ requestBody.numBytesReceived, requestBody.toByteArray(), sequenceNumber);
+ }
+
+ /**
+ * Returns a response to satisfy {@code request}.
+ */
+ private MockResponse computeResponse(RecordedRequest request) throws InterruptedException {
+ if (responseQueue.isEmpty()) {
+ throw new IllegalStateException("Unexpected request: " + request);
+ }
+ return responseQueue.take();
+ }
+
+ private void writeResponse(OutputStream out, MockResponse response) throws IOException {
+ out.write((response.getStatus() + "\r\n").getBytes(ASCII));
+ for (String header : response.getHeaders()) {
+ out.write((header + "\r\n").getBytes(ASCII));
+ }
+ out.write(("\r\n").getBytes(ASCII));
+ out.write(response.getBody());
+ out.write(("\r\n").getBytes(ASCII));
+ out.flush();
+ }
+
+ /**
+ * Transfer bytes from {@code in} to {@code out} until either {@code length}
+ * bytes have been transferred or {@code in} is exhausted.
+ */
+ private void transfer(int length, InputStream in, OutputStream out) throws IOException {
+ byte[] buffer = new byte[1024];
+ while (length > 0) {
+ int count = in.read(buffer, 0, Math.min(buffer.length, length));
+ if (count == -1) {
+ return;
+ }
+ out.write(buffer, 0, count);
+ length -= count;
+ }
+ }
+
+ /**
+ * Returns the text from {@code in} until the next "\r\n", or null if
+ * {@code in} is exhausted.
+ */
+ private String readAsciiUntilCrlf(InputStream in) throws IOException {
+ StringBuilder builder = new StringBuilder();
+ while (true) {
+ int c = in.read();
+ if (c == '\n' && builder.length() > 0 && builder.charAt(builder.length() - 1) == '\r') {
+ builder.deleteCharAt(builder.length() - 1);
+ return builder.toString();
+ } else if (c == -1) {
+ return builder.toString();
+ } else {
+ builder.append((char) c);
+ }
+ }
+ }
+
+ private void readEmptyLine(InputStream in) throws IOException {
+ String line = readAsciiUntilCrlf(in);
+ if (!line.isEmpty()) {
+ throw new IllegalStateException("Expected empty but was: " + line);
+ }
+ }
+
+ /**
+ * An output stream that drops data after bodyLimit bytes.
+ */
+ private class TruncatingOutputStream extends ByteArrayOutputStream {
+ private int numBytesReceived = 0;
+ @Override public void write(byte[] buffer, int offset, int len) {
+ numBytesReceived += len;
+ super.write(buffer, offset, Math.min(len, bodyLimit - count));
+ }
+ @Override public void write(int oneByte) {
+ numBytesReceived++;
+ if (count < bodyLimit) {
+ super.write(oneByte);
+ }
+ }
+ }
+}
diff --git a/tests/src/tests/http/RecordedRequest.java b/tests/src/tests/http/RecordedRequest.java
new file mode 100644
index 00000000..c8050065
--- /dev/null
+++ b/tests/src/tests/http/RecordedRequest.java
@@ -0,0 +1,85 @@
+/*
+ * 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 tests.http;
+
+import java.util.List;
+
+/**
+ * An HTTP request that came into the mock web server.
+ */
+public final class RecordedRequest {
+ private final String requestLine;
+ private final List<String> headers;
+ private final List<Integer> chunkSizes;
+ private final int bodySize;
+ private final byte[] body;
+ private final int sequenceNumber;
+
+ RecordedRequest(String requestLine, List<String> headers, List<Integer> chunkSizes,
+ int bodySize, byte[] body, int sequenceNumber) {
+ this.requestLine = requestLine;
+ this.headers = headers;
+ this.chunkSizes = chunkSizes;
+ this.bodySize = bodySize;
+ this.body = body;
+ this.sequenceNumber = sequenceNumber;
+ }
+
+ public String getRequestLine() {
+ return requestLine;
+ }
+
+ public List<String> getHeaders() {
+ return headers;
+ }
+
+ /**
+ * Returns the sizes of the chunks of this request's body, or an empty list
+ * if the request's body was empty or unchunked.
+ */
+ public List<Integer> getChunkSizes() {
+ return chunkSizes;
+ }
+
+ /**
+ * Returns the total size of the body of this POST request (before
+ * truncation).
+ */
+ public int getBodySize() {
+ return bodySize;
+ }
+
+ /**
+ * Returns the body of this POST request. This may be truncated.
+ */
+ public byte[] getBody() {
+ return body;
+ }
+
+ /**
+ * Returns the index of this request on its HTTP connection. Since a single
+ * HTTP connection may serve multiple requests, each request is assigned its
+ * own sequence number.
+ */
+ public int getSequenceNumber() {
+ return sequenceNumber;
+ }
+
+ @Override public String toString() {
+ return requestLine;
+ }
+}