diff options
author | Jesse Wilson <jessewilson@google.com> | 2010-05-10 14:11:57 -0700 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2010-05-10 14:11:57 -0700 |
commit | d6f5e66e31388c2777da33c30fb2194ff5b427be (patch) | |
tree | 9905758b91ad38822451e5468d7e3ee8131c0169 | |
parent | 259d900b126b15c348fb90b0abddffed292e50ce (diff) | |
parent | b1b5baac449d2725002338735f4db34bec8fd001 (diff) | |
download | libcore-d6f5e66e31388c2777da33c30fb2194ff5b427be.zip libcore-d6f5e66e31388c2777da33c30fb2194ff5b427be.tar.gz libcore-d6f5e66e31388c2777da33c30fb2194ff5b427be.tar.bz2 |
Merge "New MockWebServer for HTTP testing." into dalvik-dev
-rw-r--r-- | luni/src/test/java/java/net/URLConnectionTest.java | 183 | ||||
-rw-r--r-- | support/src/test/java/tests/http/MockResponse.java | 117 | ||||
-rw-r--r-- | support/src/test/java/tests/http/MockWebServer.java | 274 | ||||
-rw-r--r-- | support/src/test/java/tests/http/RecordedRequest.java | 85 |
4 files changed, 531 insertions, 128 deletions
diff --git a/luni/src/test/java/java/net/URLConnectionTest.java b/luni/src/test/java/java/net/URLConnectionTest.java index d634f6b..18915c4 100644 --- a/luni/src/test/java/java/net/URLConnectionTest.java +++ b/luni/src/test/java/java/net/URLConnectionTest.java @@ -16,37 +16,22 @@ package java.net; +import tests.http.MockResponse; +import tests.http.MockWebServer; +import tests.http.RecordedRequest; + import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; -import tests.support.Support_TestWebServer; public class URLConnectionTest extends junit.framework.TestCase { - private int mPort; - private Support_TestWebServer mServer; - - @Override - public void setUp() throws Exception { - super.setUp(); - mServer = new Support_TestWebServer(); - mPort = mServer.initServer(0, true); - } - - @Override - public void tearDown() throws Exception { - super.tearDown(); - mServer.close(); - } - - private String readFirstLine() throws Exception { - URLConnection connection = new URL("http://localhost:" + mPort + "/test1").openConnection(); + + private String readFirstLine(MockWebServer server) throws Exception { + URLConnection connection = server.getUrl("/").openConnection(); BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); String result = in.readLine(); in.close(); @@ -57,22 +42,38 @@ public class URLConnectionTest extends junit.framework.TestCase { // recycled connection doesn't get the unread tail of the first request's response. // http://code.google.com/p/android/issues/detail?id=2939 public void test_2939() throws Exception { - mServer.setChunked(true); - mServer.setMaxChunkSize(8); - assertTrue(readFirstLine().equals("<html>")); - assertTrue(readFirstLine().equals("<html>")); - assertEquals(1, mServer.getNumAcceptedConnections()); + MockResponse response = new MockResponse().setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8); + + MockWebServer server = new MockWebServer(); + server.enqueue(response); + server.enqueue(response); + server.play(); + + assertTrue(readFirstLine(server).equals("ABCDE")); + assertEquals(0, server.takeRequest().getSequenceNumber()); + assertTrue(readFirstLine(server).equals("ABCDE")); + assertEquals(1, server.takeRequest().getSequenceNumber()); } public void testConnectionsArePooled() throws Exception { - readFirstLine(); - readFirstLine(); - readFirstLine(); - assertEquals(1, mServer.getNumAcceptedConnections()); + MockResponse response = new MockResponse().setBody("ABCDEFGHIJKLMNOPQR"); + + MockWebServer server = new MockWebServer(); + server.enqueue(response); + server.enqueue(response); + server.enqueue(response); + server.play(); + + readFirstLine(server); + assertEquals(0, server.takeRequest().getSequenceNumber()); + readFirstLine(server); + assertEquals(1, server.takeRequest().getSequenceNumber()); + readFirstLine(server); + assertEquals(2, server.takeRequest().getSequenceNumber()); } - enum UploadKind { CHUNKED, FIXED_LENGTH }; - enum WriteKind { BYTE_BY_BYTE, SMALL_BUFFERS, LARGE_BUFFERS }; + enum UploadKind { CHUNKED, FIXED_LENGTH } + enum WriteKind { BYTE_BY_BYTE, SMALL_BUFFERS, LARGE_BUFFERS } public void test_chunkedUpload_byteByByte() throws Exception { doUpload(UploadKind.CHUNKED, WriteKind.BYTE_BY_BYTE); @@ -100,10 +101,12 @@ public class URLConnectionTest extends junit.framework.TestCase { private void doUpload(UploadKind uploadKind, WriteKind writeKind) throws Exception { int n = 512*1024; - AtomicInteger total = new AtomicInteger(0); - ServerSocket ss = startSinkServer(total); - URL url = new URL("http://localhost:" + ss.getLocalPort() + "/" + UUID.randomUUID()); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + MockWebServer server = new MockWebServer(); + server.setBodyLimit(0); + server.enqueue(new MockResponse()); + server.play(); + + HttpURLConnection conn = (HttpURLConnection) server.getUrl("/").openConnection(); conn.setDoOutput(true); conn.setRequestMethod("POST"); if (uploadKind == UploadKind.CHUNKED) { @@ -125,66 +128,13 @@ public class URLConnectionTest extends junit.framework.TestCase { } out.close(); assertEquals(200, conn.getResponseCode()); - assertEquals(uploadKind == UploadKind.CHUNKED ? -1 : n, total.get()); - } - - private ServerSocket startSinkServer(final AtomicInteger totalByteCount) throws Exception { - final ServerSocket ss = new ServerSocket(0); - ss.setReuseAddress(true); - Thread t = new Thread(new Runnable() { - public void run() { - try { - Socket s = ss.accept(); - BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream())); - int contentLength = -1; - String line; - int emptyLineCount = 0; - // read the headers - while ((line = in.readLine()) != null) { - if (contentLength == -1 && line.toLowerCase().startsWith("content-length: ")) { - contentLength = Integer.parseInt(line.substring(16)); - } - if (line.isEmpty()) { - ++emptyLineCount; - // If we had a content length, the first empty line we see marks the - // start of the payload. The loop below then skips over that. - // If we didn't get a content length, we're using chunked encoding. - // The first empty line again marks the start of the payload, and the - // second empty line is a consequence of both the last chunk ending - // CRLF and the chunked-body itself ending with a CRLF. (The fact that - // a chunk of size 0 is used to mark the end isn't sufficient because - // there may also be a "trailer": header fields deferred until after - // the payload.) - if (contentLength != -1 || emptyLineCount == 2) { - break; - } - } - } - // Skip the payload in the setFixedLengthStreamingMode case. - // In the chunked case, we read all the chunked data in the loop above. - long left = contentLength; - while (left > 0) { - left -= in.skip(left); - } - // Send a response to unblock the client. - totalByteCount.set(contentLength); - OutputStream out = s.getOutputStream(); - out.write("HTTP/1.1 200 OK\r\n\r\n".getBytes()); - out.flush(); - out.close(); - // Check there wasn't junk at the end. - try { - assertEquals(-1, in.read()); - } catch (SocketException expected) { - // The client already closed the connection. - } - } catch (Exception ex) { - throw new RuntimeException("server died unexpectedly", ex); - } - } - }); - t.start(); - return ss; + RecordedRequest request = server.takeRequest(); + assertEquals(n, request.getBodySize()); + if (uploadKind == UploadKind.CHUNKED) { + assertTrue(request.getChunkSizes().size() > 0); + } else { + assertTrue(request.getChunkSizes().isEmpty()); + } } public void test_responseCaching() throws Exception { @@ -239,41 +189,18 @@ public class URLConnectionTest extends junit.framework.TestCase { didPut = true; return null; } - }; - ServerSocket ss = startResponseCodeServer(responseCode); - URL url = new URL("http://localhost:" + ss.getLocalPort() + "/"); + } + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse() + .setResponseCode(responseCode) + .addHeader("WWW-Authenticate: challenge")); + server.play(); + MyResponseCache cache = new MyResponseCache(); ResponseCache.setDefault(cache); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + HttpURLConnection conn = (HttpURLConnection) server.getUrl("/").openConnection(); assertEquals(responseCode, conn.getResponseCode()); assertEquals(Integer.toString(responseCode), shouldPut, cache.didPut); - } - private ServerSocket startResponseCodeServer(final int responseCode) throws Exception { - final ServerSocket ss = new ServerSocket(0); - ss.setReuseAddress(true); - Thread t = new Thread(new Runnable() { - public void run() { - try { - Socket s = ss.accept(); - // Read the request. - BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream())); - String line; - while ((line = in.readLine()) != null && !line.isEmpty()) { - } - // Send a response. - OutputStream out = s.getOutputStream(); - out.write(String.format("HTTP/1.1 %d OK\r\n" + - "Content-Length: 0\r\n" + - "WWW-Authenticate: challenge\r\n\r\n", responseCode).getBytes()); - out.flush(); - out.close(); - } catch (Exception ex) { - throw new RuntimeException("server died unexpectedly", ex); - } - } - }); - t.start(); - return ss; } } diff --git a/support/src/test/java/tests/http/MockResponse.java b/support/src/test/java/tests/http/MockResponse.java new file mode 100644 index 0000000..9893e2f --- /dev/null +++ b/support/src/test/java/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/support/src/test/java/tests/http/MockWebServer.java b/support/src/test/java/tests/http/MockWebServer.java new file mode 100644 index 0000000..ea7ff84 --- /dev/null +++ b/support/src/test/java/tests/http/MockWebServer.java @@ -0,0 +1,274 @@ +/* + * 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"; + + /** sentinel object to shut down the server */ + private static final MockResponse NO_MORE_REQUESTS = new MockResponse(); + + 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) { + if (port != -1) { + throw new IllegalStateException("Cannot enqueue responses after calling play()."); + } + 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 { + responseQueue.add(NO_MORE_REQUESTS); + final ServerSocket ss = new ServerSocket(0); + ss.setReuseAddress(true); + port = ss.getLocalPort(); + executor.submit(new Callable<Void>() { + public Void call() throws Exception { + while (true) { + if (responseQueue.peek() == NO_MORE_REQUESTS) { + ss.close(); + executor.shutdown(); + return null; + } + + serveConnection(ss.accept()); + } + } + }); + } + + 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; + } + } + + TruncatingOutputStream requestBody = new TruncatingOutputStream(); + List<Integer> chunkSizes = new ArrayList<Integer>(); + if (contentLength != -1) { + transfer(contentLength, in, requestBody); + } else if (chunked) { + 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); + } + } + + 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.peek() == NO_MORE_REQUESTS) { + 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); + } + } + } +}
\ No newline at end of file diff --git a/support/src/test/java/tests/http/RecordedRequest.java b/support/src/test/java/tests/http/RecordedRequest.java new file mode 100644 index 0000000..c805006 --- /dev/null +++ b/support/src/test/java/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; + } +} |