/* * 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.browser; import android.app.Instrumentation; import android.content.Intent; import android.net.Uri; import android.net.http.SslError; import android.os.Environment; import android.provider.Browser; import android.test.ActivityInstrumentationTestCase2; import android.text.TextUtils; import android.util.Log; import android.webkit.ClientCertRequestHandler; import android.webkit.DownloadListener; import android.webkit.HttpAuthHandler; import android.webkit.JsPromptResult; import android.webkit.JsResult; import android.webkit.SslErrorHandler; import android.webkit.WebView; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.OutputStreamWriter; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * * Iterates over a list of URLs from a file and outputs the time to load each. */ public class PopularUrlsTest extends ActivityInstrumentationTestCase2<BrowserActivity> { private final static String TAG = "PopularUrlsTest"; private final static String newLine = System.getProperty("line.separator"); private final static String sInputFile = "popular_urls.txt"; private final static String sOutputFile = "test_output.txt"; private final static String sStatusFile = "test_status.txt"; private final static File sExternalStorage = Environment.getExternalStorageDirectory(); private final static int PERF_LOOPCOUNT = 10; private final static int STABILITY_LOOPCOUNT = 1; private final static int PAGE_LOAD_TIMEOUT = 120000; // 2 minutes private BrowserActivity mActivity = null; private Controller mController = null; private Instrumentation mInst = null; private CountDownLatch mLatch = new CountDownLatch(1); private RunStatus mStatus; private boolean pageLoadFinishCalled, pageProgressFull; public PopularUrlsTest() { super(BrowserActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("about:blank")); i.putExtra(Controller.NO_CRASH_RECOVERY, true); setActivityIntent(i); mActivity = getActivity(); mController = mActivity.getController(); mInst = getInstrumentation(); mInst.waitForIdleSync(); mStatus = RunStatus.load(); } @Override protected void tearDown() throws Exception { if (mStatus != null) { mStatus.cleanUp(); } super.tearDown(); } BufferedReader getInputStream() throws FileNotFoundException { return getInputStream(sInputFile); } BufferedReader getInputStream(String inputFile) throws FileNotFoundException { FileReader fileReader = new FileReader(new File(sExternalStorage, inputFile)); BufferedReader bufferedReader = new BufferedReader(fileReader); return bufferedReader; } OutputStreamWriter getOutputStream() throws IOException { return getOutputStream(sOutputFile); } OutputStreamWriter getOutputStream(String outputFile) throws IOException { return new FileWriter(new File(sExternalStorage, outputFile), mStatus.getIsRecovery()); } /** * Gets the browser ready for testing by starting the application * and wrapping the WebView's helper clients. */ void setUpBrowser() { mInst.runOnMainSync(new Runnable() { @Override public void run() { setupBrowserInternal(); } }); } void setupBrowserInternal() { Tab tab = mController.getTabControl().getCurrentTab(); WebView webView = tab.getWebView(); webView.setWebChromeClient(new TestWebChromeClient(webView.getWebChromeClient()) { @Override public void onProgressChanged(WebView view, int newProgress) { super.onProgressChanged(view, newProgress); if (newProgress >= 100) { if (!pageProgressFull) { // void duplicate calls pageProgressFull = true; if (pageLoadFinishCalled) { //reset latch and move forward only if both indicators are true resetLatch(); } } } } /** * Dismisses and logs Javascript alerts. */ @Override public boolean onJsAlert(WebView view, String url, String message, JsResult result) { String logMsg = String.format("JS Alert '%s' received from %s", message, url); Log.w(TAG, logMsg); result.confirm(); return true; } /** * Confirms and logs Javascript alerts. */ @Override public boolean onJsConfirm(WebView view, String url, String message, JsResult result) { String logMsg = String.format("JS Confirmation '%s' received from %s", message, url); Log.w(TAG, logMsg); result.confirm(); return true; } /** * Confirms and logs Javascript alerts, providing the default value. */ @Override public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { String logMsg = String.format("JS Prompt '%s' received from %s; " + "Giving default value '%s'", message, url, defaultValue); Log.w(TAG, logMsg); result.confirm(defaultValue); return true; } /* * Skip the unload confirmation */ @Override public boolean onJsBeforeUnload( WebView view, String url, String message, JsResult result) { result.confirm(); return true; } }); webView.setWebViewClient(new TestWebViewClient(webView.getWebViewClient()) { /** * Bypasses and logs errors. */ @Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { String message = String.format("Error '%s' (%d) loading url: %s", description, errorCode, failingUrl); Log.w(TAG, message); } /** * Ignores and logs SSL errors. */ @Override public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { Log.w(TAG, "SSL error: " + error); handler.proceed(); } /** * Ignores and logs SSL client certificate requests. */ @Override public void onReceivedClientCertRequest(WebView view, ClientCertRequestHandler handler, String host_and_port) { Log.w(TAG, "SSL client certificate request: " + host_and_port); handler.cancel(); } /** * Ignores http auth with dummy username and password */ @Override public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) { handler.proceed("user", "passwd"); } /* (non-Javadoc) * @see com.android.browser.TestWebViewClient#onPageFinished(android.webkit.WebView, java.lang.String) */ @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); if (!pageLoadFinishCalled) { pageLoadFinishCalled = true; if (pageProgressFull) { //reset latch and move forward only if both indicators are true resetLatch(); } } } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (!(url.startsWith("http://") || url.startsWith("https://"))) { Log.v(TAG, String.format("suppressing non-http url scheme: %s", url)); return true; } return super.shouldOverrideUrlLoading(view, url); } }); webView.setDownloadListener(new DownloadListener() { @Override public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) { Log.v(TAG, String.format("Download request ignored: %s", url)); } }); } void resetLatch() { if (mLatch.getCount() != 1) { Log.w(TAG, "Expecting latch to be 1, but it's not!"); } else { mLatch.countDown(); } } void resetForNewPage() { mLatch = new CountDownLatch(1); pageLoadFinishCalled = false; pageProgressFull = false; } void waitForLoad() throws InterruptedException { boolean timedout = !mLatch.await(PAGE_LOAD_TIMEOUT, TimeUnit.MILLISECONDS); if (timedout) { Log.w(TAG, "page timeout. trying to stop."); // try to stop page load mInst.runOnMainSync(new Runnable(){ public void run() { mController.getTabControl().getCurrentTab().getWebView().stopLoading(); } }); // try to wait for count down latch again timedout = !mLatch.await(5000, TimeUnit.MILLISECONDS); if (timedout) { throw new RuntimeException("failed to stop timedout site, is browser pegged?"); } } } private static class RunStatus { private File mFile; private int iteration; private int page; private String url; private boolean isRecovery; private boolean allClear; private RunStatus(File file) throws IOException { mFile = file; FileReader input = null; BufferedReader reader = null; isRecovery = false; allClear = false; iteration = 0; page = 0; try { input = new FileReader(mFile); isRecovery = true; reader = new BufferedReader(input); String line = reader.readLine(); if (line == null) return; iteration = Integer.parseInt(line); line = reader.readLine(); if (line == null) return; page = Integer.parseInt(line); } catch (FileNotFoundException ex) { return; } catch (NumberFormatException nfe) { Log.wtf(TAG, "unexpected data in status file, will start from begining"); return; } finally { try { if (reader != null) { reader.close(); } } finally { if (input != null) { input.close(); } } } } public static RunStatus load() throws IOException { return load(sStatusFile); } public static RunStatus load(String file) throws IOException { return new RunStatus(new File(sExternalStorage, file)); } public void write() throws IOException { FileWriter output = null; if (mFile.exists()) { mFile.delete(); } try { output = new FileWriter(mFile); output.write(iteration + newLine); output.write(page + newLine); output.write(url + newLine); } finally { if (output != null) { output.close(); } } } public void cleanUp() { // only perform cleanup when allClear flag is set // i.e. when the test was not interrupted by a Java crash if (mFile.exists() && allClear) { mFile.delete(); } } public void resetPage() { page = 0; } public void incrementPage() { ++page; allClear = true; } public void incrementIteration() { ++iteration; } public int getPage() { return page; } public int getIteration() { return iteration; } public boolean getIsRecovery() { return isRecovery; } public void setUrl(String url) { this.url = url; allClear = false; } } /** * Loops over a list of URLs, points the browser to each one, and records the time elapsed. * * @param input the reader from which to get the URLs. * @param writer the writer to which to output the results. * @param clearCache determines whether the cache is cleared before loading each page * @param loopCount the number of times to loop through the list of pages * @throws IOException unable to read from input or write to writer. * @throws InterruptedException the thread was interrupted waiting for the page to load. */ void loopUrls(BufferedReader input, OutputStreamWriter writer, boolean clearCache, int loopCount) throws IOException, InterruptedException { Tab tab = mController.getTabControl().getCurrentTab(); WebView webView = tab.getWebView(); List<String> pages = new LinkedList<String>(); String page; while (null != (page = input.readLine())) { if (!TextUtils.isEmpty(page)) { pages.add(page); } } Iterator<String> iterator = pages.iterator(); for (int i = 0; i < mStatus.getPage(); ++i) { iterator.next(); } if (mStatus.getIsRecovery()) { Log.e(TAG, "Recovering after crash: " + iterator.next()); mStatus.incrementPage(); } while (mStatus.getIteration() < loopCount) { if (clearCache) { clearCacheUiThread(webView, true); } while(iterator.hasNext()) { page = iterator.next(); mStatus.setUrl(page); mStatus.write(); Log.i(TAG, "start: " + page); Uri uri = Uri.parse(page); final Intent intent = new Intent(Intent.ACTION_VIEW, uri); intent.putExtra(Browser.EXTRA_APPLICATION_ID, getInstrumentation().getTargetContext().getPackageName()); long startTime = System.currentTimeMillis(); resetForNewPage(); mInst.runOnMainSync(new Runnable() { public void run() { mActivity.onNewIntent(intent); } }); waitForLoad(); long stopTime = System.currentTimeMillis(); String url = getUrlUiThread(webView); Log.i(TAG, "finish: " + url); if (writer != null) { writer.write(page + "|" + (stopTime - startTime) + newLine); writer.flush(); } mStatus.incrementPage(); } mStatus.incrementIteration(); mStatus.resetPage(); iterator = pages.iterator(); } } public void testLoadPerformance() throws IOException, InterruptedException { setUpBrowser(); OutputStreamWriter writer = getOutputStream(); try { BufferedReader bufferedReader = getInputStream(); try { loopUrls(bufferedReader, writer, true, PERF_LOOPCOUNT); } finally { if (bufferedReader != null) { bufferedReader.close(); } } } catch (FileNotFoundException fnfe) { Log.e(TAG, fnfe.getMessage(), fnfe); fail("Test environment not setup correctly"); } finally { if (writer != null) { writer.close(); } } } public void testStability() throws IOException, InterruptedException { setUpBrowser(); BufferedReader bufferedReader = getInputStream(); try { loopUrls(bufferedReader, null, true, STABILITY_LOOPCOUNT); } catch (FileNotFoundException fnfe) { Log.e(TAG, fnfe.getMessage(), fnfe); fail("Test environment not setup correctly"); } finally { if (bufferedReader != null) { bufferedReader.close(); } } } private void clearCacheUiThread(final WebView webView, final boolean includeDiskFiles) { Runnable runner = new Runnable() { @Override public void run() { webView.clearCache(includeDiskFiles); } }; getInstrumentation().runOnMainSync(runner); } private String getUrlUiThread(final WebView webView) { WebViewUrlGetter urlGetter = new WebViewUrlGetter(webView); getInstrumentation().runOnMainSync(urlGetter); return urlGetter.getUrl(); } private class WebViewUrlGetter implements Runnable { private WebView mWebView; private String mUrl; public WebViewUrlGetter(WebView webView) { mWebView = webView; } @Override public void run() { mUrl = null; mUrl = mWebView.getUrl(); } public String getUrl() { if (mUrl != null) { return mUrl; } else throw new IllegalStateException("url has not been fetched yet"); } } }