Import Cobalt 19.master.0.205881
diff --git a/src/base/test/android/java/src/org/chromium/base/ContentUriTestUtils.java b/src/base/test/android/java/src/org/chromium/base/ContentUriTestUtils.java
new file mode 100644
index 0000000..fe9d540
--- /dev/null
+++ b/src/base/test/android/java/src/org/chromium/base/ContentUriTestUtils.java
@@ -0,0 +1,46 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore;
+
+import org.chromium.base.annotations.CalledByNative;
+
+/**
+ * Utilities for testing operations on content URI.
+ */
+public class ContentUriTestUtils {
+    /**
+     * Insert an image into the MediaStore, and return the content URI. If the
+     * image already exists in the MediaStore, just retrieve the URI.
+     *
+     * @param path Path to the image file.
+     * @return Content URI of the image.
+     */
+    @CalledByNative
+    private static String insertImageIntoMediaStore(String path) {
+        // Check whether the content URI exists.
+        Cursor c = ContextUtils.getApplicationContext().getContentResolver().query(
+                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+                new String[] {MediaStore.Video.VideoColumns._ID},
+                MediaStore.Images.Media.DATA + " LIKE ?", new String[] {path}, null);
+        if (c != null && c.getCount() > 0) {
+            c.moveToFirst();
+            int id = c.getInt(0);
+            return Uri.withAppendedPath(
+                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "" + id).toString();
+        }
+
+        // Insert the content URI into MediaStore.
+        ContentValues values = new ContentValues();
+        values.put(MediaStore.MediaColumns.DATA, path);
+        Uri uri = ContextUtils.getApplicationContext().getContentResolver().insert(
+                MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
+        return uri.toString();
+    }
+}
diff --git a/src/base/test/android/java/src/org/chromium/base/ITestCallback.aidl b/src/base/test/android/java/src/org/chromium/base/ITestCallback.aidl
new file mode 100644
index 0000000..dd208d5
--- /dev/null
+++ b/src/base/test/android/java/src/org/chromium/base/ITestCallback.aidl
@@ -0,0 +1,23 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base;
+
+import org.chromium.base.ITestController;
+import org.chromium.base.process_launcher.FileDescriptorInfo;
+
+/**
+ * This interface is called by the child process to pass its controller to its parent.
+ */
+interface ITestCallback {
+  oneway void childConnected(ITestController controller);
+
+  /**
+    * Invoked by the service to notify that the main method returned.
+    * IMPORTANT! Should not be marked oneway as the caller will terminate the running process after
+    * this call. Marking it oneway would make the call asynchronous and the process could terminate
+    * before the call was actually sent.
+    */
+  void mainReturned(int returnCode);
+}
diff --git a/src/base/test/android/java/src/org/chromium/base/ITestController.aidl b/src/base/test/android/java/src/org/chromium/base/ITestController.aidl
new file mode 100644
index 0000000..d927ee5
--- /dev/null
+++ b/src/base/test/android/java/src/org/chromium/base/ITestController.aidl
@@ -0,0 +1,25 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base;
+
+import org.chromium.base.process_launcher.FileDescriptorInfo;
+
+/**
+ * This interface is used to control child processes.
+ */
+interface ITestController {
+  /**
+   * Forces the service process to terminate and block until the process stops.
+   * @param exitCode the exit code the process should terminate with.
+   * @return always true, a return value is only returned to force the call to be synchronous.
+   */
+  boolean forceStopSynchronous(int exitCode);
+
+  /**
+   * Forces the service process to terminate.
+   * @param exitCode the exit code the process should terminate with.
+   */
+  oneway void forceStop(int exitCode);
+}
diff --git a/src/base/test/android/java/src/org/chromium/base/JavaHandlerThreadHelpers.java b/src/base/test/android/java/src/org/chromium/base/JavaHandlerThreadHelpers.java
new file mode 100644
index 0000000..3985e6a
--- /dev/null
+++ b/src/base/test/android/java/src/org/chromium/base/JavaHandlerThreadHelpers.java
@@ -0,0 +1,65 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base;
+
+import android.os.Handler;
+import android.os.Process;
+
+import org.chromium.base.annotations.CalledByNative;
+import org.chromium.base.annotations.CalledByNativeUnchecked;
+import org.chromium.base.annotations.JNINamespace;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@JNINamespace("base::android")
+class JavaHandlerThreadHelpers {
+    private static class TestException extends Exception {}
+
+    // This is executed as part of base_unittests. This tests that JavaHandlerThread can be used
+    // by itself without attaching to its native peer.
+    @CalledByNative
+    private static JavaHandlerThread testAndGetJavaHandlerThread() {
+        final AtomicBoolean taskExecuted = new AtomicBoolean();
+        final Object lock = new Object();
+        Runnable runnable = new Runnable() {
+            @Override
+            public void run() {
+                synchronized (lock) {
+                    taskExecuted.set(true);
+                    lock.notifyAll();
+                }
+            }
+        };
+
+        JavaHandlerThread thread =
+                new JavaHandlerThread("base_unittests_java", Process.THREAD_PRIORITY_DEFAULT);
+        thread.maybeStart();
+
+        Handler handler = new Handler(thread.getLooper());
+        handler.post(runnable);
+        synchronized (lock) {
+            while (!taskExecuted.get()) {
+                try {
+                    lock.wait();
+                } catch (InterruptedException e) {
+                    // ignore interrupts
+                }
+            }
+        }
+
+        return thread;
+    }
+
+    @CalledByNativeUnchecked
+    private static void throwException() throws TestException {
+        throw new TestException();
+    }
+
+    @CalledByNative
+    private static boolean isExceptionTestException(Throwable exception) {
+        if (exception == null) return false;
+        return exception instanceof TestException;
+    }
+}
diff --git a/src/base/test/android/java/src/org/chromium/base/MainReturnCodeResult.java b/src/base/test/android/java/src/org/chromium/base/MainReturnCodeResult.java
new file mode 100644
index 0000000..9756c97
--- /dev/null
+++ b/src/base/test/android/java/src/org/chromium/base/MainReturnCodeResult.java
@@ -0,0 +1,40 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base;
+
+import org.chromium.base.annotations.CalledByNative;
+import org.chromium.base.annotations.JNINamespace;
+
+/**
+ * Contains the result of a native main method that ran in a child process.
+ */
+@JNINamespace("base::android")
+public final class MainReturnCodeResult {
+    private final int mMainReturnCode;
+    private final boolean mTimedOut;
+
+    public static MainReturnCodeResult createMainResult(int returnCode) {
+        return new MainReturnCodeResult(returnCode, false /* timedOut */);
+    }
+
+    public static MainReturnCodeResult createTimeoutMainResult() {
+        return new MainReturnCodeResult(0, true /* timedOut */);
+    }
+
+    private MainReturnCodeResult(int mainReturnCode, boolean timedOut) {
+        mMainReturnCode = mainReturnCode;
+        mTimedOut = timedOut;
+    }
+
+    @CalledByNative
+    public int getReturnCode() {
+        return mMainReturnCode;
+    }
+
+    @CalledByNative
+    public boolean hasTimedOut() {
+        return mTimedOut;
+    }
+}
diff --git a/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientLauncher.java b/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientLauncher.java
new file mode 100644
index 0000000..227d31f
--- /dev/null
+++ b/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientLauncher.java
@@ -0,0 +1,383 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.SparseArray;
+
+import org.chromium.base.annotations.CalledByNative;
+import org.chromium.base.annotations.JNINamespace;
+import org.chromium.base.process_launcher.ChildConnectionAllocator;
+import org.chromium.base.process_launcher.ChildProcessConnection;
+import org.chromium.base.process_launcher.ChildProcessLauncher;
+import org.chromium.base.process_launcher.FileDescriptorInfo;
+import org.chromium.base.process_launcher.IChildProcessService;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Helper class for launching test client processes for multiprocess unit tests.
+ */
+@JNINamespace("base::android")
+public final class MultiprocessTestClientLauncher {
+    private static final String TAG = "cr_MProcTCLauncher";
+
+    private static final int CONNECTION_TIMEOUT_MS = 10 * 1000;
+
+    private static final SparseArray<MultiprocessTestClientLauncher> sPidToLauncher =
+            new SparseArray<>();
+
+    private static final SparseArray<Integer> sPidToMainResult = new SparseArray<>();
+
+    private static final Object sLauncherHandlerInitLock = new Object();
+    private static Handler sLauncherHandler;
+
+    private static ChildConnectionAllocator sConnectionAllocator;
+
+    private final ITestCallback.Stub mCallback = new ITestCallback.Stub() {
+        @Override
+        public void childConnected(ITestController controller) {
+            mTestController = controller;
+            // This method can be called before onServiceConnected below has set the PID.
+            // Wait for mPid to be set before notifying.
+            try {
+                mPidReceived.await();
+            } catch (InterruptedException ie) {
+                Log.e(TAG, "Interrupted while waiting for connection PID.");
+                return;
+            }
+            // Now we are fully initialized, notify clients.
+            mConnectedLock.lock();
+            try {
+                mConnected = true;
+                mConnectedCondition.signal();
+            } finally {
+                mConnectedLock.unlock();
+            }
+        }
+
+        @Override
+        public void mainReturned(int returnCode) {
+            mMainReturnCodeLock.lock();
+            try {
+                mMainReturnCode = returnCode;
+                mMainReturnCodeCondition.signal();
+            } finally {
+                mMainReturnCodeLock.unlock();
+            }
+
+            // Also store the return code in a map as the connection might get disconnected
+            // before waitForMainToReturn is called and then we would not have a way to retrieve
+            // the connection.
+            sPidToMainResult.put(mPid, returnCode);
+        }
+    };
+
+    private final ChildProcessLauncher.Delegate mLauncherDelegate =
+            new ChildProcessLauncher.Delegate() {
+                @Override
+                public void onConnectionEstablished(ChildProcessConnection connection) {
+                    assert isRunningOnLauncherThread();
+                    int pid = connection.getPid();
+                    sPidToLauncher.put(pid, MultiprocessTestClientLauncher.this);
+                    mPid = pid;
+                    mPidReceived.countDown();
+                }
+
+                @Override
+                public void onConnectionLost(ChildProcessConnection connection) {
+                    assert isRunningOnLauncherThread();
+                    assert sPidToLauncher.get(connection.getPid())
+                            == MultiprocessTestClientLauncher.this;
+                    sPidToLauncher.remove(connection.getPid());
+                }
+            };
+
+    private final CountDownLatch mPidReceived = new CountDownLatch(1);
+
+    private final ChildProcessLauncher mLauncher;
+
+    private final ReentrantLock mConnectedLock = new ReentrantLock();
+    private final Condition mConnectedCondition = mConnectedLock.newCondition();
+    @GuardedBy("mConnectedLock")
+    private boolean mConnected;
+
+    private IChildProcessService mService;
+    private int mPid;
+    private ITestController mTestController;
+
+    private final ReentrantLock mMainReturnCodeLock = new ReentrantLock();
+    private final Condition mMainReturnCodeCondition = mMainReturnCodeLock.newCondition();
+    // The return code returned by the service's main method.
+    // null if the service has not sent it yet.
+    @GuardedBy("mMainReturnCodeLock")
+    private Integer mMainReturnCode;
+
+    private MultiprocessTestClientLauncher(String[] commandLine, FileDescriptorInfo[] filesToMap) {
+        assert isRunningOnLauncherThread();
+
+        if (sConnectionAllocator == null) {
+            sConnectionAllocator = ChildConnectionAllocator.create(
+                    ContextUtils.getApplicationContext(), sLauncherHandler, null,
+                    "org.chromium.native_test", "org.chromium.base.MultiprocessTestClientService",
+                    "org.chromium.native_test.NUM_TEST_CLIENT_SERVICES", false /* bindToCaller */,
+                    false /* bindAsExternalService */, false /* useStrongBinding */);
+        }
+        mLauncher = new ChildProcessLauncher(sLauncherHandler, mLauncherDelegate, commandLine,
+                filesToMap, sConnectionAllocator, Arrays.asList(mCallback));
+    }
+
+    private boolean waitForConnection(long timeoutMs) {
+        assert !isRunningOnLauncherThread();
+
+        long timeoutNs = TimeUnit.MILLISECONDS.toNanos(timeoutMs);
+        mConnectedLock.lock();
+        try {
+            while (!mConnected) {
+                if (timeoutNs <= 0L) {
+                    return false;
+                }
+                try {
+                    mConnectedCondition.awaitNanos(timeoutNs);
+                } catch (InterruptedException ie) {
+                    Log.e(TAG, "Interrupted while waiting for connection.");
+                }
+            }
+        } finally {
+            mConnectedLock.unlock();
+        }
+        return true;
+    }
+
+    private Integer getMainReturnCode(long timeoutMs) {
+        assert isRunningOnLauncherThread();
+
+        long timeoutNs = TimeUnit.MILLISECONDS.toNanos(timeoutMs);
+        mMainReturnCodeLock.lock();
+        try {
+            while (mMainReturnCode == null) {
+                if (timeoutNs <= 0L) {
+                    return null;
+                }
+                try {
+                    timeoutNs = mMainReturnCodeCondition.awaitNanos(timeoutNs);
+                } catch (InterruptedException ie) {
+                    Log.e(TAG, "Interrupted while waiting for main return code.");
+                }
+            }
+            return mMainReturnCode;
+        } finally {
+            mMainReturnCodeLock.unlock();
+        }
+    }
+
+    /**
+     * Spawns and connects to a child process.
+     * May not be called from the main thread.
+     *
+     * @param commandLine the child process command line argv.
+     * @return the PID of the started process or 0 if the process could not be started.
+     */
+    @CalledByNative
+    private static int launchClient(
+            final String[] commandLine, final FileDescriptorInfo[] filesToMap) {
+        initLauncherThread();
+
+        final MultiprocessTestClientLauncher launcher =
+                runOnLauncherAndGetResult(new Callable<MultiprocessTestClientLauncher>() {
+                    @Override
+                    public MultiprocessTestClientLauncher call() {
+                        return createAndStartLauncherOnLauncherThread(commandLine, filesToMap);
+                    }
+                });
+        if (launcher == null) {
+            return 0;
+        }
+
+        if (!launcher.waitForConnection(CONNECTION_TIMEOUT_MS)) {
+            return 0; // Timed-out.
+        }
+
+        return runOnLauncherAndGetResult(new Callable<Integer>() {
+            @Override
+            public Integer call() {
+                int pid = launcher.mLauncher.getPid();
+                assert pid > 0;
+                sPidToLauncher.put(pid, launcher);
+                return pid;
+            }
+        });
+    }
+
+    private static MultiprocessTestClientLauncher createAndStartLauncherOnLauncherThread(
+            String[] commandLine, FileDescriptorInfo[] filesToMap) {
+        assert isRunningOnLauncherThread();
+
+        MultiprocessTestClientLauncher launcher =
+                new MultiprocessTestClientLauncher(commandLine, filesToMap);
+        if (!launcher.mLauncher.start(
+                    true /* setupConnection */, true /* queueIfNoFreeConnection */)) {
+            return null;
+        }
+
+        return launcher;
+    }
+
+    /**
+     * Blocks until the main method invoked by a previous call to launchClient terminates or until
+     * the specified time-out expires.
+     * Returns immediately if main has already returned.
+     * @param pid the process ID that was returned by the call to launchClient
+     * @param timeoutMs the timeout in milliseconds after which the method returns even if main has
+     *        not returned.
+     * @return the return code returned by the main method or whether it timed-out.
+     */
+    @CalledByNative
+    private static MainReturnCodeResult waitForMainToReturn(final int pid, final int timeoutMs) {
+        return runOnLauncherAndGetResult(new Callable<MainReturnCodeResult>() {
+            @Override
+            public MainReturnCodeResult call() {
+                return waitForMainToReturnOnLauncherThread(pid, timeoutMs);
+            }
+        });
+    }
+
+    private static MainReturnCodeResult waitForMainToReturnOnLauncherThread(
+            int pid, int timeoutMs) {
+        assert isRunningOnLauncherThread();
+
+        MultiprocessTestClientLauncher launcher = sPidToLauncher.get(pid);
+        // The launcher can be null if it got cleaned-up (because the connection was lost) before
+        // this gets called.
+        if (launcher != null) {
+            Integer mainResult = launcher.getMainReturnCode(timeoutMs);
+            return mainResult == null ? MainReturnCodeResult.createTimeoutMainResult()
+                                      : MainReturnCodeResult.createMainResult(mainResult);
+        }
+
+        Integer mainResult = sPidToMainResult.get(pid);
+        if (mainResult == null) {
+            Log.e(TAG, "waitForMainToReturn called on unknown connection for pid " + pid);
+            return null;
+        }
+        sPidToMainResult.remove(pid);
+        return MainReturnCodeResult.createMainResult(mainResult);
+    }
+
+    @CalledByNative
+    private static boolean terminate(final int pid, final int exitCode, final boolean wait) {
+        return runOnLauncherAndGetResult(new Callable<Boolean>() {
+            @Override
+            public Boolean call() {
+                return terminateOnLauncherThread(pid, exitCode, wait);
+            }
+        });
+    }
+
+    private static boolean terminateOnLauncherThread(int pid, int exitCode, boolean wait) {
+        assert isRunningOnLauncherThread();
+
+        MultiprocessTestClientLauncher launcher = sPidToLauncher.get(pid);
+        if (launcher == null) {
+            Log.e(TAG, "terminate called on unknown launcher for pid " + pid);
+            return false;
+        }
+        try {
+            if (wait) {
+                launcher.mTestController.forceStopSynchronous(exitCode);
+            } else {
+                launcher.mTestController.forceStop(exitCode);
+            }
+        } catch (RemoteException e) {
+            // We expect this failure, since the forceStop's service implementation calls
+            // System.exit().
+        }
+        return true;
+    }
+
+    private static void initLauncherThread() {
+        synchronized (sLauncherHandlerInitLock) {
+            if (sLauncherHandler != null) return;
+
+            HandlerThread launcherThread = new HandlerThread("LauncherThread");
+            launcherThread.start();
+            sLauncherHandler = new Handler(launcherThread.getLooper());
+        }
+    }
+
+    /** Does not take ownership of of fds. */
+    @CalledByNative
+    private static FileDescriptorInfo[] makeFdInfoArray(int[] keys, int[] fds) {
+        FileDescriptorInfo[] fdInfos = new FileDescriptorInfo[keys.length];
+        for (int i = 0; i < keys.length; i++) {
+            FileDescriptorInfo fdInfo = makeFdInfo(keys[i], fds[i]);
+            if (fdInfo == null) {
+                Log.e(TAG, "Failed to make file descriptor (" + keys[i] + ", " + fds[i] + ").");
+                return null;
+            }
+            fdInfos[i] = fdInfo;
+        }
+        return fdInfos;
+    }
+
+    private static FileDescriptorInfo makeFdInfo(int id, int fd) {
+        ParcelFileDescriptor parcelableFd = null;
+        try {
+            parcelableFd = ParcelFileDescriptor.fromFd(fd);
+        } catch (IOException e) {
+            Log.e(TAG, "Invalid FD provided for process connection, aborting connection.", e);
+            return null;
+        }
+        return new FileDescriptorInfo(id, parcelableFd, 0 /* offset */, 0 /* size */);
+    }
+
+    private static boolean isRunningOnLauncherThread() {
+        return sLauncherHandler.getLooper() == Looper.myLooper();
+    }
+
+    private static void runOnLauncherThreadBlocking(final Runnable runnable) {
+        assert !isRunningOnLauncherThread();
+        final Semaphore done = new Semaphore(0);
+        sLauncherHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                runnable.run();
+                done.release();
+            }
+        });
+        done.acquireUninterruptibly();
+    }
+
+    private static <R> R runOnLauncherAndGetResult(Callable<R> callable) {
+        if (isRunningOnLauncherThread()) {
+            try {
+                return callable.call();
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        }
+        try {
+            FutureTask<R> task = new FutureTask<R>(callable);
+            sLauncherHandler.post(task);
+            return task.get();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService.java b/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService.java
new file mode 100644
index 0000000..9b50001
--- /dev/null
+++ b/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService.java
@@ -0,0 +1,14 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base;
+
+import org.chromium.base.process_launcher.ChildProcessService;
+
+/** The service implementation used to host all multiprocess test client code. */
+public class MultiprocessTestClientService extends ChildProcessService {
+    public MultiprocessTestClientService() {
+        super(new MultiprocessTestClientServiceDelegate());
+    }
+}
diff --git a/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService0.java b/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService0.java
new file mode 100644
index 0000000..6bdd867
--- /dev/null
+++ b/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService0.java
@@ -0,0 +1,10 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base;
+
+/**
+ * A subclass used only to differentiate different test client service process instances.
+ */
+public class MultiprocessTestClientService0 extends MultiprocessTestClientService {}
diff --git a/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService1.java b/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService1.java
new file mode 100644
index 0000000..69827f0
--- /dev/null
+++ b/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService1.java
@@ -0,0 +1,10 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base;
+
+/**
+ * A subclass used only to differentiate different test client service process instances.
+ */
+public class MultiprocessTestClientService1 extends MultiprocessTestClientService {}
diff --git a/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService2.java b/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService2.java
new file mode 100644
index 0000000..aad11f1
--- /dev/null
+++ b/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService2.java
@@ -0,0 +1,10 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base;
+
+/**
+ * A subclass used only to differentiate different test client service process instances.
+ */
+public class MultiprocessTestClientService2 extends MultiprocessTestClientService {}
diff --git a/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService3.java b/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService3.java
new file mode 100644
index 0000000..20d2561
--- /dev/null
+++ b/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService3.java
@@ -0,0 +1,10 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base;
+
+/**
+ * A subclass used only to differentiate different test client service process instances.
+ */
+public class MultiprocessTestClientService3 extends MultiprocessTestClientService {}
diff --git a/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService4.java b/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService4.java
new file mode 100644
index 0000000..4b14551
--- /dev/null
+++ b/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService4.java
@@ -0,0 +1,10 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base;
+
+/**
+ * A subclass used only to differentiate different test client service process instances.
+ */
+public class MultiprocessTestClientService4 extends MultiprocessTestClientService {}
diff --git a/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientServiceDelegate.java b/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientServiceDelegate.java
new file mode 100644
index 0000000..8a63fe8
--- /dev/null
+++ b/src/base/test/android/java/src/org/chromium/base/MultiprocessTestClientServiceDelegate.java
@@ -0,0 +1,94 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+package org.chromium.base;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.SparseArray;
+
+import org.chromium.base.library_loader.LibraryLoader;
+import org.chromium.base.library_loader.ProcessInitException;
+import org.chromium.base.process_launcher.ChildProcessServiceDelegate;
+import org.chromium.native_test.MainRunner;
+
+import java.util.List;
+
+/** Implementation of the ChildProcessServiceDelegate used for the Multiprocess tests. */
+public class MultiprocessTestClientServiceDelegate implements ChildProcessServiceDelegate {
+    private static final String TAG = "MPTestCSDelegate";
+
+    private ITestCallback mTestCallback;
+
+    private final ITestController.Stub mTestController = new ITestController.Stub() {
+        @Override
+        public boolean forceStopSynchronous(int exitCode) {
+            System.exit(exitCode);
+            return true;
+        }
+
+        @Override
+        public void forceStop(int exitCode) {
+            System.exit(exitCode);
+        }
+    };
+
+    @Override
+    public void onServiceCreated() {
+        PathUtils.setPrivateDataDirectorySuffix("chrome_multiprocess_test_client_service");
+    }
+
+    @Override
+    public void onServiceBound(Intent intent) {}
+
+    @Override
+    public void onConnectionSetup(Bundle connectionBundle, List<IBinder> callbacks) {
+        mTestCallback = ITestCallback.Stub.asInterface(callbacks.get(0));
+    }
+
+    @Override
+    public void onDestroy() {}
+
+    @Override
+    public void preloadNativeLibrary(Context hostContext) {
+        LibraryLoader.getInstance().preloadNow();
+    }
+
+    @Override
+    public boolean loadNativeLibrary(Context hostContext) {
+        try {
+            LibraryLoader.getInstance().loadNow();
+            return true;
+        } catch (ProcessInitException pie) {
+            Log.e(TAG, "Unable to load native libraries.", pie);
+            return false;
+        }
+    }
+
+    @Override
+    public SparseArray<String> getFileDescriptorsIdsToKeys() {
+        return null;
+    }
+
+    @Override
+    public void onBeforeMain() {
+        try {
+            mTestCallback.childConnected(mTestController);
+        } catch (RemoteException re) {
+            Log.e(TAG, "Failed to notify parent process of connection.");
+        }
+    }
+
+    @Override
+    public void runMain() {
+        int result = MainRunner.runMain(CommandLine.getJavaSwitchesOrNull());
+        try {
+            mTestCallback.mainReturned(result);
+        } catch (RemoteException re) {
+            Log.e(TAG, "Failed to notify parent process of main returning.");
+        }
+    }
+}
diff --git a/src/base/test/android/java_handler_thread_helpers.cc b/src/base/test/android/java_handler_thread_helpers.cc
new file mode 100644
index 0000000..b805e02
--- /dev/null
+++ b/src/base/test/android/java_handler_thread_helpers.cc
@@ -0,0 +1,39 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/test/android/java_handler_thread_helpers.h"
+
+#include "base/android/java_handler_thread.h"
+#include "base/message_loop/message_loop_current.h"
+#include "base/synchronization/waitable_event.h"
+#include "jni/JavaHandlerThreadHelpers_jni.h"
+
+namespace base {
+namespace android {
+
+// static
+std::unique_ptr<JavaHandlerThread> JavaHandlerThreadHelpers::CreateJavaFirst() {
+  return std::make_unique<JavaHandlerThread>(
+      nullptr, Java_JavaHandlerThreadHelpers_testAndGetJavaHandlerThread(
+                   base::android::AttachCurrentThread()));
+}
+
+// static
+void JavaHandlerThreadHelpers::ThrowExceptionAndAbort(WaitableEvent* event) {
+  JNIEnv* env = AttachCurrentThread();
+  Java_JavaHandlerThreadHelpers_throwException(env);
+  DCHECK(HasException(env));
+  base::MessageLoopCurrentForUI::Get()->Abort();
+  event->Signal();
+}
+
+// static
+bool JavaHandlerThreadHelpers::IsExceptionTestException(
+    ScopedJavaLocalRef<jthrowable> exception) {
+  JNIEnv* env = AttachCurrentThread();
+  return Java_JavaHandlerThreadHelpers_isExceptionTestException(env, exception);
+}
+
+}  // namespace android
+}  // namespace base
diff --git a/src/base/test/android/java_handler_thread_helpers.h b/src/base/test/android/java_handler_thread_helpers.h
new file mode 100644
index 0000000..364fb2c
--- /dev/null
+++ b/src/base/test/android/java_handler_thread_helpers.h
@@ -0,0 +1,43 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef BASE_ANDROID_JAVA_HANDLER_THREAD_FOR_TESTING_H_
+#define BASE_ANDROID_JAVA_HANDLER_THREAD_FOR_TESTING_H_
+
+#include <jni.h>
+
+#include <memory>
+
+#include "base/android/scoped_java_ref.h"
+#include "starboard/types.h"
+
+namespace base {
+
+class WaitableEvent;
+
+namespace android {
+
+class JavaHandlerThread;
+
+// Test-only helpers for working with JavaHandlerThread.
+class JavaHandlerThreadHelpers {
+ public:
+  // Create the Java peer first and test that it works before connecting to the
+  // native object.
+  static std::unique_ptr<JavaHandlerThread> CreateJavaFirst();
+
+  static void ThrowExceptionAndAbort(WaitableEvent* event);
+
+  static bool IsExceptionTestException(
+      ScopedJavaLocalRef<jthrowable> exception);
+
+ private:
+  JavaHandlerThreadHelpers() = default;
+  ~JavaHandlerThreadHelpers() = default;
+};
+
+}  // namespace android
+}  // namespace base
+
+#endif  // BASE_ANDROID_JAVA_HANDLER_THREAD_FOR_TESTING_H_
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumAndroidJUnitRunner.java b/src/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumAndroidJUnitRunner.java
new file mode 100644
index 0000000..f4ab126
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumAndroidJUnitRunner.java
@@ -0,0 +1,297 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test;
+
+import android.app.Activity;
+import android.app.Application;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.pm.InstrumentationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.internal.runner.RunnerArgs;
+import android.support.test.internal.runner.TestExecutor;
+import android.support.test.internal.runner.TestLoader;
+import android.support.test.internal.runner.TestRequest;
+import android.support.test.internal.runner.TestRequestBuilder;
+import android.support.test.runner.AndroidJUnitRunner;
+
+import dalvik.system.DexFile;
+
+import org.chromium.base.BuildConfig;
+import org.chromium.base.Log;
+import org.chromium.base.annotations.MainDex;
+import org.chromium.base.multidex.ChromiumMultiDexInstaller;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.List;
+
+/**
+ * A custom AndroidJUnitRunner that supports multidex installer and list out test information.
+ *
+ * This class is the equivalent of BaseChromiumInstrumentationTestRunner in JUnit3. Please
+ * beware that is this not a class runner. It is declared in test apk AndroidManifest.xml
+ * <instrumentation>
+ *
+ * TODO(yolandyan): remove this class after all tests are converted to JUnit4. Use class runner
+ * for test listing.
+ */
+@MainDex
+public class BaseChromiumAndroidJUnitRunner extends AndroidJUnitRunner {
+    private static final String LIST_ALL_TESTS_FLAG =
+            "org.chromium.base.test.BaseChromiumAndroidJUnitRunner.TestList";
+    private static final String LIST_TESTS_PACKAGE_FLAG =
+            "org.chromium.base.test.BaseChromiumAndroidJUnitRunner.TestListPackage";
+    /**
+     * This flag is supported by AndroidJUnitRunner.
+     *
+     * See the following page for detail
+     * https://developer.android.com/reference/android/support/test/runner/AndroidJUnitRunner.html
+     */
+    private static final String ARGUMENT_TEST_PACKAGE = "package";
+
+    /**
+     * The following arguments are corresponding to AndroidJUnitRunner command line arguments.
+     * `annotation`: run with only the argument annotation
+     * `notAnnotation`: run all tests except the ones with argument annotation
+     * `log`: run in log only mode, do not execute tests
+     *
+     * For more detail, please check
+     * https://developer.android.com/reference/android/support/test/runner/AndroidJUnitRunner.html
+     */
+    private static final String ARGUMENT_ANNOTATION = "annotation";
+    private static final String ARGUMENT_NOT_ANNOTATION = "notAnnotation";
+    private static final String ARGUMENT_LOG_ONLY = "log";
+
+    private static final String TAG = "BaseJUnitRunner";
+
+    @Override
+    public Application newApplication(ClassLoader cl, String className, Context context)
+            throws ClassNotFoundException, IllegalAccessException, InstantiationException {
+        // The multidex support library doesn't currently support having the test apk be multidex
+        // as well as the under-test apk being multidex. If MultiDex.install() is called for both,
+        // then re-extraction is triggered every time due to the support library caching only a
+        // single timestamp & crc.
+        //
+        // Attempt to install test apk multidex only if the apk-under-test is not multidex.
+        // It will likely continue to be true that the two are mutually exclusive because:
+        // * ProGuard enabled =>
+        //      Under-test apk is single dex.
+        //      Test apk duplicates under-test classes, so may need multidex.
+        // * ProGuard disabled =>
+        //      Under-test apk might be multidex
+        //      Test apk does not duplicate classes, so does not need multidex.
+        // https://crbug.com/824523
+        if (!BuildConfig.IS_MULTIDEX_ENABLED) {
+            ChromiumMultiDexInstaller.install(new BaseChromiumRunnerCommon.MultiDexContextWrapper(
+                    getContext(), getTargetContext()));
+            BaseChromiumRunnerCommon.reorderDexPathElements(cl, getContext(), getTargetContext());
+        }
+        return super.newApplication(cl, className, context);
+    }
+
+    /**
+     * Add TestListInstrumentationRunListener when argument ask the runner to list tests info.
+     *
+     * The running mechanism when argument has "listAllTests" is equivalent to that of
+     * {@link android.support.test.runner.AndroidJUnitRunner#onStart()} except it adds
+     * only TestListInstrumentationRunListener to monitor the tests.
+     */
+    @Override
+    public void onStart() {
+        Bundle arguments = InstrumentationRegistry.getArguments();
+        if (arguments != null && arguments.getString(LIST_ALL_TESTS_FLAG) != null) {
+            Log.w(TAG,
+                    String.format("Runner will list out tests info in JSON without running tests. "
+                                    + "Arguments: %s",
+                            arguments.toString()));
+            listTests(); // Intentionally not calling super.onStart() to avoid additional work.
+        } else {
+            if (arguments != null && arguments.getString(ARGUMENT_LOG_ONLY) != null) {
+                Log.e(TAG,
+                        String.format("Runner will log the tests without running tests."
+                                        + " If this cause a test run to fail, please report to"
+                                        + " crbug.com/754015. Arguments: %s",
+                                arguments.toString()));
+            }
+            super.onStart();
+        }
+    }
+
+    // TODO(yolandyan): Move this to test harness side once this class gets removed
+    private void addTestListPackage(Bundle bundle) {
+        PackageManager pm = getContext().getPackageManager();
+        InstrumentationInfo info;
+        try {
+            info = pm.getInstrumentationInfo(getComponentName(), PackageManager.GET_META_DATA);
+        } catch (NameNotFoundException e) {
+            Log.e(TAG, String.format("Could not find component %s", getComponentName()));
+            throw new RuntimeException(e);
+        }
+        Bundle metaDataBundle = info.metaData;
+        if (metaDataBundle != null && metaDataBundle.getString(LIST_TESTS_PACKAGE_FLAG) != null) {
+            bundle.putString(
+                    ARGUMENT_TEST_PACKAGE, metaDataBundle.getString(LIST_TESTS_PACKAGE_FLAG));
+        }
+    }
+
+    private void listTests() {
+        Bundle results = new Bundle();
+        TestListInstrumentationRunListener listener = new TestListInstrumentationRunListener();
+        try {
+            TestExecutor.Builder executorBuilder = new TestExecutor.Builder(this);
+            executorBuilder.addRunListener(listener);
+            Bundle junit3Arguments = new Bundle(InstrumentationRegistry.getArguments());
+            junit3Arguments.putString(ARGUMENT_NOT_ANNOTATION, "org.junit.runner.RunWith");
+            addTestListPackage(junit3Arguments);
+            TestRequest listJUnit3TestRequest = createListTestRequest(junit3Arguments);
+            results = executorBuilder.build().execute(listJUnit3TestRequest);
+
+            Bundle junit4Arguments = new Bundle(InstrumentationRegistry.getArguments());
+            junit4Arguments.putString(ARGUMENT_ANNOTATION, "org.junit.runner.RunWith");
+            addTestListPackage(junit4Arguments);
+
+            // Do not use Log runner from android test support.
+            //
+            // Test logging and execution skipping is handled by BaseJUnit4ClassRunner,
+            // having ARGUMENT_LOG_ONLY in argument bundle here causes AndroidJUnitRunner
+            // to use its own log-only class runner instead of BaseJUnit4ClassRunner.
+            junit4Arguments.remove(ARGUMENT_LOG_ONLY);
+
+            TestRequest listJUnit4TestRequest = createListTestRequest(junit4Arguments);
+            results.putAll(executorBuilder.build().execute(listJUnit4TestRequest));
+            listener.saveTestsToJson(
+                    InstrumentationRegistry.getArguments().getString(LIST_ALL_TESTS_FLAG));
+        } catch (IOException | RuntimeException e) {
+            String msg = "Fatal exception when running tests";
+            Log.e(TAG, msg, e);
+            // report the exception to instrumentation out
+            results.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
+                    msg + "\n" + Log.getStackTraceString(e));
+        }
+        finish(Activity.RESULT_OK, results);
+    }
+
+    private TestRequest createListTestRequest(Bundle arguments) {
+        DexFile[] incrementalJars = null;
+        try {
+            Class<?> bootstrapClass =
+                    Class.forName("org.chromium.incrementalinstall.BootstrapApplication");
+            incrementalJars =
+                    (DexFile[]) bootstrapClass.getDeclaredField("sIncrementalDexFiles").get(null);
+        } catch (Exception e) {
+            // Not an incremental apk.
+        }
+        RunnerArgs runnerArgs =
+                new RunnerArgs.Builder().fromManifest(this).fromBundle(arguments).build();
+        TestRequestBuilder builder;
+        if (incrementalJars != null) {
+            builder = new IncrementalInstallTestRequestBuilder(this, arguments, incrementalJars);
+        } else {
+            builder = new TestRequestBuilder(this, arguments);
+        }
+        builder.addFromRunnerArgs(runnerArgs);
+        builder.addApkToScan(getContext().getPackageCodePath());
+        // See crbug://841695. TestLoader.isTestClass is incorrectly deciding that
+        // InstrumentationTestSuite is a test class.
+        builder.removeTestClass("android.test.InstrumentationTestSuite");
+        return builder.build();
+    }
+
+    static boolean shouldListTests(Bundle arguments) {
+        return arguments != null && arguments.getString(LIST_ALL_TESTS_FLAG) != null;
+    }
+
+    /**
+     * Wraps TestRequestBuilder to make it work with incremental install.
+     * TestRequestBuilder does not know to look through the incremental install dex files, and has
+     * no api for telling it to do so. This class checks to see if the list of tests was given
+     * by the runner (mHasClassList), and if not overrides the auto-detection logic in build()
+     * to manually scan all .dex files.
+     */
+    private static class IncrementalInstallTestRequestBuilder extends TestRequestBuilder {
+        final List<String> mExcludedPrefixes = new ArrayList<String>();
+        final DexFile[] mIncrementalJars;
+        boolean mHasClassList;
+
+        IncrementalInstallTestRequestBuilder(
+                Instrumentation instr, Bundle bundle, DexFile[] incrementalJars) {
+            super(instr, bundle);
+            mIncrementalJars = incrementalJars;
+            try {
+                Field excludedPackagesField =
+                        TestRequestBuilder.class.getDeclaredField("DEFAULT_EXCLUDED_PACKAGES");
+                excludedPackagesField.setAccessible(true);
+                mExcludedPrefixes.addAll(Arrays.asList((String[]) excludedPackagesField.get(null)));
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        @Override
+        public TestRequestBuilder addFromRunnerArgs(RunnerArgs runnerArgs) {
+            mExcludedPrefixes.addAll(runnerArgs.notTestPackages);
+            // Without clearing, You get IllegalArgumentException:
+            // Ambiguous arguments: cannot provide both test package and test class(es) to run
+            runnerArgs.notTestPackages.clear();
+            return super.addFromRunnerArgs(runnerArgs);
+        }
+
+        @Override
+        public TestRequestBuilder addTestClass(String className) {
+            mHasClassList = true;
+            return super.addTestClass(className);
+        }
+
+        @Override
+        public TestRequestBuilder addTestMethod(String testClassName, String testMethodName) {
+            mHasClassList = true;
+            return super.addTestMethod(testClassName, testMethodName);
+        }
+
+        @Override
+        public TestRequest build() {
+            // If a test class was requested, then no need to iterate class loader.
+            if (!mHasClassList) {
+                // builder.addApkToScan uses new DexFile(path) under the hood, which on Dalvik OS's
+                // assumes that the optimized dex is in the default location (crashes).
+                // Perform our own dex file scanning instead as a workaround.
+                scanIncrementalJarsForTestClasses();
+            }
+            return super.build();
+        }
+
+        private static boolean startsWithAny(String str, List<String> prefixes) {
+            for (String prefix : prefixes) {
+                if (str.startsWith(prefix)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        private void scanIncrementalJarsForTestClasses() {
+            Log.i(TAG, "Scanning incremental classpath.");
+            // Mirror TestRequestBuilder.getClassNamesFromClassPath().
+            TestLoader loader = new TestLoader();
+            for (DexFile dexFile : mIncrementalJars) {
+                Enumeration<String> classNames = dexFile.entries();
+                while (classNames.hasMoreElements()) {
+                    String className = classNames.nextElement();
+                    if (!className.contains("$") && !startsWithAny(className, mExcludedPrefixes)
+                            && loader.loadIfTest(className) != null) {
+                        addTestClass(className);
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumRunnerCommon.java b/src/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumRunnerCommon.java
new file mode 100644
index 0000000..e5eb273
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumRunnerCommon.java
@@ -0,0 +1,162 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.support.v4.content.ContextCompat;
+
+import org.chromium.android.support.PackageManagerWrapper;
+import org.chromium.base.Log;
+import org.chromium.base.annotations.MainDex;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Comparator;
+
+/**
+ *  Functionality common to the JUnit3 and JUnit4 runners.
+ */
+@MainDex
+class BaseChromiumRunnerCommon {
+    private static final String TAG = "base_test";
+
+    /**
+     *  A ContextWrapper that allows multidex test APKs to extract secondary dexes into
+     *  the APK under test's data directory.
+     */
+    @MainDex
+    static class MultiDexContextWrapper extends ContextWrapper {
+        private Context mAppContext;
+
+        MultiDexContextWrapper(Context instrContext, Context appContext) {
+            super(instrContext);
+            mAppContext = appContext;
+        }
+
+        @Override
+        public File getFilesDir() {
+            return mAppContext.getFilesDir();
+        }
+
+        @Override
+        public SharedPreferences getSharedPreferences(String name, int mode) {
+            return mAppContext.getSharedPreferences(name, mode);
+        }
+
+        @Override
+        public PackageManager getPackageManager() {
+            return new PackageManagerWrapper(super.getPackageManager()) {
+                @Override
+                public ApplicationInfo getApplicationInfo(String packageName, int flags) {
+                    try {
+                        ApplicationInfo ai = super.getApplicationInfo(packageName, flags);
+                        if (packageName.equals(getPackageName())) {
+                            File dataDir = new File(
+                                    ContextCompat.getCodeCacheDir(mAppContext), "test-multidex");
+                            if (!dataDir.exists() && !dataDir.mkdirs()) {
+                                throw new IOException(String.format(
+                                        "Unable to create test multidex directory \"%s\"",
+                                        dataDir.getPath()));
+                            }
+                            ai.dataDir = dataDir.getPath();
+                        }
+                        return ai;
+                    } catch (Exception e) {
+                        Log.e(TAG, "Failed to get application info for %s", packageName, e);
+                    }
+                    return null;
+                }
+            };
+        }
+    }
+
+    /**
+     * Ensure all test dex entries precede app dex entries.
+     *
+     * @param cl ClassLoader to modify. Assumed to be a derivative of
+     *        {@link dalvik.system.BaseDexClassLoader}. If this isn't
+     *        the case, reordering will fail.
+     */
+    static void reorderDexPathElements(ClassLoader cl, Context context, Context targetContext) {
+        try {
+            Log.i(TAG,
+                    "Reordering dex files. If you're building a multidex test APK and see a "
+                            + "class resolving to an unexpected implementation, this may be why.");
+            Field pathListField = findField(cl, "pathList");
+            Object dexPathList = pathListField.get(cl);
+            Field dexElementsField = findField(dexPathList, "dexElements");
+            Object[] dexElementsList = (Object[]) dexElementsField.get(dexPathList);
+            Arrays.sort(dexElementsList,
+                    new DexListReorderingComparator(
+                            context.getPackageName(), targetContext.getPackageName()));
+            dexElementsField.set(dexPathList, dexElementsList);
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to reorder dex elements for testing.", e);
+        }
+    }
+
+    /**
+     *  Comparator for sorting dex list entries.
+     *
+     *  Using this to sort a list of dex list entries will result in the following order:
+     *   - Strings that contain neither the test package nor the app package in lexicographical
+     *     order.
+     *   - Strings that contain the test package in lexicographical order.
+     *   - Strings that contain the app package but not the test package in lexicographical order.
+     */
+    private static class DexListReorderingComparator implements Comparator<Object>, Serializable {
+        private String mTestPackage;
+        private String mAppPackage;
+
+        public DexListReorderingComparator(String testPackage, String appPackage) {
+            mTestPackage = testPackage;
+            mAppPackage = appPackage;
+        }
+
+        @Override
+        public int compare(Object o1, Object o2) {
+            String s1 = o1.toString();
+            String s2 = o2.toString();
+            if (s1.contains(mTestPackage)) {
+                if (!s2.contains(mTestPackage)) {
+                    if (s2.contains(mAppPackage)) {
+                        return -1;
+                    } else {
+                        return 1;
+                    }
+                }
+            } else if (s1.contains(mAppPackage)) {
+                if (s2.contains(mTestPackage)) {
+                    return 1;
+                } else if (!s2.contains(mAppPackage)) {
+                    return 1;
+                }
+            } else if (s2.contains(mTestPackage) || s2.contains(mAppPackage)) {
+                return -1;
+            }
+            return s1.compareTo(s2);
+        }
+    }
+
+    private static Field findField(Object instance, String name) throws NoSuchFieldException {
+        for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
+            try {
+                Field f = clazz.getDeclaredField(name);
+                f.setAccessible(true);
+                return f;
+            } catch (NoSuchFieldException e) {
+            }
+        }
+        throw new NoSuchFieldException(
+                "Unable to find field " + name + " in " + instance.getClass());
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/BaseJUnit4ClassRunner.java b/src/base/test/android/javatests/src/org/chromium/base/test/BaseJUnit4ClassRunner.java
new file mode 100644
index 0000000..49f27b5
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/BaseJUnit4ClassRunner.java
@@ -0,0 +1,277 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test;
+
+import static org.chromium.base.test.BaseChromiumAndroidJUnitRunner.shouldListTests;
+
+import android.content.Context;
+import android.support.annotation.CallSuper;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+import android.support.test.internal.util.AndroidRunnerParams;
+
+import org.junit.rules.MethodRule;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.Statement;
+
+import org.chromium.base.CommandLine;
+import org.chromium.base.ContextUtils;
+import org.chromium.base.Log;
+import org.chromium.base.test.BaseTestResult.PreTestHook;
+import org.chromium.base.test.params.MethodParamAnnotationRule;
+import org.chromium.base.test.util.DisableIfSkipCheck;
+import org.chromium.base.test.util.MinAndroidSdkLevelSkipCheck;
+import org.chromium.base.test.util.RestrictionSkipCheck;
+import org.chromium.base.test.util.SkipCheck;
+
+import java.io.File;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ *  A custom runner for JUnit4 tests that checks requirements to conditionally ignore tests.
+ *
+ *  This ClassRunner imports from AndroidJUnit4ClassRunner which is a hidden but accessible
+ *  class. The reason is that default JUnit4 runner for Android is a final class,
+ *  AndroidJUnit4. We need to extends an inheritable class to change {@link #runChild}
+ *  and {@link #isIgnored} to add SkipChecks and PreTesthook.
+ */
+public class BaseJUnit4ClassRunner extends AndroidJUnit4ClassRunner {
+    private static final String TAG = "BaseJUnit4ClassRunnr";
+
+    private static final String EXTRA_TRACE_FILE =
+            "org.chromium.base.test.BaseJUnit4ClassRunner.TraceFile";
+
+    /**
+     * Create a BaseJUnit4ClassRunner to run {@code klass} and initialize values.
+     *
+     * To add more SkipCheck or PreTestHook in subclass, create Lists of checks and hooks,
+     * pass them into the super constructors. If you want make a subclass extendable by other
+     * class runners, you also have to create a constructor similar to the following one that
+     * merges default checks or hooks with this checks and hooks passed in by constructor.
+     *
+     * <pre>
+     * <code>
+     * e.g.
+     * public ChildRunner extends BaseJUnit4ClassRunner {
+     *     public ChildRunner(final Class<?> klass) {
+     *             throws InitializationError {
+     *         this(klass, Collections.emptyList(), Collections.emptyList(),
+     * Collections.emptyList());
+     *     }
+     *
+     *     public ChildRunner(
+     *             final Class<?> klass, List<SkipCheck> checks, List<PreTestHook> hook,
+     * List<TestRule> rules) { throws InitializationError { super(klass, mergeList( checks,
+     * getSkipChecks()), mergeList(hooks, getPreTestHooks()));
+     *     }
+     *
+     *     public List<SkipCheck> getSkipChecks() {...}
+     *
+     *     public List<PreTestHook> getPreTestHooks() {...}
+     * </code>
+     * </pre>
+     *
+     * @throws InitializationError if the test class malformed
+     */
+    public BaseJUnit4ClassRunner(final Class<?> klass) throws InitializationError {
+        super(klass,
+                new AndroidRunnerParams(InstrumentationRegistry.getInstrumentation(),
+                        InstrumentationRegistry.getArguments(), false, 0L, false));
+
+        String traceOutput = InstrumentationRegistry.getArguments().getString(EXTRA_TRACE_FILE);
+
+        if (traceOutput != null) {
+            File traceOutputFile = new File(traceOutput);
+            File traceOutputDir = traceOutputFile.getParentFile();
+
+            if (traceOutputDir != null) {
+                if (traceOutputDir.exists() || traceOutputDir.mkdirs()) {
+                    TestTraceEvent.enable(traceOutputFile);
+                }
+            }
+        }
+    }
+
+    /**
+     * Merge two List into a new ArrayList.
+     *
+     * Used to merge the default SkipChecks/PreTestHooks with the subclasses's
+     * SkipChecks/PreTestHooks.
+     */
+    private static <T> List<T> mergeList(List<T> listA, List<T> listB) {
+        List<T> l = new ArrayList<>(listA);
+        l.addAll(listB);
+        return l;
+    }
+
+    @SafeVarargs
+    protected static <T> List<T> addToList(List<T> list, T... additionalEntries) {
+        return mergeList(list, Arrays.asList(additionalEntries));
+    }
+
+    @Override
+    protected void collectInitializationErrors(List<Throwable> errors) {
+        super.collectInitializationErrors(errors);
+        // Log any initialization errors to help debugging, as the host-side test runner can get
+        // confused by the thrown exception.
+        if (!errors.isEmpty()) {
+            Log.e(TAG, "Initialization errors in %s: %s", getTestClass().getName(), errors);
+        }
+    }
+
+    /**
+     * Override this method to return a list of {@link SkipCheck}s}.
+     *
+     * Additional hooks can be added to the list using {@link #addToList}:
+     * {@code return addToList(super.getSkipChecks(), check1, check2);}
+     */
+    @CallSuper
+    protected List<SkipCheck> getSkipChecks() {
+        return Arrays.asList(new RestrictionSkipCheck(InstrumentationRegistry.getTargetContext()),
+                new MinAndroidSdkLevelSkipCheck(), new DisableIfSkipCheck());
+    }
+
+    /**
+     * Override this method to return a list of {@link PreTestHook}s.
+     *
+     * Additional hooks can be added to the list using {@link #addToList}:
+     * {@code return addToList(super.getPreTestHooks(), hook1, hook2);}
+     * TODO(bauerb): Migrate PreTestHook to TestRule.
+     */
+    @CallSuper
+    protected List<PreTestHook> getPreTestHooks() {
+        return Collections.emptyList();
+    }
+
+    /**
+     * Override this method to return a list of method rules that should be applied to all tests
+     * run with this test runner.
+     *
+     * Additional rules can be added to the list using {@link #addToList}:
+     * {@code return addToList(super.getDefaultMethodRules(), rule1, rule2);}
+     */
+    @CallSuper
+    protected List<MethodRule> getDefaultMethodRules() {
+        return Collections.singletonList(new MethodParamAnnotationRule());
+    }
+
+    /**
+     * Override this method to return a list of rules that should be applied to all tests run with
+     * this test runner.
+     *
+     * Additional rules can be added to the list using {@link #addToList}:
+     * {@code return addToList(super.getDefaultTestRules(), rule1, rule2);}
+     */
+    @CallSuper
+    protected List<TestRule> getDefaultTestRules() {
+        return Collections.emptyList();
+    }
+
+    /**
+     * Evaluate whether a FrameworkMethod is ignored based on {@code SkipCheck}s.
+     */
+    @Override
+    protected boolean isIgnored(FrameworkMethod method) {
+        return super.isIgnored(method) || shouldSkip(method);
+    }
+
+    @Override
+    protected List<MethodRule> rules(Object target) {
+        List<MethodRule> declaredRules = super.rules(target);
+        List<MethodRule> defaultRules = getDefaultMethodRules();
+        return mergeList(defaultRules, declaredRules);
+    }
+
+    @Override
+    protected final List<TestRule> getTestRules(Object target) {
+        List<TestRule> declaredRules = super.getTestRules(target);
+        List<TestRule> defaultRules = getDefaultTestRules();
+        return mergeList(declaredRules, defaultRules);
+    }
+
+    /**
+     * Run test with or without execution based on bundle arguments.
+     */
+    @Override
+    public void run(RunNotifier notifier) {
+        ContextUtils.initApplicationContext(
+                InstrumentationRegistry.getTargetContext().getApplicationContext());
+        if (shouldListTests(InstrumentationRegistry.getArguments())) {
+            for (Description child : getDescription().getChildren()) {
+                notifier.fireTestStarted(child);
+                notifier.fireTestFinished(child);
+            }
+            return;
+        }
+
+        if (!CommandLine.isInitialized()) {
+            initCommandLineForTest();
+        }
+        super.run(notifier);
+    }
+
+    /**
+     * Override this method to change how test class runner initiate commandline flags
+     */
+    protected void initCommandLineForTest() {
+        CommandLine.init(null);
+    }
+
+    @Override
+    protected void runChild(FrameworkMethod method, RunNotifier notifier) {
+        String testName = method.getName();
+        TestTraceEvent.begin(testName);
+
+        runPreTestHooks(method);
+
+        super.runChild(method, notifier);
+
+        TestTraceEvent.end(testName);
+
+        // A new instance of BaseJUnit4ClassRunner is created on the device
+        // for each new method, so runChild will only be called once. Thus, we
+        // can disable tracing, and dump the output, once we get here.
+        TestTraceEvent.disable();
+    }
+
+    /**
+     * Loop through all the {@code PreTestHook}s to run them
+     */
+    private void runPreTestHooks(FrameworkMethod frameworkMethod) {
+        Method testMethod = frameworkMethod.getMethod();
+        Context targetContext = InstrumentationRegistry.getTargetContext();
+        for (PreTestHook hook : getPreTestHooks()) {
+            hook.run(targetContext, testMethod);
+        }
+    }
+
+    /**
+     * Loop through all the {@code SkipCheck}s to confirm whether a test should be ignored
+     */
+    private boolean shouldSkip(FrameworkMethod method) {
+        for (SkipCheck s : getSkipChecks()) {
+            if (s.shouldSkip(method)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /*
+     * Overriding this method to take screenshot of failure before tear down functions are run.
+     */
+    @Override
+    protected Statement withAfters(FrameworkMethod method, Object test, Statement base) {
+        return super.withAfters(method, test, new ScreenshotOnFailureStatement(base));
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/BaseTestResult.java b/src/base/test/android/javatests/src/org/chromium/base/test/BaseTestResult.java
new file mode 100644
index 0000000..a80e0cc
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/BaseTestResult.java
@@ -0,0 +1,137 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.SystemClock;
+
+import junit.framework.TestCase;
+import junit.framework.TestResult;
+
+import org.chromium.base.Log;
+import org.chromium.base.test.util.SkipCheck;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A test result that can skip tests.
+ */
+public class BaseTestResult extends TestResult {
+    private static final String TAG = "base_test";
+
+    private static final int SLEEP_INTERVAL_MS = 50;
+    private static final int WAIT_DURATION_MS = 5000;
+
+    private final Instrumentation mInstrumentation;
+    private final List<SkipCheck> mSkipChecks;
+    private final List<PreTestHook> mPreTestHooks;
+
+    /**
+     * Creates an instance of BaseTestResult.
+     */
+    public BaseTestResult(Instrumentation instrumentation) {
+        mSkipChecks = new ArrayList<>();
+        mPreTestHooks = new ArrayList<>();
+        mInstrumentation = instrumentation;
+    }
+
+    /**
+     * An interface for classes that have some code to run before a test. They run after
+     * {@link SkipCheck}s. Provides access to the test method (and the annotations defined for it)
+     * and the instrumentation context.
+     */
+    public interface PreTestHook {
+        /**
+         * @param targetContext the instrumentation context that will be used during the test.
+         * @param testMethod the test method to be run.
+         */
+        public void run(Context targetContext, Method testMethod);
+    }
+
+    /**
+     * Adds a check for whether a test should run.
+     *
+     * @param skipCheck The check to add.
+     */
+    public void addSkipCheck(SkipCheck skipCheck) {
+        mSkipChecks.add(skipCheck);
+    }
+
+    /**
+     * Adds hooks that will be executed before each test that runs.
+     *
+     * @param preTestHook The hook to add.
+     */
+    public void addPreTestHook(PreTestHook preTestHook) {
+        mPreTestHooks.add(preTestHook);
+    }
+
+    protected boolean shouldSkip(TestCase test) {
+        for (SkipCheck s : mSkipChecks) {
+            if (s.shouldSkip(test)) return true;
+        }
+        return false;
+    }
+
+    private void runPreTestHooks(TestCase test) {
+        try {
+            Method testMethod = test.getClass().getMethod(test.getName());
+            Context targetContext = getTargetContext();
+
+            for (PreTestHook hook : mPreTestHooks) {
+                hook.run(targetContext, testMethod);
+            }
+        } catch (NoSuchMethodException e) {
+            Log.e(TAG, "Unable to run pre test hooks.", e);
+        }
+    }
+
+    @Override
+    protected void run(TestCase test) {
+        runPreTestHooks(test);
+
+        if (shouldSkip(test)) {
+            startTest(test);
+
+            Bundle skipResult = new Bundle();
+            skipResult.putString("class", test.getClass().getName());
+            skipResult.putString("test", test.getName());
+            skipResult.putBoolean("test_skipped", true);
+            mInstrumentation.sendStatus(0, skipResult);
+
+            endTest(test);
+        } else {
+            super.run(test);
+        }
+    }
+
+    /**
+     * Gets the target context.
+     *
+     * On older versions of Android, getTargetContext() may initially return null, so we have to
+     * wait for it to become available.
+     *
+     * @return The target {@link Context} if available; null otherwise.
+     */
+    public Context getTargetContext() {
+        Context targetContext = mInstrumentation.getTargetContext();
+        try {
+            long startTime = SystemClock.uptimeMillis();
+            // TODO(jbudorick): Convert this to CriteriaHelper once that moves to base/.
+            while (targetContext == null
+                    && SystemClock.uptimeMillis() - startTime < WAIT_DURATION_MS) {
+                Thread.sleep(SLEEP_INTERVAL_MS);
+                targetContext = mInstrumentation.getTargetContext();
+            }
+        } catch (InterruptedException e) {
+            Log.e(TAG, "Interrupted while attempting to initialize the command line.");
+        }
+        return targetContext;
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/ScreenshotOnFailureStatement.java b/src/base/test/android/javatests/src/org/chromium/base/test/ScreenshotOnFailureStatement.java
new file mode 100644
index 0000000..397e8ab
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/ScreenshotOnFailureStatement.java
@@ -0,0 +1,83 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.UiDevice;
+
+import org.junit.runners.model.Statement;
+
+import org.chromium.base.Log;
+
+import java.io.File;
+
+/**
+ * Statement that captures screenshots if |base| statement fails.
+ *
+ * If --screenshot-path commandline flag is given, this |Statement|
+ * will save a screenshot to the specified path in the case of a test failure.
+ */
+public class ScreenshotOnFailureStatement extends Statement {
+    private static final String TAG = "ScreenshotOnFail";
+
+    private static final String EXTRA_SCREENSHOT_FILE =
+            "org.chromium.base.test.ScreenshotOnFailureStatement.ScreenshotFile";
+
+    private final Statement mBase;
+
+    public ScreenshotOnFailureStatement(final Statement base) {
+        mBase = base;
+    }
+
+    @Override
+    public void evaluate() throws Throwable {
+        try {
+            mBase.evaluate();
+        } catch (Throwable e) {
+            takeScreenshot();
+            throw e;
+        }
+    }
+
+    private void takeScreenshot() {
+        String screenshotFilePath =
+                InstrumentationRegistry.getArguments().getString(EXTRA_SCREENSHOT_FILE);
+        if (screenshotFilePath == null) {
+            Log.d(TAG,
+                    String.format("Did not save screenshot of failure. Must specify %s "
+                                    + "instrumentation argument to enable this feature.",
+                            EXTRA_SCREENSHOT_FILE));
+            return;
+        }
+
+        UiDevice uiDevice = null;
+        try {
+            uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        } catch (RuntimeException ex) {
+            Log.d(TAG, "Failed to initialize UiDevice", ex);
+            return;
+        }
+
+        File screenshotFile = new File(screenshotFilePath);
+        File screenshotDir = screenshotFile.getParentFile();
+        if (screenshotDir == null) {
+            Log.d(TAG,
+                    String.format(
+                            "Failed to create parent directory for %s. Can't save screenshot.",
+                            screenshotFile));
+            return;
+        }
+        if (!screenshotDir.exists()) {
+            if (!screenshotDir.mkdirs()) {
+                Log.d(TAG,
+                        String.format(
+                                "Failed to create %s. Can't save screenshot.", screenshotDir));
+                return;
+            }
+        }
+        Log.d(TAG, String.format("Saving screenshot of test failure, %s", screenshotFile));
+        uiDevice.takeScreenshot(screenshotFile);
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/SetUpStatement.java b/src/base/test/android/javatests/src/org/chromium/base/test/SetUpStatement.java
new file mode 100644
index 0000000..30ac2b6
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/SetUpStatement.java
@@ -0,0 +1,35 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test;
+
+import org.junit.rules.TestRule;
+import org.junit.runners.model.Statement;
+
+/**
+ * Custom Statement for SetUpTestRules.
+ *
+ * Calls {@link SetUpTestRule#setUp} before evaluating {@link SetUpTestRule#base} if
+ * {@link SetUpTestRule#shouldSetUp} is true
+ */
+public class SetUpStatement extends Statement {
+    private final Statement mBase;
+    private final SetUpTestRule<? extends TestRule> mSetUpTestRule;
+    private final boolean mShouldSetUp;
+
+    public SetUpStatement(
+            final Statement base, SetUpTestRule<? extends TestRule> callback, boolean shouldSetUp) {
+        mBase = base;
+        mSetUpTestRule = callback;
+        mShouldSetUp = shouldSetUp;
+    }
+
+    @Override
+    public void evaluate() throws Throwable {
+        if (mShouldSetUp) {
+            mSetUpTestRule.setUp();
+        }
+        mBase.evaluate();
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/SetUpTestRule.java b/src/base/test/android/javatests/src/org/chromium/base/test/SetUpTestRule.java
new file mode 100644
index 0000000..57dd8db
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/SetUpTestRule.java
@@ -0,0 +1,35 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test;
+
+import org.junit.rules.TestRule;
+
+/**
+ * An interface for TestRules that can be configured to automatically run set-up logic prior
+ * to &#064;Before.
+ *
+ * TestRules that implement this interface should return a {@link SetUpStatement} from their {@link
+ * TestRule#apply} method
+ *
+ * @param <T> TestRule type that implements this SetUpTestRule
+ */
+public interface SetUpTestRule<T extends TestRule> {
+    /**
+     * Set whether the TestRule should run setUp automatically.
+     *
+     * So TestRule can be declared in test like this:
+     * <code>
+     * &#064;Rule TestRule mRule = new MySetUpTestRule().shouldSetUp(true);
+     * </code>
+     *
+     * @return itself to chain up the calls for convenience
+     */
+    T shouldSetUp(boolean runSetUp);
+
+    /**
+     * Specify setUp action in this method
+     */
+    void setUp();
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/TestChildProcessConnection.java b/src/base/test/android/javatests/src/org/chromium/base/test/TestChildProcessConnection.java
new file mode 100644
index 0000000..ae91b44
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/TestChildProcessConnection.java
@@ -0,0 +1,87 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Bundle;
+
+import org.chromium.base.process_launcher.ChildProcessConnection;
+
+/** An implementation of ChildProcessConnection that does not connect to a real service. */
+public class TestChildProcessConnection extends ChildProcessConnection {
+    private static class MockChildServiceConnection
+            implements ChildProcessConnection.ChildServiceConnection {
+        private boolean mBound;
+
+        @Override
+        public boolean bind() {
+            mBound = true;
+            return true;
+        }
+
+        @Override
+        public void unbind() {
+            mBound = false;
+        }
+
+        @Override
+        public boolean isBound() {
+            return mBound;
+        }
+    }
+
+    private int mPid;
+    private boolean mConnected;
+    private ServiceCallback mServiceCallback;
+
+    /**
+     * Creates a mock binding corresponding to real ManagedChildProcessConnection after the
+     * connection is established: with initial binding bound and no strong binding.
+     */
+    public TestChildProcessConnection(ComponentName serviceName, boolean bindToCaller,
+            boolean bindAsExternalService, Bundle serviceBundle) {
+        super(null /* context */, serviceName, bindToCaller, bindAsExternalService, serviceBundle,
+                new ChildServiceConnectionFactory() {
+                    @Override
+                    public ChildServiceConnection createConnection(Intent bindIntent, int bindFlags,
+                            ChildServiceConnectionDelegate delegate) {
+                        return new MockChildServiceConnection();
+                    }
+                });
+    }
+
+    public void setPid(int pid) {
+        mPid = pid;
+    }
+
+    @Override
+    public int getPid() {
+        return mPid;
+    }
+
+    // We don't have a real service so we have to mock the connection status.
+    @Override
+    public void start(boolean useStrongBinding, ServiceCallback serviceCallback) {
+        super.start(useStrongBinding, serviceCallback);
+        mConnected = true;
+        mServiceCallback = serviceCallback;
+    }
+
+    @Override
+    public void stop() {
+        super.stop();
+        mConnected = false;
+    }
+
+    @Override
+    public boolean isConnected() {
+        return mConnected;
+    }
+
+    public ServiceCallback getServiceCallback() {
+        return mServiceCallback;
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/TestListInstrumentationRunListener.java b/src/base/test/android/javatests/src/org/chromium/base/test/TestListInstrumentationRunListener.java
new file mode 100644
index 0000000..1bd869c
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/TestListInstrumentationRunListener.java
@@ -0,0 +1,191 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test;
+
+import android.support.test.internal.runner.listener.InstrumentationRunListener;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.runner.Description;
+
+import org.chromium.base.Log;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Array;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A RunListener that list out all the test information into a json file.
+ */
+public class TestListInstrumentationRunListener extends InstrumentationRunListener {
+    private static final String TAG = "TestListRunListener";
+    private static final Set<String> SKIP_METHODS = new HashSet<>(
+            Arrays.asList(new String[] {"toString", "hashCode", "annotationType", "equals"}));
+
+    private final Map<Class<?>, JSONObject> mTestClassJsonMap = new HashMap<>();
+
+    /**
+     * Store the test method description to a Map at the beginning of a test run.
+     */
+    @Override
+    public void testStarted(Description desc) throws Exception {
+        if (mTestClassJsonMap.containsKey(desc.getTestClass())) {
+            ((JSONArray) mTestClassJsonMap.get(desc.getTestClass()).get("methods"))
+                .put(getTestMethodJSON(desc));
+        } else {
+            Class<?> testClass = desc.getTestClass();
+            mTestClassJsonMap.put(desc.getTestClass(), new JSONObject()
+                    .put("class", testClass.getName())
+                    .put("superclass", testClass.getSuperclass().getName())
+                    .put("annotations",
+                            getAnnotationJSON(Arrays.asList(testClass.getAnnotations())))
+                    .put("methods", new JSONArray().put(getTestMethodJSON(desc))));
+        }
+    }
+
+    /**
+     * Create a JSONArray with all the test class JSONObjects and save it to listed output path.
+     */
+    public void saveTestsToJson(String outputPath) throws IOException {
+        Writer writer = null;
+        File file = new File(outputPath);
+        try {
+            writer = new OutputStreamWriter(new FileOutputStream(file), "UTF-8");
+            JSONArray allTestClassesJSON = new JSONArray(mTestClassJsonMap.values());
+            writer.write(allTestClassesJSON.toString());
+        } catch (IOException e) {
+            Log.e(TAG, "failed to write json to file", e);
+            throw e;
+        } finally {
+            if (writer != null) {
+                try {
+                    writer.close();
+                } catch (IOException e) {
+                    // Intentionally ignore IOException when closing writer
+                }
+            }
+        }
+    }
+
+    /**
+     * Return a JSONOject that represent a Description of a method".
+     */
+    static JSONObject getTestMethodJSON(Description desc) throws Exception {
+        return new JSONObject()
+                .put("method", desc.getMethodName())
+                .put("annotations", getAnnotationJSON(desc.getAnnotations()));
+    }
+
+    /**
+     * Create a JSONObject that represent a collection of anntations.
+     *
+     * For example, for the following group of annotations for ExampleClass
+     * <code>
+     * @A
+     * @B(message = "hello", level = 3)
+     * public class ExampleClass() {}
+     * </code>
+     *
+     * This method would return a JSONObject as such:
+     * <code>
+     * {
+     *   "A": {},
+     *   "B": {
+     *     "message": "hello",
+     *     "level": "3"
+     *   }
+     * }
+     * </code>
+     *
+     * The method accomplish this by though through each annotation and reflectively call the
+     * annotation's method to get the element value, with exceptions to methods like "equals()"
+     * or "hashCode".
+     */
+    static JSONObject getAnnotationJSON(Collection<Annotation> annotations)
+            throws Exception {
+        JSONObject annotationsJsons = new JSONObject();
+        for (Annotation a : annotations) {
+            JSONObject elementJsonObject = new JSONObject();
+            for (Method method : a.annotationType().getMethods()) {
+                if (SKIP_METHODS.contains(method.getName())) {
+                    continue;
+                }
+                try {
+                    Object value = method.invoke(a);
+                    if (value == null) {
+                        elementJsonObject.put(method.getName(), null);
+                    } else if (value.getClass().isArray()) {
+                        Class<?> componentClass = value.getClass().getComponentType();
+                        // Arrays of primitives can't be cast to Object arrays, so we have to
+                        // special case them and manually make a copy.
+                        // This could be done more cleanly with something like
+                        // Arrays.stream(value).boxed().toArray(Integer[]::new), but that requires
+                        // a minimum SDK level of 24 to use.
+                        Object[] arrayValue = componentClass.isPrimitive()
+                                ? copyPrimitiveArrayToObjectArray(value)
+                                : ((Object[]) value);
+                        elementJsonObject.put(
+                                method.getName(), new JSONArray(Arrays.asList(arrayValue)));
+                    } else {
+                        elementJsonObject.put(method.getName(), value.toString());
+                    }
+                } catch (IllegalArgumentException e) {
+                }
+            }
+            annotationsJsons.put(a.annotationType().getSimpleName(), elementJsonObject);
+        }
+        return annotationsJsons;
+    }
+
+    private static Object[] copyPrimitiveArrayToObjectArray(Object primitiveArray)
+            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException,
+                   ClassCastException {
+        Class<?> primitiveClass = primitiveArray.getClass();
+        Class<?> componentClass = primitiveClass.getComponentType();
+        Class<?> wrapperClass = null;
+        if (componentClass == Boolean.TYPE) {
+            wrapperClass = Boolean.class;
+        } else if (componentClass == Byte.TYPE) {
+            wrapperClass = Byte.class;
+        } else if (componentClass == Character.TYPE) {
+            wrapperClass = Character.class;
+        } else if (componentClass == Double.TYPE) {
+            wrapperClass = Double.class;
+        } else if (componentClass == Float.TYPE) {
+            wrapperClass = Float.class;
+        } else if (componentClass == Integer.TYPE) {
+            wrapperClass = Integer.class;
+        } else if (componentClass == Long.TYPE) {
+            wrapperClass = Long.class;
+        } else if (componentClass == Short.TYPE) {
+            wrapperClass = Short.class;
+        } else {
+            // This should only be void since there are 8 primitives + void, but we can't support
+            // void.
+            throw new ClassCastException(
+                    "Cannot cast a primitive void array to Object void array.");
+        }
+        Method converterMethod = wrapperClass.getMethod("valueOf", componentClass);
+        ArrayList<Object> arrayValue = new ArrayList<Object>();
+        for (int i = 0; i < Array.getLength(primitiveClass.cast(primitiveArray)); i++) {
+            arrayValue.add(
+                    converterMethod.invoke(Array.get(primitiveClass.cast(primitiveArray), i)));
+        }
+        return arrayValue.toArray();
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/TestTraceEvent.java b/src/base/test/android/javatests/src/org/chromium/base/test/TestTraceEvent.java
new file mode 100644
index 0000000..5e0f6b3
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/TestTraceEvent.java
@@ -0,0 +1,168 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import org.chromium.base.Log;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.PrintStream;
+
+/**
+ * TestTraceEvent is a modified version of TraceEvent, intended for tracing test runs.
+ */
+public class TestTraceEvent {
+    private static final String TAG = "TestTraceEvent";
+
+    /** The event types understood by the trace scripts. */
+    private enum EventType {
+        BEGIN("B"),
+        END("E"),
+        INSTANT("I");
+
+        private final String mTypeStr;
+
+        EventType(String typeStr) {
+            mTypeStr = typeStr;
+        }
+
+        @Override
+        public String toString() {
+            return mTypeStr;
+        }
+    }
+
+    // Locks internal fields.
+    private static final Object sLock = new Object();
+
+    private static File sOutputFile;
+
+    private static boolean sEnabled;
+
+    // A list of trace event strings.
+    private static JSONArray sTraceStrings;
+
+    /**
+     * Enable tracing, and set a specific output file. If tracing was previously enabled and
+     * disabled, that data is cleared.
+     *
+     * @param file Which file to append the trace data to.
+     */
+    public static void enable(File outputFile) {
+        synchronized (sLock) {
+            if (sEnabled) return;
+
+            sEnabled = true;
+            sOutputFile = outputFile;
+            sTraceStrings = new JSONArray();
+        }
+    }
+
+    /**
+     * Disabling of tracing will dump trace data to the system log.
+     */
+    public static void disable() {
+        synchronized (sLock) {
+            if (!sEnabled) return;
+
+            sEnabled = false;
+            dumpTraceOutput();
+            sTraceStrings = null;
+        }
+    }
+
+    /**
+     * @return True if tracing is enabled, false otherwise.
+     */
+    public static boolean isEnabled() {
+        synchronized (sLock) {
+            return sEnabled;
+        }
+    }
+
+    /**
+     * Record an "instant" trace event. E.g. "screen update happened".
+     */
+    public static void instant(String name) {
+        synchronized (sLock) {
+            if (!sEnabled) return;
+
+            saveTraceString(name, name.hashCode(), EventType.INSTANT);
+        }
+    }
+
+    /**
+     * Record an "begin" trace event. Begin trace events should have a matching end event (recorded
+     * by calling {@link #end(String)}).
+     */
+    public static void begin(String name) {
+        synchronized (sLock) {
+            if (!sEnabled) return;
+
+            saveTraceString(name, name.hashCode(), EventType.BEGIN);
+        }
+    }
+
+    /**
+     * Record an "end" trace event, to match a begin event (recorded by calling {@link
+     * #begin(String)}). The time delta between begin and end is usually interesting to graph code.
+     */
+    public static void end(String name) {
+        synchronized (sLock) {
+            if (!sEnabled) return;
+
+            saveTraceString(name, name.hashCode(), EventType.END);
+        }
+    }
+
+    /**
+     * Save a trace event as a JSON dict.
+     *
+     * @param name The trace data.
+     * @param id An identifier for the event, to be saved as the thread ID.
+     * @param type the type of trace event (B, E, I).
+     */
+    private static void saveTraceString(String name, long id, EventType type) {
+        // We use System.currentTimeMillis() because it agrees with the value of
+        // the $EPOCHREALTIME environment variable. The Python test runner code
+        // uses that variable to synchronize timing.
+        long timeMicroseconds = System.currentTimeMillis() * 1000;
+
+        try {
+            JSONObject traceObj = new JSONObject();
+            traceObj.put("cat", "Java");
+            traceObj.put("ts", timeMicroseconds);
+            traceObj.put("ph", type);
+            traceObj.put("name", name);
+            traceObj.put("tid", id);
+
+            sTraceStrings.put(traceObj);
+        } catch (JSONException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Dump all tracing data we have saved up to the log.
+     * Output as JSON for parsing convenience.
+     */
+    private static void dumpTraceOutput() {
+        try {
+            PrintStream stream = new PrintStream(new FileOutputStream(sOutputFile, true));
+            try {
+                stream.print(sTraceStrings);
+            } finally {
+                if (stream != null) stream.close();
+            }
+        } catch (FileNotFoundException ex) {
+            Log.e(TAG, "Unable to dump trace data to output file.");
+        }
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/params/BaseJUnit4RunnerDelegate.java b/src/base/test/android/javatests/src/org/chromium/base/test/params/BaseJUnit4RunnerDelegate.java
new file mode 100644
index 0000000..c0dcd46
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/params/BaseJUnit4RunnerDelegate.java
@@ -0,0 +1,42 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.params;
+
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+
+import org.chromium.base.test.BaseJUnit4ClassRunner;
+import org.chromium.base.test.params.ParameterizedRunner.ParameterizedTestInstantiationException;
+
+import java.util.List;
+
+/**
+ * Class runner delegate that extends BaseJUnit4ClassRunner
+ */
+public final class BaseJUnit4RunnerDelegate
+        extends BaseJUnit4ClassRunner implements ParameterizedRunnerDelegate {
+    private ParameterizedRunnerDelegateCommon mDelegateCommon;
+
+    public BaseJUnit4RunnerDelegate(Class<?> klass,
+            ParameterizedRunnerDelegateCommon delegateCommon) throws InitializationError {
+        super(klass);
+        mDelegateCommon = delegateCommon;
+    }
+
+    @Override
+    public void collectInitializationErrors(List<Throwable> errors) {
+        ParameterizedRunnerDelegateCommon.collectInitializationErrors(errors);
+    }
+
+    @Override
+    public List<FrameworkMethod> computeTestMethods() {
+        return mDelegateCommon.computeTestMethods();
+    }
+
+    @Override
+    public Object createTest() throws ParameterizedTestInstantiationException {
+        return mDelegateCommon.createTest();
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/params/BlockJUnit4RunnerDelegate.java b/src/base/test/android/javatests/src/org/chromium/base/test/params/BlockJUnit4RunnerDelegate.java
new file mode 100644
index 0000000..7c948bb
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/params/BlockJUnit4RunnerDelegate.java
@@ -0,0 +1,42 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.params;
+
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+
+import org.chromium.base.test.params.ParameterizedRunner.ParameterizedTestInstantiationException;
+
+import java.util.List;
+
+/**
+ * Parameterized class runner delegate that extends BlockJUnit4ClassRunner
+ */
+public final class BlockJUnit4RunnerDelegate
+        extends BlockJUnit4ClassRunner implements ParameterizedRunnerDelegate {
+    private ParameterizedRunnerDelegateCommon mDelegateCommon;
+
+    public BlockJUnit4RunnerDelegate(Class<?> klass,
+            ParameterizedRunnerDelegateCommon delegateCommon) throws InitializationError {
+        super(klass);
+        mDelegateCommon = delegateCommon;
+    }
+
+    @Override
+    public void collectInitializationErrors(List<Throwable> errors) {
+        ParameterizedRunnerDelegateCommon.collectInitializationErrors(errors);
+    }
+
+    @Override
+    public List<FrameworkMethod> computeTestMethods() {
+        return mDelegateCommon.computeTestMethods();
+    }
+
+    @Override
+    public Object createTest() throws ParameterizedTestInstantiationException {
+        return mDelegateCommon.createTest();
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/params/MethodParamAnnotationRule.java b/src/base/test/android/javatests/src/org/chromium/base/test/params/MethodParamAnnotationRule.java
new file mode 100644
index 0000000..2986b96
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/params/MethodParamAnnotationRule.java
@@ -0,0 +1,62 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.params;
+
+import org.junit.runners.model.Statement;
+
+import org.chromium.base.test.params.ParameterAnnotations.UseMethodParameterAfter;
+import org.chromium.base.test.params.ParameterAnnotations.UseMethodParameterBefore;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Processes {@link UseMethodParameterBefore} and {@link UseMethodParameterAfter} annotations to run
+ * the corresponding methods. To use, add an instance to the test class and annotate it with
+ * {@code @}{@link org.junit.Rule Rule}.
+ */
+public class MethodParamAnnotationRule extends MethodParamRule {
+    @Override
+    protected Statement applyParameterAndValues(final Statement base, Object target,
+            Class<? extends ParameterProvider> parameterProvider, List<Object> values) {
+        final List<Method> beforeMethods = new ArrayList<>();
+        final List<Method> afterMethods = new ArrayList<>();
+        for (Method m : target.getClass().getDeclaredMethods()) {
+            if (!m.getReturnType().equals(Void.TYPE)) continue;
+            if (!Modifier.isPublic(m.getModifiers())) continue;
+
+            UseMethodParameterBefore beforeAnnotation =
+                    m.getAnnotation(UseMethodParameterBefore.class);
+            if (beforeAnnotation != null && beforeAnnotation.value().equals(parameterProvider)) {
+                beforeMethods.add(m);
+            }
+
+            UseMethodParameterAfter afterAnnotation =
+                    m.getAnnotation(UseMethodParameterAfter.class);
+            if (afterAnnotation != null && afterAnnotation.value().equals(parameterProvider)) {
+                afterMethods.add(m);
+            }
+        }
+
+        if (beforeMethods.isEmpty() && afterMethods.isEmpty()) return base;
+
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                for (Method m : beforeMethods) {
+                    m.invoke(target, values.toArray());
+                }
+
+                base.evaluate();
+
+                for (Method m : afterMethods) {
+                    m.invoke(target, values.toArray());
+                }
+            }
+        };
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/params/MethodParamRule.java b/src/base/test/android/javatests/src/org/chromium/base/test/params/MethodParamRule.java
new file mode 100644
index 0000000..440831a
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/params/MethodParamRule.java
@@ -0,0 +1,35 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.params;
+
+import org.junit.rules.MethodRule;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.Statement;
+
+import org.chromium.base.test.params.ParameterAnnotations.UseMethodParameter;
+
+import java.util.List;
+
+/**
+ * Abstract base class for rules that are applied to test methods using
+ * {@link org.chromium.base.test.params.ParameterAnnotations.UseMethodParameter method parameters}.
+ */
+public abstract class MethodParamRule implements MethodRule {
+    @Override
+    public Statement apply(final Statement base, FrameworkMethod method, Object target) {
+        UseMethodParameter useParameterProvider = method.getAnnotation(UseMethodParameter.class);
+        if (useParameterProvider == null) return base;
+        Class<? extends ParameterProvider> parameterProvider = useParameterProvider.value();
+
+        if (!(method instanceof ParameterizedFrameworkMethod)) return base;
+        ParameterSet parameters = ((ParameterizedFrameworkMethod) method).getParameterSet();
+        List<Object> values = parameters.getValues();
+
+        return applyParameterAndValues(base, target, parameterProvider, values);
+    }
+
+    protected abstract Statement applyParameterAndValues(final Statement base, Object target,
+            Class<? extends ParameterProvider> parameterProvider, List<Object> values);
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterAnnotations.java b/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterAnnotations.java
new file mode 100644
index 0000000..7918369
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterAnnotations.java
@@ -0,0 +1,78 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.params;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotations for Parameterized Tests
+ */
+public class ParameterAnnotations {
+    /**
+     * Annotation for test methods to indicate associated {@link ParameterProvider}.
+     * Note: the class referred to must be public and have a public default constructor.
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.METHOD)
+    public @interface UseMethodParameter {
+        Class<? extends ParameterProvider> value();
+    }
+
+    /**
+     * Annotation for methods that should be called before running a test with method parameters.
+     *
+     * In order to use this, add a {@link MethodParamAnnotationRule} annotated with
+     * {@code @}{@link org.junit.Rule Rule} to your test class.
+     * @see ParameterProvider
+     * @see UseMethodParameterAfter
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.METHOD)
+    public @interface UseMethodParameterBefore {
+        Class<? extends ParameterProvider> value();
+    }
+
+    /**
+     * Annotation for methods that should be called after running a test with method parameters.
+     *
+     * In order to use this, add a {@link MethodParamAnnotationRule} annotated with
+     * {@code @}{@link org.junit.Rule Rule} to your test class.
+     * @see ParameterProvider
+     * @see UseMethodParameterBefore
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.METHOD)
+    public @interface UseMethodParameterAfter {
+        Class<? extends ParameterProvider> value();
+    }
+
+    /**
+     * Annotation for static field of a `List<ParameterSet>` for entire test class
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.FIELD)
+    public @interface ClassParameter {}
+
+    /**
+     * Annotation for static field of a `List<ParameterSet>` of TestRule
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.FIELD)
+    public @interface RuleParameter {}
+
+    /**
+     * Annotation for test class, it specifies which ParameterizeRunnerDelegate to use.
+     *
+     * The default ParameterizedRunnerDelegate is BaseJUnit4RunnerDelegate.class
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target(ElementType.TYPE)
+    public @interface UseRunnerDelegate {
+        Class<? extends ParameterizedRunnerDelegate> value();
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterProvider.java b/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterProvider.java
new file mode 100644
index 0000000..9bf27bd
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterProvider.java
@@ -0,0 +1,11 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.params;
+
+/**
+ * Generator to use generate arguments for parameterized test methods.
+ * @see ParameterAnnotations.UseMethodParameter
+ */
+public interface ParameterProvider { Iterable<ParameterSet> getParameters(); }
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterSet.java b/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterSet.java
new file mode 100644
index 0000000..1cdb576
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterSet.java
@@ -0,0 +1,129 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.params;
+
+import org.junit.Assert;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
+/**
+ * A set of parameters for one *SINGLE* test method or test class constructor.
+ *
+ * For example, <code>new ParameterSet().value("a", "b")</code> is intended for
+ * a test method/constructor that takes in two string as arguments.
+ * <code>public void testSimple(String a, String b) {...}</code>
+ * or
+ * <code>public MyTestClass(String a, String b) {...}</code>
+ *
+ * To parameterize testSimple or MyTestClass's tests, create multiple ParameterSets
+ * <code>
+ * static List<ParameterSet> sAllParameterSets = new ArrayList<>();
+ * static {
+ *   sAllParameterSets.add(new ParameterSet().value("a", "b");
+ *   sAllParameterSets.add(new ParameterSet().value("c", "d");
+ * }
+ */
+public class ParameterSet {
+    private List<Object> mValues;
+    private String mName;
+
+    public ParameterSet() {}
+
+    public ParameterSet value(Object firstArg, Object... objects) {
+        List<Object> parameterList = new ArrayList<Object>();
+        parameterList.add(firstArg);
+        parameterList.addAll(Arrays.asList(objects));
+        Assert.assertTrue(
+                "Can not create ParameterSet with no parameters", parameterList.size() != 0);
+        mValues = validateAndCopy(parameterList);
+        return this;
+    }
+
+    public ParameterSet name(String name) {
+        mName = name;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        if (mValues == null) {
+            return "null";
+        }
+        return Arrays.toString(mValues.toArray());
+    }
+
+    private List<Object> validateAndCopy(List<Object> values) {
+        List<Object> tempValues = new ArrayList<>();
+        for (Object o : values) {
+            if (o == null) {
+                tempValues.add(null);
+            } else {
+                if (o.getClass().isPrimitive() || ACCEPTABLE_TYPES.contains(o.getClass())
+                        || o instanceof Callable) {
+                    tempValues.add(o);
+                } else {
+                    // TODO(yolandyan): maybe come up with way to support
+                    // complex object while handling immutability at the
+                    // same time
+                    throw new IllegalArgumentException("Type \"%s\" is not supported in"
+                            + " parameterized testing at this time. Accepted types include"
+                            + " all primitive types along with "
+                            + Arrays.toString(ACCEPTABLE_TYPES.toArray(
+                                      new String[ACCEPTABLE_TYPES.size()])));
+                }
+            }
+        }
+        return Collections.unmodifiableList(tempValues);
+    }
+
+    String getName() {
+        if (mName == null) {
+            return "";
+        }
+        return mName;
+    }
+
+    List<Object> getValues() {
+        return mValues;
+    }
+
+    int size() {
+        if (mValues == null) return 0;
+        return mValues.size();
+    }
+
+    private static final Set<Class<?>> ACCEPTABLE_TYPES = getAcceptableTypes();
+
+    /**
+     * Any immutable class is acceptable.
+     */
+    private static Set<Class<?>> getAcceptableTypes() {
+        Set<Class<?>> ret = new HashSet<Class<?>>();
+        ret.add(Boolean.class);
+        ret.add(Byte.class);
+        ret.add(Character.class);
+        ret.add(Class.class);
+        ret.add(Double.class);
+        ret.add(File.class);
+        ret.add(Float.class);
+        ret.add(Integer.class);
+        ret.add(Long.class);
+        ret.add(Short.class);
+        ret.add(String.class);
+        ret.add(URI.class);
+        ret.add(URL.class);
+        ret.add(Void.class);
+        return ret;
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedFrameworkMethod.java b/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedFrameworkMethod.java
new file mode 100644
index 0000000..f3333b5
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedFrameworkMethod.java
@@ -0,0 +1,94 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.params;
+
+import org.junit.runners.model.FrameworkMethod;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Custom FrameworkMethod that includes a {@code ParameterSet} that
+ * represents the parameters for this test method
+ */
+public class ParameterizedFrameworkMethod extends FrameworkMethod {
+    private ParameterSet mParameterSet;
+    private String mName;
+
+    public ParameterizedFrameworkMethod(
+            Method method, ParameterSet parameterSet, String classParameterSetName) {
+        super(method);
+        mParameterSet = parameterSet;
+        String postFix = "";
+        if (classParameterSetName != null && !classParameterSetName.isEmpty()) {
+            postFix += "_" + classParameterSetName;
+        }
+        if (parameterSet != null && !parameterSet.getName().isEmpty()) {
+            postFix += "_" + parameterSet.getName();
+        }
+        mName = postFix.isEmpty() ? method.getName() : method.getName() + "_" + postFix;
+    }
+
+    @Override
+    public String getName() {
+        return mName;
+    }
+
+    @Override
+    public Object invokeExplosively(Object target, Object... params) throws Throwable {
+        if (mParameterSet != null) {
+            return super.invokeExplosively(target, mParameterSet.getValues().toArray());
+        }
+        return super.invokeExplosively(target, params);
+    }
+
+    static List<FrameworkMethod> wrapAllFrameworkMethods(
+            Collection<FrameworkMethod> frameworkMethods, String classParameterSetName) {
+        List<FrameworkMethod> results = new ArrayList<>();
+        for (FrameworkMethod frameworkMethod : frameworkMethods) {
+            results.add(new ParameterizedFrameworkMethod(
+                    frameworkMethod.getMethod(), null, classParameterSetName));
+        }
+        return results;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof ParameterizedFrameworkMethod) {
+            ParameterizedFrameworkMethod method = (ParameterizedFrameworkMethod) obj;
+            return super.equals(obj) && method.getParameterSet().equals(getParameterSet())
+                    && method.getName().equals(getName());
+        }
+        return false;
+    }
+
+    /**
+     * Override hashCode method to distinguish two ParameterizedFrameworkmethod with same
+     * Method object.
+     */
+    @Override
+    public int hashCode() {
+        int result = 17;
+        result = 31 * result + super.hashCode();
+        result = 31 * result + getName().hashCode();
+        if (getParameterSet() != null) {
+            result = 31 * result + getParameterSet().hashCode();
+        }
+        return result;
+    }
+
+    Annotation[] getTestAnnotations() {
+        // TODO(yolandyan): add annotation from the ParameterSet, enable
+        // test writing to add SkipCheck for an individual parameter
+        return getMethod().getAnnotations();
+    }
+
+    public ParameterSet getParameterSet() {
+        return mParameterSet;
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunner.java b/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunner.java
new file mode 100644
index 0000000..834f261
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunner.java
@@ -0,0 +1,221 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.params;
+
+import org.junit.Test;
+import org.junit.runner.Runner;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.Suite;
+import org.junit.runners.model.FrameworkField;
+import org.junit.runners.model.TestClass;
+
+import org.chromium.base.test.params.ParameterAnnotations.ClassParameter;
+import org.chromium.base.test.params.ParameterAnnotations.UseMethodParameter;
+import org.chromium.base.test.params.ParameterAnnotations.UseRunnerDelegate;
+import org.chromium.base.test.params.ParameterizedRunnerDelegateFactory.ParameterizedRunnerDelegateInstantiationException;
+
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * ParameterizedRunner generates a list of runners for each of class parameter set in a test class.
+ *
+ * ParameterizedRunner looks for {@code @ClassParameter} annotation in test class and
+ * generates a list of ParameterizedRunnerDelegate runners for each ParameterSet.
+ */
+public final class ParameterizedRunner extends Suite {
+    private final List<Runner> mRunners;
+
+    /**
+     * Create a ParameterizedRunner to run test class
+     *
+     * @param klass the Class of the test class, test class should be atomic
+     *              (extends only Object)
+     */
+    public ParameterizedRunner(Class<?> klass) throws Throwable {
+        super(klass, Collections.emptyList()); // pass in empty list of runners
+        validate();
+        mRunners = createRunners(getTestClass());
+    }
+
+    @Override
+    protected List<Runner> getChildren() {
+        return mRunners;
+    }
+
+    /**
+     * ParentRunner calls collectInitializationErrors() to check for errors in Test class.
+     * Parameterized tests are written in unconventional ways, therefore, this method is
+     * overridden and validation is done seperately.
+     */
+    @Override
+    protected void collectInitializationErrors(List<Throwable> errors) {
+        // Do not call super collectInitializationErrors
+    }
+
+    private void validate() throws Throwable {
+        validateNoNonStaticInnerClass();
+        validateOnlyOneConstructor();
+        validateInstanceMethods();
+        validateOnlyOneClassParameterField();
+        validateAtLeastOneParameterSetField();
+    }
+
+    private void validateNoNonStaticInnerClass() throws Exception {
+        if (getTestClass().isANonStaticInnerClass()) {
+            throw new Exception("The inner class " + getTestClass().getName() + " is not static.");
+        }
+    }
+
+    private void validateOnlyOneConstructor() throws Exception {
+        if (!hasOneConstructor()) {
+            throw new Exception("Test class should have exactly one public constructor");
+        }
+    }
+
+    private boolean hasOneConstructor() {
+        return getTestClass().getJavaClass().getConstructors().length == 1;
+    }
+
+    private void validateOnlyOneClassParameterField() {
+        if (getTestClass().getAnnotatedFields(ClassParameter.class).size() > 1) {
+            throw new IllegalParameterArgumentException(String.format(Locale.getDefault(),
+                    "%s class has more than one @ClassParameter, only one is allowed",
+                    getTestClass().getName()));
+        }
+    }
+
+    private void validateAtLeastOneParameterSetField() {
+        if (getTestClass().getAnnotatedFields(ClassParameter.class).isEmpty()
+                && getTestClass().getAnnotatedMethods(UseMethodParameter.class).isEmpty()) {
+            throw new IllegalArgumentException(String.format(Locale.getDefault(),
+                    "%s has no field annotated with @ClassParameter or method annotated with"
+                            + "@UseMethodParameter; it should not use ParameterizedRunner",
+                    getTestClass().getName()));
+        }
+    }
+
+    private void validateInstanceMethods() throws Exception {
+        if (getTestClass().getAnnotatedMethods(Test.class).size() == 0) {
+            throw new Exception("No runnable methods");
+        }
+    }
+
+    /**
+     * Return a list of runner delegates through ParameterizedRunnerDelegateFactory.
+     *
+     * For class parameter set: each class can only have one list of class parameter sets.
+     * Each parameter set will be used to create one runner.
+     *
+     * For method parameter set: a single list method parameter sets is associated with
+     * a string tag, an immutable map of string to parameter set list will be created and
+     * passed into factory for each runner delegate to create multiple tests. Only one
+     * Runner will be created for a method that uses @UseMethodParameter, regardless of the
+     * number of ParameterSets in the associated list.
+     *
+     * @return a list of runners
+     * @throws ParameterizedRunnerDelegateInstantiationException if runner delegate can not
+     *         be instantiated with constructor reflectively
+     * @throws IllegalAccessError if the field in tests are not accessible
+     */
+    static List<Runner> createRunners(TestClass testClass)
+            throws IllegalAccessException, ParameterizedRunnerDelegateInstantiationException {
+        List<ParameterSet> classParameterSetList;
+        if (testClass.getAnnotatedFields(ClassParameter.class).isEmpty()) {
+            classParameterSetList = new ArrayList<>();
+            classParameterSetList.add(null);
+        } else {
+            classParameterSetList = getParameterSetList(
+                    testClass.getAnnotatedFields(ClassParameter.class).get(0), testClass);
+            validateWidth(classParameterSetList);
+        }
+
+        Class<? extends ParameterizedRunnerDelegate> runnerDelegateClass =
+                getRunnerDelegateClass(testClass);
+        ParameterizedRunnerDelegateFactory factory = new ParameterizedRunnerDelegateFactory();
+        List<Runner> runnersForTestClass = new ArrayList<>();
+        for (ParameterSet classParameterSet : classParameterSetList) {
+            BlockJUnit4ClassRunner runner = (BlockJUnit4ClassRunner) factory.createRunner(
+                    testClass, classParameterSet, runnerDelegateClass);
+            runnersForTestClass.add(runner);
+        }
+        return runnersForTestClass;
+    }
+
+    /**
+     * Return an unmodifiable list of ParameterSet through a FrameworkField
+     */
+    private static List<ParameterSet> getParameterSetList(FrameworkField field, TestClass testClass)
+            throws IllegalAccessException {
+        field.getField().setAccessible(true);
+        if (!Modifier.isStatic(field.getField().getModifiers())) {
+            throw new IllegalParameterArgumentException(String.format(Locale.getDefault(),
+                    "ParameterSetList fields must be static, this field %s in %s is not",
+                    field.getName(), testClass.getName()));
+        }
+        if (!(field.get(testClass.getJavaClass()) instanceof List)) {
+            throw new IllegalArgumentException(String.format(Locale.getDefault(),
+                    "Fields with @ClassParameter annotations must be an instance of List, "
+                            + "this field %s in %s is not list",
+                    field.getName(), testClass.getName()));
+        }
+        @SuppressWarnings("unchecked") // checked above
+        List<ParameterSet> result = (List<ParameterSet>) field.get(testClass.getJavaClass());
+        return Collections.unmodifiableList(result);
+    }
+
+    static void validateWidth(Iterable<ParameterSet> parameterSetList) {
+        int lastSize = -1;
+        for (ParameterSet set : parameterSetList) {
+            if (set.size() == 0) {
+                throw new IllegalParameterArgumentException(
+                        "No parameter is added to method ParameterSet");
+            }
+            if (lastSize == -1 || set.size() == lastSize) {
+                lastSize = set.size();
+            } else {
+                throw new IllegalParameterArgumentException(String.format(Locale.getDefault(),
+                        "All ParameterSets in a list of ParameterSet must have equal"
+                                + " length. The current ParameterSet (%s) contains %d parameters,"
+                                + " while previous ParameterSet contains %d parameters",
+                        Arrays.toString(set.getValues().toArray()), set.size(), lastSize));
+            }
+        }
+    }
+
+    /**
+     * Get the runner delegate class for the test class if {@code @UseRunnerDelegate} is used.
+     * The default runner delegate is BaseJUnit4RunnerDelegate.class
+     */
+    private static Class<? extends ParameterizedRunnerDelegate> getRunnerDelegateClass(
+            TestClass testClass) {
+        if (testClass.getAnnotation(UseRunnerDelegate.class) != null) {
+            return testClass.getAnnotation(UseRunnerDelegate.class).value();
+        }
+        return BaseJUnit4RunnerDelegate.class;
+    }
+
+    static class IllegalParameterArgumentException extends IllegalArgumentException {
+        IllegalParameterArgumentException(String msg) {
+            super(msg);
+        }
+    }
+
+    public static class ParameterizedTestInstantiationException extends Exception {
+        ParameterizedTestInstantiationException(
+                TestClass testClass, String parameterSetString, Exception e) {
+            super(String.format(
+                          "Test class %s can not be initiated, the provided parameters are %s,"
+                                  + " the required parameter types are %s",
+                          testClass.getJavaClass().toString(), parameterSetString,
+                          Arrays.toString(testClass.getOnlyConstructor().getParameterTypes())),
+                    e);
+        }
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunnerDelegate.java b/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunnerDelegate.java
new file mode 100644
index 0000000..d3698a9
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunnerDelegate.java
@@ -0,0 +1,36 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.params;
+
+import org.junit.runners.model.FrameworkMethod;
+
+import org.chromium.base.test.params.ParameterizedRunner.ParameterizedTestInstantiationException;
+
+import java.util.List;
+
+/**
+ * This interface defines the methods that needs to be overriden for a Runner to
+ * be used by ParameterizedRunner to generate individual runners for parameters.
+ *
+ * To create a ParameterizedRunnerDelegate, extends from any BlockJUnit4Runner
+ * children class. You can copy all the implementation from
+ * org.chromium.base.test.params.BaseJUnit4RunnerDelegate.
+ */
+public interface ParameterizedRunnerDelegate {
+    /**
+     * Override to use DelegateCommon's implementation
+     */
+    void collectInitializationErrors(List<Throwable> errors);
+
+    /**
+     * Override to use DelegateCommon's implementation
+     */
+    List<FrameworkMethod> computeTestMethods();
+
+    /**
+     * Override to use DelegateCommon's implementation
+     */
+    Object createTest() throws ParameterizedTestInstantiationException;
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunnerDelegateCommon.java b/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunnerDelegateCommon.java
new file mode 100644
index 0000000..f25e2b2
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunnerDelegateCommon.java
@@ -0,0 +1,69 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.params;
+
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.TestClass;
+
+import org.chromium.base.test.params.ParameterizedRunner.ParameterizedTestInstantiationException;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+
+/**
+ * Parameterized runner delegate common that implements method that needed to be
+ * delegated for parameterization purposes
+ */
+public final class ParameterizedRunnerDelegateCommon {
+    private final TestClass mTestClass;
+    private final ParameterSet mClassParameterSet;
+    private final List<FrameworkMethod> mParameterizedFrameworkMethodList;
+
+    public ParameterizedRunnerDelegateCommon(TestClass testClass, ParameterSet classParameterSet,
+            List<FrameworkMethod> parameterizedFrameworkMethods) {
+        mTestClass = testClass;
+        mClassParameterSet = classParameterSet;
+        mParameterizedFrameworkMethodList = parameterizedFrameworkMethods;
+    }
+
+    /**
+     * Do not do any validation here because running the default class runner's
+     * collectInitializationErrors fail due to the overridden computeTestMethod relying on a local
+     * member variable
+     *
+     * The validation needed for parameterized tests is already done by ParameterizedRunner.
+     */
+    public static void collectInitializationErrors(
+            @SuppressWarnings("unused") List<Throwable> errors) {}
+
+    public List<FrameworkMethod> computeTestMethods() {
+        return mParameterizedFrameworkMethodList;
+    }
+
+    private void throwInstantiationException(Exception e)
+            throws ParameterizedTestInstantiationException {
+        String parameterSetString =
+                mClassParameterSet == null ? "null" : mClassParameterSet.toString();
+        throw new ParameterizedTestInstantiationException(mTestClass, parameterSetString, e);
+    }
+
+    public Object createTest() throws ParameterizedTestInstantiationException {
+        try {
+            if (mClassParameterSet == null) {
+                return mTestClass.getOnlyConstructor().newInstance();
+            }
+            return mTestClass.getOnlyConstructor().newInstance(
+                    mClassParameterSet.getValues().toArray());
+        } catch (InstantiationException e) {
+            throwInstantiationException(e);
+        } catch (IllegalAccessException e) {
+            throwInstantiationException(e);
+        } catch (InvocationTargetException e) {
+            throwInstantiationException(e);
+        }
+        assert false;
+        return null;
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunnerDelegateFactory.java b/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunnerDelegateFactory.java
new file mode 100644
index 0000000..f829981
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunnerDelegateFactory.java
@@ -0,0 +1,115 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.params;
+
+import org.junit.Test;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.TestClass;
+
+import org.chromium.base.test.params.ParameterAnnotations.UseMethodParameter;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Factory to generate delegate class runners for ParameterizedRunner
+ */
+public class ParameterizedRunnerDelegateFactory {
+    /**
+     * Create a runner that implements ParameterizedRunner and extends BlockJUnit4ClassRunner
+     *
+     * @param testClass the TestClass object for current test class
+     * @param classParameterSet A parameter set for test constructor arguments
+     * @param parameterizedRunnerDelegateClass the parameterized runner delegate class specified
+     *                                         through {@code @UseRunnerDelegate}
+     */
+    <T extends ParameterizedRunnerDelegate> T createRunner(TestClass testClass,
+            ParameterSet classParameterSet, Class<T> parameterizedRunnerDelegateClass)
+            throws ParameterizedRunnerDelegateInstantiationException {
+        String testMethodPostfix = classParameterSet == null ? null : classParameterSet.getName();
+        List<FrameworkMethod> unmodifiableFrameworkMethodList =
+                generateUnmodifiableFrameworkMethodList(testClass, testMethodPostfix);
+        ParameterizedRunnerDelegateCommon delegateCommon = new ParameterizedRunnerDelegateCommon(
+                testClass, classParameterSet, unmodifiableFrameworkMethodList);
+        try {
+            return parameterizedRunnerDelegateClass
+                    .getDeclaredConstructor(Class.class, ParameterizedRunnerDelegateCommon.class)
+                    .newInstance(testClass.getJavaClass(), delegateCommon);
+        } catch (Exception e) {
+            throw new ParameterizedRunnerDelegateInstantiationException(
+                    parameterizedRunnerDelegateClass.toString(), e);
+        }
+    }
+
+    /**
+     * Match test methods annotated by @UseMethodParameter(X) with
+     * ParameterSetList annotated by @MethodParameter(X)
+     *
+     * @param testClass a {@code TestClass} that wraps around the actual java
+     *            test class
+     * @param postFix a name postfix for each test
+     * @return a list of ParameterizedFrameworkMethod
+     */
+    static List<FrameworkMethod> generateUnmodifiableFrameworkMethodList(
+            TestClass testClass, String postFix) {
+        // Represent the list of all ParameterizedFrameworkMethod in this test class
+        List<FrameworkMethod> returnList = new ArrayList<>();
+
+        for (FrameworkMethod method : testClass.getAnnotatedMethods(Test.class)) {
+            if (method.getMethod().isAnnotationPresent(UseMethodParameter.class)) {
+                Iterable<ParameterSet> parameterSets =
+                        getParameters(method.getAnnotation(UseMethodParameter.class).value());
+                returnList.addAll(createParameterizedMethods(method, parameterSets, postFix));
+            } else {
+                // If test method is not parameterized (does not have UseMethodParameter annotation)
+                returnList.add(new ParameterizedFrameworkMethod(method.getMethod(), null, postFix));
+            }
+        }
+
+        return Collections.unmodifiableList(returnList);
+    }
+
+    /**
+     * Exception caused by instantiating the provided Runner delegate
+     * Potentially caused by not overriding collecInitializationErrors() method
+     * to be empty
+     */
+    public static class ParameterizedRunnerDelegateInstantiationException extends Exception {
+        private ParameterizedRunnerDelegateInstantiationException(
+                String runnerDelegateClass, Exception e) {
+            super(String.format("Current class runner delegate %s can not be instantiated.",
+                          runnerDelegateClass),
+                    e);
+        }
+    }
+
+    private static Iterable<ParameterSet> getParameters(Class<? extends ParameterProvider> clazz) {
+        ParameterProvider parameterProvider;
+        try {
+            parameterProvider = clazz.getDeclaredConstructor().newInstance();
+        } catch (IllegalAccessException e) {
+            throw new IllegalStateException("Failed instantiating " + clazz.getCanonicalName(), e);
+        } catch (InstantiationException e) {
+            throw new IllegalStateException("Failed instantiating " + clazz.getCanonicalName(), e);
+        } catch (NoSuchMethodException e) {
+            throw new IllegalStateException("Failed instantiating " + clazz.getCanonicalName(), e);
+        } catch (InvocationTargetException e) {
+            throw new IllegalStateException("Failed instantiating " + clazz.getCanonicalName(), e);
+        }
+        return parameterProvider.getParameters();
+    }
+
+    private static List<FrameworkMethod> createParameterizedMethods(
+            FrameworkMethod baseMethod, Iterable<ParameterSet> parameterSetList, String suffix) {
+        ParameterizedRunner.validateWidth(parameterSetList);
+        List<FrameworkMethod> returnList = new ArrayList<>();
+        for (ParameterSet set : parameterSetList) {
+            returnList.add(new ParameterizedFrameworkMethod(baseMethod.getMethod(), set, suffix));
+        }
+        return returnList;
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/AdvancedMockContext.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/AdvancedMockContext.java
new file mode 100644
index 0000000..c8117f7
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/AdvancedMockContext.java
@@ -0,0 +1,118 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import android.content.ComponentCallbacks;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.SharedPreferences;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * ContextWrapper that adds functionality for SharedPreferences and a way to set and retrieve flags.
+ */
+public class AdvancedMockContext extends ContextWrapper {
+
+    private final MockContentResolver mMockContentResolver = new MockContentResolver();
+
+    private final Map<String, SharedPreferences> mSharedPreferences =
+            new HashMap<String, SharedPreferences>();
+
+    private final Map<String, Boolean> mFlags = new HashMap<String, Boolean>();
+
+    public AdvancedMockContext(Context base) {
+        super(base);
+    }
+
+    public AdvancedMockContext() {
+        super(new MockContext());
+    }
+
+    @Override
+    public String getPackageName() {
+        return getBaseContext().getPackageName();
+    }
+
+    @Override
+    public Context getApplicationContext() {
+        return this;
+    }
+
+    @Override
+    public ContentResolver getContentResolver() {
+        return mMockContentResolver;
+    }
+
+    public MockContentResolver getMockContentResolver() {
+        return mMockContentResolver;
+    }
+
+    @Override
+    public SharedPreferences getSharedPreferences(String name, int mode) {
+        synchronized (mSharedPreferences) {
+            if (!mSharedPreferences.containsKey(name)) {
+                // Auto-create shared preferences to mimic Android Context behavior
+                mSharedPreferences.put(name, new InMemorySharedPreferences());
+            }
+            return mSharedPreferences.get(name);
+        }
+    }
+
+    @Override
+    public void registerComponentCallbacks(ComponentCallbacks callback) {
+        getBaseContext().registerComponentCallbacks(callback);
+    }
+
+    @Override
+    public void unregisterComponentCallbacks(ComponentCallbacks callback) {
+        getBaseContext().unregisterComponentCallbacks(callback);
+    }
+
+    public void addSharedPreferences(String name, Map<String, Object> data) {
+        synchronized (mSharedPreferences) {
+            mSharedPreferences.put(name, new InMemorySharedPreferences(data));
+        }
+    }
+
+    public void setFlag(String key) {
+        mFlags.put(key, true);
+    }
+
+    public void clearFlag(String key) {
+        mFlags.remove(key);
+    }
+
+    public boolean isFlagSet(String key) {
+        return mFlags.containsKey(key) && mFlags.get(key);
+    }
+
+    /**
+     * Builder for maps of type Map<String, Object> to be used with
+     * {@link #addSharedPreferences(String, java.util.Map)}.
+     */
+    public static class MapBuilder {
+
+        private final Map<String, Object> mData = new HashMap<String, Object>();
+
+        public static MapBuilder create() {
+            return new MapBuilder();
+        }
+
+        public MapBuilder add(String key, Object value) {
+            mData.put(key, value);
+            return this;
+        }
+
+        public Map<String, Object> build() {
+            return mData;
+        }
+
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/AnnotationProcessingUtils.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/AnnotationProcessingUtils.java
new file mode 100644
index 0000000..d335412
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/AnnotationProcessingUtils.java
@@ -0,0 +1,259 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import android.support.annotation.Nullable;
+
+import org.junit.runner.Description;
+
+import org.chromium.base.VisibleForTesting;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.Set;
+
+/**
+ * Utility class to help with processing annotations, going around the code to collect them, etc.
+ */
+public abstract class AnnotationProcessingUtils {
+    /**
+     * Returns the closest instance of the requested annotation or null if there is none.
+     * See {@link AnnotationExtractor} for context of "closest".
+     */
+    @SuppressWarnings("unchecked")
+    public static <A extends Annotation> A getAnnotation(Description description, Class<A> clazz) {
+        AnnotationExtractor extractor = new AnnotationExtractor(clazz);
+        return (A) extractor.getClosest(extractor.getMatchingAnnotations(description));
+    }
+
+    /**
+     * Returns the closest instance of the requested annotation or null if there is none.
+     * See {@link AnnotationExtractor} for context of "closest".
+     */
+    @SuppressWarnings("unchecked")
+    public static <A extends Annotation> A getAnnotation(AnnotatedElement element, Class<A> clazz) {
+        AnnotationExtractor extractor = new AnnotationExtractor(clazz);
+        return (A) extractor.getClosest(extractor.getMatchingAnnotations(element));
+    }
+
+    /** See {@link AnnotationExtractor} for details about the output sorting order. */
+    @SuppressWarnings("unchecked")
+    public static <A extends Annotation> List<A> getAnnotations(
+            Description description, Class<A> annotationType) {
+        return (List<A>) new AnnotationExtractor(annotationType)
+                .getMatchingAnnotations(description);
+    }
+
+    /** See {@link AnnotationExtractor} for details about the output sorting order. */
+    @SuppressWarnings("unchecked")
+    public static <A extends Annotation> List<A> getAnnotations(
+            AnnotatedElement annotatedElement, Class<A> annotationType) {
+        return (List<A>) new AnnotationExtractor(annotationType)
+                .getMatchingAnnotations(annotatedElement);
+    }
+
+    private static boolean isChromiumAnnotation(Annotation annotation) {
+        Package pkg = annotation.annotationType().getPackage();
+        return pkg != null && pkg.getName().startsWith("org.chromium");
+    }
+
+    /**
+     * Processes various types of annotated elements ({@link Class}es, {@link Annotation}s,
+     * {@link Description}s, etc.) and extracts the targeted annotations from it. The output will be
+     * sorted in BFS-like order.
+     *
+     * For example, for a method we would get in reverse order:
+     * - the method annotations
+     * - the meta-annotations present on the method annotations,
+     * - the class annotations
+     * - the meta-annotations present on the class annotations,
+     * - the annotations present on the super class,
+     * - the meta-annotations present on the super class annotations,
+     * - etc.
+     *
+     * When multiple annotations are targeted, if more than one is picked up at a given level (for
+     * example directly on the method), they will be returned in the reverse order that they were
+     * provided to the constructor.
+     *
+     * Note: We return the annotations in reverse order because we assume that if some processing
+     * is going to be made on related annotations, the later annotations would likely override
+     * modifications made by the former.
+     *
+     * Note: While resolving meta annotations, we don't expand the explorations to annotations types
+     * that have already been visited. Please file a bug and assign to dgn@ if you think it caused
+     * an issue.
+     */
+    public static class AnnotationExtractor {
+        private final List<Class<? extends Annotation>> mAnnotationTypes;
+        private final Comparator<Class<? extends Annotation>> mAnnotationTypeComparator;
+        private final Comparator<Annotation> mAnnotationComparator;
+
+        @SafeVarargs
+        public AnnotationExtractor(Class<? extends Annotation>... additionalTypes) {
+            this(Arrays.asList(additionalTypes));
+        }
+
+        public AnnotationExtractor(List<Class<? extends Annotation>> additionalTypes) {
+            assert !additionalTypes.isEmpty();
+            mAnnotationTypes = Collections.unmodifiableList(additionalTypes);
+            mAnnotationTypeComparator =
+                    (t1, t2) -> mAnnotationTypes.indexOf(t1) - mAnnotationTypes.indexOf(t2);
+            mAnnotationComparator = (t1, t2)
+                    -> mAnnotationTypeComparator.compare(t1.annotationType(), t2.annotationType());
+        }
+
+        public List<Annotation> getMatchingAnnotations(Description description) {
+            return getMatchingAnnotations(new AnnotatedNode.DescriptionNode(description));
+        }
+
+        public List<Annotation> getMatchingAnnotations(AnnotatedElement annotatedElement) {
+            AnnotatedNode annotatedNode;
+            if (annotatedElement instanceof Method) {
+                annotatedNode = new AnnotatedNode.MethodNode((Method) annotatedElement);
+            } else if (annotatedElement instanceof Class) {
+                annotatedNode = new AnnotatedNode.ClassNode((Class) annotatedElement);
+            } else {
+                throw new IllegalArgumentException("Unsupported type for " + annotatedElement);
+            }
+
+            return getMatchingAnnotations(annotatedNode);
+        }
+
+        /**
+         * For a given list obtained from the extractor, returns the {@link Annotation} that would
+         * be closest from the extraction point, or {@code null} if the list is empty.
+         */
+        @Nullable
+        public Annotation getClosest(List<Annotation> annotationList) {
+            return annotationList.isEmpty() ? null : annotationList.get(annotationList.size() - 1);
+        }
+
+        @VisibleForTesting
+        Comparator<Class<? extends Annotation>> getTypeComparator() {
+            return mAnnotationTypeComparator;
+        }
+
+        private List<Annotation> getMatchingAnnotations(AnnotatedNode annotatedNode) {
+            List<Annotation> collectedAnnotations = new ArrayList<>();
+            Queue<Annotation> workingSet = new LinkedList<>();
+            Set<Class<? extends Annotation>> visited = new HashSet<>();
+
+            AnnotatedNode currentAnnotationLayer = annotatedNode;
+            while (currentAnnotationLayer != null) {
+                queueAnnotations(currentAnnotationLayer.getAnnotations(), workingSet);
+
+                while (!workingSet.isEmpty()) {
+                    sweepAnnotations(collectedAnnotations, workingSet, visited);
+                }
+
+                currentAnnotationLayer = currentAnnotationLayer.getParent();
+            }
+
+            return collectedAnnotations;
+        }
+
+        private void queueAnnotations(List<Annotation> annotations, Queue<Annotation> workingSet) {
+            Collections.sort(annotations, mAnnotationComparator);
+            workingSet.addAll(annotations);
+        }
+
+        private void sweepAnnotations(List<Annotation> collectedAnnotations,
+                Queue<Annotation> workingSet, Set<Class<? extends Annotation>> visited) {
+            // 1. Grab node at the front of the working set.
+            Annotation annotation = workingSet.remove();
+
+            // 2. If it's an annotation of interest, put it aside for the output.
+            if (mAnnotationTypes.contains(annotation.annotationType())) {
+                collectedAnnotations.add(0, annotation);
+            }
+
+            // 3. Check if we can get skip some redundant iterations and avoid cycles.
+            if (!visited.add(annotation.annotationType())) return;
+            if (!isChromiumAnnotation(annotation)) return;
+
+            // 4. Expand the working set
+            queueAnnotations(Arrays.asList(annotation.annotationType().getDeclaredAnnotations()),
+                    workingSet);
+        }
+    }
+
+    /**
+     * Abstraction to hide differences between Class, Method and Description with regards to their
+     * annotations and what should be analyzed next.
+     */
+    private static abstract class AnnotatedNode {
+        @Nullable
+        abstract AnnotatedNode getParent();
+
+        abstract List<Annotation> getAnnotations();
+
+        static class DescriptionNode extends AnnotatedNode {
+            final Description mDescription;
+
+            DescriptionNode(Description description) {
+                mDescription = description;
+            }
+
+            @Nullable
+            @Override
+            AnnotatedNode getParent() {
+                return new ClassNode(mDescription.getTestClass());
+            }
+
+            @Override
+            List<Annotation> getAnnotations() {
+                return new ArrayList<>(mDescription.getAnnotations());
+            }
+        }
+
+        static class ClassNode extends AnnotatedNode {
+            final Class<?> mClass;
+
+            ClassNode(Class<?> clazz) {
+                mClass = clazz;
+            }
+
+            @Nullable
+            @Override
+            AnnotatedNode getParent() {
+                Class<?> superClass = mClass.getSuperclass();
+                return superClass == null ? null : new ClassNode(superClass);
+            }
+
+            @Override
+            List<Annotation> getAnnotations() {
+                return Arrays.asList(mClass.getDeclaredAnnotations());
+            }
+        }
+
+        static class MethodNode extends AnnotatedNode {
+            final Method mMethod;
+
+            MethodNode(Method method) {
+                mMethod = method;
+            }
+
+            @Nullable
+            @Override
+            AnnotatedNode getParent() {
+                return new ClassNode(mMethod.getDeclaringClass());
+            }
+
+            @Override
+            List<Annotation> getAnnotations() {
+                return Arrays.asList(mMethod.getDeclaredAnnotations());
+            }
+        }
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/AnnotationRule.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/AnnotationRule.java
new file mode 100644
index 0000000..a361ac3
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/AnnotationRule.java
@@ -0,0 +1,139 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import android.support.annotation.CallSuper;
+import android.support.annotation.Nullable;
+
+import org.junit.rules.ExternalResource;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.ListIterator;
+
+/**
+ * Test rule that collects specific annotations to help with test set up and tear down. It is set up
+ * with a list of annotations to look for and exposes the ones picked up on the test through
+ * {@link #getAnnotations()} and related methods.
+ *
+ * Note: The rule always apply, whether it picked up annotations or not.
+ *
+ * Usage:
+ *
+ * <pre>
+ * public class Test {
+ *    &#64;Rule
+ *    public AnnotationRule rule = new AnnotationRule(Foo.class) {
+ *          &#64;Override
+ *          protected void before() { ... }
+ *
+ *          &#64;Override
+ *          protected void after() { ... }
+ *    };
+ *
+ *    &#64;Test
+ *    &#64;Foo
+ *    public void myTest() { ... }
+ * }
+ * </pre>
+ *
+ * It can also be used to trigger for multiple annotations:
+ *
+ * <pre>
+ * &#64;DisableFoo
+ * public class Test {
+ *    &#64;Rule
+ *    public AnnotationRule rule = new AnnotationRule(EnableFoo.class, DisableFoo.class) {
+ *          &#64;Override
+ *          protected void before() {
+ *            // Loops through all the picked up annotations. For myTest(), it would process
+ *            // DisableFoo first, then EnableFoo.
+ *            for (Annotation annotation : getAnnotations()) {
+ *                if (annotation instanceof EnableFoo) { ... }
+ *                else if (annotation instanceof DisableFoo) { ... }
+ *            }
+ *          }
+ *
+ *          &#64;Override
+ *          protected void after() {
+ *            // For myTest(), would return EnableFoo as it's directly set on the method.
+ *            Annotation a = getClosestAnnotation();
+ *            ...
+ *          }
+ *    };
+ *
+ *    &#64;Test
+ *    &#64;EnableFoo
+ *    public void myTest() { ... }
+ * }
+ * </pre>
+ *
+ * @see AnnotationProcessingUtils.AnnotationExtractor
+ */
+public abstract class AnnotationRule extends ExternalResource {
+    private final AnnotationProcessingUtils.AnnotationExtractor mAnnotationExtractor;
+    private List<Annotation> mCollectedAnnotations;
+    private Description mTestDescription;
+
+    @SafeVarargs
+    public AnnotationRule(Class<? extends Annotation> firstAnnotationType,
+            Class<? extends Annotation>... additionalTypes) {
+        List<Class<? extends Annotation>> mAnnotationTypes = new ArrayList<>();
+        mAnnotationTypes.add(firstAnnotationType);
+        mAnnotationTypes.addAll(Arrays.asList(additionalTypes));
+        mAnnotationExtractor = new AnnotationProcessingUtils.AnnotationExtractor(mAnnotationTypes);
+    }
+
+    @CallSuper
+    @Override
+    public Statement apply(Statement base, Description description) {
+        mTestDescription = description;
+
+        mCollectedAnnotations = mAnnotationExtractor.getMatchingAnnotations(description);
+
+        // Return the wrapped statement to execute before() and after().
+        return super.apply(base, description);
+    }
+
+    /** @return {@link Description} of the current test. */
+    protected Description getTestDescription() {
+        return mTestDescription;
+    }
+
+    /**
+     * @return The collected annotations that match the declared type(s).
+     * @throws NullPointerException if this is called before annotations have been collected,
+     * which happens when the rule is applied to the {@link Statement}.
+     */
+    protected List<Annotation> getAnnotations() {
+        return Collections.unmodifiableList(mCollectedAnnotations);
+    }
+
+    /**
+     * @return The closest annotation matching the provided type, or {@code null} if there is none.
+     */
+    @SuppressWarnings("unchecked")
+    protected @Nullable <A extends Annotation> A getAnnotation(Class<A> annnotationType) {
+        ListIterator<Annotation> iteratorFromEnd =
+                mCollectedAnnotations.listIterator(mCollectedAnnotations.size());
+        while (iteratorFromEnd.hasPrevious()) {
+            Annotation annotation = iteratorFromEnd.previous();
+            if (annnotationType.isAssignableFrom(annotation.annotationType())) {
+                return (A) annotation;
+            }
+        }
+        return null;
+    }
+
+    protected @Nullable Annotation getClosestAnnotation() {
+        if (mCollectedAnnotations.isEmpty()) return null;
+        return mCollectedAnnotations.get(mCollectedAnnotations.size() - 1);
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/CallbackHelper.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/CallbackHelper.java
new file mode 100644
index 0000000..3b9c216
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/CallbackHelper.java
@@ -0,0 +1,266 @@
+// Copyright 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import static org.chromium.base.test.util.ScalableTimeout.scaleTimeout;
+
+import org.junit.Assert;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A helper class that encapsulates listening and blocking for callbacks.
+ *
+ * Sample usage:
+ *
+ * // Let us assume that this interface is defined by some piece of production code and is used
+ * // to communicate events that occur in that piece of code. Let us further assume that the
+ * // production code runs on the main thread test code runs on a separate test thread.
+ * // An instance that implements this interface would be injected by test code to ensure that the
+ * // methods are being called on another thread.
+ * interface Delegate {
+ *     void onOperationFailed(String errorMessage);
+ *     void onDataPersisted();
+ * }
+ *
+ * // This is the inner class you'd write in your test case to later inject into the production
+ * // code.
+ * class TestDelegate implements Delegate {
+ *     // This is the preferred way to create a helper that stores the parameters it receives
+ *     // when called by production code.
+ *     public static class OnOperationFailedHelper extends CallbackHelper {
+ *         private String mErrorMessage;
+ *
+ *         public void getErrorMessage() {
+ *             assert getCallCount() > 0;
+ *             return mErrorMessage;
+ *         }
+ *
+ *         public void notifyCalled(String errorMessage) {
+ *             mErrorMessage = errorMessage;
+ *             // It's important to call this after all parameter assignments.
+ *             notifyCalled();
+ *         }
+ *     }
+ *
+ *     // There should be one CallbackHelper instance per method.
+ *     private OnOperationFailedHelper mOnOperationFailedHelper;
+ *     private CallbackHelper mOnDataPersistedHelper;
+ *
+ *     public OnOperationFailedHelper getOnOperationFailedHelper() {
+ *         return mOnOperationFailedHelper;
+ *     }
+ *
+ *     public CallbackHelper getOnDataPersistedHelper() {
+ *         return mOnDataPersistedHelper;
+ *     }
+ *
+ *     @Override
+ *     public void onOperationFailed(String errorMessage) {
+ *         mOnOperationFailedHelper.notifyCalled(errorMessage);
+ *     }
+ *
+ *     @Override
+ *     public void onDataPersisted() {
+ *         mOnDataPersistedHelper.notifyCalled();
+ *     }
+ * }
+ *
+ * // This is a sample test case.
+ * public void testCase() throws Exception {
+ *     // Create the TestDelegate to inject into production code.
+ *     TestDelegate delegate = new TestDelegate();
+ *     // Create the production class instance that is being tested and inject the test delegate.
+ *     CodeUnderTest codeUnderTest = new CodeUnderTest();
+ *     codeUnderTest.setDelegate(delegate);
+ *
+ *     // Typically you'd get the current call count before performing the operation you expect to
+ *     // trigger the callback. There can't be any callbacks 'in flight' at this moment, otherwise
+ *     // the call count is unpredictable and the test will be flaky.
+ *     int onOperationFailedCallCount = delegate.getOnOperationFailedHelper().getCallCount();
+ *     codeUnderTest.doSomethingThatEndsUpCallingOnOperationFailedFromAnotherThread();
+ *     // It's safe to do other stuff here, if needed.
+ *     ....
+ *     // Wait for the callback if it hadn't been called yet, otherwise return immediately. This
+ *     // can throw an exception if the callback doesn't arrive within the timeout.
+ *     delegate.getOnOperationFailedHelper().waitForCallback(onOperationFailedCallCount);
+ *     // Access to method parameters is now safe.
+ *     assertEquals("server error", delegate.getOnOperationFailedHelper().getErrorMessage());
+ *
+ *     // Being able to pass the helper around lets us build methods which encapsulate commonly
+ *     // performed tasks.
+ *     doSomeOperationAndWait(codeUnerTest, delegate.getOnOperationFailedHelper());
+ *
+ *     // The helper can be reused for as many calls as needed, just be sure to get the count each
+ *     // time.
+ *     onOperationFailedCallCount = delegate.getOnOperationFailedHelper().getCallCount();
+ *     codeUnderTest.doSomethingElseButStillFailOnAnotherThread();
+ *     delegate.getOnOperationFailedHelper().waitForCallback(onOperationFailedCallCount);
+ *
+ *     // It is also possible to use more than one helper at a time.
+ *     onOperationFailedCallCount = delegate.getOnOperationFailedHelper().getCallCount();
+ *     int onDataPersistedCallCount = delegate.getOnDataPersistedHelper().getCallCount();
+ *     codeUnderTest.doSomethingThatPersistsDataButFailsInSomeOtherWayOnAnotherThread();
+ *     delegate.getOnDataPersistedHelper().waitForCallback(onDataPersistedCallCount);
+ *     delegate.getOnOperationFailedHelper().waitForCallback(onOperationFailedCallCount);
+ * }
+ *
+ * // Shows how to turn an async operation + completion callback into a synchronous operation.
+ * private void doSomeOperationAndWait(final CodeUnderTest underTest,
+ *         CallbackHelper operationHelper) throws InterruptedException, TimeoutException {
+ *     final int callCount = operationHelper.getCallCount();
+ *     getInstrumentation().runOnMainSync(new Runnable() {
+ *         @Override
+ *         public void run() {
+ *             // This schedules a call to a method on the injected TestDelegate. The TestDelegate
+ *             // implementation will then call operationHelper.notifyCalled().
+ *             underTest.operation();
+ *         }
+ *      });
+ *      operationHelper.waitForCallback(callCount);
+ * }
+ *
+ */
+public class CallbackHelper {
+    /** The default timeout (in seconds) for a callback to wait. */
+    public static final long WAIT_TIMEOUT_SECONDS = scaleTimeout(5);
+
+    private final Object mLock = new Object();
+    private int mCallCount;
+    private String mFailureString;
+
+    /**
+     * Gets the number of times the callback has been called.
+     *
+     * The call count can be used with the waitForCallback() method, indicating a point
+     * in time after which the caller wishes to record calls to the callback.
+     *
+     * In order to wait for a callback caused by X, the call count should be obtained
+     * before X occurs.
+     *
+     * NOTE: any call to the callback that occurs after the call count is obtained
+     * will result in the corresponding wait call to resume execution. The call count
+     * is intended to 'catch' callbacks that occur after X but before waitForCallback()
+     * is called.
+     */
+    public int getCallCount() {
+        synchronized (mLock) {
+            return mCallCount;
+        }
+    }
+
+    /**
+     * Blocks until the callback is called the specified number of
+     * times or throws an exception if we exceeded the specified time frame.
+     *
+     * This will wait for a callback to be called a specified number of times after
+     * the point in time at which the call count was obtained.  The method will return
+     * immediately if a call occurred the specified number of times after the
+     * call count was obtained but before the method was called, otherwise the method will
+     * block until the specified call count is reached.
+     *
+     * @param msg The error message to use if the callback times out.
+     * @param currentCallCount the value obtained by calling getCallCount().
+     * @param numberOfCallsToWaitFor number of calls (counting since
+     *                               currentCallCount was obtained) that we will wait for.
+     * @param timeout timeout value. We will wait the specified amount of time for a single
+     *                callback to occur so the method call may block up to
+     *                <code>numberOfCallsToWaitFor * timeout</code> units.
+     * @param unit timeout unit.
+     * @throws InterruptedException
+     * @throws TimeoutException Thrown if the method times out before onPageFinished is called.
+     */
+    public void waitForCallback(String msg, int currentCallCount, int numberOfCallsToWaitFor,
+            long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {
+        assert mCallCount >= currentCallCount;
+        assert numberOfCallsToWaitFor > 0;
+        synchronized (mLock) {
+            int callCountWhenDoneWaiting = currentCallCount + numberOfCallsToWaitFor;
+            while (callCountWhenDoneWaiting > mCallCount) {
+                int callCountBeforeWait = mCallCount;
+                mLock.wait(unit.toMillis(timeout));
+                if (mFailureString != null) {
+                    String s = mFailureString;
+                    mFailureString = null;
+                    Assert.fail(s);
+                }
+                if (callCountBeforeWait == mCallCount) {
+                    throw new TimeoutException(msg == null ? "waitForCallback timed out!" : msg);
+                }
+            }
+        }
+    }
+
+    /**
+     * @see #waitForCallback(String, int, int, long, TimeUnit)
+     */
+    public void waitForCallback(int currentCallCount, int numberOfCallsToWaitFor, long timeout,
+            TimeUnit unit) throws InterruptedException, TimeoutException {
+        waitForCallback(null, currentCallCount, numberOfCallsToWaitFor, timeout, unit);
+    }
+
+    /**
+     * @see #waitForCallback(String, int, int, long, TimeUnit)
+     */
+    public void waitForCallback(int currentCallCount, int numberOfCallsToWaitFor)
+            throws InterruptedException, TimeoutException {
+        waitForCallback(null, currentCallCount, numberOfCallsToWaitFor,
+                WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+    }
+
+    /**
+     * @see #waitForCallback(String, int, int, long, TimeUnit)
+     */
+    public void waitForCallback(String msg, int currentCallCount)
+            throws InterruptedException, TimeoutException {
+        waitForCallback(msg, currentCallCount, 1, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+    }
+
+    /**
+     * @see #waitForCallback(String, int, int, long, TimeUnit)
+     */
+    public void waitForCallback(int currentCallCount)
+            throws InterruptedException, TimeoutException {
+        waitForCallback(null, currentCallCount, 1, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+    }
+
+    /**
+     * @see #waitForCallback(String, int, int, long, TimeUnit)
+     */
+    public void waitForCallback(String msg) throws InterruptedException, TimeoutException {
+        waitForCallback(msg, getCallCount());
+    }
+
+    /**
+     * @see #waitForCallback(String, int, int, long, TimeUnit)
+     */
+    public void waitForCallback() throws InterruptedException, TimeoutException {
+        waitForCallback(getCallCount());
+    }
+
+    /**
+     * Should be called when the callback associated with this helper object is called.
+     */
+    public void notifyCalled() {
+        synchronized (mLock) {
+            mCallCount++;
+            mLock.notifyAll();
+        }
+    }
+
+    /**
+     * Should be called when the callback associated with this helper object wants to
+     * indicate a failure.
+     *
+     * @param s The failure message.
+     */
+    public void notifyFailed(String s) {
+        synchronized (mLock) {
+            mFailureString = s;
+            mLock.notifyAll();
+        }
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/CommandLineFlags.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/CommandLineFlags.java
new file mode 100644
index 0000000..33e46d9
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/CommandLineFlags.java
@@ -0,0 +1,195 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import android.text.TextUtils;
+
+import org.junit.Assert;
+import org.junit.Rule;
+
+import org.chromium.base.CommandLine;
+import org.chromium.base.CommandLineInitUtil;
+import org.chromium.base.test.BaseTestResult.PreTestHook;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Provides annotations related to command-line flag handling.
+ *
+ * Uses of these annotations on a derived class will take precedence over uses on its base classes,
+ * so a derived class can add a command-line flag that a base class has removed (or vice versa).
+ * Similarly, uses of these annotations on a test method will take precedence over uses on the
+ * containing class.
+ * <p>
+ * These annonations may also be used on Junit4 Rule classes and on their base classes. Note,
+ * however that the annotation processor only looks at the declared type of the Rule, not its actual
+ * type, so in, for example:
+ *
+ * <pre>
+ *     &#64Rule
+ *     TestRule mRule = new ChromeActivityTestRule();
+ * </pre>
+ *
+ * will only look for CommandLineFlags annotations on TestRule, not for CommandLineFlags annotations
+ * on ChromeActivityTestRule.
+ * <p>
+ * In addition a rule may not remove flags added by an independently invoked rule, although it may
+ * remove flags added by its base classes.
+ * <p>
+ * Uses of these annotations on the test class or methods take precedence over uses on Rule classes.
+ * <p>
+ * Note that this class should never be instantiated.
+ */
+public final class CommandLineFlags {
+    private static final String DISABLE_FEATURES = "disable-features";
+    private static final String ENABLE_FEATURES = "enable-features";
+
+    /**
+     * Adds command-line flags to the {@link org.chromium.base.CommandLine} for this test.
+     */
+    @Inherited
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target({ElementType.METHOD, ElementType.TYPE})
+    public @interface Add {
+        String[] value();
+    }
+
+    /**
+     * Removes command-line flags from the {@link org.chromium.base.CommandLine} from this test.
+     *
+     * Note that this can only remove flags added via {@link Add} above.
+     */
+    @Inherited
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target({ElementType.METHOD, ElementType.TYPE})
+    public @interface Remove {
+        String[] value();
+    }
+
+    /**
+     * Sets up the CommandLine with the appropriate flags.
+     *
+     * This will add the difference of the sets of flags specified by {@link CommandLineFlags.Add}
+     * and {@link CommandLineFlags.Remove} to the {@link org.chromium.base.CommandLine}. Note that
+     * trying to remove a flag set externally, i.e. by the command-line flags file, will not work.
+     */
+    public static void setUp(AnnotatedElement element) {
+        CommandLine.reset();
+        CommandLineInitUtil.initCommandLine(getTestCmdLineFile());
+        Set<String> enableFeatures = new HashSet<String>(getFeatureValues(ENABLE_FEATURES));
+        Set<String> disableFeatures = new HashSet<String>(getFeatureValues(DISABLE_FEATURES));
+        Set<String> flags = getFlags(element);
+        for (String flag : flags) {
+            String[] parsedFlags = flag.split("=", 2);
+            if (parsedFlags.length == 1) {
+                CommandLine.getInstance().appendSwitch(flag);
+            } else if (ENABLE_FEATURES.equals(parsedFlags[0])) {
+                // We collect enable/disable features flags separately and aggregate them because
+                // they may be specified multiple times, in which case the values will trample each
+                // other.
+                Collections.addAll(enableFeatures, parsedFlags[1].split(","));
+            } else if (DISABLE_FEATURES.equals(parsedFlags[0])) {
+                Collections.addAll(disableFeatures, parsedFlags[1].split(","));
+            } else {
+                CommandLine.getInstance().appendSwitchWithValue(parsedFlags[0], parsedFlags[1]);
+            }
+        }
+
+        if (enableFeatures.size() > 0) {
+            CommandLine.getInstance().appendSwitchWithValue(
+                    ENABLE_FEATURES, TextUtils.join(",", enableFeatures));
+        }
+        if (disableFeatures.size() > 0) {
+            CommandLine.getInstance().appendSwitchWithValue(
+                    DISABLE_FEATURES, TextUtils.join(",", disableFeatures));
+        }
+    }
+
+    private static Set<String> getFlags(AnnotatedElement type) {
+        Set<String> rule_flags = new HashSet<>();
+        updateFlagsForElement(type, rule_flags);
+        return rule_flags;
+    }
+
+    private static void updateFlagsForElement(AnnotatedElement element, Set<String> flags) {
+        if (element instanceof Class<?>) {
+            // Get flags from rules within the class.
+            for (Field field : ((Class<?>) element).getFields()) {
+                if (field.isAnnotationPresent(Rule.class)) {
+                    // The order in which fields are returned is undefined, so, for consistency,
+                    // a rule must not remove a flag added by a different rule. Ensure this by
+                    // initially getting the flags into a new set.
+                    Set<String> rule_flags = getFlags(field.getType());
+                    flags.addAll(rule_flags);
+                }
+            }
+            for (Method method : ((Class<?>) element).getMethods()) {
+                if (method.isAnnotationPresent(Rule.class)) {
+                    // The order in which methods are returned is undefined, so, for consistency,
+                    // a rule must not remove a flag added by a different rule. Ensure this by
+                    // initially getting the flags into a new set.
+                    Set<String> rule_flags = getFlags(method.getReturnType());
+                    flags.addAll(rule_flags);
+                }
+            }
+        }
+
+        // Add the flags from the parent. Override any flags defined by the rules.
+        AnnotatedElement parent = (element instanceof Method)
+                ? ((Method) element).getDeclaringClass()
+                : ((Class<?>) element).getSuperclass();
+        if (parent != null) updateFlagsForElement(parent, flags);
+
+        // Flags on the element itself override all other flag sources.
+        if (element.isAnnotationPresent(CommandLineFlags.Add.class)) {
+            flags.addAll(
+                    Arrays.asList(element.getAnnotation(CommandLineFlags.Add.class).value()));
+        }
+
+        if (element.isAnnotationPresent(CommandLineFlags.Remove.class)) {
+            List<String> flagsToRemove =
+                    Arrays.asList(element.getAnnotation(CommandLineFlags.Remove.class).value());
+            for (String flagToRemove : flagsToRemove) {
+                // If your test fails here, you have tried to remove a command-line flag via
+                // CommandLineFlags.Remove that was loaded into CommandLine via something other
+                // than CommandLineFlags.Add (probably the command-line flag file).
+                Assert.assertFalse("Unable to remove command-line flag \"" + flagToRemove + "\".",
+                        CommandLine.getInstance().hasSwitch(flagToRemove));
+            }
+            flags.removeAll(flagsToRemove);
+        }
+    }
+
+    private static List<String> getFeatureValues(String flag) {
+        String value = CommandLine.getInstance().getSwitchValue(flag);
+        if (value == null) return new ArrayList<>();
+        return Arrays.asList(value.split(","));
+    }
+
+    private CommandLineFlags() {
+        throw new AssertionError("CommandLineFlags is a non-instantiable class");
+    }
+
+    public static PreTestHook getRegistrationHook() {
+        return (targetContext, testMethod) -> CommandLineFlags.setUp(testMethod);
+    }
+
+    public static String getTestCmdLineFile() {
+        return "test-cmdline-file";
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/DisableIf.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/DisableIf.java
new file mode 100644
index 0000000..c0303b6
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/DisableIf.java
@@ -0,0 +1,49 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotations to support conditional test disabling.
+ *
+ * These annotations should only be used to disable tests that are temporarily failing
+ * in some configurations. If a test should never run at all in some configurations, use
+ * {@link Restriction}.
+ */
+public class DisableIf {
+
+    /** Conditional disabling based on {@link android.os.Build}.
+     */
+    @Target({ElementType.METHOD, ElementType.TYPE})
+    @Retention(RetentionPolicy.RUNTIME)
+    public static @interface Build {
+        String message() default "";
+
+        int sdk_is_greater_than() default 0;
+        int sdk_is_less_than() default Integer.MAX_VALUE;
+
+        String supported_abis_includes() default "";
+
+        String hardware_is() default "";
+
+        String product_name_includes() default "";
+    }
+
+    @Target({ElementType.METHOD, ElementType.TYPE})
+    @Retention(RetentionPolicy.RUNTIME)
+    public static @interface Device {
+        /**
+         * @return A list of disabled types.
+         */
+        public String[] type();
+    }
+
+    /* Objects of this type should not be created. */
+    private DisableIf() {}
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/DisableIfSkipCheck.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/DisableIfSkipCheck.java
new file mode 100644
index 0000000..e46b979
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/DisableIfSkipCheck.java
@@ -0,0 +1,84 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import android.os.Build;
+
+import org.junit.runners.model.FrameworkMethod;
+
+import org.chromium.base.Log;
+
+import java.util.Arrays;
+
+/**
+ * Checks for conditional disables.
+ *
+ * Currently, this only includes checks against a few {@link android.os.Build} values.
+ */
+public class DisableIfSkipCheck extends SkipCheck {
+
+    private static final String TAG = "cr_base_test";
+
+    @Override
+    public boolean shouldSkip(FrameworkMethod method) {
+        if (method == null) return true;
+        for (DisableIf.Build v : AnnotationProcessingUtils.getAnnotations(
+                     method.getMethod(), DisableIf.Build.class)) {
+            if (abi(v) && hardware(v) && product(v) && sdk(v)) {
+                if (!v.message().isEmpty()) {
+                    Log.i(TAG, "%s is disabled: %s", method.getName(), v.message());
+                }
+                return true;
+            }
+        }
+
+        for (DisableIf.Device d : AnnotationProcessingUtils.getAnnotations(
+                     method.getMethod(), DisableIf.Device.class)) {
+            for (String deviceType : d.type()) {
+                if (deviceTypeApplies(deviceType)) {
+                    Log.i(TAG, "Test " + method.getDeclaringClass().getName() + "#"
+                            + method.getName() + " disabled because of "
+                            + d);
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    @SuppressWarnings("deprecation")
+    private boolean abi(DisableIf.Build v) {
+        if (v.supported_abis_includes().isEmpty()) return true;
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            return Arrays.asList(Build.SUPPORTED_ABIS).contains(
+                    v.supported_abis_includes());
+        } else {
+            return Build.CPU_ABI.equals(v.supported_abis_includes())
+                    || Build.CPU_ABI2.equals(v.supported_abis_includes());
+        }
+    }
+
+    private boolean hardware(DisableIf.Build v) {
+        return v.hardware_is().isEmpty() || Build.HARDWARE.equals(v.hardware_is());
+    }
+
+    private boolean product(DisableIf.Build v) {
+        return v.product_name_includes().isEmpty()
+                || Build.PRODUCT.contains(v.product_name_includes());
+    }
+
+    private boolean sdk(DisableIf.Build v) {
+        return Build.VERSION.SDK_INT > v.sdk_is_greater_than()
+                && Build.VERSION.SDK_INT < v.sdk_is_less_than();
+    }
+
+    protected boolean deviceTypeApplies(String type) {
+        return false;
+    }
+
+}
+
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/DisabledTest.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/DisabledTest.java
new file mode 100644
index 0000000..a3e4e8e
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/DisabledTest.java
@@ -0,0 +1,22 @@
+// Copyright 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation is for disabled tests.
+ * <p>
+ * Tests with this annotation will not be run on any of the normal bots.
+ * Please note that they might eventually run on a special bot.
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface DisabledTest {
+    String message() default "";
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/EnormousTest.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/EnormousTest.java
new file mode 100644
index 0000000..af483ec
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/EnormousTest.java
@@ -0,0 +1,24 @@
+// Copyright 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation is for enormous tests.
+ * <p>
+ * Examples of enormous tests are tests that depend on external web sites or
+ * tests that are long running.
+ * <p>
+ * Such tests are likely NOT reliable enough to run on tree closing bots and
+ * should only be run on FYI bots.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface EnormousTest {
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/Feature.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/Feature.java
new file mode 100644
index 0000000..1bc9226
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/Feature.java
@@ -0,0 +1,29 @@
+// Copyright 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The java instrumentation tests are normally fairly large (in terms of
+ * dependencies), and the test suite ends up containing a large amount of
+ * tests that are not trivial to filter / group just by their names.
+ * Instead, we use this annotation: each test should be annotated as:
+ *     @Feature({"Foo", "Bar"})
+ * in order for the test runner scripts to be able to filter and group
+ * them accordingly (for instance, this enable us to run all tests that exercise
+ * feature Foo).
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Feature {
+    /**
+     * @return A list of feature names.
+     */
+    public String[] value();
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/FlakyTest.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/FlakyTest.java
new file mode 100644
index 0000000..83f8e9f
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/FlakyTest.java
@@ -0,0 +1,22 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation is for flaky tests.
+ * <p>
+ * Tests with this annotation will not be run on any of the normal bots.
+ * Please note that they might eventually run on a special bot.
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface FlakyTest {
+    String message() default "";
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/InMemorySharedPreferences.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/InMemorySharedPreferences.java
new file mode 100644
index 0000000..2587d72
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/InMemorySharedPreferences.java
@@ -0,0 +1,238 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import android.content.SharedPreferences;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An implementation of SharedPreferences that can be used in tests.
+ * <p/>
+ * It keeps all state in memory, and there is no difference between apply() and commit().
+ */
+public class InMemorySharedPreferences implements SharedPreferences {
+
+    // Guarded on its own monitor.
+    private final Map<String, Object> mData;
+
+    public InMemorySharedPreferences() {
+        mData = new HashMap<String, Object>();
+    }
+
+    public InMemorySharedPreferences(Map<String, Object> data) {
+        mData = data;
+    }
+
+    @Override
+    public Map<String, ?> getAll() {
+        synchronized (mData) {
+            return Collections.unmodifiableMap(mData);
+        }
+    }
+
+    @Override
+    public String getString(String key, String defValue) {
+        synchronized (mData) {
+            if (mData.containsKey(key)) {
+                return (String) mData.get(key);
+            }
+        }
+        return defValue;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public Set<String> getStringSet(String key, Set<String> defValues) {
+        synchronized (mData) {
+            if (mData.containsKey(key)) {
+                return Collections.unmodifiableSet((Set<String>) mData.get(key));
+            }
+        }
+        return defValues;
+    }
+
+    @Override
+    public int getInt(String key, int defValue) {
+        synchronized (mData) {
+            if (mData.containsKey(key)) {
+                return (Integer) mData.get(key);
+            }
+        }
+        return defValue;
+    }
+
+    @Override
+    public long getLong(String key, long defValue) {
+        synchronized (mData) {
+            if (mData.containsKey(key)) {
+                return (Long) mData.get(key);
+            }
+        }
+        return defValue;
+    }
+
+    @Override
+    public float getFloat(String key, float defValue) {
+        synchronized (mData) {
+            if (mData.containsKey(key)) {
+                return (Float) mData.get(key);
+            }
+        }
+        return defValue;
+    }
+
+    @Override
+    public boolean getBoolean(String key, boolean defValue) {
+        synchronized (mData) {
+            if (mData.containsKey(key)) {
+                return (Boolean) mData.get(key);
+            }
+        }
+        return defValue;
+    }
+
+    @Override
+    public boolean contains(String key) {
+        synchronized (mData) {
+            return mData.containsKey(key);
+        }
+    }
+
+    @Override
+    public SharedPreferences.Editor edit() {
+        return new InMemoryEditor();
+    }
+
+    @Override
+    public void registerOnSharedPreferenceChangeListener(
+            SharedPreferences.OnSharedPreferenceChangeListener
+                    listener) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void unregisterOnSharedPreferenceChangeListener(
+            SharedPreferences.OnSharedPreferenceChangeListener listener) {
+        throw new UnsupportedOperationException();
+    }
+
+    private class InMemoryEditor implements SharedPreferences.Editor {
+
+        // All guarded by |mChanges|
+        private boolean mClearCalled;
+        private volatile boolean mApplyCalled;
+        private final Map<String, Object> mChanges = new HashMap<String, Object>();
+
+        @Override
+        public SharedPreferences.Editor putString(String key, String value) {
+            synchronized (mChanges) {
+                if (mApplyCalled) throw new IllegalStateException();
+                mChanges.put(key, value);
+                return this;
+            }
+        }
+
+        @Override
+        public SharedPreferences.Editor putStringSet(String key, Set<String> values) {
+            synchronized (mChanges) {
+                if (mApplyCalled) throw new IllegalStateException();
+                mChanges.put(key, values);
+                return this;
+            }
+        }
+
+        @Override
+        public SharedPreferences.Editor putInt(String key, int value) {
+            synchronized (mChanges) {
+                if (mApplyCalled) throw new IllegalStateException();
+                mChanges.put(key, value);
+                return this;
+            }
+        }
+
+        @Override
+        public SharedPreferences.Editor putLong(String key, long value) {
+            synchronized (mChanges) {
+                if (mApplyCalled) throw new IllegalStateException();
+                mChanges.put(key, value);
+                return this;
+            }
+        }
+
+        @Override
+        public SharedPreferences.Editor putFloat(String key, float value) {
+            synchronized (mChanges) {
+                if (mApplyCalled) throw new IllegalStateException();
+                mChanges.put(key, value);
+                return this;
+            }
+        }
+
+        @Override
+        public SharedPreferences.Editor putBoolean(String key, boolean value) {
+            synchronized (mChanges) {
+                if (mApplyCalled) throw new IllegalStateException();
+                mChanges.put(key, value);
+                return this;
+            }
+        }
+
+        @Override
+        public SharedPreferences.Editor remove(String key) {
+            synchronized (mChanges) {
+                if (mApplyCalled) throw new IllegalStateException();
+                // Magic value for removes
+                mChanges.put(key, this);
+                return this;
+            }
+        }
+
+        @Override
+        public SharedPreferences.Editor clear() {
+            synchronized (mChanges) {
+                if (mApplyCalled) throw new IllegalStateException();
+                mClearCalled = true;
+                return this;
+            }
+        }
+
+        @Override
+        public boolean commit() {
+            apply();
+            return true;
+        }
+
+        @Override
+        public void apply() {
+            synchronized (mData) {
+                synchronized (mChanges) {
+                    if (mApplyCalled) throw new IllegalStateException();
+                    if (mClearCalled) {
+                        mData.clear();
+                    }
+                    for (Map.Entry<String, Object> entry : mChanges.entrySet()) {
+                        String key = entry.getKey();
+                        Object value = entry.getValue();
+                        if (value == this) {
+                            // Special value for removal
+                            mData.remove(key);
+                        } else {
+                            mData.put(key, value);
+                        }
+                    }
+                    // The real shared prefs clears out the temporaries allowing the caller to
+                    // reuse the Editor instance, however this is undocumented behavior and subtle
+                    // to read, so instead we just ban any future use of this instance.
+                    mApplyCalled = true;
+                }
+            }
+        }
+    }
+
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/InstrumentationUtils.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/InstrumentationUtils.java
new file mode 100644
index 0000000..20cfd9d
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/InstrumentationUtils.java
@@ -0,0 +1,32 @@
+// Copyright 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import android.app.Instrumentation;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+/**
+ * Utility methods built around the android.app.Instrumentation class.
+ */
+public final class InstrumentationUtils {
+
+    private InstrumentationUtils() {
+    }
+
+    public static <R> R runOnMainSyncAndGetResult(Instrumentation instrumentation,
+            Callable<R> callable) throws Throwable {
+        FutureTask<R> task = new FutureTask<R>(callable);
+        instrumentation.runOnMainSync(task);
+        try {
+            return task.get();
+        } catch (ExecutionException e) {
+            // Unwrap the cause of the exception and re-throw it.
+            throw e.getCause();
+        }
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/IntegrationTest.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/IntegrationTest.java
new file mode 100644
index 0000000..8b6550d
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/IntegrationTest.java
@@ -0,0 +1,26 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation is for integration tests.
+ * <p>
+ * Examples of integration tests are tests that rely on real instances of the
+ * application's services and components (e.g. Search) to test the system as
+ * a whole. These tests may use additional command-line flags to configure the
+ * existing backends to use.
+ * <p>
+ * Such tests are likely NOT reliable enough to run on tree closing bots and
+ * should only be run on FYI bots.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface IntegrationTest {
+}
\ No newline at end of file
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/Manual.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/Manual.java
new file mode 100644
index 0000000..31f3977
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/Manual.java
@@ -0,0 +1,21 @@
+// Copyright 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation can be used to mark a test that should only be run manually.
+ * <p>
+ * Tests with this annotation will not be run on bots, because they take too long
+ * or need manual monitoring.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Manual {
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/Matchers.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/Matchers.java
new file mode 100644
index 0000000..fc9d689
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/Matchers.java
@@ -0,0 +1,44 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+/**
+ * Helper class containing Hamcrest matchers.
+ */
+public class Matchers extends CoreMatchers {
+    private static class GreaterThanOrEqualTo<T extends Comparable<T>>
+            extends TypeSafeMatcher<T> {
+
+        private final T mComparisonValue;
+
+        public GreaterThanOrEqualTo(T comparisonValue) {
+            mComparisonValue = comparisonValue;
+        }
+
+        @Override
+        public void describeTo(Description description) {
+            description.appendText("greater than or equal to ").appendValue(mComparisonValue);
+        }
+
+        @Override
+        protected boolean matchesSafely(T item) {
+            return item.compareTo(mComparisonValue) >= 0;
+        }
+    }
+
+    /**
+     * @param <T> A Comparable type.
+     * @param comparisonValue The value to be compared against.
+     * @return A matcher that expects the value to be greater than the |comparisonValue|.
+     */
+    public static <T extends Comparable<T>> Matcher<T> greaterThanOrEqualTo(T comparisonValue) {
+        return new GreaterThanOrEqualTo<>(comparisonValue);
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/MetricsUtils.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/MetricsUtils.java
new file mode 100644
index 0000000..c4664d6
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/MetricsUtils.java
@@ -0,0 +1,43 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import org.chromium.base.metrics.RecordHistogram;
+
+/**
+ * Helpers for testing UMA metrics.
+ */
+public class MetricsUtils {
+    /**
+     * Helper class that snapshots the given bucket of the given UMA histogram on its creation,
+     * allowing to inspect the number of samples recorded during its lifetime.
+     */
+    public static class HistogramDelta {
+        private final String mHistogram;
+        private final int mSampleValue;
+
+        private final int mInitialCount;
+
+        private int get() {
+            return RecordHistogram.getHistogramValueCountForTesting(mHistogram, mSampleValue);
+        }
+
+        /**
+         * Snapshots the given bucket of the given histogram.
+         * @param histogram name of the histogram to snapshot
+         * @param sampleValue the bucket that contains this value will be snapshot
+         */
+        public HistogramDelta(String histogram, int sampleValue) {
+            mHistogram = histogram;
+            mSampleValue = sampleValue;
+            mInitialCount = get();
+        }
+
+        /** Returns the number of samples of the snapshot bucket recorded since creation */
+        public int getDelta() {
+            return get() - mInitialCount;
+        }
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/MinAndroidSdkLevel.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/MinAndroidSdkLevel.java
new file mode 100644
index 0000000..13e2578
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/MinAndroidSdkLevel.java
@@ -0,0 +1,19 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface MinAndroidSdkLevel {
+    int value() default 0;
+}
+
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/MinAndroidSdkLevelSkipCheck.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/MinAndroidSdkLevelSkipCheck.java
new file mode 100644
index 0000000..8b07c0f
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/MinAndroidSdkLevelSkipCheck.java
@@ -0,0 +1,43 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import android.os.Build;
+
+import org.junit.runners.model.FrameworkMethod;
+
+import org.chromium.base.Log;
+
+/**
+ * Checks the device's SDK level against any specified minimum requirement.
+ */
+public class MinAndroidSdkLevelSkipCheck extends SkipCheck {
+
+    private static final String TAG = "base_test";
+
+    /**
+     * If {@link MinAndroidSdkLevel} is present, checks its value
+     * against the device's SDK level.
+     *
+     * @param testCase The test to check.
+     * @return true if the device's SDK level is below the specified minimum.
+     */
+    @Override
+    public boolean shouldSkip(FrameworkMethod frameworkMethod) {
+        int minSdkLevel = 0;
+        for (MinAndroidSdkLevel m : AnnotationProcessingUtils.getAnnotations(
+                     frameworkMethod.getMethod(), MinAndroidSdkLevel.class)) {
+            minSdkLevel = Math.max(minSdkLevel, m.value());
+        }
+        if (Build.VERSION.SDK_INT < minSdkLevel) {
+            Log.i(TAG, "Test " + frameworkMethod.getDeclaringClass().getName() + "#"
+                    + frameworkMethod.getName() + " is not enabled at SDK level "
+                    + Build.VERSION.SDK_INT + ".");
+            return true;
+        }
+        return false;
+    }
+
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/Restriction.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/Restriction.java
new file mode 100644
index 0000000..f39bfbd
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/Restriction.java
@@ -0,0 +1,37 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation for listing restrictions for a test method. For example, if a test method is only
+ * applicable on a phone with small memory:
+ *     @Restriction({RESTRICTION_TYPE_PHONE, RESTRICTION_TYPE_SMALL_MEMORY})
+ * Test classes are free to define restrictions and enforce them using reflection at runtime.
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Restriction {
+    /** Specifies the test is only valid on low end devices that have less memory. */
+    public static final String RESTRICTION_TYPE_LOW_END_DEVICE = "Low_End_Device";
+
+    /** Specifies the test is only valid on non-low end devices. */
+    public static final String RESTRICTION_TYPE_NON_LOW_END_DEVICE = "Non_Low_End_Device";
+
+    /** Specifies the test is only valid on a device that can reach the internet. */
+    public static final String RESTRICTION_TYPE_INTERNET = "Internet";
+
+    /** Specifies the test is only valid on a device that has a camera. */
+    public static final String RESTRICTION_TYPE_HAS_CAMERA = "Has_Camera";
+
+    /**
+     * @return A list of restrictions.
+     */
+    public String[] value();
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/RestrictionSkipCheck.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/RestrictionSkipCheck.java
new file mode 100644
index 0000000..a27dd1f
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/RestrictionSkipCheck.java
@@ -0,0 +1,78 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.text.TextUtils;
+
+import org.junit.runners.model.FrameworkMethod;
+
+import org.chromium.base.Log;
+import org.chromium.base.SysUtils;
+
+/**
+ * Checks if any restrictions exist and skip the test if it meets those restrictions.
+ */
+public class RestrictionSkipCheck extends SkipCheck {
+
+    private static final String TAG = "base_test";
+
+    private final Context mTargetContext;
+
+    public RestrictionSkipCheck(Context targetContext) {
+        mTargetContext = targetContext;
+    }
+
+    protected Context getTargetContext() {
+        return mTargetContext;
+    }
+
+    @Override
+    public boolean shouldSkip(FrameworkMethod frameworkMethod) {
+        if (frameworkMethod == null) return true;
+
+        for (Restriction restriction : AnnotationProcessingUtils.getAnnotations(
+                     frameworkMethod.getMethod(), Restriction.class)) {
+            for (String restrictionVal : restriction.value()) {
+                if (restrictionApplies(restrictionVal)) {
+                    Log.i(TAG, "Test " + frameworkMethod.getDeclaringClass().getName() + "#"
+                            + frameworkMethod.getName() + " skipped because of restriction "
+                            + restriction);
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    protected boolean restrictionApplies(String restriction) {
+        if (TextUtils.equals(restriction, Restriction.RESTRICTION_TYPE_LOW_END_DEVICE)
+                && !SysUtils.isLowEndDevice()) {
+            return true;
+        }
+        if (TextUtils.equals(restriction, Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE)
+                && SysUtils.isLowEndDevice()) {
+            return true;
+        }
+        if (TextUtils.equals(restriction, Restriction.RESTRICTION_TYPE_INTERNET)
+                && !isNetworkAvailable()) {
+            return true;
+        }
+        if (TextUtils.equals(restriction, Restriction.RESTRICTION_TYPE_HAS_CAMERA)
+                && !SysUtils.hasCamera(mTargetContext)) {
+            return true;
+        }
+        return false;
+    }
+
+    private boolean isNetworkAvailable() {
+        final ConnectivityManager connectivityManager = (ConnectivityManager)
+                mTargetContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+        final NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
+        return activeNetworkInfo != null && activeNetworkInfo.isConnected();
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/RetryOnFailure.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/RetryOnFailure.java
new file mode 100644
index 0000000..eb98008
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/RetryOnFailure.java
@@ -0,0 +1,25 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+// Note this annotation may be a NOOP. Check http://crbug.com/797002 for latest status (also see
+// http://crbug.com/619055). Current default behavior is to retry all tests on failure.
+/**
+ * Mark a test as flaky and should be retried on failure. The test is
+ * considered passed by the test script if any retry succeeds.
+ *
+ * Long term, this should be merged with @FlakyTest. But @FlakyTest means
+ * has specific meaning that is currently different from RetryOnFailure.
+ */
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RetryOnFailure {
+    String message() default "";
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/ScalableTimeout.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/ScalableTimeout.java
new file mode 100644
index 0000000..7a815c0
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/ScalableTimeout.java
@@ -0,0 +1,29 @@
+// Copyright 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+/**
+ * Utility class for scaling various timeouts by a common factor.
+ * For example, to run tests under slow memory tools, you might do
+ * something like this:
+ *   adb shell "echo 20.0 > /data/local/tmp/chrome_timeout_scale"
+ */
+public class ScalableTimeout {
+    private static Double sTimeoutScale;
+    public static final String PROPERTY_FILE = "/data/local/tmp/chrome_timeout_scale";
+
+    public static long scaleTimeout(long timeout) {
+        if (sTimeoutScale == null) {
+            try {
+                char[] data = TestFileUtil.readUtf8File(PROPERTY_FILE, 32);
+                sTimeoutScale = Double.parseDouble(new String(data));
+            } catch (Exception e) {
+                // NumberFormatException, FileNotFoundException, IOException
+                sTimeoutScale = 1.0;
+            }
+        }
+        return (long) (timeout * sTimeoutScale);
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/SkipCheck.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/SkipCheck.java
new file mode 100644
index 0000000..d1dd7be
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/SkipCheck.java
@@ -0,0 +1,49 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import junit.framework.TestCase;
+
+import org.junit.runners.model.FrameworkMethod;
+
+import org.chromium.base.Log;
+
+import java.lang.reflect.Method;
+
+/**
+ * Check whether a test case should be skipped.
+ */
+public abstract class SkipCheck {
+
+    private static final String TAG = "base_test";
+
+    /**
+     *
+     * Checks whether the given test method should be skipped.
+     *
+     * @param testMethod The test method to check.
+     * @return Whether the test case should be skipped.
+     */
+    public abstract boolean shouldSkip(FrameworkMethod testMethod);
+
+    /**
+     *
+     * Checks whether the given test case should be skipped.
+     *
+     * @param testCase The test case to check.
+     * @return Whether the test case should be skipped.
+     */
+    public boolean shouldSkip(TestCase testCase) {
+        try {
+            Method m = testCase.getClass().getMethod(testCase.getName(), (Class[]) null);
+            return shouldSkip(new FrameworkMethod(m));
+        } catch (NoSuchMethodException e) {
+            Log.e(TAG, "Unable to find %s in %s", testCase.getName(),
+                    testCase.getClass().getName(), e);
+            return false;
+        }
+    }
+}
+
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/TestFileUtil.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/TestFileUtil.java
new file mode 100644
index 0000000..6d89121
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/TestFileUtil.java
@@ -0,0 +1,85 @@
+// Copyright 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+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.Writer;
+import java.util.Arrays;
+
+/**
+ * Utility class for dealing with files for test.
+ */
+public class TestFileUtil {
+    public static void createNewHtmlFile(String name, String title, String body)
+            throws IOException {
+        createNewHtmlFile(new File(name), title, body);
+    }
+
+    public static void createNewHtmlFile(File file, String title, String body)
+            throws IOException {
+        if (!file.createNewFile()) {
+            throw new IOException("File \"" + file.getAbsolutePath() + "\" already exists");
+        }
+
+        Writer writer = null;
+        try {
+            writer = new OutputStreamWriter(new FileOutputStream(file), "UTF-8");
+            writer.write("<html><meta charset=\"UTF-8\" />"
+                    + "     <head><title>" + title + "</title></head>"
+                    + "     <body>"
+                    + (body != null ? body : "")
+                    + "     </body>"
+                    + "   </html>");
+        } finally {
+            if (writer != null) {
+                writer.close();
+            }
+        }
+    }
+
+    public static void deleteFile(String name) {
+        deleteFile(new File(name));
+    }
+
+    public static void deleteFile(File file) {
+        boolean deleted = file.delete();
+        assert (deleted || !file.exists());
+    }
+
+    /**
+     * @param fileName the file to read in.
+     * @param sizeLimit cap on the file size: will throw an exception if exceeded
+     * @return Array of chars read from the file
+     * @throws FileNotFoundException file does not exceed
+     * @throws IOException error encountered accessing the file
+     */
+    public static char[] readUtf8File(String fileName, int sizeLimit) throws
+            FileNotFoundException, IOException {
+        Reader reader = null;
+        try {
+            File f = new File(fileName);
+            if (f.length() > sizeLimit) {
+                throw new IOException("File " + fileName + " length " + f.length()
+                        + " exceeds limit " + sizeLimit);
+            }
+            char[] buffer = new char[(int) f.length()];
+            reader = new InputStreamReader(new FileInputStream(f), "UTF-8");
+            int charsRead = reader.read(buffer);
+            // Debug check that we've exhausted the input stream (will fail e.g. if the
+            // file grew after we inspected its length).
+            assert !reader.ready();
+            return charsRead < buffer.length ? Arrays.copyOfRange(buffer, 0, charsRead) : buffer;
+        } finally {
+            if (reader != null) reader.close();
+        }
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/TimeoutScale.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/TimeoutScale.java
new file mode 100644
index 0000000..5aee05e
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/TimeoutScale.java
@@ -0,0 +1,22 @@
+// Copyright 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * This annotation can be used to scale a specific test timeout.
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface TimeoutScale {
+    /**
+     * @return A number to scale the test timeout.
+     */
+    public int value();
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/UrlUtils.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/UrlUtils.java
new file mode 100644
index 0000000..9ca3fcc
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/UrlUtils.java
@@ -0,0 +1,84 @@
+// Copyright 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import org.junit.Assert;
+
+import org.chromium.base.PathUtils;
+import org.chromium.base.annotations.CalledByNative;
+import org.chromium.base.annotations.MainDex;
+
+/**
+ * Collection of URL utilities.
+ */
+@MainDex
+public class UrlUtils {
+    private static final String DATA_DIR = "/chrome/test/data/";
+
+    /**
+     * Construct the full path of a test data file.
+     * @param path Pathname relative to external/chrome/test/data
+     */
+    public static String getTestFilePath(String path) {
+        // TODO(jbudorick): Remove DATA_DIR once everything has been isolated. crbug/400499
+        return getIsolatedTestFilePath(DATA_DIR + path);
+    }
+
+    // TODO(jbudorick): Remove this function once everything has been isolated and switched back
+    // to getTestFilePath. crbug/400499
+    /**
+     * Construct the full path of a test data file.
+     * @param path Pathname relative to external/
+     */
+    public static String getIsolatedTestFilePath(String path) {
+        return getIsolatedTestRoot() + "/" + path;
+    }
+
+    /**
+     * Returns the root of the test data directory.
+     */
+    @CalledByNative
+    public static String getIsolatedTestRoot() {
+        return PathUtils.getExternalStorageDirectory() + "/chromium_tests_root";
+    }
+
+    /**
+     * Construct a suitable URL for loading a test data file.
+     * @param path Pathname relative to external/chrome/test/data
+     */
+    public static String getTestFileUrl(String path) {
+        return "file://" + getTestFilePath(path);
+    }
+
+    // TODO(jbudorick): Remove this function once everything has been isolated and switched back
+    // to getTestFileUrl. crbug/400499
+    /**
+     * Construct a suitable URL for loading a test data file.
+     * @param path Pathname relative to external/
+     */
+    public static String getIsolatedTestFileUrl(String path) {
+        return "file://" + getIsolatedTestFilePath(path);
+    }
+
+    /**
+     * Construct a data:text/html URI for loading from an inline HTML.
+     * @param html An unencoded HTML
+     * @return String An URI that contains the given HTML
+     */
+    public static String encodeHtmlDataUri(String html) {
+        try {
+            // URLEncoder encodes into application/x-www-form-encoded, so
+            // ' '->'+' needs to be undone and replaced with ' '->'%20'
+            // to match the Data URI requirements.
+            String encoded =
+                    "data:text/html;utf-8," + java.net.URLEncoder.encode(html, "UTF-8");
+            encoded = encoded.replace("+", "%20");
+            return encoded;
+        } catch (java.io.UnsupportedEncodingException e) {
+            Assert.fail("Unsupported encoding: " + e.getMessage());
+            return null;
+        }
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/UserActionTester.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/UserActionTester.java
new file mode 100644
index 0000000..88e3551
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/UserActionTester.java
@@ -0,0 +1,51 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import org.chromium.base.ThreadUtils;
+import org.chromium.base.metrics.RecordUserAction;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A util class that records UserActions.
+ */
+public class UserActionTester implements RecordUserAction.UserActionCallback {
+    private List<String> mActions;
+
+    public UserActionTester() {
+        mActions = new ArrayList<>();
+        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
+            @Override
+            public void run() {
+                RecordUserAction.setActionCallbackForTesting(UserActionTester.this);
+            }
+        });
+    }
+
+    public void tearDown() {
+        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
+            @Override
+            public void run() {
+                RecordUserAction.removeActionCallbackForTesting();
+            }
+        });
+    }
+
+    @Override
+    public void onActionRecorded(String action) {
+        mActions.add(action);
+    }
+
+    public List<String> getActions() {
+        return mActions;
+    }
+
+    @Override
+    public String toString() {
+        return "Actions: " + mActions.toString();
+    }
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/parameter/CommandLineParameter.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/parameter/CommandLineParameter.java
new file mode 100644
index 0000000..e6f5506
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/parameter/CommandLineParameter.java
@@ -0,0 +1,32 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util.parameter;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The annotation for parametering CommandLineFlags in JUnit3 instrumentation tests.
+ *
+ * E.g. if you add the following annotation to your test class:
+ *
+ * <code>
+ * @CommandLineParameter({"", FLAG_A, FLAG_B})
+ * public class MyTestClass
+ * </code>
+ *
+ * The test harness would run the test 3 times with each of the flag added to commandline
+ * file.
+ */
+
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface CommandLineParameter {
+    String[] value() default {};
+}
diff --git a/src/base/test/android/javatests/src/org/chromium/base/test/util/parameter/SkipCommandLineParameterization.java b/src/base/test/android/javatests/src/org/chromium/base/test/util/parameter/SkipCommandLineParameterization.java
new file mode 100644
index 0000000..5387423
--- /dev/null
+++ b/src/base/test/android/javatests/src/org/chromium/base/test/util/parameter/SkipCommandLineParameterization.java
@@ -0,0 +1,20 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+package org.chromium.base.test.util.parameter;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * BaseJUnit4ClassRunner and host side test harness skips commandline parameterization for test
+ * classes or methods annotated with SkipCommandLineParameterization.
+ */
+
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface SkipCommandLineParameterization {}
diff --git a/src/base/test/android/junit/src/org/chromium/base/task/test/BackgroundShadowAsyncTask.java b/src/base/test/android/junit/src/org/chromium/base/task/test/BackgroundShadowAsyncTask.java
new file mode 100644
index 0000000..6d53954
--- /dev/null
+++ b/src/base/test/android/junit/src/org/chromium/base/task/test/BackgroundShadowAsyncTask.java
@@ -0,0 +1,69 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.task.test;
+
+import static org.junit.Assert.fail;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowApplication;
+
+import org.chromium.base.task.AsyncTask;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Executes async tasks on a background thread and waits on the result on the main thread.
+ * This is useful for users of AsyncTask on Roboelectric who check if the code is actually being
+ * run on a background thread (i.e. through the use of {@link ThreadUtils#runningOnUiThread()}).
+ * @param <Result>     type for reporting result
+ */
+@Implements(AsyncTask.class)
+public class BackgroundShadowAsyncTask<Result> extends ShadowAsyncTask<Result> {
+    private static final ExecutorService sExecutorService = Executors.newSingleThreadExecutor();
+
+    @Override
+    @Implementation
+    public final AsyncTask<Result> executeOnExecutor(Executor e) {
+        try {
+            return sExecutorService
+                    .submit(new Callable<AsyncTask<Result>>() {
+                        @Override
+                        public AsyncTask<Result> call() throws Exception {
+                            return BackgroundShadowAsyncTask.super.executeInRobolectric();
+                        }
+                    })
+                    .get();
+        } catch (Exception ex) {
+            fail(ex.getMessage());
+            return null;
+        }
+    }
+
+    @Override
+    @Implementation
+    public final Result get() {
+        try {
+            runBackgroundTasks();
+            return BackgroundShadowAsyncTask.super.get();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    public static void runBackgroundTasks() throws Exception {
+        sExecutorService
+                .submit(new Runnable() {
+                    @Override
+                    public void run() {
+                        ShadowApplication.runBackgroundTasks();
+                    }
+                })
+                .get();
+    }
+}
diff --git a/src/base/test/android/junit/src/org/chromium/base/task/test/CustomShadowAsyncTask.java b/src/base/test/android/junit/src/org/chromium/base/task/test/CustomShadowAsyncTask.java
new file mode 100644
index 0000000..310a084
--- /dev/null
+++ b/src/base/test/android/junit/src/org/chromium/base/task/test/CustomShadowAsyncTask.java
@@ -0,0 +1,27 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.task.test;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+import org.chromium.base.task.AsyncTask;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Forces async tasks to execute with the default executor.
+ * This works around Robolectric not working out of the box with custom executors.
+ *
+ * @param <Result>
+ */
+@Implements(AsyncTask.class)
+public class CustomShadowAsyncTask<Result> extends ShadowAsyncTask<Result> {
+    @Override
+    @Implementation
+    public final AsyncTask<Result> executeOnExecutor(Executor executor) {
+        return super.executeInRobolectric();
+    }
+}
diff --git a/src/base/test/android/junit/src/org/chromium/base/test/BaseRobolectricTestRunner.java b/src/base/test/android/junit/src/org/chromium/base/test/BaseRobolectricTestRunner.java
new file mode 100644
index 0000000..3ca756a
--- /dev/null
+++ b/src/base/test/android/junit/src/org/chromium/base/test/BaseRobolectricTestRunner.java
@@ -0,0 +1,49 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test;
+
+import org.junit.runners.model.InitializationError;
+import org.robolectric.DefaultTestLifecycle;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.TestLifecycle;
+
+import org.chromium.base.ApplicationStatus;
+import org.chromium.base.CommandLine;
+import org.chromium.base.ContextUtils;
+import org.chromium.testing.local.LocalRobolectricTestRunner;
+
+import java.lang.reflect.Method;
+
+/**
+ * A Robolectric Test Runner that initializes base globals.
+ */
+public class BaseRobolectricTestRunner extends LocalRobolectricTestRunner {
+    /**
+     * Enables a per-test setUp / tearDown hook.
+     */
+    public static class BaseTestLifecycle extends DefaultTestLifecycle {
+        @Override
+        public void beforeTest(Method method) {
+            ContextUtils.initApplicationContextForTests(RuntimeEnvironment.application);
+            CommandLine.init(null);
+            super.beforeTest(method);
+        }
+
+        @Override
+        public void afterTest(Method method) {
+            ApplicationStatus.destroyForJUnitTests();
+            super.afterTest(method);
+        }
+    }
+
+    public BaseRobolectricTestRunner(Class<?> testClass) throws InitializationError {
+        super(testClass);
+    }
+
+    @Override
+    protected Class<? extends TestLifecycle> getTestLifecycleClass() {
+        return BaseTestLifecycle.class;
+    }
+}
diff --git a/src/base/test/android/junit/src/org/chromium/base/test/SetUpStatementTest.java b/src/base/test/android/junit/src/org/chromium/base/test/SetUpStatementTest.java
new file mode 100644
index 0000000..722bd1a
--- /dev/null
+++ b/src/base/test/android/junit/src/org/chromium/base/test/SetUpStatementTest.java
@@ -0,0 +1,64 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.Statement;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test SetUpStatement is working as intended with SetUpTestRule.
+ */
+@RunWith(BlockJUnit4ClassRunner.class)
+public class SetUpStatementTest {
+    private Statement mBase;
+    private SetUpTestRule<TestRule> mRule;
+    private List<Integer> mList;
+
+    @Before
+    public void setUp() {
+        mBase = new Statement() {
+            @Override
+            public void evaluate() {
+                mList.add(1);
+            }
+        };
+        mList = new ArrayList<>();
+        mRule = new SetUpTestRule<TestRule>() {
+            @Override
+            public void setUp() {
+                mList.add(0);
+            }
+
+            @Override
+            public TestRule shouldSetUp(boolean toSetUp) {
+                return null;
+            }
+        };
+    }
+
+    @Test
+    public void testSetUpStatementShouldSetUp() throws Throwable {
+        SetUpStatement statement = new SetUpStatement(mBase, mRule, true);
+        statement.evaluate();
+        Integer[] expected = {0, 1};
+        Assert.assertArrayEquals(expected, mList.toArray());
+    }
+
+    @Test
+    public void testSetUpStatementShouldNotSetUp() throws Throwable {
+        SetUpStatement statement = new SetUpStatement(mBase, mRule, false);
+        statement.evaluate();
+        Integer[] expected = {1};
+        Assert.assertArrayEquals(expected, mList.toArray());
+    }
+}
diff --git a/src/base/test/android/junit/src/org/chromium/base/test/TestListInstrumentationRunListenerTest.java b/src/base/test/android/junit/src/org/chromium/base/test/TestListInstrumentationRunListenerTest.java
new file mode 100644
index 0000000..63fa560
--- /dev/null
+++ b/src/base/test/android/junit/src/org/chromium/base/test/TestListInstrumentationRunListenerTest.java
@@ -0,0 +1,119 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test;
+
+import static org.chromium.base.test.TestListInstrumentationRunListener.getAnnotationJSON;
+import static org.chromium.base.test.TestListInstrumentationRunListener.getTestMethodJSON;
+
+import org.json.JSONObject;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+import org.chromium.base.test.util.CommandLineFlags;
+
+import java.util.Arrays;
+
+/**
+ * Robolectric test to ensure static methods in TestListInstrumentationRunListener works properly.
+ */
+@RunWith(BaseRobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class TestListInstrumentationRunListenerTest {
+    @CommandLineFlags.Add("hello")
+    private static class ParentClass {
+        public void testA() {}
+
+        @CommandLineFlags.Add("world")
+        public void testB() {}
+    }
+
+    @CommandLineFlags.Remove("hello")
+    private static class ChildClass extends ParentClass {
+    }
+
+    @Test
+    public void testGetTestMethodJSON_testA() throws Throwable {
+        Description desc = Description.createTestDescription(
+                ParentClass.class, "testA",
+                ParentClass.class.getMethod("testA").getAnnotations());
+        JSONObject json = getTestMethodJSON(desc);
+        String expectedJsonString =
+                "{"
+                + "'method': 'testA',"
+                + "'annotations': {}"
+                + "}";
+        expectedJsonString = expectedJsonString
+            .replaceAll("\\s", "")
+            .replaceAll("'", "\"");
+        Assert.assertEquals(expectedJsonString, json.toString());
+    }
+
+    @Test
+    public void testGetTestMethodJSON_testB() throws Throwable {
+        Description desc = Description.createTestDescription(
+                ParentClass.class, "testB",
+                ParentClass.class.getMethod("testB").getAnnotations());
+        JSONObject json = getTestMethodJSON(desc);
+        String expectedJsonString =
+                "{"
+                + "'method': 'testB',"
+                + "'annotations': {"
+                + "  'Add': {"
+                + "    'value': ['world']"
+                + "    }"
+                + "  }"
+                + "}";
+        expectedJsonString = expectedJsonString
+            .replaceAll("\\s", "")
+            .replaceAll("'", "\"");
+        Assert.assertEquals(expectedJsonString, json.toString());
+    }
+
+
+    @Test
+    public void testGetTestMethodJSONForInheritedClass() throws Throwable {
+        Description desc = Description.createTestDescription(
+                ChildClass.class, "testB",
+                ChildClass.class.getMethod("testB").getAnnotations());
+        JSONObject json = getTestMethodJSON(desc);
+        String expectedJsonString =
+                "{"
+                + "'method': 'testB',"
+                + "'annotations': {"
+                + "  'Add': {"
+                + "    'value': ['world']"
+                + "    }"
+                + "  }"
+                + "}";
+        expectedJsonString = expectedJsonString
+            .replaceAll("\\s", "")
+            .replaceAll("'", "\"");
+        Assert.assertEquals(expectedJsonString, json.toString());
+    }
+
+    @Test
+    public void testGetAnnotationJSONForParentClass() throws Throwable {
+        JSONObject json = getAnnotationJSON(Arrays.asList(ParentClass.class.getAnnotations()));
+        String expectedJsonString = "{'Add':{'value':['hello']}}";
+        expectedJsonString = expectedJsonString
+            .replaceAll("\\s", "")
+            .replaceAll("'", "\"");
+        Assert.assertEquals(expectedJsonString, json.toString());
+    }
+
+    @Test
+    public void testGetAnnotationJSONForChildClass() throws Throwable {
+        JSONObject json = getAnnotationJSON(Arrays.asList(ChildClass.class.getAnnotations()));
+        String expectedJsonString = "{'Add':{'value':['hello']},'Remove':{'value':['hello']}}";
+        expectedJsonString = expectedJsonString
+            .replaceAll("\\s", "")
+            .replaceAll("'", "\"");
+        Assert.assertEquals(expectedJsonString, json.toString());
+    }
+}
+
diff --git a/src/base/test/android/junit/src/org/chromium/base/test/params/ExampleParameterizedTest.java b/src/base/test/android/junit/src/org/chromium/base/test/params/ExampleParameterizedTest.java
new file mode 100644
index 0000000..6ffccad
--- /dev/null
+++ b/src/base/test/android/junit/src/org/chromium/base/test/params/ExampleParameterizedTest.java
@@ -0,0 +1,105 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.params;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.MethodRule;
+import org.junit.runner.RunWith;
+
+import org.chromium.base.test.params.ParameterAnnotations.ClassParameter;
+import org.chromium.base.test.params.ParameterAnnotations.UseMethodParameter;
+import org.chromium.base.test.params.ParameterAnnotations.UseMethodParameterAfter;
+import org.chromium.base.test.params.ParameterAnnotations.UseMethodParameterBefore;
+import org.chromium.base.test.params.ParameterAnnotations.UseRunnerDelegate;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Example test that uses ParameterizedRunner
+ */
+@RunWith(ParameterizedRunner.class)
+@UseRunnerDelegate(BlockJUnit4RunnerDelegate.class)
+public class ExampleParameterizedTest {
+    @ClassParameter
+    private static List<ParameterSet> sClassParams =
+            Arrays.asList(new ParameterSet().value("hello", "world").name("HelloWorld"),
+                    new ParameterSet().value("Xxxx", "Yyyy").name("XxxxYyyy"),
+                    new ParameterSet().value("aa", "yy").name("AaYy"));
+
+    public static class MethodParamsA implements ParameterProvider {
+        private static List<ParameterSet> sMethodParamA =
+                Arrays.asList(new ParameterSet().value(1, 2).name("OneTwo"),
+                        new ParameterSet().value(2, 3).name("TwoThree"),
+                        new ParameterSet().value(3, 4).name("ThreeFour"));
+
+        @Override
+        public List<ParameterSet> getParameters() {
+            return sMethodParamA;
+        }
+    }
+
+    public static class MethodParamsB implements ParameterProvider {
+        private static List<ParameterSet> sMethodParamB =
+                Arrays.asList(new ParameterSet().value("a", "b").name("Ab"),
+                        new ParameterSet().value("b", "c").name("Bc"),
+                        new ParameterSet().value("c", "d").name("Cd"),
+                        new ParameterSet().value("d", "e").name("De"));
+
+        @Override
+        public List<ParameterSet> getParameters() {
+            return sMethodParamB;
+        }
+    }
+
+    private String mStringA;
+    private String mStringB;
+
+    public ExampleParameterizedTest(String a, String b) {
+        mStringA = a;
+        mStringB = b;
+    }
+
+    @Test
+    public void testSimple() {
+        Assert.assertEquals(
+                "A and B string length aren't equal", mStringA.length(), mStringB.length());
+    }
+
+    @Rule
+    public MethodRule mMethodParamAnnotationProcessor = new MethodParamAnnotationRule();
+
+    private Integer mSum;
+
+    @UseMethodParameterBefore(MethodParamsA.class)
+    public void setupWithOnlyA(int intA, int intB) {
+        mSum = intA + intB;
+    }
+
+    @Test
+    @UseMethodParameter(MethodParamsA.class)
+    public void testWithOnlyA(int intA, int intB) {
+        Assert.assertEquals(intA + 1, intB);
+        Assert.assertEquals(mSum, Integer.valueOf(intA + intB));
+        mSum = null;
+    }
+
+    private String mConcatenation;
+
+    @Test
+    @UseMethodParameter(MethodParamsB.class)
+    public void testWithOnlyB(String a, String b) {
+        Assert.assertTrue(!a.equals(b));
+        mConcatenation = a + b;
+    }
+
+    @UseMethodParameterAfter(MethodParamsB.class)
+    public void teardownWithOnlyB(String a, String b) {
+        Assert.assertEquals(mConcatenation, a + b);
+        mConcatenation = null;
+    }
+}
diff --git a/src/base/test/android/junit/src/org/chromium/base/test/params/ParameterizedRunnerDelegateCommonTest.java b/src/base/test/android/junit/src/org/chromium/base/test/params/ParameterizedRunnerDelegateCommonTest.java
new file mode 100644
index 0000000..6d854c5
--- /dev/null
+++ b/src/base/test/android/junit/src/org/chromium/base/test/params/ParameterizedRunnerDelegateCommonTest.java
@@ -0,0 +1,77 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.params;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.TestClass;
+
+import org.chromium.base.test.params.ParameterizedRunner.ParameterizedTestInstantiationException;
+
+import java.util.Collections;
+
+@RunWith(BlockJUnit4ClassRunner.class)
+public class ParameterizedRunnerDelegateCommonTest {
+    /**
+     * Create a test object using the list of class parameter set
+     *
+     * @param testClass the {@link TestClass} object for current test class
+     * @param classParameterSet the parameter set needed for the test class constructor
+     */
+    private static Object createTest(TestClass testClass, ParameterSet classParameterSet)
+            throws ParameterizedTestInstantiationException {
+        return new ParameterizedRunnerDelegateCommon(
+                testClass, classParameterSet, Collections.emptyList())
+                .createTest();
+    }
+
+    static class BadTestClassWithMoreThanOneConstructor {
+        public BadTestClassWithMoreThanOneConstructor() {}
+        @SuppressWarnings("unused")
+        public BadTestClassWithMoreThanOneConstructor(String argument) {}
+    }
+
+    static class BadTestClassWithTwoArgumentConstructor {
+        @SuppressWarnings("unused")
+        public BadTestClassWithTwoArgumentConstructor(int a, int b) {}
+    }
+
+    static abstract class BadTestClassAbstract {
+        public BadTestClassAbstract() {}
+    }
+
+    static class BadTestClassConstructorThrows {
+        public BadTestClassConstructorThrows() {
+            throw new RuntimeException();
+        }
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testCreateTestWithMoreThanOneConstructor() throws Throwable {
+        TestClass testClass = new TestClass(BadTestClassWithMoreThanOneConstructor.class);
+        createTest(testClass, null);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testCreateTestWithIncorrectArguments() throws Throwable {
+        TestClass testClass = new TestClass(BadTestClassWithTwoArgumentConstructor.class);
+        ParameterSet pSet = new ParameterSet().value(1, 2, 3);
+        createTest(testClass, pSet);
+    }
+
+    @Test(expected = ParameterizedTestInstantiationException.class)
+    public void testCreateTestWithAbstractClass() throws ParameterizedTestInstantiationException {
+        TestClass testClass = new TestClass(BadTestClassAbstract.class);
+        createTest(testClass, null);
+    }
+
+    @Test(expected = ParameterizedTestInstantiationException.class)
+    public void testCreateTestWithThrowingConstructor()
+            throws ParameterizedTestInstantiationException {
+        TestClass testClass = new TestClass(BadTestClassConstructorThrows.class);
+        createTest(testClass, null);
+    }
+}
diff --git a/src/base/test/android/junit/src/org/chromium/base/test/params/ParameterizedRunnerDelegateFactoryTest.java b/src/base/test/android/junit/src/org/chromium/base/test/params/ParameterizedRunnerDelegateFactoryTest.java
new file mode 100644
index 0000000..723382d
--- /dev/null
+++ b/src/base/test/android/junit/src/org/chromium/base/test/params/ParameterizedRunnerDelegateFactoryTest.java
@@ -0,0 +1,133 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.params;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.TestClass;
+
+import org.chromium.base.test.params.ParameterAnnotations.UseMethodParameter;
+import org.chromium.base.test.params.ParameterizedRunnerDelegateFactory.ParameterizedRunnerDelegateInstantiationException;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Test for org.chromium.base.test.params.ParameterizedRunnerDelegateFactory
+ */
+@RunWith(BlockJUnit4ClassRunner.class)
+public class ParameterizedRunnerDelegateFactoryTest {
+    /**
+     * This RunnerDelegate calls `super.collectInitializationErrors()` and would
+     * cause BlockJUnit4ClassRunner to validate test classes.
+     */
+    public static class BadExampleRunnerDelegate
+            extends BlockJUnit4ClassRunner implements ParameterizedRunnerDelegate {
+        public static class LalaTestClass {}
+
+        private final List<FrameworkMethod> mParameterizedFrameworkMethodList;
+
+        BadExampleRunnerDelegate(Class<?> klass,
+                List<FrameworkMethod> parameterizedFrameworkMethods) throws InitializationError {
+            super(klass);
+            mParameterizedFrameworkMethodList = parameterizedFrameworkMethods;
+        }
+
+        @Override
+        public void collectInitializationErrors(List<Throwable> errors) {
+            super.collectInitializationErrors(errors); // This is wrong!!
+        }
+
+        @Override
+        public List<FrameworkMethod> computeTestMethods() {
+            return mParameterizedFrameworkMethodList;
+        }
+
+        @Override
+        public Object createTest() {
+            return null;
+        }
+    }
+
+    static class ExampleTestClass {
+        static class MethodParamsA implements ParameterProvider {
+            @Override
+            public Iterable<ParameterSet> getParameters() {
+                return Arrays.asList(
+                        new ParameterSet().value("a").name("testWithValue_a"),
+                        new ParameterSet().value("b").name("testWithValue_b")
+                );
+            }
+        }
+
+        @SuppressWarnings("unused")
+        @UseMethodParameter(MethodParamsA.class)
+        @Test
+        public void testA(String a) {}
+
+        static class MethodParamsB implements ParameterProvider {
+            @Override
+            public Iterable<ParameterSet> getParameters() {
+                return Arrays.asList(
+                        new ParameterSet().value(1).name("testWithValue_1"),
+                        new ParameterSet().value(2).name("testWithValue_2"),
+                        new ParameterSet().value(3).name("testWithValue_3")
+                );
+            }
+        }
+
+        @SuppressWarnings("unused")
+        @UseMethodParameter(MethodParamsB.class)
+        @Test
+        public void testB(int b) {}
+
+        @Test
+        public void testByMyself() {}
+    }
+
+    /**
+     * This test validates ParameterizedRunnerDelegateFactory throws exception when
+     * a runner delegate does not override the collectInitializationErrors method.
+     */
+    @Test(expected = ParameterizedRunnerDelegateInstantiationException.class)
+    public void testBadRunnerDelegateWithIncorrectValidationCall() throws Throwable {
+        ParameterizedRunnerDelegateFactory factory = new ParameterizedRunnerDelegateFactory();
+        TestClass testClass = new TestClass(BadExampleRunnerDelegate.LalaTestClass.class);
+        factory.createRunner(testClass, null, BadExampleRunnerDelegate.class);
+    }
+
+    @Test
+    public void testGenerateParameterizedFrameworkMethod() throws Throwable {
+        List<FrameworkMethod> methods =
+                ParameterizedRunnerDelegateFactory.generateUnmodifiableFrameworkMethodList(
+                        new TestClass(ExampleTestClass.class), "");
+
+        Assert.assertEquals(methods.size(), 6);
+
+        Map<String, Method> expectedTests = new HashMap<>();
+        Method testMethodA = ExampleTestClass.class.getDeclaredMethod("testA", String.class);
+        Method testMethodB = ExampleTestClass.class.getDeclaredMethod("testB", int.class);
+        Method testMethodByMyself = ExampleTestClass.class.getDeclaredMethod("testByMyself");
+        expectedTests.put("testA__testWithValue_a", testMethodA);
+        expectedTests.put("testA__testWithValue_b", testMethodA);
+        expectedTests.put("testB__testWithValue_1", testMethodB);
+        expectedTests.put("testB__testWithValue_2", testMethodB);
+        expectedTests.put("testB__testWithValue_3", testMethodB);
+        expectedTests.put("testByMyself", testMethodByMyself);
+        for (FrameworkMethod method : methods) {
+            Assert.assertNotNull(expectedTests.get(method.getName()));
+            Assert.assertEquals(expectedTests.get(method.getName()), method.getMethod());
+            expectedTests.remove(method.getName());
+        }
+        Assert.assertTrue(expectedTests.isEmpty());
+    }
+}
diff --git a/src/base/test/android/junit/src/org/chromium/base/test/params/ParameterizedRunnerTest.java b/src/base/test/android/junit/src/org/chromium/base/test/params/ParameterizedRunnerTest.java
new file mode 100644
index 0000000..170ff69
--- /dev/null
+++ b/src/base/test/android/junit/src/org/chromium/base/test/params/ParameterizedRunnerTest.java
@@ -0,0 +1,108 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.params;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.BlockJUnit4ClassRunner;
+
+import org.chromium.base.test.params.ParameterAnnotations.ClassParameter;
+import org.chromium.base.test.params.ParameterAnnotations.UseRunnerDelegate;
+import org.chromium.base.test.params.ParameterizedRunner.IllegalParameterArgumentException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test for org.chromium.base.test.params.ParameterizedRunner
+ */
+@RunWith(BlockJUnit4ClassRunner.class)
+public class ParameterizedRunnerTest {
+    @UseRunnerDelegate(BlockJUnit4RunnerDelegate.class)
+    public static class BadTestClassWithMoreThanOneConstructor {
+        @ClassParameter
+        static List<ParameterSet> sClassParams = new ArrayList<>();
+
+        public BadTestClassWithMoreThanOneConstructor() {}
+
+        public BadTestClassWithMoreThanOneConstructor(String x) {}
+    }
+
+    @UseRunnerDelegate(BlockJUnit4RunnerDelegate.class)
+    public static class BadTestClassWithNonListParameters {
+        @ClassParameter
+        static String[] sMethodParamA = {"1", "2"};
+
+        @Test
+        public void test() {}
+    }
+
+    @UseRunnerDelegate(BlockJUnit4RunnerDelegate.class)
+    public static class BadTestClassWithoutNeedForParameterization {
+        @Test
+        public void test() {}
+    }
+
+    @UseRunnerDelegate(BlockJUnit4RunnerDelegate.class)
+    public static class BadTestClassWithNonStaticParameterSetList {
+        @ClassParameter
+        public List<ParameterSet> mClassParams = new ArrayList<>();
+
+        @Test
+        public void test() {}
+    }
+
+    @UseRunnerDelegate(BlockJUnit4RunnerDelegate.class)
+    public static class BadTestClassWithMultipleClassParameter {
+        @ClassParameter
+        private static List<ParameterSet> sParamA = new ArrayList<>();
+
+        @ClassParameter
+        private static List<ParameterSet> sParamB = new ArrayList<>();
+    }
+
+    @Test(expected = ParameterizedRunner.IllegalParameterArgumentException.class)
+    public void testEmptyParameterSet() {
+        List<ParameterSet> paramList = new ArrayList<>();
+        paramList.add(new ParameterSet());
+        ParameterizedRunner.validateWidth(paramList);
+    }
+
+    @Test(expected = ParameterizedRunner.IllegalParameterArgumentException.class)
+    public void testUnequalWidthParameterSetList() {
+        List<ParameterSet> paramList = new ArrayList<>();
+        paramList.add(new ParameterSet().value(1, 2));
+        paramList.add(new ParameterSet().value(3, 4, 5));
+        ParameterizedRunner.validateWidth(paramList);
+    }
+
+    @Test(expected = ParameterizedRunner.IllegalParameterArgumentException.class)
+    public void testUnequalWidthParameterSetListWithNull() {
+        List<ParameterSet> paramList = new ArrayList<>();
+        paramList.add(new ParameterSet().value(null));
+        paramList.add(new ParameterSet().value(1, 2));
+        ParameterizedRunner.validateWidth(paramList);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testBadClassWithNonListParameters() throws Throwable {
+        new ParameterizedRunner(BadTestClassWithNonListParameters.class);
+    }
+
+    @Test(expected = IllegalParameterArgumentException.class)
+    public void testBadClassWithNonStaticParameterSetList() throws Throwable {
+        new ParameterizedRunner(BadTestClassWithNonStaticParameterSetList.class);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testBadClassWithoutNeedForParameterization() throws Throwable {
+        new ParameterizedRunner(BadTestClassWithoutNeedForParameterization.class);
+    }
+
+    @Test(expected = Exception.class)
+    public void testBadClassWithMoreThanOneConstructor() throws Throwable {
+        new ParameterizedRunner(BadTestClassWithMoreThanOneConstructor.class);
+    }
+}
diff --git a/src/base/test/android/junit/src/org/chromium/base/test/params/ParameterizedTestNameTest.java b/src/base/test/android/junit/src/org/chromium/base/test/params/ParameterizedTestNameTest.java
new file mode 100644
index 0000000..e79f5c5
--- /dev/null
+++ b/src/base/test/android/junit/src/org/chromium/base/test/params/ParameterizedTestNameTest.java
@@ -0,0 +1,201 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.params;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runner.Runner;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.TestClass;
+
+import org.chromium.base.test.params.ParameterAnnotations.ClassParameter;
+import org.chromium.base.test.params.ParameterAnnotations.UseMethodParameter;
+import org.chromium.base.test.params.ParameterAnnotations.UseRunnerDelegate;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Test for verify the names and test method Description works properly
+ */
+@RunWith(BlockJUnit4ClassRunner.class)
+public class ParameterizedTestNameTest {
+    @UseRunnerDelegate(BlockJUnit4RunnerDelegate.class)
+    public static class TestClassWithClassParameterAppendName {
+        @ClassParameter
+        static List<ParameterSet> sAllName = Arrays.asList(
+                new ParameterSet().value("hello").name("Hello"),
+                new ParameterSet().value("world").name("World")
+        );
+
+        public TestClassWithClassParameterAppendName(String a) {}
+
+        @Test
+        public void test() {}
+    }
+
+    @UseRunnerDelegate(BlockJUnit4RunnerDelegate.class)
+    public static class TestClassWithClassParameterDefaultName {
+        @ClassParameter
+        static List<ParameterSet> sAllName = Arrays.asList(
+                new ParameterSet().value("hello"),
+                new ParameterSet().value("world")
+        );
+
+        public TestClassWithClassParameterDefaultName(String a) {}
+
+        @Test
+        public void test() {}
+    }
+
+    @UseRunnerDelegate(BlockJUnit4RunnerDelegate.class)
+    public static class TestClassWithMethodParameter {
+        static class AppendNameParams implements ParameterProvider {
+            @Override
+            public Iterable<ParameterSet> getParameters() {
+                return Arrays.asList(
+                        new ParameterSet().value("hello").name("Hello"),
+                        new ParameterSet().value("world").name("World")
+                );
+            }
+        }
+
+        static class DefaultNameParams implements ParameterProvider {
+            @Override
+            public Iterable<ParameterSet> getParameters() {
+                return Arrays.asList(
+                        new ParameterSet().value("hello"),
+                        new ParameterSet().value("world")
+                );
+            }
+        }
+
+        @UseMethodParameter(AppendNameParams.class)
+        @Test
+        public void test(String a) {}
+
+        @UseMethodParameter(DefaultNameParams.class)
+        @Test
+        public void testDefaultName(String b) {}
+    }
+
+    @UseRunnerDelegate(BlockJUnit4RunnerDelegate.class)
+    public static class TestClassWithMixedParameter {
+        @ClassParameter
+        static List<ParameterSet> sAllName = Arrays.asList(
+                new ParameterSet().value("hello").name("Hello"),
+                new ParameterSet().value("world").name("World")
+        );
+
+        static class AppendNameParams implements ParameterProvider {
+            @Override
+            public Iterable<ParameterSet> getParameters() {
+                return Arrays.asList(
+                        new ParameterSet().value("1").name("A"),
+                        new ParameterSet().value("2").name("B")
+                );
+            }
+        }
+
+        public TestClassWithMixedParameter(String a) {}
+
+        @UseMethodParameter(AppendNameParams.class)
+        @Test
+        public void testA(String a) {}
+
+        @Test
+        public void test() {}
+    }
+
+    @Test
+    public void testClassParameterAppendName() throws Throwable {
+        List<Runner> runners = ParameterizedRunner.createRunners(
+                new TestClass(TestClassWithClassParameterAppendName.class));
+        List<String> expectedTestNames =
+                new LinkedList<String>(Arrays.asList("test__Hello", "test__World"));
+        List<String> computedMethodNames = new ArrayList<>();
+        for (Runner r : runners) {
+            BlockJUnit4RunnerDelegate castedRunner = (BlockJUnit4RunnerDelegate) r;
+            for (FrameworkMethod method : castedRunner.computeTestMethods()) {
+                computedMethodNames.add(method.getName());
+                Assert.assertTrue("This test name is not expected: " + method.getName(),
+                        expectedTestNames.contains(method.getName()));
+                expectedTestNames.remove(method.getName());
+                method.getName();
+            }
+        }
+        Assert.assertTrue(
+                String.format(
+                        "These names were provided: %s, these expected names are not found: %s",
+                        Arrays.toString(computedMethodNames.toArray()),
+                        Arrays.toString(expectedTestNames.toArray())),
+                expectedTestNames.isEmpty());
+    }
+
+    @Test
+    public void testClassParameterDefaultName() throws Throwable {
+        List<Runner> runners = ParameterizedRunner.createRunners(
+                new TestClass(TestClassWithClassParameterDefaultName.class));
+        List<String> expectedTestNames = new LinkedList<String>(Arrays.asList("test", "test"));
+        for (Runner r : runners) {
+            @SuppressWarnings("unchecked")
+            BlockJUnit4RunnerDelegate castedRunner = (BlockJUnit4RunnerDelegate) r;
+            for (FrameworkMethod method : castedRunner.computeTestMethods()) {
+                Assert.assertTrue("This test name is not expected: " + method.getName(),
+                        expectedTestNames.contains(method.getName()));
+                expectedTestNames.remove(method.getName());
+                method.getName();
+            }
+        }
+        Assert.assertTrue("These expected names are not found: "
+                        + Arrays.toString(expectedTestNames.toArray()),
+                expectedTestNames.isEmpty());
+    }
+
+    @Test
+    public void testMethodParameter() throws Throwable {
+        List<Runner> runners = ParameterizedRunner.createRunners(
+                new TestClass(TestClassWithMethodParameter.class));
+        List<String> expectedTestNames = new LinkedList<String>(
+                Arrays.asList("test__Hello", "test__World", "testDefaultName", "testDefaultName"));
+        for (Runner r : runners) {
+            BlockJUnit4RunnerDelegate castedRunner = (BlockJUnit4RunnerDelegate) r;
+            for (FrameworkMethod method : castedRunner.computeTestMethods()) {
+                Assert.assertTrue("This test name is not expected: " + method.getName(),
+                        expectedTestNames.contains(method.getName()));
+                expectedTestNames.remove(method.getName());
+                method.getName();
+            }
+        }
+        Assert.assertTrue("These expected names are not found: "
+                        + Arrays.toString(expectedTestNames.toArray()),
+                expectedTestNames.isEmpty());
+    }
+
+    @Test
+    public void testMixedParameterTestA() throws Throwable {
+        List<Runner> runners =
+                ParameterizedRunner.createRunners(new TestClass(TestClassWithMixedParameter.class));
+        List<String> expectedTestNames =
+                new LinkedList<String>(Arrays.asList("testA__Hello_A", "testA__World_A",
+                        "testA__Hello_B", "testA__World_B", "test__Hello", "test__World"));
+        for (Runner r : runners) {
+            BlockJUnit4RunnerDelegate castedRunner = (BlockJUnit4RunnerDelegate) r;
+            for (FrameworkMethod method : castedRunner.computeTestMethods()) {
+                Assert.assertTrue("This test name is not expected: " + method.getName(),
+                        expectedTestNames.contains(method.getName()));
+                expectedTestNames.remove(method.getName());
+                method.getName();
+            }
+        }
+        Assert.assertTrue("These expected names are not found: "
+                        + Arrays.toString(expectedTestNames.toArray()),
+                expectedTestNames.isEmpty());
+    }
+}
diff --git a/src/base/test/android/junit/src/org/chromium/base/test/util/AnnotationProcessingUtilsTest.java b/src/base/test/android/junit/src/org/chromium/base/test/util/AnnotationProcessingUtilsTest.java
new file mode 100644
index 0000000..9acd141
--- /dev/null
+++ b/src/base/test/android/junit/src/org/chromium/base/test/util/AnnotationProcessingUtilsTest.java
@@ -0,0 +1,377 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import static org.hamcrest.Matchers.contains;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import static org.junit.runner.Description.createTestDescription;
+
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.Statement;
+
+import org.chromium.base.test.util.AnnotationProcessingUtils.AnnotationExtractor;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+/** Test for {@link AnnotationProcessingUtils}. */
+@RunWith(BlockJUnit4ClassRunner.class)
+public class AnnotationProcessingUtilsTest {
+    @Test
+    public void testGetTargetAnnotation_NotOnClassNorMethod() {
+        TargetAnnotation retrievedAnnotation;
+
+        retrievedAnnotation = AnnotationProcessingUtils.getAnnotation(
+                createTestDescription(
+                        ClassWithoutTargetAnnotation.class, "methodWithoutAnnotation"),
+                TargetAnnotation.class);
+        assertNull(retrievedAnnotation);
+    }
+
+    @Test
+    public void testGetTargetAnnotation_NotOnClassButOnMethod() {
+        TargetAnnotation retrievedAnnotation;
+
+        retrievedAnnotation = AnnotationProcessingUtils.getAnnotation(
+                getTest(ClassWithoutTargetAnnotation.class, "methodWithTargetAnnotation"),
+                TargetAnnotation.class);
+        assertNotNull(retrievedAnnotation);
+    }
+
+    @Test
+    public void testGetTargetAnnotation_NotOnClassDifferentOneOnMethod() {
+        TargetAnnotation retrievedAnnotation;
+
+        retrievedAnnotation = AnnotationProcessingUtils.getAnnotation(
+                getTest(ClassWithoutTargetAnnotation.class, "methodWithAnnotatedAnnotation"),
+                TargetAnnotation.class);
+        assertNull(retrievedAnnotation);
+    }
+
+    @Test
+    public void testGetTargetAnnotation_OnClassButNotOnMethod() {
+        TargetAnnotation retrievedAnnotation;
+
+        retrievedAnnotation = AnnotationProcessingUtils.getAnnotation(
+                getTest(ClassWithAnnotation.class, "methodWithoutAnnotation"),
+                TargetAnnotation.class);
+        assertNotNull(retrievedAnnotation);
+        assertEquals(Location.Class, retrievedAnnotation.value());
+    }
+
+    @Test
+    public void testGetTargetAnnotation_OnClassAndMethod() {
+        TargetAnnotation retrievedAnnotation;
+
+        retrievedAnnotation = AnnotationProcessingUtils.getAnnotation(
+                getTest(ClassWithAnnotation.class, "methodWithTargetAnnotation"),
+                TargetAnnotation.class);
+        assertNotNull(retrievedAnnotation);
+        assertEquals(Location.Method, retrievedAnnotation.value());
+    }
+
+    @Test
+    @Ignore("Rules not supported yet.")
+    public void testGetTargetAnnotation_OnRuleButNotOnMethod() {
+        TargetAnnotation retrievedAnnotation;
+
+        retrievedAnnotation = AnnotationProcessingUtils.getAnnotation(
+                getTest(ClassWithRule.class, "methodWithoutAnnotation"), TargetAnnotation.class);
+        assertNotNull(retrievedAnnotation);
+        assertEquals(Location.Rule, retrievedAnnotation.value());
+    }
+
+    @Test
+    @Ignore("Rules not supported yet.")
+    public void testGetTargetAnnotation_OnRuleAndMethod() {
+        TargetAnnotation retrievedAnnotation;
+
+        retrievedAnnotation = AnnotationProcessingUtils.getAnnotation(
+                getTest(ClassWithRule.class, "methodWithTargetAnnotation"), TargetAnnotation.class);
+        assertNotNull(retrievedAnnotation);
+        assertEquals(Location.Method, retrievedAnnotation.value());
+    }
+
+    @Test
+    public void testGetMetaAnnotation_Indirectly() {
+        MetaAnnotation retrievedAnnotation;
+
+        retrievedAnnotation = AnnotationProcessingUtils.getAnnotation(
+                getTest(ClassWithoutTargetAnnotation.class, "methodWithAnnotatedAnnotation"),
+                MetaAnnotation.class);
+        assertNotNull(retrievedAnnotation);
+    }
+
+    @Test
+    public void testGetAllTargetAnnotations() {
+        List<TargetAnnotation> retrievedAnnotations;
+
+        retrievedAnnotations = AnnotationProcessingUtils.getAnnotations(
+                getTest(ClassWithAnnotation.class, "methodWithTargetAnnotation"),
+                TargetAnnotation.class);
+        assertEquals(2, retrievedAnnotations.size());
+        assertEquals(Location.Class, retrievedAnnotations.get(0).value());
+        assertEquals(Location.Method, retrievedAnnotations.get(1).value());
+    }
+
+    @Test
+    public void testGetAllTargetAnnotations_OnParentClass() {
+        List<TargetAnnotation> retrievedAnnotations;
+
+        retrievedAnnotations = AnnotationProcessingUtils.getAnnotations(
+                getTest(DerivedClassWithoutAnnotation.class, "newMethodWithoutAnnotation"),
+                TargetAnnotation.class);
+        assertEquals(1, retrievedAnnotations.size());
+        assertEquals(Location.Class, retrievedAnnotations.get(0).value());
+    }
+
+    @Test
+    public void testGetAllTargetAnnotations_OnDerivedMethodAndParentClass() {
+        List<TargetAnnotation> retrievedAnnotations;
+
+        retrievedAnnotations = AnnotationProcessingUtils.getAnnotations(
+                getTest(DerivedClassWithoutAnnotation.class, "newMethodWithTargetAnnotation"),
+                TargetAnnotation.class);
+        assertEquals(2, retrievedAnnotations.size());
+        assertEquals(Location.Class, retrievedAnnotations.get(0).value());
+        assertEquals(Location.DerivedMethod, retrievedAnnotations.get(1).value());
+    }
+
+    @Test
+    public void testGetAllTargetAnnotations_OnDerivedMethodAndParentClassAndMethod() {
+        List<TargetAnnotation> retrievedAnnotations;
+
+        retrievedAnnotations = AnnotationProcessingUtils.getAnnotations(
+                getTest(DerivedClassWithoutAnnotation.class, "methodWithTargetAnnotation"),
+                TargetAnnotation.class);
+        // We should not look at the base implementation of the method. Mostly it should not happen
+        // in the context of tests.
+        assertEquals(2, retrievedAnnotations.size());
+        assertEquals(Location.Class, retrievedAnnotations.get(0).value());
+        assertEquals(Location.DerivedMethod, retrievedAnnotations.get(1).value());
+    }
+
+    @Test
+    public void testGetAllTargetAnnotations_OnDerivedParentAndParentClass() {
+        List<TargetAnnotation> retrievedAnnotations;
+
+        retrievedAnnotations = AnnotationProcessingUtils.getAnnotations(
+                getTest(DerivedClassWithAnnotation.class, "methodWithoutAnnotation"),
+                TargetAnnotation.class);
+        assertEquals(2, retrievedAnnotations.size());
+        assertEquals(Location.Class, retrievedAnnotations.get(0).value());
+        assertEquals(Location.DerivedClass, retrievedAnnotations.get(1).value());
+    }
+
+    @Test
+    public void testGetAllAnnotations() {
+        List<Annotation> annotations;
+
+        AnnotationExtractor annotationExtractor = new AnnotationExtractor(
+                TargetAnnotation.class, MetaAnnotation.class, AnnotatedAnnotation.class);
+        annotations = annotationExtractor.getMatchingAnnotations(
+                getTest(DerivedClassWithAnnotation.class, "methodWithTwoAnnotations"));
+        assertEquals(5, annotations.size());
+
+        // Retrieved annotation order:
+        // On Parent Class
+        assertEquals(TargetAnnotation.class, annotations.get(0).annotationType());
+        assertEquals(Location.Class, ((TargetAnnotation) annotations.get(0)).value());
+
+        // On Class
+        assertEquals(TargetAnnotation.class, annotations.get(1).annotationType());
+        assertEquals(Location.DerivedClass, ((TargetAnnotation) annotations.get(1)).value());
+
+        // Meta-annotations from method
+        assertEquals(MetaAnnotation.class, annotations.get(2).annotationType());
+
+        // On Method
+        assertEquals(AnnotatedAnnotation.class, annotations.get(3).annotationType());
+        assertEquals(TargetAnnotation.class, annotations.get(4).annotationType());
+        assertEquals(Location.DerivedMethod, ((TargetAnnotation) annotations.get(4)).value());
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testAnnotationExtractorSortOrder_UnknownAnnotations() {
+        AnnotationExtractor annotationExtractor = new AnnotationExtractor(Target.class);
+        Comparator<Class<? extends Annotation>> comparator =
+                annotationExtractor.getTypeComparator();
+        List<Class<? extends Annotation>> testList =
+                Arrays.asList(Rule.class, Test.class, Override.class, Target.class, Rule.class);
+        testList.sort(comparator);
+        assertThat("Unknown annotations should not be reordered and come before the known ones.",
+                testList,
+                contains(Rule.class, Test.class, Override.class, Rule.class, Target.class));
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testAnnotationExtractorSortOrder_KnownAnnotations() {
+        AnnotationExtractor annotationExtractor =
+                new AnnotationExtractor(Test.class, Target.class, Rule.class);
+        Comparator<Class<? extends Annotation>> comparator =
+                annotationExtractor.getTypeComparator();
+        List<Class<? extends Annotation>> testList =
+                Arrays.asList(Rule.class, Test.class, Override.class, Target.class, Rule.class);
+        testList.sort(comparator);
+        assertThat(
+                "Known annotations should be sorted in the same order as provided to the extractor",
+                testList,
+                contains(Override.class, Test.class, Target.class, Rule.class, Rule.class));
+    }
+
+    private static Description getTest(Class<?> klass, String testName) {
+        Description description = null;
+        try {
+            description = new DummyTestRunner(klass).describe(testName);
+        } catch (InitializationError initializationError) {
+            initializationError.printStackTrace();
+            fail("DummyTestRunner initialization failed:" + initializationError.getMessage());
+        }
+        if (description == null) {
+            fail("Not test named '" + testName + "' in class" + klass.getSimpleName());
+        }
+        return description;
+    }
+
+    // region Test Data: Annotations and dummy test classes
+    private enum Location { Unspecified, Class, Method, Rule, DerivedClass, DerivedMethod }
+
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target({ElementType.TYPE, ElementType.METHOD})
+    private @interface TargetAnnotation {
+        Location value() default Location.Unspecified;
+    }
+
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.METHOD})
+    private @interface MetaAnnotation {}
+
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target({ElementType.TYPE, ElementType.METHOD})
+    @MetaAnnotation
+    private @interface AnnotatedAnnotation {}
+
+    private @interface SimpleAnnotation {}
+
+    @SimpleAnnotation
+    private static class ClassWithoutTargetAnnotation {
+        @Test
+        public void methodWithoutAnnotation() {}
+
+        @Test
+        @TargetAnnotation
+        public void methodWithTargetAnnotation() {}
+
+        @Test
+        @AnnotatedAnnotation
+        public void methodWithAnnotatedAnnotation() {}
+    }
+
+    @TargetAnnotation(Location.Class)
+    private static class ClassWithAnnotation {
+        @Test
+        public void methodWithoutAnnotation() {}
+
+        @Test
+        @TargetAnnotation(Location.Method)
+        public void methodWithTargetAnnotation() {}
+
+        @Test
+        @MetaAnnotation
+        public void methodWithMetaAnnotation() {}
+
+        @Test
+        @AnnotatedAnnotation
+        public void methodWithAnnotatedAnnotation() {}
+    }
+
+    private static class DerivedClassWithoutAnnotation extends ClassWithAnnotation {
+        @Test
+        public void newMethodWithoutAnnotation() {}
+
+        @Test
+        @TargetAnnotation(Location.DerivedMethod)
+        public void newMethodWithTargetAnnotation() {}
+
+        @Test
+        @Override
+        @TargetAnnotation(Location.DerivedMethod)
+        public void methodWithTargetAnnotation() {}
+    }
+
+    @TargetAnnotation(Location.DerivedClass)
+    private static class DerivedClassWithAnnotation extends ClassWithAnnotation {
+        @Test
+        public void newMethodWithoutAnnotation() {}
+
+        @Test
+        @AnnotatedAnnotation
+        @TargetAnnotation(Location.DerivedMethod)
+        public void methodWithTwoAnnotations() {}
+    }
+
+    private static class ClassWithRule {
+        @Rule
+        Rule1 mRule = new Rule1();
+
+        @Test
+        public void methodWithoutAnnotation() {}
+
+        @Test
+        @TargetAnnotation
+        public void methodWithTargetAnnotation() {}
+    }
+
+    @TargetAnnotation(Location.Rule)
+    @MetaAnnotation
+    private static class Rule1 implements TestRule {
+        @Override
+        public Statement apply(Statement statement, Description description) {
+            return null;
+        }
+    }
+
+    private static class DummyTestRunner extends BlockJUnit4ClassRunner {
+        public DummyTestRunner(Class<?> klass) throws InitializationError {
+            super(klass);
+        }
+
+        @Override
+        protected void collectInitializationErrors(List<Throwable> errors) {
+            // Do nothing. BlockJUnit4ClassRunner requires the class to be public, but we don't
+            // want/need it.
+        }
+
+        public Description describe(String testName) {
+            List<FrameworkMethod> tests = getTestClass().getAnnotatedMethods(Test.class);
+            for (FrameworkMethod testMethod : tests) {
+                if (testMethod.getName().equals(testName)) return describeChild(testMethod);
+            }
+            return null;
+        }
+    }
+
+    // endregion
+    }
diff --git a/src/base/test/android/junit/src/org/chromium/base/test/util/DisableIfTest.java b/src/base/test/android/junit/src/org/chromium/base/test/util/DisableIfTest.java
new file mode 100644
index 0000000..a147435
--- /dev/null
+++ b/src/base/test/android/junit/src/org/chromium/base/test/util/DisableIfTest.java
@@ -0,0 +1,193 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import android.os.Build;
+
+import junit.framework.TestCase;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+import org.robolectric.util.ReflectionHelpers;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+
+/** Unit tests for the DisableIf annotation and its SkipCheck implementation. */
+@RunWith(BaseRobolectricTestRunner.class)
+@Config(manifest = Config.NONE, sdk = 21)
+public class DisableIfTest {
+    @Test
+    public void testSdkIsLessThanAndIsLessThan() {
+        TestCase sdkIsLessThan = new TestCase("sdkIsLessThan") {
+            @DisableIf.Build(sdk_is_less_than = 22)
+            public void sdkIsLessThan() {}
+        };
+        Assert.assertTrue(new DisableIfSkipCheck().shouldSkip(sdkIsLessThan));
+    }
+
+    @Test
+    public void testSdkIsLessThanButIsEqual() {
+        TestCase sdkIsEqual = new TestCase("sdkIsEqual") {
+            @DisableIf.Build(sdk_is_less_than = 21)
+            public void sdkIsEqual() {}
+        };
+        Assert.assertFalse(new DisableIfSkipCheck().shouldSkip(sdkIsEqual));
+    }
+
+    @Test
+    public void testSdkIsLessThanButIsGreaterThan() {
+        TestCase sdkIsGreaterThan = new TestCase("sdkIsGreaterThan") {
+            @DisableIf.Build(sdk_is_less_than = 20)
+            public void sdkIsGreaterThan() {}
+        };
+        Assert.assertFalse(new DisableIfSkipCheck().shouldSkip(sdkIsGreaterThan));
+    }
+
+    @Test
+    public void testSdkIsGreaterThanButIsLessThan() {
+        TestCase sdkIsLessThan = new TestCase("sdkIsLessThan") {
+            @DisableIf.Build(sdk_is_greater_than = 22)
+            public void sdkIsLessThan() {}
+        };
+        Assert.assertFalse(new DisableIfSkipCheck().shouldSkip(sdkIsLessThan));
+    }
+
+    @Test
+    public void testSdkIsGreaterThanButIsEqual() {
+        TestCase sdkIsEqual = new TestCase("sdkIsEqual") {
+            @DisableIf.Build(sdk_is_greater_than = 21)
+            public void sdkIsEqual() {}
+        };
+        Assert.assertFalse(new DisableIfSkipCheck().shouldSkip(sdkIsEqual));
+    }
+
+    @Test
+    public void testSdkIsGreaterThanAndIsGreaterThan() {
+        TestCase sdkIsGreaterThan = new TestCase("sdkIsGreaterThan") {
+            @DisableIf.Build(sdk_is_greater_than = 20)
+            public void sdkIsGreaterThan() {}
+        };
+        Assert.assertTrue(new DisableIfSkipCheck().shouldSkip(sdkIsGreaterThan));
+    }
+
+    @Test
+    public void testSupportedAbiIncludesAndCpuAbiMatches() {
+        TestCase supportedAbisCpuAbiMatch = new TestCase("supportedAbisCpuAbiMatch") {
+            @DisableIf.Build(supported_abis_includes = "foo")
+            public void supportedAbisCpuAbiMatch() {}
+        };
+        String[] originalAbis = Build.SUPPORTED_ABIS;
+        try {
+            ReflectionHelpers.setStaticField(Build.class, "SUPPORTED_ABIS",
+                    new String[] {"foo", "bar"});
+            Assert.assertTrue(new DisableIfSkipCheck().shouldSkip(supportedAbisCpuAbiMatch));
+        } finally {
+            ReflectionHelpers.setStaticField(Build.class, "SUPPORTED_ABIS", originalAbis);
+        }
+    }
+
+    @Test
+    public void testSupportedAbiIncludesAndCpuAbi2Matches() {
+        TestCase supportedAbisCpuAbi2Match = new TestCase("supportedAbisCpuAbi2Match") {
+            @DisableIf.Build(supported_abis_includes = "bar")
+            public void supportedAbisCpuAbi2Match() {}
+        };
+        String[] originalAbis = Build.SUPPORTED_ABIS;
+        try {
+            ReflectionHelpers.setStaticField(Build.class, "SUPPORTED_ABIS",
+                    new String[] {"foo", "bar"});
+            Assert.assertTrue(new DisableIfSkipCheck().shouldSkip(supportedAbisCpuAbi2Match));
+        } finally {
+            ReflectionHelpers.setStaticField(Build.class, "SUPPORTED_ABIS", originalAbis);
+        }
+    }
+
+    @Test
+    public void testSupportedAbiIncludesButNoMatch() {
+        TestCase supportedAbisNoMatch = new TestCase("supportedAbisNoMatch") {
+            @DisableIf.Build(supported_abis_includes = "baz")
+            public void supportedAbisNoMatch() {}
+        };
+        String[] originalAbis = Build.SUPPORTED_ABIS;
+        try {
+            ReflectionHelpers.setStaticField(Build.class, "SUPPORTED_ABIS",
+                    new String[] {"foo", "bar"});
+            Assert.assertFalse(new DisableIfSkipCheck().shouldSkip(supportedAbisNoMatch));
+        } finally {
+            ReflectionHelpers.setStaticField(Build.class, "SUPPORTED_ABIS", originalAbis);
+        }
+    }
+
+    @Test
+    public void testHardwareIsMatches() {
+        TestCase hardwareIsMatches = new TestCase("hardwareIsMatches") {
+            @DisableIf.Build(hardware_is = "hammerhead")
+            public void hardwareIsMatches() {}
+        };
+        String originalHardware = Build.HARDWARE;
+        try {
+            ReflectionHelpers.setStaticField(Build.class, "HARDWARE", "hammerhead");
+            Assert.assertTrue(new DisableIfSkipCheck().shouldSkip(hardwareIsMatches));
+        } finally {
+            ReflectionHelpers.setStaticField(Build.class, "HARDWARE", originalHardware);
+        }
+    }
+
+    @Test
+    public void testHardwareIsDoesntMatch() {
+        TestCase hardwareIsDoesntMatch = new TestCase("hardwareIsDoesntMatch") {
+            @DisableIf.Build(hardware_is = "hammerhead")
+            public void hardwareIsDoesntMatch() {}
+        };
+        String originalHardware = Build.HARDWARE;
+        try {
+            ReflectionHelpers.setStaticField(Build.class, "HARDWARE", "mako");
+            Assert.assertFalse(new DisableIfSkipCheck().shouldSkip(hardwareIsDoesntMatch));
+        } finally {
+            ReflectionHelpers.setStaticField(Build.class, "HARDWARE", originalHardware);
+        }
+    }
+
+    @DisableIf.Build(supported_abis_includes = "foo")
+    private static class DisableIfSuperclassTestCase extends TestCase {
+        public DisableIfSuperclassTestCase(String name) {
+            super(name);
+        }
+    }
+
+    @DisableIf.Build(hardware_is = "hammerhead")
+    private static class DisableIfTestCase extends DisableIfSuperclassTestCase {
+        public DisableIfTestCase(String name) {
+            super(name);
+        }
+        public void sampleTestMethod() {}
+    }
+
+    @Test
+    public void testDisableClass() {
+        TestCase sampleTestMethod = new DisableIfTestCase("sampleTestMethod");
+        String originalHardware = Build.HARDWARE;
+        try {
+            ReflectionHelpers.setStaticField(Build.class, "HARDWARE", "hammerhead");
+            Assert.assertTrue(new DisableIfSkipCheck().shouldSkip(sampleTestMethod));
+        } finally {
+            ReflectionHelpers.setStaticField(Build.class, "HARDWARE", originalHardware);
+        }
+    }
+
+    @Test
+    public void testDisableSuperClass() {
+        TestCase sampleTestMethod = new DisableIfTestCase("sampleTestMethod");
+        String[] originalAbis = Build.SUPPORTED_ABIS;
+        try {
+            ReflectionHelpers.setStaticField(Build.class, "SUPPORTED_ABIS", new String[] {"foo"});
+            Assert.assertTrue(new DisableIfSkipCheck().shouldSkip(sampleTestMethod));
+        } finally {
+            ReflectionHelpers.setStaticField(Build.class, "SUPPORTED_ABIS", originalAbis);
+        }
+    }
+}
diff --git a/src/base/test/android/junit/src/org/chromium/base/test/util/MinAndroidSdkLevelSkipCheckTest.java b/src/base/test/android/junit/src/org/chromium/base/test/util/MinAndroidSdkLevelSkipCheckTest.java
new file mode 100644
index 0000000..2236646
--- /dev/null
+++ b/src/base/test/android/junit/src/org/chromium/base/test/util/MinAndroidSdkLevelSkipCheckTest.java
@@ -0,0 +1,95 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.isIn;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.model.FrameworkMethod;
+import org.robolectric.annotation.Config;
+
+import org.chromium.base.test.BaseJUnit4ClassRunner;
+import org.chromium.base.test.BaseRobolectricTestRunner;
+
+/** Unit tests for MinAndroidSdkLevelSkipCheck. */
+@RunWith(BaseRobolectricTestRunner.class)
+@Config(manifest = Config.NONE, sdk = 18)
+public class MinAndroidSdkLevelSkipCheckTest {
+    public static class UnannotatedBaseClass {
+        @Test @MinAndroidSdkLevel(17) public void min17Method() {}
+        @Test @MinAndroidSdkLevel(20) public void min20Method() {}
+    }
+
+    @MinAndroidSdkLevel(17)
+    public static class Min17Class extends UnannotatedBaseClass {
+        @Test public void unannotatedMethod() {}
+    }
+
+    @MinAndroidSdkLevel(20)
+    public static class Min20Class extends UnannotatedBaseClass {
+        @Test public void unannotatedMethod() {}
+    }
+
+    public static class ExtendsMin17Class extends Min17Class {
+        @Override
+        @Test public void unannotatedMethod() {}
+    }
+
+    public static class ExtendsMin20Class extends Min20Class {
+        @Override
+        @Test public void unannotatedMethod() {}
+    }
+
+    private MinAndroidSdkLevelSkipCheck mSkipCheck = new MinAndroidSdkLevelSkipCheck();
+
+    @Rule
+    public TestRunnerTestRule mTestRunnerTestRule =
+            new TestRunnerTestRule(BaseJUnit4ClassRunner.class);
+
+    private void expectShouldSkip(Class<?> testClass, String methodName, boolean shouldSkip)
+            throws Exception {
+        Assert.assertThat(
+                mSkipCheck.shouldSkip(new FrameworkMethod(testClass.getMethod(methodName))),
+                equalTo(shouldSkip));
+        TestRunnerTestRule.TestLog runListener = mTestRunnerTestRule.runTest(testClass);
+        Assert.assertThat(Description.createTestDescription(testClass, methodName),
+                isIn(shouldSkip ? runListener.skippedTests : runListener.runTests));
+    }
+
+    @Test
+    public void testAnnotatedMethodAboveMin() throws Exception {
+        expectShouldSkip(UnannotatedBaseClass.class, "min17Method", false);
+    }
+
+    @Test
+    public void testAnnotatedMethodBelowMin() throws Exception {
+        expectShouldSkip(UnannotatedBaseClass.class, "min20Method", true);
+    }
+
+    @Test
+    public void testAnnotatedClassAboveMin() throws Exception {
+        expectShouldSkip(Min17Class.class, "unannotatedMethod", false);
+    }
+
+    @Test
+    public void testAnnotatedClassBelowMin() throws Exception {
+        expectShouldSkip(Min20Class.class, "unannotatedMethod", true);
+    }
+
+    @Test
+    public void testAnnotatedSuperclassAboveMin() throws Exception {
+        expectShouldSkip(ExtendsMin17Class.class, "unannotatedMethod", false);
+    }
+
+    @Test
+    public void testAnnotatedSuperclassBelowMin() throws Exception {
+        expectShouldSkip(ExtendsMin20Class.class, "unannotatedMethod", true);
+    }
+}
diff --git a/src/base/test/android/junit/src/org/chromium/base/test/util/RestrictionSkipCheckTest.java b/src/base/test/android/junit/src/org/chromium/base/test/util/RestrictionSkipCheckTest.java
new file mode 100644
index 0000000..86285de
--- /dev/null
+++ b/src/base/test/android/junit/src/org/chromium/base/test/util/RestrictionSkipCheckTest.java
@@ -0,0 +1,129 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import android.text.TextUtils;
+
+import junit.framework.TestCase;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+
+/** Unit tests for RestrictionSkipCheck. */
+@RunWith(BaseRobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class RestrictionSkipCheckTest {
+    private static final String TEST_RESTRICTION_APPLIES =
+            "org.chromium.base.test.util.RestrictionSkipCheckTest.TEST_RESTRICTION_APPLIES";
+    private static final String TEST_RESTRICTION_DOES_NOT_APPLY =
+            "org.chromium.base.test.util.RestrictionSkipCheckTest.TEST_RESTRICTION_DOES_NOT_APPLY";
+
+    private static class TestRestrictionSkipCheck extends RestrictionSkipCheck {
+        public TestRestrictionSkipCheck() {
+            super(null);
+        }
+        @Override
+        protected boolean restrictionApplies(String restriction) {
+            return TextUtils.equals(restriction, TEST_RESTRICTION_APPLIES);
+        }
+    }
+
+    private static class UnannotatedBaseClass extends TestCase {
+        public UnannotatedBaseClass(String name) {
+            super(name);
+        }
+        @Restriction({TEST_RESTRICTION_APPLIES}) public void restrictedMethod() {}
+        @Restriction({TEST_RESTRICTION_DOES_NOT_APPLY}) public void unrestrictedMethod() {}
+    }
+
+    @Restriction({TEST_RESTRICTION_APPLIES})
+    private static class RestrictedClass extends UnannotatedBaseClass {
+        public RestrictedClass(String name) {
+            super(name);
+        }
+        public void unannotatedMethod() {}
+    }
+
+    @Restriction({TEST_RESTRICTION_DOES_NOT_APPLY})
+    private static class UnrestrictedClass extends UnannotatedBaseClass {
+        public UnrestrictedClass(String name) {
+            super(name);
+        }
+        public void unannotatedMethod() {}
+    }
+
+    @Restriction({
+            TEST_RESTRICTION_APPLIES,
+            TEST_RESTRICTION_DOES_NOT_APPLY})
+    private static class MultipleRestrictionsRestrictedClass extends UnannotatedBaseClass {
+        public MultipleRestrictionsRestrictedClass(String name) {
+            super(name);
+        }
+        public void unannotatedMethod() {}
+    }
+
+    private static class ExtendsRestrictedClass extends RestrictedClass {
+        public ExtendsRestrictedClass(String name) {
+            super(name);
+        }
+        @Override
+        public void unannotatedMethod() {}
+    }
+
+    private static class ExtendsUnrestrictedClass extends UnrestrictedClass {
+        public ExtendsUnrestrictedClass(String name) {
+            super(name);
+        }
+        @Override
+        public void unannotatedMethod() {}
+    }
+
+    @Test
+    public void testMethodRestricted() {
+        Assert.assertTrue(new TestRestrictionSkipCheck().shouldSkip(
+                new UnannotatedBaseClass("restrictedMethod")));
+    }
+
+    @Test
+    public void testMethodUnrestricted() {
+        Assert.assertFalse(new TestRestrictionSkipCheck().shouldSkip(
+                new UnannotatedBaseClass("unrestrictedMethod")));
+    }
+
+    @Test
+    public void testClassRestricted() {
+        Assert.assertTrue(new TestRestrictionSkipCheck().shouldSkip(
+                new RestrictedClass("unannotatedMethod")));
+    }
+
+    @Test
+    public void testClassUnrestricted() {
+        Assert.assertFalse(new TestRestrictionSkipCheck().shouldSkip(
+                new UnrestrictedClass("unannotatedMethod")));
+    }
+
+    @Test
+    public void testMultipleRestrictionsClassRestricted() {
+        Assert.assertTrue(new TestRestrictionSkipCheck().shouldSkip(
+                new MultipleRestrictionsRestrictedClass("unannotatedMethod")));
+    }
+
+    @Test
+    public void testSuperclassRestricted() {
+        Assert.assertTrue(new TestRestrictionSkipCheck().shouldSkip(
+                new ExtendsRestrictedClass("unannotatedMethod")));
+    }
+
+    @Test
+    public void testSuperclassUnrestricted() {
+        Assert.assertFalse(new TestRestrictionSkipCheck().shouldSkip(
+                new ExtendsUnrestrictedClass("unannotatedMethod")));
+    }
+}
+
diff --git a/src/base/test/android/junit/src/org/chromium/base/test/util/SkipCheckTest.java b/src/base/test/android/junit/src/org/chromium/base/test/util/SkipCheckTest.java
new file mode 100644
index 0000000..51c7516
--- /dev/null
+++ b/src/base/test/android/junit/src/org/chromium/base/test/util/SkipCheckTest.java
@@ -0,0 +1,130 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import junit.framework.TestCase;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.model.FrameworkMethod;
+import org.robolectric.annotation.Config;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Method;
+import java.util.List;
+
+/** Unit tests for SkipCheck. */
+@RunWith(BaseRobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class SkipCheckTest {
+    private static class TestableSkipCheck extends SkipCheck {
+        public static <T extends Annotation> List<T> getAnnotationsForTesting(
+                AnnotatedElement element, Class<T> annotationClass) {
+            return AnnotationProcessingUtils.getAnnotations(element, annotationClass);
+        }
+
+        @Override
+        public boolean shouldSkip(FrameworkMethod m) {
+            return false;
+        }
+    }
+
+    @Retention(RetentionPolicy.RUNTIME)
+    private @interface TestAnnotation {}
+
+    @TestAnnotation
+    private class AnnotatedBaseClass {
+        public void unannotatedMethod() {}
+        @TestAnnotation public void annotatedMethod() {}
+    }
+
+    private class ExtendsAnnotatedBaseClass extends AnnotatedBaseClass {
+        public void anotherUnannotatedMethod() {}
+    }
+
+    private class ExtendsTestCaseClass extends TestCase {
+        public ExtendsTestCaseClass(String name) {
+            super(name);
+        }
+        public void testMethodA() {}
+    }
+
+    private class UnannotatedBaseClass {
+        public void unannotatedMethod() {}
+        @TestAnnotation public void annotatedMethod() {}
+    }
+
+    @Test
+    public void getAnnotationsForClassNone() {
+        List<TestAnnotation> annotations = TestableSkipCheck.getAnnotationsForTesting(
+                UnannotatedBaseClass.class, TestAnnotation.class);
+        Assert.assertEquals(0, annotations.size());
+    }
+
+    @Test
+    public void getAnnotationsForClassOnClass() {
+        List<TestAnnotation> annotations = TestableSkipCheck.getAnnotationsForTesting(
+                AnnotatedBaseClass.class, TestAnnotation.class);
+        Assert.assertEquals(1, annotations.size());
+    }
+
+    @Test
+    public void getAnnotationsForClassOnSuperclass() {
+        List<TestAnnotation> annotations = TestableSkipCheck.getAnnotationsForTesting(
+                ExtendsAnnotatedBaseClass.class, TestAnnotation.class);
+        Assert.assertEquals(1, annotations.size());
+    }
+
+    @Test
+    public void getAnnotationsForMethodNone() throws NoSuchMethodException {
+        Method testMethod = UnannotatedBaseClass.class.getMethod("unannotatedMethod",
+                (Class[]) null);
+        List<TestAnnotation> annotations = TestableSkipCheck.getAnnotationsForTesting(
+                testMethod, TestAnnotation.class);
+        Assert.assertEquals(0, annotations.size());
+    }
+
+    @Test
+    public void getAnnotationsForMethodOnMethod() throws NoSuchMethodException {
+        Method testMethod = UnannotatedBaseClass.class.getMethod("annotatedMethod",
+                (Class[]) null);
+        List<TestAnnotation> annotations = TestableSkipCheck.getAnnotationsForTesting(
+                testMethod, TestAnnotation.class);
+        Assert.assertEquals(1, annotations.size());
+    }
+
+    @Test
+    public void getAnnotationsForMethodOnClass() throws NoSuchMethodException {
+        Method testMethod = AnnotatedBaseClass.class.getMethod("unannotatedMethod",
+                (Class[]) null);
+        List<TestAnnotation> annotations = TestableSkipCheck.getAnnotationsForTesting(
+                testMethod, TestAnnotation.class);
+        Assert.assertEquals(1, annotations.size());
+    }
+
+    @Test
+    public void getAnnotationsForMethodOnSuperclass() throws NoSuchMethodException {
+        Method testMethod = ExtendsAnnotatedBaseClass.class.getMethod("unannotatedMethod",
+                (Class[]) null);
+        List<TestAnnotation> annotations = TestableSkipCheck.getAnnotationsForTesting(
+                testMethod, TestAnnotation.class);
+        Assert.assertEquals(1, annotations.size());
+    }
+
+    @Test
+    public void getAnnotationsOverlapping() throws NoSuchMethodException {
+        Method testMethod = AnnotatedBaseClass.class.getMethod("annotatedMethod",
+                (Class[]) null);
+        List<TestAnnotation> annotations = TestableSkipCheck.getAnnotationsForTesting(
+                testMethod, TestAnnotation.class);
+        Assert.assertEquals(2, annotations.size());
+    }
+}
diff --git a/src/base/test/android/junit/src/org/chromium/base/test/util/TestRunnerTestRule.java b/src/base/test/android/junit/src/org/chromium/base/test/util/TestRunnerTestRule.java
new file mode 100644
index 0000000..64805c1
--- /dev/null
+++ b/src/base/test/android/junit/src/org/chromium/base/test/util/TestRunnerTestRule.java
@@ -0,0 +1,132 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.base.test.util;
+
+import static org.hamcrest.Matchers.isIn;
+import static org.junit.Assert.fail;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.test.InstrumentationRegistry;
+
+import org.junit.Assert;
+import org.junit.rules.ExternalResource;
+import org.junit.runner.Description;
+import org.junit.runner.Runner;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.InitializationError;
+import org.robolectric.RuntimeEnvironment;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper rule to allow executing test runners in tests.
+ *
+ * Quis probat ipsas probas?
+ */
+class TestRunnerTestRule extends ExternalResource {
+    final Class<? extends BlockJUnit4ClassRunner> mRunnerClass;
+
+    /**
+     * @param runnerClass The runner class to run the test
+     */
+    TestRunnerTestRule(Class<? extends BlockJUnit4ClassRunner> runnerClass) {
+        mRunnerClass = runnerClass;
+    }
+
+    @Override
+    protected void before() throws Throwable {
+        // Register a fake Instrumentation so that class runners for instrumentation tests
+        // can be run even in Robolectric tests.
+        Instrumentation instrumentation = new Instrumentation() {
+            @Override
+            public Context getTargetContext() {
+                return RuntimeEnvironment.application;
+            }
+        };
+        InstrumentationRegistry.registerInstance(instrumentation, new Bundle());
+    }
+
+    @Override
+    protected void after() {
+        InstrumentationRegistry.registerInstance(null, new Bundle());
+    }
+
+    /**
+     * A struct-like class containing lists of run and skipped tests.
+     */
+    public static class TestLog {
+        public final List<Description> runTests = new ArrayList<>();
+        public final List<Description> skippedTests = new ArrayList<>();
+    }
+
+    /**
+     * Creates a new test runner and executes the test in the given {@code testClass} on it,
+     * returning lists of tests that were run and tests that were skipped.
+     * @param testClass The test class
+     * @return A {@link TestLog} that contains lists of run and skipped tests.
+     */
+    public TestLog runTest(Class<?> testClass) throws InvocationTargetException,
+                                                      NoSuchMethodException, InstantiationException,
+                                                      IllegalAccessException {
+        TestLog testLog = new TestLog();
+
+        // TODO(bauerb): Using Mockito mock() or spy() throws a ClassCastException.
+        RunListener runListener = new RunListener() {
+            @Override
+            public void testStarted(Description description) throws Exception {
+                testLog.runTests.add(description);
+            }
+
+            @Override
+            public void testFinished(Description description) throws Exception {
+                Assert.assertThat(description, isIn(testLog.runTests));
+            }
+
+            @Override
+            public void testFailure(Failure failure) throws Exception {
+                fail(failure.toString());
+            }
+
+            @Override
+            public void testAssumptionFailure(Failure failure) {
+                fail(failure.toString());
+            }
+
+            @Override
+            public void testIgnored(Description description) throws Exception {
+                testLog.skippedTests.add(description);
+            }
+        };
+        RunNotifier runNotifier = new RunNotifier();
+        runNotifier.addListener(runListener);
+        Runner runner;
+        try {
+            runner = mRunnerClass.getConstructor(Class.class).newInstance(testClass);
+        } catch (InvocationTargetException e) {
+            // If constructing the runner caused initialization errors, unwrap them from the
+            // InvocationTargetException.
+            Throwable cause = e.getCause();
+            if (!(cause instanceof InitializationError)) throw e;
+            List<Throwable> causes = ((InitializationError) cause).getCauses();
+
+            // If there was exactly one initialization error, rewrap that one.
+            if (causes.size() == 1) {
+                throw new InvocationTargetException(causes.get(0), "Initialization error");
+            }
+
+            // Otherwise, serialize all initialization errors to a string and throw that.
+            throw new AssertionError(causes.toString());
+        }
+        runner.run(runNotifier);
+        return testLog;
+    }
+}
diff --git a/src/base/test/android/url_utils.cc b/src/base/test/android/url_utils.cc
new file mode 100644
index 0000000..7d2a8ed
--- /dev/null
+++ b/src/base/test/android/url_utils.cc
@@ -0,0 +1,24 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/test/android/url_utils.h"
+
+#include "base/android/jni_string.h"
+#include "base/android/scoped_java_ref.h"
+#include "jni/UrlUtils_jni.h"
+
+namespace base {
+namespace android {
+
+FilePath GetIsolatedTestRoot() {
+  JNIEnv* env = base::android::AttachCurrentThread();
+  ScopedJavaLocalRef<jstring> jtest_data_dir =
+      Java_UrlUtils_getIsolatedTestRoot(env);
+  base::FilePath test_data_dir(
+      base::android::ConvertJavaStringToUTF8(env, jtest_data_dir));
+  return test_data_dir;
+}
+
+}  // namespace android
+}  // namespace base
diff --git a/src/base/test/android/url_utils.h b/src/base/test/android/url_utils.h
new file mode 100644
index 0000000..073223c
--- /dev/null
+++ b/src/base/test/android/url_utils.h
@@ -0,0 +1,24 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef BASE_TEST_ANDROID_URL_UTILS_H_
+#define BASE_TEST_ANDROID_URL_UTILS_H_
+
+#include <jni.h>
+
+#include "base/base_export.h"
+#include "base/files/file_path.h"
+#include "starboard/types.h"
+
+namespace base {
+namespace android {
+
+// Returns the root of the test data directory. This function will call into
+// Java class UrlUtils through JNI bridge.
+BASE_EXPORT FilePath GetIsolatedTestRoot();
+
+}  // namespace android
+}  // namespace base
+
+#endif  // BASE_TEST_ANDROID_URL_UTILS_H_