summaryrefslogtreecommitdiff
path: root/base/test/android
diff options
context:
space:
mode:
Diffstat (limited to 'base/test/android')
-rw-r--r--base/test/android/java/src/org/chromium/base/ContentUriTestUtils.java46
-rw-r--r--base/test/android/java/src/org/chromium/base/ITestCallback.aidl23
-rw-r--r--base/test/android/java/src/org/chromium/base/ITestController.aidl25
-rw-r--r--base/test/android/java/src/org/chromium/base/JavaHandlerThreadHelpers.java65
-rw-r--r--base/test/android/java/src/org/chromium/base/MainReturnCodeResult.java40
-rw-r--r--base/test/android/java/src/org/chromium/base/MultiprocessTestClientLauncher.java383
-rw-r--r--base/test/android/java/src/org/chromium/base/MultiprocessTestClientService.java14
-rw-r--r--base/test/android/java/src/org/chromium/base/MultiprocessTestClientService0.java10
-rw-r--r--base/test/android/java/src/org/chromium/base/MultiprocessTestClientService1.java10
-rw-r--r--base/test/android/java/src/org/chromium/base/MultiprocessTestClientService2.java10
-rw-r--r--base/test/android/java/src/org/chromium/base/MultiprocessTestClientService3.java10
-rw-r--r--base/test/android/java/src/org/chromium/base/MultiprocessTestClientService4.java10
-rw-r--r--base/test/android/java/src/org/chromium/base/MultiprocessTestClientServiceDelegate.java94
-rw-r--r--base/test/android/java/src/org/chromium/base/TestUiThread.java51
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/BaseChromiumAndroidJUnitRunner.java291
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/BaseChromiumRunnerCommon.java162
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/BaseJUnit4ClassRunner.java277
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/BaseTestResult.java137
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/ScreenshotOnFailureStatement.java83
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/SetUpStatement.java35
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/SetUpTestRule.java35
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/TestChildProcessConnection.java87
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/TestListInstrumentationRunListener.java142
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/TestTraceEvent.java168
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/params/BaseJUnit4RunnerDelegate.java42
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/params/BlockJUnit4RunnerDelegate.java42
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/params/MethodParamAnnotationRule.java62
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/params/MethodParamRule.java35
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/params/ParameterAnnotations.java78
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/params/ParameterSet.java129
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedFrameworkMethod.java94
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunner.java221
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunnerDelegate.java36
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunnerDelegateCommon.java69
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunnerDelegateFactory.java115
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/AdvancedMockContext.java118
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/CallbackHelper.java252
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/CommandLineFlags.java188
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/DisableIf.java49
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/DisableIfSkipCheck.java84
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/DisabledTest.java22
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/EnormousTest.java24
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/Feature.java29
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/FlakyTest.java22
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/InMemorySharedPreferences.java238
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/InstrumentationUtils.java32
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/IntegrationTest.java26
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/Manual.java21
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/Matchers.java44
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/MetricsUtils.java43
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/MinAndroidSdkLevel.java19
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/MinAndroidSdkLevelSkipCheck.java43
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/Restriction.java37
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/RestrictionSkipCheck.java78
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/RetryOnFailure.java25
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/ScalableTimeout.java29
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/SkipCheck.java49
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/TestFileUtil.java85
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/TestThread.java143
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/TimeoutScale.java22
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/UrlUtils.java84
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/UserActionTester.java51
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/parameter/CommandLineParameter.java32
-rw-r--r--base/test/android/javatests/src/org/chromium/base/test/util/parameter/SkipCommandLineParameterization.java23
-rw-r--r--base/test/android/junit/src/org/chromium/base/test/BaseRobolectricTestRunner.java49
-rw-r--r--base/test/android/junit/src/org/chromium/base/test/SetUpStatementTest.java64
-rw-r--r--base/test/android/junit/src/org/chromium/base/test/TestListInstrumentationRunListenerTest.java119
-rw-r--r--base/test/android/junit/src/org/chromium/base/test/asynctask/BackgroundShadowAsyncTask.java72
-rw-r--r--base/test/android/junit/src/org/chromium/base/test/asynctask/CustomShadowAsyncTask.java32
-rw-r--r--base/test/android/junit/src/org/chromium/base/test/params/ExampleParameterizedTest.java105
-rw-r--r--base/test/android/junit/src/org/chromium/base/test/params/ParameterizedRunnerDelegateCommonTest.java77
-rw-r--r--base/test/android/junit/src/org/chromium/base/test/params/ParameterizedRunnerDelegateFactoryTest.java133
-rw-r--r--base/test/android/junit/src/org/chromium/base/test/params/ParameterizedRunnerTest.java108
-rw-r--r--base/test/android/junit/src/org/chromium/base/test/params/ParameterizedTestNameTest.java201
-rw-r--r--base/test/android/junit/src/org/chromium/base/test/util/DisableIfTest.java193
-rw-r--r--base/test/android/junit/src/org/chromium/base/test/util/MinAndroidSdkLevelSkipCheckTest.java95
-rw-r--r--base/test/android/junit/src/org/chromium/base/test/util/RestrictionSkipCheckTest.java129
-rw-r--r--base/test/android/junit/src/org/chromium/base/test/util/SkipCheckTest.java130
-rw-r--r--base/test/android/junit/src/org/chromium/base/test/util/TestRunnerTestRule.java132
79 files changed, 6682 insertions, 0 deletions
diff --git a/base/test/android/java/src/org/chromium/base/ContentUriTestUtils.java b/base/test/android/java/src/org/chromium/base/ContentUriTestUtils.java
new file mode 100644
index 0000000000..fe9d5403de
--- /dev/null
+++ b/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/base/test/android/java/src/org/chromium/base/ITestCallback.aidl b/base/test/android/java/src/org/chromium/base/ITestCallback.aidl
new file mode 100644
index 0000000000..dd208d55da
--- /dev/null
+++ b/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/base/test/android/java/src/org/chromium/base/ITestController.aidl b/base/test/android/java/src/org/chromium/base/ITestController.aidl
new file mode 100644
index 0000000000..d927ee5c87
--- /dev/null
+++ b/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/base/test/android/java/src/org/chromium/base/JavaHandlerThreadHelpers.java b/base/test/android/java/src/org/chromium/base/JavaHandlerThreadHelpers.java
new file mode 100644
index 0000000000..3985e6a893
--- /dev/null
+++ b/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/base/test/android/java/src/org/chromium/base/MainReturnCodeResult.java b/base/test/android/java/src/org/chromium/base/MainReturnCodeResult.java
new file mode 100644
index 0000000000..9756c97602
--- /dev/null
+++ b/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/base/test/android/java/src/org/chromium/base/MultiprocessTestClientLauncher.java b/base/test/android/java/src/org/chromium/base/MultiprocessTestClientLauncher.java
new file mode 100644
index 0000000000..d0b1850bfc
--- /dev/null
+++ b/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 = null;
+ 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/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService.java b/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService.java
new file mode 100644
index 0000000000..9b500018bd
--- /dev/null
+++ b/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/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService0.java b/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService0.java
new file mode 100644
index 0000000000..6bdd867e12
--- /dev/null
+++ b/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/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService1.java b/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService1.java
new file mode 100644
index 0000000000..69827f0e8b
--- /dev/null
+++ b/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/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService2.java b/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService2.java
new file mode 100644
index 0000000000..aad11f1c23
--- /dev/null
+++ b/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/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService3.java b/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService3.java
new file mode 100644
index 0000000000..20d2561b93
--- /dev/null
+++ b/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/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService4.java b/base/test/android/java/src/org/chromium/base/MultiprocessTestClientService4.java
new file mode 100644
index 0000000000..4b14551dc8
--- /dev/null
+++ b/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/base/test/android/java/src/org/chromium/base/MultiprocessTestClientServiceDelegate.java b/base/test/android/java/src/org/chromium/base/MultiprocessTestClientServiceDelegate.java
new file mode 100644
index 0000000000..8a63fe8acb
--- /dev/null
+++ b/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/base/test/android/java/src/org/chromium/base/TestUiThread.java b/base/test/android/java/src/org/chromium/base/TestUiThread.java
new file mode 100644
index 0000000000..237c0ec64b
--- /dev/null
+++ b/base/test/android/java/src/org/chromium/base/TestUiThread.java
@@ -0,0 +1,51 @@
+// 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;
+
+import android.os.Looper;
+
+import org.chromium.base.annotations.CalledByNative;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Set up a thread as the Chromium UI Thread, and run its looper. This is is intended for C++ unit
+ * tests (e.g. the net unit tests) that don't run with the UI thread as their main looper, but test
+ * code that, on Android, uses UI thread events, so need a running UI thread.
+ */
+@ThreadSafe
+public class TestUiThread {
+ private static final AtomicBoolean sStarted = new AtomicBoolean(false);
+ private static final String TAG = "cr.TestUiThread";
+
+ @CalledByNative
+ private static void loop() {
+ // @{link ThreadUtils#setUiThread(Looper)} can only be called once in a test run, so do this
+ // once, and leave it running.
+ if (sStarted.getAndSet(true)) return;
+
+ final CountDownLatch startLatch = new CountDownLatch(1);
+ new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ Looper.prepare();
+ ThreadUtils.setUiThread(Looper.myLooper());
+ startLatch.countDown();
+ Looper.loop();
+ }
+
+ }).start();
+
+ try {
+ startLatch.await();
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Failed to set UI Thread");
+ }
+ }
+}
diff --git a/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumAndroidJUnitRunner.java b/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumAndroidJUnitRunner.java
new file mode 100644
index 0000000000..1476e9ef4a
--- /dev/null
+++ b/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumAndroidJUnitRunner.java
@@ -0,0 +1,291 @@
+// 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) {
+ RunnerArgs runnerArgs =
+ new RunnerArgs.Builder().fromManifest(this).fromBundle(arguments).build();
+ TestRequestBuilder builder = new IncrementalInstallTestRequestBuilder(this, arguments);
+ builder.addFromRunnerArgs(runnerArgs);
+ builder.addApkToScan(getContext().getPackageCodePath());
+ 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.
+ */
+ private static class IncrementalInstallTestRequestBuilder extends TestRequestBuilder {
+ List<String> mExcludedPrefixes = new ArrayList<String>();
+ boolean mHasClassList;
+
+ public IncrementalInstallTestRequestBuilder(Instrumentation instr, Bundle bundle) {
+ super(instr, bundle);
+ }
+
+ @Override
+ public TestRequestBuilder addFromRunnerArgs(RunnerArgs runnerArgs) {
+ mExcludedPrefixes.addAll(runnerArgs.notTestPackages);
+ 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() {
+ // See crbug://841695. TestLoader.isTestClass is incorrectly deciding that
+ // InstrumentationTestSuite is a test class.
+ removeTestClass("android.test.InstrumentationTestSuite");
+ // If a test class was requested, then no need to iterate class loader.
+ if (mHasClassList) {
+ return super.build();
+ }
+ maybeScanIncrementalClasspath();
+ return super.build();
+ }
+
+ private void maybeScanIncrementalClasspath() {
+ 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.
+ }
+ if (incrementalJars != null) {
+ // 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.
+ addTestClasses(incrementalJars, this);
+ }
+ }
+
+ private boolean startsWithAny(String str, List<String> prefixes) {
+ for (String prefix : prefixes) {
+ if (str.startsWith(prefix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void addTestClasses(DexFile[] dexFiles, TestRequestBuilder builder) {
+ Log.i(TAG, "Scanning incremental classpath.");
+ 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);
+ }
+
+ // Mirror TestRequestBuilder.getClassNamesFromClassPath().
+ TestLoader loader = new TestLoader();
+ for (DexFile dexFile : dexFiles) {
+ 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/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumRunnerCommon.java b/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumRunnerCommon.java
new file mode 100644
index 0000000000..e5eb2731b7
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/BaseJUnit4ClassRunner.java b/base/test/android/javatests/src/org/chromium/base/test/BaseJUnit4ClassRunner.java
new file mode 100644
index 0000000000..49f27b5089
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/BaseTestResult.java b/base/test/android/javatests/src/org/chromium/base/test/BaseTestResult.java
new file mode 100644
index 0000000000..a80e0cc4a0
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/ScreenshotOnFailureStatement.java b/base/test/android/javatests/src/org/chromium/base/test/ScreenshotOnFailureStatement.java
new file mode 100644
index 0000000000..397e8abf13
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/SetUpStatement.java b/base/test/android/javatests/src/org/chromium/base/test/SetUpStatement.java
new file mode 100644
index 0000000000..30ac2b6c5c
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/SetUpTestRule.java b/base/test/android/javatests/src/org/chromium/base/test/SetUpTestRule.java
new file mode 100644
index 0000000000..57dd8db552
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/TestChildProcessConnection.java b/base/test/android/javatests/src/org/chromium/base/test/TestChildProcessConnection.java
new file mode 100644
index 0000000000..ae91b44cf3
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/TestListInstrumentationRunListener.java b/base/test/android/javatests/src/org/chromium/base/test/TestListInstrumentationRunListener.java
new file mode 100644
index 0000000000..8cde57003c
--- /dev/null
+++ b/base/test/android/javatests/src/org/chromium/base/test/TestListInstrumentationRunListener.java
@@ -0,0 +1,142 @@
+// 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.Method;
+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 {
+ elementJsonObject.put(method.getName(),
+ value.getClass().isArray()
+ ? new JSONArray(Arrays.asList((Object[]) value))
+ : value.toString());
+ }
+ } catch (IllegalArgumentException e) {
+ }
+ }
+ annotationsJsons.put(a.annotationType().getSimpleName(), elementJsonObject);
+ }
+ return annotationsJsons;
+ }
+}
diff --git a/base/test/android/javatests/src/org/chromium/base/test/TestTraceEvent.java b/base/test/android/javatests/src/org/chromium/base/test/TestTraceEvent.java
new file mode 100644
index 0000000000..5e0f6b31f1
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/params/BaseJUnit4RunnerDelegate.java b/base/test/android/javatests/src/org/chromium/base/test/params/BaseJUnit4RunnerDelegate.java
new file mode 100644
index 0000000000..c0dcd469d2
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/params/BlockJUnit4RunnerDelegate.java b/base/test/android/javatests/src/org/chromium/base/test/params/BlockJUnit4RunnerDelegate.java
new file mode 100644
index 0000000000..7c948bb141
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/params/MethodParamAnnotationRule.java b/base/test/android/javatests/src/org/chromium/base/test/params/MethodParamAnnotationRule.java
new file mode 100644
index 0000000000..2986b96c08
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/params/MethodParamRule.java b/base/test/android/javatests/src/org/chromium/base/test/params/MethodParamRule.java
new file mode 100644
index 0000000000..440831af2f
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/params/ParameterAnnotations.java b/base/test/android/javatests/src/org/chromium/base/test/params/ParameterAnnotations.java
new file mode 100644
index 0000000000..79183693ec
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/params/ParameterSet.java b/base/test/android/javatests/src/org/chromium/base/test/params/ParameterSet.java
new file mode 100644
index 0000000000..1cdb576b05
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedFrameworkMethod.java b/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedFrameworkMethod.java
new file mode 100644
index 0000000000..f3333b5720
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunner.java b/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunner.java
new file mode 100644
index 0000000000..834f26139f
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunnerDelegate.java b/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunnerDelegate.java
new file mode 100644
index 0000000000..d3698a95b4
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunnerDelegateCommon.java b/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunnerDelegateCommon.java
new file mode 100644
index 0000000000..f25e2b2ab9
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunnerDelegateFactory.java b/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunnerDelegateFactory.java
new file mode 100644
index 0000000000..f829981c77
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/AdvancedMockContext.java b/base/test/android/javatests/src/org/chromium/base/test/util/AdvancedMockContext.java
new file mode 100644
index 0000000000..c8117f7fad
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/CallbackHelper.java b/base/test/android/javatests/src/org/chromium/base/test/util/CallbackHelper.java
new file mode 100644
index 0000000000..bf064c4fce
--- /dev/null
+++ b/base/test/android/javatests/src/org/chromium/base/test/util/CallbackHelper.java
@@ -0,0 +1,252 @@
+// 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);
+ }
+
+ /**
+ * 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/base/test/android/javatests/src/org/chromium/base/test/util/CommandLineFlags.java b/base/test/android/javatests/src/org/chromium/base/test/util/CommandLineFlags.java
new file mode 100644
index 0000000000..71ef8e91ff
--- /dev/null
+++ b/base/test/android/javatests/src/org/chromium/base/test/util/CommandLineFlags.java
@@ -0,0 +1,188 @@
+// 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.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>();
+ Set<String> disableFeatures = new HashSet<String>();
+ 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 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/base/test/android/javatests/src/org/chromium/base/test/util/DisableIf.java b/base/test/android/javatests/src/org/chromium/base/test/util/DisableIf.java
new file mode 100644
index 0000000000..c0303b68d4
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/DisableIfSkipCheck.java b/base/test/android/javatests/src/org/chromium/base/test/util/DisableIfSkipCheck.java
new file mode 100644
index 0000000000..e46b9799a6
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/DisabledTest.java b/base/test/android/javatests/src/org/chromium/base/test/util/DisabledTest.java
new file mode 100644
index 0000000000..a3e4e8ee7f
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/EnormousTest.java b/base/test/android/javatests/src/org/chromium/base/test/util/EnormousTest.java
new file mode 100644
index 0000000000..af483ec3f9
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/Feature.java b/base/test/android/javatests/src/org/chromium/base/test/util/Feature.java
new file mode 100644
index 0000000000..1bc9226441
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/FlakyTest.java b/base/test/android/javatests/src/org/chromium/base/test/util/FlakyTest.java
new file mode 100644
index 0000000000..83f8e9f43d
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/InMemorySharedPreferences.java b/base/test/android/javatests/src/org/chromium/base/test/util/InMemorySharedPreferences.java
new file mode 100644
index 0000000000..2587d724a5
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/InstrumentationUtils.java b/base/test/android/javatests/src/org/chromium/base/test/util/InstrumentationUtils.java
new file mode 100644
index 0000000000..20cfd9d620
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/IntegrationTest.java b/base/test/android/javatests/src/org/chromium/base/test/util/IntegrationTest.java
new file mode 100644
index 0000000000..8b6550d62d
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/Manual.java b/base/test/android/javatests/src/org/chromium/base/test/util/Manual.java
new file mode 100644
index 0000000000..31f3977bef
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/Matchers.java b/base/test/android/javatests/src/org/chromium/base/test/util/Matchers.java
new file mode 100644
index 0000000000..fc9d68907b
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/MetricsUtils.java b/base/test/android/javatests/src/org/chromium/base/test/util/MetricsUtils.java
new file mode 100644
index 0000000000..c4664d68b0
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/MinAndroidSdkLevel.java b/base/test/android/javatests/src/org/chromium/base/test/util/MinAndroidSdkLevel.java
new file mode 100644
index 0000000000..13e25786a7
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/MinAndroidSdkLevelSkipCheck.java b/base/test/android/javatests/src/org/chromium/base/test/util/MinAndroidSdkLevelSkipCheck.java
new file mode 100644
index 0000000000..8b07c0f19b
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/Restriction.java b/base/test/android/javatests/src/org/chromium/base/test/util/Restriction.java
new file mode 100644
index 0000000000..f39bfbd783
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/RestrictionSkipCheck.java b/base/test/android/javatests/src/org/chromium/base/test/util/RestrictionSkipCheck.java
new file mode 100644
index 0000000000..a27dd1fe08
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/RetryOnFailure.java b/base/test/android/javatests/src/org/chromium/base/test/util/RetryOnFailure.java
new file mode 100644
index 0000000000..eb98008d00
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/ScalableTimeout.java b/base/test/android/javatests/src/org/chromium/base/test/util/ScalableTimeout.java
new file mode 100644
index 0000000000..7a815c09a4
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/SkipCheck.java b/base/test/android/javatests/src/org/chromium/base/test/util/SkipCheck.java
new file mode 100644
index 0000000000..d1dd7be17f
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/TestFileUtil.java b/base/test/android/javatests/src/org/chromium/base/test/util/TestFileUtil.java
new file mode 100644
index 0000000000..6d891210fc
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/TestThread.java b/base/test/android/javatests/src/org/chromium/base/test/util/TestThread.java
new file mode 100644
index 0000000000..4f6296924c
--- /dev/null
+++ b/base/test/android/javatests/src/org/chromium/base/test/util/TestThread.java
@@ -0,0 +1,143 @@
+// 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.os.Handler;
+import android.os.Looper;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * This class is usefull when writing instrumentation tests that exercise code that posts tasks
+ * (to the same thread).
+ * Since the test code is run in a single thread, the posted tasks are never executed.
+ * The TestThread class lets you run that code on a specific thread synchronously and flush the
+ * message loop on that thread.
+ *
+ * Example of test using this:
+ *
+ * public void testMyAwesomeClass() {
+ * TestThread testThread = new TestThread();
+ * testThread.startAndWaitForReadyState();
+ *
+ * testThread.runOnTestThreadSyncAndProcessPendingTasks(new Runnable() {
+ * @Override
+ * public void run() {
+ * MyAwesomeClass.doStuffAsync();
+ * }
+ * });
+ * // Once we get there we know doStuffAsync has been executed and all the tasks it posted.
+ * assertTrue(MyAwesomeClass.stuffWasDone());
+ * }
+ *
+ * Notes:
+ * - this is only for tasks posted to the same thread. Anyway if you were posting to a different
+ * thread, you'd probably need to set that other thread up.
+ * - this only supports tasks posted using Handler.post(), it won't work with postDelayed and
+ * postAtTime.
+ * - if your test instanciates an object and that object is the one doing the posting of tasks, you
+ * probably want to instanciate it on the test thread as it might create the Handler it posts
+ * tasks to in the constructor.
+ */
+
+public class TestThread extends Thread {
+ private final Object mThreadReadyLock;
+ private AtomicBoolean mThreadReady;
+ private Handler mMainThreadHandler;
+ private Handler mTestThreadHandler;
+
+ public TestThread() {
+ mMainThreadHandler = new Handler();
+ // We can't use the AtomicBoolean as the lock or findbugs will freak out...
+ mThreadReadyLock = new Object();
+ mThreadReady = new AtomicBoolean();
+ }
+
+ @Override
+ public void run() {
+ Looper.prepare();
+ mTestThreadHandler = new Handler();
+ mTestThreadHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mThreadReadyLock) {
+ mThreadReady.set(true);
+ mThreadReadyLock.notify();
+ }
+ }
+ });
+ Looper.loop();
+ }
+
+ /**
+ * Starts this TestThread and blocks until it's ready to accept calls.
+ */
+ public void startAndWaitForReadyState() {
+ checkOnMainThread();
+ start();
+ synchronized (mThreadReadyLock) {
+ try {
+ // Note the mThreadReady and while are not really needed.
+ // There are there so findbugs don't report warnings.
+ while (!mThreadReady.get()) {
+ mThreadReadyLock.wait();
+ }
+ } catch (InterruptedException ie) {
+ System.err.println("Error starting TestThread.");
+ ie.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * Runs the passed Runnable synchronously on the TestThread and returns when all pending
+ * runnables have been excuted.
+ * Should be called from the main thread.
+ */
+ public void runOnTestThreadSyncAndProcessPendingTasks(Runnable r) {
+ checkOnMainThread();
+
+ runOnTestThreadSync(r);
+
+ // Run another task, when it's done it means all pendings tasks have executed.
+ runOnTestThreadSync(null);
+ }
+
+ /**
+ * Runs the passed Runnable on the test thread and blocks until it has finished executing.
+ * Should be called from the main thread.
+ * @param r The runnable to be executed.
+ */
+ public void runOnTestThreadSync(final Runnable r) {
+ checkOnMainThread();
+ final Object lock = new Object();
+ // Task executed is not really needed since we are only on one thread, it is here to appease
+ // findbugs.
+ final AtomicBoolean taskExecuted = new AtomicBoolean();
+ mTestThreadHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (r != null) r.run();
+ synchronized (lock) {
+ taskExecuted.set(true);
+ lock.notify();
+ }
+ }
+ });
+ synchronized (lock) {
+ try {
+ while (!taskExecuted.get()) {
+ lock.wait();
+ }
+ } catch (InterruptedException ie) {
+ ie.printStackTrace();
+ }
+ }
+ }
+
+ private void checkOnMainThread() {
+ assert Looper.myLooper() == mMainThreadHandler.getLooper();
+ }
+}
diff --git a/base/test/android/javatests/src/org/chromium/base/test/util/TimeoutScale.java b/base/test/android/javatests/src/org/chromium/base/test/util/TimeoutScale.java
new file mode 100644
index 0000000000..5aee05e73a
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/UrlUtils.java b/base/test/android/javatests/src/org/chromium/base/test/util/UrlUtils.java
new file mode 100644
index 0000000000..9ca3fcc33c
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/UserActionTester.java b/base/test/android/javatests/src/org/chromium/base/test/util/UserActionTester.java
new file mode 100644
index 0000000000..88e3551131
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/parameter/CommandLineParameter.java b/base/test/android/javatests/src/org/chromium/base/test/util/parameter/CommandLineParameter.java
new file mode 100644
index 0000000000..e6f5506899
--- /dev/null
+++ b/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/base/test/android/javatests/src/org/chromium/base/test/util/parameter/SkipCommandLineParameterization.java b/base/test/android/javatests/src/org/chromium/base/test/util/parameter/SkipCommandLineParameterization.java
new file mode 100644
index 0000000000..2181031617
--- /dev/null
+++ b/base/test/android/javatests/src/org/chromium/base/test/util/parameter/SkipCommandLineParameterization.java
@@ -0,0 +1,23 @@
+// 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.
+ *
+ * This usually used by test that only runs in WebView javatests that only runs in sandboxed mode
+ * or single process mode.
+ */
+
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface SkipCommandLineParameterization {}
diff --git a/base/test/android/junit/src/org/chromium/base/test/BaseRobolectricTestRunner.java b/base/test/android/junit/src/org/chromium/base/test/BaseRobolectricTestRunner.java
new file mode 100644
index 0000000000..3ca756ad9a
--- /dev/null
+++ b/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/base/test/android/junit/src/org/chromium/base/test/SetUpStatementTest.java b/base/test/android/junit/src/org/chromium/base/test/SetUpStatementTest.java
new file mode 100644
index 0000000000..722bd1acfe
--- /dev/null
+++ b/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/base/test/android/junit/src/org/chromium/base/test/TestListInstrumentationRunListenerTest.java b/base/test/android/junit/src/org/chromium/base/test/TestListInstrumentationRunListenerTest.java
new file mode 100644
index 0000000000..63fa5601fb
--- /dev/null
+++ b/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/base/test/android/junit/src/org/chromium/base/test/asynctask/BackgroundShadowAsyncTask.java b/base/test/android/junit/src/org/chromium/base/test/asynctask/BackgroundShadowAsyncTask.java
new file mode 100644
index 0000000000..c75e7948f0
--- /dev/null
+++ b/base/test/android/junit/src/org/chromium/base/test/asynctask/BackgroundShadowAsyncTask.java
@@ -0,0 +1,72 @@
+// 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.asynctask;
+
+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.AsyncTask;
+
+import java.util.concurrent.Callable;
+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 <Params> type for execute function parameters
+ * @param <Progress> type for reporting Progress
+ * @param <Result> type for reporting result
+ */
+@Implements(AsyncTask.class)
+public class BackgroundShadowAsyncTask<Params, Progress, Result>
+ extends ShadowAsyncTask<Params, Progress, Result> {
+ private static final ExecutorService sExecutorService = Executors.newSingleThreadExecutor();
+
+ @Override
+ @Implementation
+ @SafeVarargs
+ public final AsyncTask<Params, Progress, Result> execute(final Params... params) {
+ try {
+ return sExecutorService
+ .submit(new Callable<AsyncTask<Params, Progress, Result>>() {
+ @Override
+ public AsyncTask<Params, Progress, Result> call() throws Exception {
+ return BackgroundShadowAsyncTask.super.execute(params);
+ }
+ })
+ .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/base/test/android/junit/src/org/chromium/base/test/asynctask/CustomShadowAsyncTask.java b/base/test/android/junit/src/org/chromium/base/test/asynctask/CustomShadowAsyncTask.java
new file mode 100644
index 0000000000..bd581c1377
--- /dev/null
+++ b/base/test/android/junit/src/org/chromium/base/test/asynctask/CustomShadowAsyncTask.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.asynctask;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+import org.chromium.base.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 <Params>
+ * @param <Progress>
+ * @param <Result>
+ */
+@Implements(AsyncTask.class)
+public class CustomShadowAsyncTask<Params, Progress, Result>
+ extends ShadowAsyncTask<Params, Progress, Result> {
+ @SafeVarargs
+ @Override
+ @Implementation
+ public final AsyncTask<Params, Progress, Result> executeOnExecutor(
+ Executor executor, Params... params) {
+ return super.execute(params);
+ }
+}
diff --git a/base/test/android/junit/src/org/chromium/base/test/params/ExampleParameterizedTest.java b/base/test/android/junit/src/org/chromium/base/test/params/ExampleParameterizedTest.java
new file mode 100644
index 0000000000..6ffccad44b
--- /dev/null
+++ b/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/base/test/android/junit/src/org/chromium/base/test/params/ParameterizedRunnerDelegateCommonTest.java b/base/test/android/junit/src/org/chromium/base/test/params/ParameterizedRunnerDelegateCommonTest.java
new file mode 100644
index 0000000000..6d854c57e6
--- /dev/null
+++ b/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/base/test/android/junit/src/org/chromium/base/test/params/ParameterizedRunnerDelegateFactoryTest.java b/base/test/android/junit/src/org/chromium/base/test/params/ParameterizedRunnerDelegateFactoryTest.java
new file mode 100644
index 0000000000..723382d71d
--- /dev/null
+++ b/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/base/test/android/junit/src/org/chromium/base/test/params/ParameterizedRunnerTest.java b/base/test/android/junit/src/org/chromium/base/test/params/ParameterizedRunnerTest.java
new file mode 100644
index 0000000000..170ff69eaf
--- /dev/null
+++ b/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/base/test/android/junit/src/org/chromium/base/test/params/ParameterizedTestNameTest.java b/base/test/android/junit/src/org/chromium/base/test/params/ParameterizedTestNameTest.java
new file mode 100644
index 0000000000..e79f5c5304
--- /dev/null
+++ b/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/base/test/android/junit/src/org/chromium/base/test/util/DisableIfTest.java b/base/test/android/junit/src/org/chromium/base/test/util/DisableIfTest.java
new file mode 100644
index 0000000000..a147435355
--- /dev/null
+++ b/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/base/test/android/junit/src/org/chromium/base/test/util/MinAndroidSdkLevelSkipCheckTest.java b/base/test/android/junit/src/org/chromium/base/test/util/MinAndroidSdkLevelSkipCheckTest.java
new file mode 100644
index 0000000000..2236646938
--- /dev/null
+++ b/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/base/test/android/junit/src/org/chromium/base/test/util/RestrictionSkipCheckTest.java b/base/test/android/junit/src/org/chromium/base/test/util/RestrictionSkipCheckTest.java
new file mode 100644
index 0000000000..86285de3f0
--- /dev/null
+++ b/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/base/test/android/junit/src/org/chromium/base/test/util/SkipCheckTest.java b/base/test/android/junit/src/org/chromium/base/test/util/SkipCheckTest.java
new file mode 100644
index 0000000000..51c7516e71
--- /dev/null
+++ b/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/base/test/android/junit/src/org/chromium/base/test/util/TestRunnerTestRule.java b/base/test/android/junit/src/org/chromium/base/test/util/TestRunnerTestRule.java
new file mode 100644
index 0000000000..64805c124e
--- /dev/null
+++ b/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;
+ }
+}