From 4c54154c83a5b3516d81903140b31349f196cbe2 Mon Sep 17 00:00:00 2001 From: Steve Howard Date: Thu, 17 Jun 2010 14:56:48 -0700 Subject: 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 --- tests/src/tests/http/MockResponse.java | 117 ++++++++++++ tests/src/tests/http/MockWebServer.java | 284 ++++++++++++++++++++++++++++++ tests/src/tests/http/RecordedRequest.java | 85 +++++++++ 3 files changed, 486 insertions(+) create mode 100644 tests/src/tests/http/MockResponse.java create mode 100644 tests/src/tests/http/MockWebServer.java create mode 100644 tests/src/tests/http/RecordedRequest.java (limited to 'tests') 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 headers = new ArrayList(); + 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 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 requestQueue + = new LinkedBlockingQueue(); + private final BlockingQueue responseQueue + = new LinkedBlockingDeque(); + 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() { + 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() { + 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 headers = new ArrayList(); + 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 chunkSizes = new ArrayList(); + 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 headers; + private final List chunkSizes; + private final int bodySize; + private final byte[] body; + private final int sequenceNumber; + + RecordedRequest(String requestLine, List headers, List 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 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 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; + } +} -- cgit v1.2.3