chromium/chrome/android/javatests/src/org/chromium/chrome/browser/PopularUrlsTest.java

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser;

import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;

import androidx.test.platform.app.InstrumentationRegistry;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Manual;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.ui.base.PageTransition;
import org.chromium.url.GURL;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * Popular URL tests (ported from {@link com.android.browser.PopularUrlsTest}).
 *
 * <p>These tests read popular URLs from /sdcard/popular_urls.txt, open them one by one and verify
 * page load. When aborted, they save the last opened URL in /sdcard/test_status.txt, so that they
 * can continue opening the next URL when they are restarted.
 */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class PopularUrlsTest {
    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    private static final String TAG = "PopularUrlsTest";
    private static final String NEW_LINE = System.getProperty("line.separator");
    private static final File INPUT_FILE =
            new File(Environment.getExternalStorageDirectory(), "popular_urls.txt");
    private static final File OUTPUT_FILE =
            new File(Environment.getExternalStorageDirectory(), "test_output.txt");
    private static final File STATUS_FILE =
            new File(Environment.getExternalStorageDirectory(), "test_status.txt");
    private static final File FAILURE_FILE =
            new File(Environment.getExternalStorageDirectory(), "failures.txt");
    private static final File WAIT_FLAG_FILE =
            new File(Environment.getExternalStorageDirectory(), "url-test-short-wait");
    private static final int PERF_LOOPCOUNT = 10;
    private static final int STABILITY_LOOPCOUNT = 1;
    private static final int SHORT_WAIT_TIMEOUT = 1000;

    private RunStatus mStatus;
    private boolean mFailed;
    private boolean mDoShortWait;

    @Before
    public void setUp() throws Exception {
        mStatus = new RunStatus(STATUS_FILE);
        mFailed = false;
        mDoShortWait = checkDoShortWait();
        mActivityTestRule.startMainActivityFromLauncher();
    }

    @After
    public void tearDown() {
        if (mStatus != null) {
            mStatus.cleanUp();
        }
    }

    private BufferedReader getInputStream(File inputFile) throws FileNotFoundException {
        try {
            Reader fileReader = new InputStreamReader(new FileInputStream(inputFile), "UTF-8");
            return new BufferedReader(fileReader);
        } catch (UnsupportedEncodingException ex) {
            throw new RuntimeException("UTF-8 not present...time to give up on this charade.", ex);
        }
    }

    private OutputStreamWriter getOutputStream(File outputFile) throws IOException {
        return new OutputStreamWriter(
                new FileOutputStream(outputFile, mStatus.getIsRecovery()), "UTF-8");
    }

    private void logToStream(String str, OutputStreamWriter writer) throws IOException {
        if (writer != null) {
            writer.write(str);
            writer.flush();
        }
    }

    private boolean checkDoShortWait() {
        return WAIT_FLAG_FILE.isFile() && WAIT_FLAG_FILE.exists();
    }

    private static class RunStatus {
        private File mFile;
        private int mIteration;
        private int mPage;
        private String mUrl;
        private boolean mIsRecovery;
        private boolean mAllClear;

        public RunStatus(File file) throws IOException {
            mFile = file;
            Reader input = null;
            BufferedReader reader = null;
            mIsRecovery = false;
            mAllClear = false;
            mIteration = 0;
            mPage = 0;
            try {
                input = new InputStreamReader(new FileInputStream(mFile), "UTF-8");
                mIsRecovery = true;
                reader = new BufferedReader(input);
                String line = reader.readLine();
                if (line == null) {
                    return;
                }
                mIteration = Integer.parseInt(line);
                line = reader.readLine();
                if (line == null) {
                    return;
                }
                mPage = Integer.parseInt(line);
            } catch (FileNotFoundException ex) {
                return;
            } catch (NumberFormatException nfe) {
                Log.wtf(TAG, "Unexpected data in status file. Will run for all URLs.");
                return;
            } finally {
                try {
                    if (reader != null) {
                        reader.close();
                    }
                } finally {
                    if (input != null) {
                        input.close();
                    }
                }
            }
        }

        public void write() throws IOException {
            Writer output = null;
            if (mFile.exists()) {
                mFile.delete();
            }
            try {
                output = new OutputStreamWriter(new FileOutputStream(mFile), "UTF-8");
                output.write(mIteration + NEW_LINE);
                output.write(mPage + NEW_LINE);
                output.write(mUrl + NEW_LINE);
            } finally {
                if (output != null) {
                    output.close();
                }
            }
        }

        public void cleanUp() {
            // Only perform cleanup when mAllClear flag is set, i.e.
            // when the test was not interrupted by a Java crash.
            if (mFile.exists() && mAllClear) {
                mFile.delete();
            }
        }

        public void resetPage() {
            mPage = 0;
        }

        public void incrementPage() {
            ++mPage;
            mAllClear = true;
        }

        public void incrementIteration() {
            ++mIteration;
        }

        public int getPage() {
            return mPage;
        }

        public int getIteration() {
            return mIteration;
        }

        public boolean getIsRecovery() {
            return mIsRecovery;
        }

        public void setUrl(String url) {
            mUrl = url;
            mAllClear = false;
        }
    }

    /**
     * Navigates to a URL directly without going through the UrlBar. This bypasses the page
     * preloading mechanism of the UrlBar.
     *
     * @param url the page URL
     * @param failureWriter the writer where failures/crashes/timeouts are logged.
     * @throws IOException unable to read from input or write to writer.
     */
    public void loadUrl(final String url, OutputStreamWriter failureWriter) throws IOException {
        Tab tab = mActivityTestRule.getActivity().getActivityTab();
        final CallbackHelper loadedCallback = new CallbackHelper();
        final CallbackHelper failedCallback = new CallbackHelper();
        final CallbackHelper crashedCallback = new CallbackHelper();

        tab.addObserver(
                new EmptyTabObserver() {
                    @Override
                    public void onPageLoadFinished(Tab tab, GURL url) {
                        loadedCallback.notifyCalled();
                    }

                    @Override
                    public void onPageLoadFailed(Tab tab, int errorCode) {
                        failedCallback.notifyCalled();
                    }

                    @Override
                    public void onCrash(Tab tab) {
                        crashedCallback.notifyCalled();
                    }
                });

        InstrumentationRegistry.getInstrumentation()
                .runOnMainSync(
                        () -> {
                            Tab tab1 = mActivityTestRule.getActivity().getActivityTab();
                            int pageTransition =
                                    PageTransition.TYPED | PageTransition.FROM_ADDRESS_BAR;
                            tab1.loadUrl(new LoadUrlParams(url, pageTransition));
                        });
        // There are a combination of events ordering in a failure case.
        // There might be TAB_CRASHED with or without PAGE_LOAD_FINISHED preceding it.
        // It is possible to get PAGE_LOAD_FINISHED followed by PAGE_LOAD_FAILED due to redirects.
        boolean timedout = false;
        try {
            loadedCallback.waitForCallback(0, 1, 2, TimeUnit.MINUTES);
        } catch (TimeoutException ex) {
            timedout = true;
        }

        boolean failed = true;
        boolean crashed = true;
        if (mDoShortWait) {
            try {
                failedCallback.waitForCallback(0, 1, SHORT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS);
            } catch (TimeoutException ex) {
                failed = false;
            }
            try {
                crashedCallback.waitForCallback(0, 1, SHORT_WAIT_TIMEOUT, TimeUnit.MILLISECONDS);
            } catch (TimeoutException ex) {
                crashed = false;
            }
        } else {
            try {
                failedCallback.waitForCallback(0, 1, (long) (100 * 1000), TimeUnit.MILLISECONDS);
            } catch (TimeoutException ex) {
                failed = false;
            }
            try {
                crashedCallback.waitForCallback(0, 1, (long) (100 * 1000), TimeUnit.MILLISECONDS);
            } catch (TimeoutException ex) {
                crashed = false;
            }
        }
        if (crashed) {
            logToStream(url + " crashed!" + NEW_LINE, failureWriter);
            mFailed = true;
        }
        if (failed) {
            logToStream(url + " failed to load!" + NEW_LINE, failureWriter);
            mFailed = true;
        }
        if (timedout) {
            logToStream(url + " timed out!" + NEW_LINE, failureWriter);
            mFailed = true;
        }
        // Try to stop page load.
        InstrumentationRegistry.getInstrumentation()
                .runOnMainSync(
                        () -> mActivityTestRule.getActivity().getActivityTab().stopLoading());
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
    }

    /**
     * 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 outputWriter the writer to which to output the results.
     * @param failureWriter the writer where failures/crashes/timeouts are logged.
     * @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.
     */
    private void loopUrls(
            BufferedReader input,
            OutputStreamWriter outputWriter,
            OutputStreamWriter failureWriter,
            boolean clearCache,
            int loopCount)
            throws IOException {
        List<String> pages = new ArrayList<>();

        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) {
                // TODO(jingzhao): Clear cache before loading the URL.
            }
            while (iterator.hasNext()) {
                page = iterator.next();
                mStatus.setUrl(page);
                mStatus.write();
                Log.i(TAG, "Start: " + page);

                long startTime = System.currentTimeMillis();
                loadUrl(page, failureWriter);
                long stopTime = System.currentTimeMillis();

                String currentUrl =
                        mActivityTestRule.getActivity().getActivityTab().getUrl().getSpec();
                Log.i(TAG, "Finish: " + currentUrl);
                logToStream(page + "|" + (stopTime - startTime) + NEW_LINE, outputWriter);
                mStatus.incrementPage();
            }
            mStatus.incrementIteration();
            mStatus.resetPage();
            iterator = pages.iterator();
        }
    }

    /**
     * Navigate to all the pages listed in the input.
     *
     * @param perf Whether this is a performance test or stability test.
     * @throws IOException
     */
    public void loadPages(boolean perf) throws IOException {
        OutputStreamWriter outputWriter = null;
        if (perf) {
            outputWriter = getOutputStream(OUTPUT_FILE);
        }
        OutputStreamWriter failureWriter = getOutputStream(FAILURE_FILE);
        try {
            BufferedReader bufferedReader = getInputStream(INPUT_FILE);
            int loopCount = perf ? PERF_LOOPCOUNT : STABILITY_LOOPCOUNT;
            try {
                loopUrls(bufferedReader, outputWriter, failureWriter, true, loopCount);
                Assert.assertFalse(
                        String.format("Failed to load all pages. Take a look at %s", FAILURE_FILE),
                        mFailed);
            } finally {
                if (bufferedReader != null) {
                    bufferedReader.close();
                }
            }
        } catch (FileNotFoundException fnfe) {
            Log.e(TAG, fnfe.getMessage(), fnfe);
            Assert.fail(String.format("URL file %s is not found.", INPUT_FILE));
        } finally {
            if (outputWriter != null) {
                outputWriter.close();
            }
            if (failureWriter != null) {
                failureWriter.close();
            }
        }
    }

    /** Repeats loading all URLs by PERF_LOOPCOUNT times, and records the time each load takes. */
    @Test
    @Manual
    public void testLoadPerformance() throws IOException {
        loadPages(true);
    }

    /** Loads all URLs. */
    @Test
    @Manual
    public void testStability() throws IOException {
        loadPages(false);
    }
}