diff options
Diffstat (limited to 'base/test/android/javatests/src/org/chromium/base/test/util')
29 files changed, 1912 insertions, 0 deletions
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 {} |