diff options
author | Neal Nguyen <tommyn@google.com> | 2010-08-09 14:08:26 -0700 |
---|---|---|
committer | Neal Nguyen <tommyn@google.com> | 2010-09-08 17:02:53 -0700 |
commit | 5f53bca55b2c9e217dee12bff8ce55e168829783 (patch) | |
tree | 14f9203ef32283086d3fe6f1ac06d30804c1e2c9 /core/tests/utillib | |
parent | 3fa7d8af6560de07ef673f73308f7e51de64e4ec (diff) | |
download | frameworks_base-5f53bca55b2c9e217dee12bff8ce55e168829783.zip frameworks_base-5f53bca55b2c9e217dee12bff8ce55e168829783.tar.gz frameworks_base-5f53bca55b2c9e217dee12bff8ce55e168829783.tar.bz2 |
Adding Download Manager Integration, stress, and hosts-based tests.
Change-Id: I97008f6cfd95ea9950db0b4e093da02528849b63
Diffstat (limited to 'core/tests/utillib')
-rw-r--r-- | core/tests/utillib/Android.mk | 27 | ||||
-rw-r--r-- | core/tests/utillib/src/coretestutils/http/MockResponse.java | 239 | ||||
-rw-r--r-- | core/tests/utillib/src/coretestutils/http/MockWebServer.java | 426 | ||||
-rw-r--r-- | core/tests/utillib/src/coretestutils/http/RecordedRequest.java | 93 |
4 files changed, 785 insertions, 0 deletions
diff --git a/core/tests/utillib/Android.mk b/core/tests/utillib/Android.mk new file mode 100644 index 0000000..299ea5a --- /dev/null +++ b/core/tests/utillib/Android.mk @@ -0,0 +1,27 @@ +# 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. + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_MODULE := frameworks-core-util-lib + +include $(BUILD_STATIC_JAVA_LIBRARY) + +# Build the test APKs using their own makefiles +include $(call all-makefiles-under,$(LOCAL_PATH)) + diff --git a/core/tests/utillib/src/coretestutils/http/MockResponse.java b/core/tests/utillib/src/coretestutils/http/MockResponse.java new file mode 100644 index 0000000..5b03e5f --- /dev/null +++ b/core/tests/utillib/src/coretestutils/http/MockResponse.java @@ -0,0 +1,239 @@ +/* + * 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 coretestutils.http; + +import static coretestutils.http.MockWebServer.ASCII; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import android.util.Log; + +/** + * A scripted response to be replayed by the mock web server. + */ +public class MockResponse { + private static final byte[] EMPTY_BODY = new byte[0]; + static final String LOG_TAG = "coretestutils.http.MockResponse"; + + private String status = "HTTP/1.1 200 OK"; + private Map<String, String> headers = new HashMap<String, String>(); + private byte[] body = EMPTY_BODY; + private boolean closeConnectionAfter = false; + private String closeConnectionAfterHeader = null; + private int closeConnectionAfterXBytes = -1; + private int pauseConnectionAfterXBytes = -1; + private File bodyExternalFile = null; + + public MockResponse() { + addHeader("Content-Length", 0); + } + + /** + * 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() { + List<String> headerStrings = new ArrayList<String>(); + for (String header : headers.keySet()) { + headerStrings.add(header + ": " + headers.get(header)); + } + return headerStrings; + } + + public MockResponse addHeader(String header, String value) { + headers.put(header.toLowerCase(), value); + return this; + } + + public MockResponse addHeader(String header, long value) { + return addHeader(header, Long.toString(value)); + } + + public MockResponse removeHeader(String header) { + headers.remove(header.toLowerCase()); + return this; + } + + /** + * Returns true if the body should come from an external file, false otherwise. + */ + private boolean bodyIsExternal() { + return bodyExternalFile != null; + } + + /** + * Returns an input stream containing the raw HTTP payload. + */ + public InputStream getBody() { + if (bodyIsExternal()) { + try { + return new FileInputStream(bodyExternalFile); + } catch (FileNotFoundException e) { + Log.e(LOG_TAG, "File not found: " + bodyExternalFile.getAbsolutePath()); + } + } + return new ByteArrayInputStream(this.body); + } + + public MockResponse setBody(File body) { + addHeader("Content-Length", body.length()); + this.bodyExternalFile = body; + return this; + } + + public MockResponse setBody(byte[] body) { + addHeader("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(); + } + } + + /** + * Sets the body as chunked. + * + * Currently chunked body is not supported for external files as bodies. + */ + public MockResponse setChunkedBody(byte[] body, int maxChunkSize) throws IOException { + addHeader("Transfer-encoding", "chunked"); + + 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; + } + + public boolean shouldCloseConnectionAfter() { + return closeConnectionAfter; + } + + public MockResponse setCloseConnectionAfter(boolean closeConnectionAfter) { + this.closeConnectionAfter = closeConnectionAfter; + return this; + } + + /** + * Sets the header after which sending the server should close the connection. + */ + public MockResponse setCloseConnectionAfterHeader(String header) { + closeConnectionAfterHeader = header; + setCloseConnectionAfter(true); + return this; + } + + /** + * Returns the header after which sending the server should close the connection. + */ + public String getCloseConnectionAfterHeader() { + return closeConnectionAfterHeader; + } + + /** + * Sets the number of bytes in the body to send before which the server should close the + * connection. Set to -1 to unset and send the entire body (default). + */ + public MockResponse setCloseConnectionAfterXBytes(int position) { + closeConnectionAfterXBytes = position; + setCloseConnectionAfter(true); + return this; + } + + /** + * Returns the number of bytes in the body to send before which the server should close the + * connection. Returns -1 if the entire body should be sent (default). + */ + public int getCloseConnectionAfterXBytes() { + return closeConnectionAfterXBytes; + } + + /** + * Sets the number of bytes in the body to send before which the server should pause the + * connection (stalls in sending data). Only one pause per response is supported. + * Set to -1 to unset pausing (default). + */ + public MockResponse setPauseConnectionAfterXBytes(int position) { + pauseConnectionAfterXBytes = position; + return this; + } + + /** + * Returns the number of bytes in the body to send before which the server should pause the + * connection (stalls in sending data). (Returns -1 if it should not pause). + */ + public int getPauseConnectionAfterXBytes() { + return pauseConnectionAfterXBytes; + } + + /** + * Returns true if this response is flagged to pause the connection mid-stream, false otherwise + */ + public boolean getShouldPause() { + return (pauseConnectionAfterXBytes != -1); + } + + /** + * Returns true if this response is flagged to close the connection mid-stream, false otherwise + */ + public boolean getShouldClose() { + return (closeConnectionAfterXBytes != -1); + } +} diff --git a/core/tests/utillib/src/coretestutils/http/MockWebServer.java b/core/tests/utillib/src/coretestutils/http/MockWebServer.java new file mode 100644 index 0000000..c329ffa --- /dev/null +++ b/core/tests/utillib/src/coretestutils/http/MockWebServer.java @@ -0,0 +1,426 @@ +/* + * 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 coretestutils.http; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +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.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import android.util.Log; + +/** + * 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"; + static final String LOG_TAG = "coretestutils.http.MockWebServer"; + + private final BlockingQueue<RecordedRequest> requestQueue + = new LinkedBlockingQueue<RecordedRequest>(); + private final BlockingQueue<MockResponse> responseQueue + = new LinkedBlockingQueue<MockResponse>(); + private int bodyLimit = Integer.MAX_VALUE; + private final ExecutorService executor = Executors.newCachedThreadPool(); + // keep Futures around so we can rethrow any exceptions thrown by Callables + private final Queue<Future<?>> futures = new LinkedList<Future<?>>(); + private final Object downloadPauseLock = new Object(); + // global flag to signal when downloads should resume on the server + private volatile boolean downloadResume = false; + + 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(); + } + + 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 using the default (server-assigned) port. + */ + public void play() throws IOException { + play(0); + } + + /** + * Starts the server, serves all enqueued requests, and shuts the server + * down. + * + * @param port The port number to use to listen to connections on; pass in 0 to have the + * server automatically assign a free port + */ + public void play(int portNumber) throws IOException { + final ServerSocket ss = new ServerSocket(portNumber); + ss.setReuseAddress(true); + port = ss.getLocalPort(); + submitCallable(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) { + submitCallable(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); + MockResponse response = computeResponse(request); + writeResponse(out, response); + if (response.shouldCloseConnectionAfter()) { + break; + } + sequenceNumber++; + } + + in.close(); + out.close(); + return null; + } + }); + } + + private void submitCallable(Callable<?> callable) { + Future<?> future = executor.submit(callable); + futures.add(future); + } + + /** + * Check for and raise any exceptions that have been thrown by child threads. Will not block on + * children still running. + * @throws ExecutionException for the first child thread that threw an exception + */ + public void checkForExceptions() throws ExecutionException, InterruptedException { + final int originalSize = futures.size(); + for (int i = 0; i < originalSize; i++) { + Future<?> future = futures.remove(); + try { + future.get(0, TimeUnit.SECONDS); + } catch (TimeoutException e) { + futures.add(future); // still running + } + } + } + + /** + * @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.equals("")) { + 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)).equals("")) { + 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)); + boolean doCloseConnectionAfterHeader = (response.getCloseConnectionAfterHeader() != null); + + // Send headers + String closeConnectionAfterHeader = response.getCloseConnectionAfterHeader(); + for (String header : response.getHeaders()) { + out.write((header + "\r\n").getBytes(ASCII)); + + if (doCloseConnectionAfterHeader && header.startsWith(closeConnectionAfterHeader)) { + Log.i(LOG_TAG, "Closing connection after header" + header); + break; + } + } + + // Send actual body data + if (!doCloseConnectionAfterHeader) { + out.write(("\r\n").getBytes(ASCII)); + + InputStream body = response.getBody(); + final int READ_BLOCK_SIZE = 10000; // process blocks this size + byte[] currentBlock = new byte[READ_BLOCK_SIZE]; + int currentBlockSize = 0; + int writtenSoFar = 0; + + boolean shouldPause = response.getShouldPause(); + boolean shouldClose = response.getShouldClose(); + int pause = response.getPauseConnectionAfterXBytes(); + int close = response.getCloseConnectionAfterXBytes(); + + // Don't bother pausing if it's set to pause -after- the connection should be dropped + if (shouldPause && shouldClose && (pause > close)) { + shouldPause = false; + } + + // Process each block we read in... + while ((currentBlockSize = body.read(currentBlock)) != -1) { + int startIndex = 0; + int writeLength = currentBlockSize; + + // handle the case of pausing + if (shouldPause && (writtenSoFar + currentBlockSize >= pause)) { + writeLength = pause - writtenSoFar; + out.write(currentBlock, 0, writeLength); + out.flush(); + writtenSoFar += writeLength; + + // now pause... + try { + Log.i(LOG_TAG, "Pausing connection after " + pause + " bytes"); + // Wait until someone tells us to resume sending... + synchronized(downloadPauseLock) { + while (!downloadResume) { + downloadPauseLock.wait(); + } + // reset resume back to false + downloadResume = false; + } + } catch (InterruptedException e) { + Log.e(LOG_TAG, "Server was interrupted during pause in download."); + } + + startIndex = writeLength; + writeLength = currentBlockSize - writeLength; + } + + // handle the case of closing the connection + if (shouldClose && (writtenSoFar + writeLength > close)) { + writeLength = close - writtenSoFar; + out.write(currentBlock, startIndex, writeLength); + writtenSoFar += writeLength; + Log.i(LOG_TAG, "Closing connection after " + close + " bytes"); + break; + } + out.write(currentBlock, startIndex, writeLength); + writtenSoFar += writeLength; + } + } + 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.equals("")) { + 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); + } + } + } + + /** + * Trigger the server to resume sending the download + */ + public void doResumeDownload() { + synchronized (downloadPauseLock) { + downloadResume = true; + downloadPauseLock.notifyAll(); + } + } +} diff --git a/core/tests/utillib/src/coretestutils/http/RecordedRequest.java b/core/tests/utillib/src/coretestutils/http/RecordedRequest.java new file mode 100644 index 0000000..293ff80 --- /dev/null +++ b/core/tests/utillib/src/coretestutils/http/RecordedRequest.java @@ -0,0 +1,93 @@ +/* + * 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 coretestutils.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; + } + + public String getMethod() { + return getRequestLine().split(" ")[0]; + } + + public String getPath() { + return getRequestLine().split(" ")[1]; + } +} |