diff options
Diffstat (limited to 'tests/src/com/android/providers')
-rw-r--r-- | tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java | 395 | ||||
-rw-r--r-- | tests/src/com/android/providers/downloads/FakeIConnectivityManager.java | 149 |
2 files changed, 544 insertions, 0 deletions
diff --git a/tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java b/tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java new file mode 100644 index 00000000..76b3d589 --- /dev/null +++ b/tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java @@ -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 index 00000000..0e92bd5f --- /dev/null +++ b/tests/src/com/android/providers/downloads/FakeIConnectivityManager.java @@ -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(); + } +} |