diff options
Diffstat (limited to 'base/test/android')
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 @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> + * @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> + * @Rule + * 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; + } +} |