| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.base.test.util; |
| |
| import android.app.Activity; |
| import android.text.TextUtils; |
| |
| import org.junit.Assert; |
| import org.junit.Rule; |
| import org.junit.rules.TestRule; |
| import org.junit.runner.Description; |
| import org.junit.runners.model.Statement; |
| |
| import org.chromium.base.ActivityState; |
| import org.chromium.base.ApplicationStatus; |
| import org.chromium.base.CommandLine; |
| import org.chromium.base.CommandLineInitUtil; |
| import org.chromium.base.Log; |
| import org.chromium.base.test.BaseJUnit4ClassRunner.ClassHook; |
| import org.chromium.base.test.BaseJUnit4ClassRunner.TestHook; |
| |
| 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.Field; |
| import java.lang.reflect.Method; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| |
| /** |
| * Provides annotations related to command-line flag handling. |
| * |
| * <p>This can be used in either an on-device instrumentation test or a junit (robolectric) test |
| * running on the host. To use in an instrumentation test, just {@code RunWith} {@link |
| * BaseJUnit4ClassRunner} (or a runner which extends that class). To use from a robolectric test, |
| * add the following test rule to your class: |
| * |
| * <pre> |
| * @Rule |
| * TestRule mRule = CommandLineFlags.getTestRule(); |
| * </pre> |
| * |
| * <p>Then you can annotate the test class, test methods, or test rules with {@code |
| * CommandLineFlags.Add} or {@code CommandLineFlags.Remove}. 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 annotations 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 TAG = "CommandLineFlags"; |
| private static final String DISABLE_FEATURES = "disable-features"; |
| private static final String ENABLE_FEATURES = "enable-features"; |
| |
| // These members are used to track CommandLine state modifications made by the class/test method |
| // currently being run, to be undone when the class/test method finishes. |
| private static Set<String> sClassFlagsToRemove; |
| private static Map<String, String> sClassFlagsToAdd; |
| private static Set<String> sMethodFlagsToRemove; |
| private static Map<String, String> sMethodFlagsToAdd; |
| |
| /** 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 be applied to test methods. This restriction is due to complexities |
| * in resolving the order that annotations are applied, and given how rare it is to need to |
| * remove command line flags, this annotation must be applied directly to each test method |
| * wishing to remove a flag. |
| */ |
| @Inherited |
| @Retention(RetentionPolicy.RUNTIME) |
| @Target({ElementType.METHOD}) |
| 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 setUpClass(Class<?> clazz) { |
| if (!CommandLine.isInitialized()) { |
| CommandLineInitUtil.initCommandLine(getTestCmdLineFile()); |
| } |
| |
| Set<String> flags = new HashSet<>(); |
| updateFlagsForClass(clazz, flags); |
| sClassFlagsToRemove = new HashSet<>(); |
| sClassFlagsToAdd = new HashMap<>(); |
| applyFlags(flags, null, sClassFlagsToRemove, sClassFlagsToAdd); |
| } |
| |
| public static void tearDownClass() { |
| if (ApplicationStatus.isInitialized()) { |
| for (Activity a : ApplicationStatus.getRunningActivities()) { |
| if (ApplicationStatus.getStateForActivity(a) < ActivityState.RESUMED) { |
| Log.w( |
| TAG, |
| "Activity " |
| + a |
| + ", is still starting up while the Command Line flags " |
| + "are being reset. This is a known source of flakiness."); |
| } |
| } |
| } |
| restoreFlags(sClassFlagsToRemove, sClassFlagsToAdd); |
| sClassFlagsToRemove = null; |
| sClassFlagsToAdd = null; |
| } |
| |
| public static void setUpMethod(Method method) { |
| Set<String> flagsToAdd = new HashSet<>(); |
| Set<String> flagsToRemove = new HashSet<>(); |
| updateFlagsForMethod(method, flagsToAdd, flagsToRemove); |
| sMethodFlagsToRemove = new HashSet<>(); |
| sMethodFlagsToAdd = new HashMap<>(); |
| applyFlags(flagsToAdd, flagsToRemove, sMethodFlagsToRemove, sMethodFlagsToAdd); |
| } |
| |
| public static void tearDownMethod() { |
| restoreFlags(sMethodFlagsToRemove, sMethodFlagsToAdd); |
| sMethodFlagsToRemove = null; |
| sMethodFlagsToAdd = null; |
| } |
| |
| private static void restoreFlags(Set<String> flagsToRemove, Map<String, String> flagsToAdd) { |
| for (String flag : flagsToRemove) { |
| CommandLine.getInstance().removeSwitch(flag); |
| } |
| for (Entry<String, String> flag : flagsToAdd.entrySet()) { |
| if (flag.getValue() == null) { |
| CommandLine.getInstance().appendSwitch(flag.getKey()); |
| } else { |
| CommandLine.getInstance().appendSwitchWithValue(flag.getKey(), flag.getValue()); |
| } |
| } |
| } |
| |
| private static void applyFlags( |
| Set<String> flagsToAdd, |
| Set<String> flagsToRemove, |
| Set<String> flagsToRemoveForRestore, |
| Map<String, String> flagsToAddForRestore) { |
| if (flagsToRemove != null) { |
| for (String flag : flagsToRemove) { |
| if (CommandLine.getInstance().hasSwitch(flag)) { |
| String existingValue = CommandLine.getInstance().getSwitchValue(flag); |
| CommandLine.getInstance().removeSwitch(flag); |
| flagsToAddForRestore.put(flag, existingValue); |
| } |
| } |
| } |
| |
| Set<String> enableFeatures = new HashSet<String>(getFeatureValues(ENABLE_FEATURES)); |
| Set<String> disableFeatures = new HashSet<String>(getFeatureValues(DISABLE_FEATURES)); |
| for (String flag : flagsToAdd) { |
| String[] parsedFlags = flag.split("=", 2); |
| if (parsedFlags.length == 1) { |
| if (!CommandLine.getInstance().hasSwitch(flag)) { |
| CommandLine.getInstance().appendSwitch(flag); |
| flagsToRemoveForRestore.add(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 { |
| String existingValue = CommandLine.getInstance().getSwitchValue(parsedFlags[0]); |
| if (parsedFlags[1].equals(existingValue)) continue; |
| if (existingValue != null) { |
| flagsToAddForRestore.put(parsedFlags[0], existingValue); |
| CommandLine.getInstance().removeSwitch(parsedFlags[0]); |
| } |
| CommandLine.getInstance().appendSwitchWithValue(parsedFlags[0], parsedFlags[1]); |
| flagsToRemoveForRestore.add(parsedFlags[0]); |
| } |
| } |
| |
| if (enableFeatures.size() > 0) { |
| String existingValue = CommandLine.getInstance().getSwitchValue(ENABLE_FEATURES); |
| if (existingValue != null) { |
| flagsToAddForRestore.put(ENABLE_FEATURES, existingValue); |
| CommandLine.getInstance().removeSwitch(ENABLE_FEATURES); |
| } |
| CommandLine.getInstance() |
| .appendSwitchWithValue(ENABLE_FEATURES, TextUtils.join(",", enableFeatures)); |
| flagsToRemoveForRestore.add(ENABLE_FEATURES); |
| } |
| if (disableFeatures.size() > 0) { |
| String existingValue = CommandLine.getInstance().getSwitchValue(DISABLE_FEATURES); |
| if (existingValue != null) { |
| flagsToAddForRestore.put(DISABLE_FEATURES, existingValue); |
| CommandLine.getInstance().removeSwitch(DISABLE_FEATURES); |
| } |
| CommandLine.getInstance() |
| .appendSwitchWithValue(DISABLE_FEATURES, TextUtils.join(",", disableFeatures)); |
| flagsToRemoveForRestore.add(DISABLE_FEATURES); |
| } |
| } |
| |
| private static void updateFlagsForClass(Class<?> clazz, Set<String> flags) { |
| // Get flags from rules within the class. |
| for (Field field : clazz.getFields()) { |
| if (field.isAnnotationPresent(Rule.class)) { |
| // The order in which fields are returned is undefined, so, for consistency, |
| // a rule must only ever add flags. |
| updateFlagsForClass(field.getType(), flags); |
| } |
| } |
| for (Method method : clazz.getMethods()) { |
| Assert.assertFalse( |
| "@Rule annotations on methods are unsupported. Cause: " |
| + method.toGenericString(), |
| method.isAnnotationPresent(Rule.class)); |
| } |
| |
| // Add the flags from the parent. Override any flags defined by the rules. |
| Class<?> parent = clazz.getSuperclass(); |
| if (parent != null) updateFlagsForClass(parent, flags); |
| |
| // Flags on the element itself override all other flag sources. |
| if (clazz.isAnnotationPresent(CommandLineFlags.Add.class)) { |
| flags.addAll(Arrays.asList(clazz.getAnnotation(CommandLineFlags.Add.class).value())); |
| } |
| } |
| |
| private static void updateFlagsForMethod( |
| Method method, Set<String> flagsToAdd, Set<String> flagsToRemove) { |
| if (method.isAnnotationPresent(CommandLineFlags.Add.class)) { |
| flagsToAdd.addAll( |
| Arrays.asList(method.getAnnotation(CommandLineFlags.Add.class).value())); |
| } |
| if (method.isAnnotationPresent(CommandLineFlags.Remove.class)) { |
| flagsToRemove.addAll( |
| Arrays.asList(method.getAnnotation(CommandLineFlags.Remove.class).value())); |
| } |
| } |
| |
| private static List<String> getFeatureValues(String flag) { |
| String value = CommandLine.getInstance().getSwitchValue(flag); |
| if (value == null) return new ArrayList<>(); |
| return Arrays.asList(value.split(",")); |
| } |
| |
| private CommandLineFlags() { |
| throw new AssertionError("CommandLineFlags is a non-instantiable class"); |
| } |
| |
| private static class CommandLineFlagsTestRule implements TestRule { |
| @Override |
| public Statement apply(final Statement base, Description description) { |
| return new Statement() { |
| @Override |
| public void evaluate() throws Throwable { |
| try { |
| Class<?> clazz = description.getTestClass(); |
| CommandLineFlags.setUpClass(clazz); |
| CommandLineFlags.setUpMethod(clazz.getMethod(description.getMethodName())); |
| |
| base.evaluate(); |
| } finally { |
| CommandLineFlags.tearDownMethod(); |
| CommandLineFlags.tearDownClass(); |
| } |
| } |
| }; |
| } |
| } |
| |
| public static TestRule getTestRule() { |
| return new CommandLineFlagsTestRule(); |
| } |
| |
| public static TestHook getPreTestHook() { |
| return (targetContext, testMethod) -> CommandLineFlags.setUpMethod(testMethod.getMethod()); |
| } |
| |
| public static ClassHook getPreClassHook() { |
| return (targetContext, testClass) -> CommandLineFlags.setUpClass(testClass); |
| } |
| |
| public static TestHook getPostTestHook() { |
| return (targetContext, testMethod) -> CommandLineFlags.tearDownMethod(); |
| } |
| |
| public static ClassHook getPostClassHook() { |
| return (targetContext, testClass) -> CommandLineFlags.tearDownClass(); |
| } |
| |
| public static String getTestCmdLineFile() { |
| return "test-cmdline-file"; |
| } |
| } |