1 // Copyright 2015 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.base.test.util; 6 7 import android.text.TextUtils; 8 9 import org.junit.Assert; 10 import org.junit.Rule; 11 12 import org.chromium.base.CommandLine; 13 import org.chromium.base.CommandLineInitUtil; 14 import org.chromium.base.test.BaseJUnit4ClassRunner.ClassHook; 15 import org.chromium.base.test.BaseJUnit4ClassRunner.TestHook; 16 17 import java.lang.annotation.ElementType; 18 import java.lang.annotation.Inherited; 19 import java.lang.annotation.Retention; 20 import java.lang.annotation.RetentionPolicy; 21 import java.lang.annotation.Target; 22 import java.lang.reflect.Field; 23 import java.lang.reflect.Method; 24 import java.util.ArrayList; 25 import java.util.Arrays; 26 import java.util.Collections; 27 import java.util.HashMap; 28 import java.util.HashSet; 29 import java.util.List; 30 import java.util.Map; 31 import java.util.Map.Entry; 32 import java.util.Set; 33 34 /** 35 * Provides annotations related to command-line flag handling. 36 * 37 * Uses of these annotations on a derived class will take precedence over uses on its base classes, 38 * so a derived class can add a command-line flag that a base class has removed (or vice versa). 39 * Similarly, uses of these annotations on a test method will take precedence over uses on the 40 * containing class. 41 * <p> 42 * These annonations may also be used on Junit4 Rule classes and on their base classes. Note, 43 * however that the annotation processor only looks at the declared type of the Rule, not its actual 44 * type, so in, for example: 45 * 46 * <pre> 47 * @Rule 48 * TestRule mRule = new ChromeActivityTestRule(); 49 * </pre> 50 * 51 * will only look for CommandLineFlags annotations on TestRule, not for CommandLineFlags annotations 52 * on ChromeActivityTestRule. 53 * <p> 54 * In addition a rule may not remove flags added by an independently invoked rule, although it may 55 * remove flags added by its base classes. 56 * <p> 57 * Uses of these annotations on the test class or methods take precedence over uses on Rule classes. 58 * <p> 59 * Note that this class should never be instantiated. 60 */ 61 public final class CommandLineFlags { 62 private static final String DISABLE_FEATURES = "disable-features"; 63 private static final String ENABLE_FEATURES = "enable-features"; 64 65 private static boolean sInitializedForTest; 66 67 // These members are used to track CommandLine state modifications made by the class/test method 68 // currently being run, to be undone when the class/test method finishes. 69 private static Set<String> sClassFlagsToRemove; 70 private static Map<String, String> sClassFlagsToAdd; 71 private static Set<String> sMethodFlagsToRemove; 72 private static Map<String, String> sMethodFlagsToAdd; 73 74 /** 75 * Adds command-line flags to the {@link org.chromium.base.CommandLine} for this test. 76 */ 77 @Inherited 78 @Retention(RetentionPolicy.RUNTIME) 79 @Target({ElementType.METHOD, ElementType.TYPE}) 80 public @interface Add { value()81 String[] value(); 82 } 83 84 /** 85 * Removes command-line flags from the {@link org.chromium.base.CommandLine} from this test. 86 * 87 * Note that this can only be applied to test methods. This restriction is due to complexities 88 * in resolving the order that annotations are applied, and given how rare it is to need to 89 * remove command line flags, this annotation must be applied directly to each test method 90 * wishing to remove a flag. 91 */ 92 @Inherited 93 @Retention(RetentionPolicy.RUNTIME) 94 @Target({ElementType.METHOD}) 95 public @interface Remove { value()96 String[] value(); 97 } 98 99 /** 100 * Sets up the CommandLine with the appropriate flags. 101 * 102 * This will add the difference of the sets of flags specified by {@link CommandLineFlags.Add} 103 * and {@link CommandLineFlags.Remove} to the {@link org.chromium.base.CommandLine}. Note that 104 * trying to remove a flag set externally, i.e. by the command-line flags file, will not work. 105 */ setUpClass(Class<?> clazz)106 private static void setUpClass(Class<?> clazz) { 107 // The command line may already have been initialized by Application-level init. We need to 108 // re-initialize it with test flags. 109 if (!sInitializedForTest) { 110 CommandLine.reset(); 111 CommandLineInitUtil.initCommandLine(getTestCmdLineFile()); 112 sInitializedForTest = true; 113 } 114 115 Set<String> flags = new HashSet<>(); 116 updateFlagsForClass(clazz, flags); 117 sClassFlagsToRemove = new HashSet<>(); 118 sClassFlagsToAdd = new HashMap<>(); 119 applyFlags(flags, null, sClassFlagsToRemove, sClassFlagsToAdd); 120 } 121 tearDownClass()122 private static void tearDownClass() { 123 restoreFlags(sClassFlagsToRemove, sClassFlagsToAdd); 124 sClassFlagsToRemove = null; 125 sClassFlagsToAdd = null; 126 } 127 setUpMethod(Method method)128 private static void setUpMethod(Method method) { 129 Set<String> flagsToAdd = new HashSet<>(); 130 Set<String> flagsToRemove = new HashSet<>(); 131 updateFlagsForMethod(method, flagsToAdd, flagsToRemove); 132 sMethodFlagsToRemove = new HashSet<>(); 133 sMethodFlagsToAdd = new HashMap<>(); 134 applyFlags(flagsToAdd, flagsToRemove, sMethodFlagsToRemove, sMethodFlagsToAdd); 135 } 136 tearDownMethod()137 private static void tearDownMethod() { 138 restoreFlags(sMethodFlagsToRemove, sMethodFlagsToAdd); 139 sMethodFlagsToRemove = null; 140 sMethodFlagsToAdd = null; 141 } 142 restoreFlags(Set<String> flagsToRemove, Map<String, String> flagsToAdd)143 private static void restoreFlags(Set<String> flagsToRemove, Map<String, String> flagsToAdd) { 144 for (String flag : flagsToRemove) { 145 CommandLine.getInstance().removeSwitch(flag); 146 } 147 for (Entry<String, String> flag : flagsToAdd.entrySet()) { 148 CommandLine.getInstance().appendSwitchWithValue(flag.getKey(), flag.getValue()); 149 } 150 } 151 applyFlags(Set<String> flagsToAdd, Set<String> flagsToRemove, Set<String> flagsToRemoveForRestore, Map<String, String> flagsToAddForRestore)152 private static void applyFlags(Set<String> flagsToAdd, Set<String> flagsToRemove, 153 Set<String> flagsToRemoveForRestore, Map<String, String> flagsToAddForRestore) { 154 Set<String> enableFeatures = new HashSet<String>(getFeatureValues(ENABLE_FEATURES)); 155 Set<String> disableFeatures = new HashSet<String>(getFeatureValues(DISABLE_FEATURES)); 156 for (String flag : flagsToAdd) { 157 String[] parsedFlags = flag.split("=", 2); 158 if (parsedFlags.length == 1) { 159 if (!CommandLine.getInstance().hasSwitch(flag)) { 160 CommandLine.getInstance().appendSwitch(flag); 161 flagsToRemoveForRestore.add(flag); 162 } 163 } else if (ENABLE_FEATURES.equals(parsedFlags[0])) { 164 // We collect enable/disable features flags separately and aggregate them because 165 // they may be specified multiple times, in which case the values will trample each 166 // other. 167 Collections.addAll(enableFeatures, parsedFlags[1].split(",")); 168 } else if (DISABLE_FEATURES.equals(parsedFlags[0])) { 169 Collections.addAll(disableFeatures, parsedFlags[1].split(",")); 170 } else { 171 String existingValue = CommandLine.getInstance().getSwitchValue(parsedFlags[0]); 172 if (parsedFlags[1].equals(existingValue)) continue; 173 if (existingValue != null) { 174 flagsToAddForRestore.put(parsedFlags[0], existingValue); 175 CommandLine.getInstance().removeSwitch(parsedFlags[0]); 176 } 177 CommandLine.getInstance().appendSwitchWithValue(parsedFlags[0], parsedFlags[1]); 178 flagsToRemoveForRestore.add(parsedFlags[0]); 179 } 180 } 181 182 if (enableFeatures.size() > 0) { 183 String existingValue = CommandLine.getInstance().getSwitchValue(ENABLE_FEATURES); 184 flagsToAddForRestore.put(ENABLE_FEATURES, existingValue); 185 CommandLine.getInstance().appendSwitchWithValue( 186 ENABLE_FEATURES, TextUtils.join(",", enableFeatures)); 187 flagsToRemoveForRestore.add(ENABLE_FEATURES); 188 } 189 if (disableFeatures.size() > 0) { 190 String existingValue = CommandLine.getInstance().getSwitchValue(DISABLE_FEATURES); 191 flagsToAddForRestore.put(DISABLE_FEATURES, existingValue); 192 CommandLine.getInstance().appendSwitchWithValue( 193 DISABLE_FEATURES, TextUtils.join(",", disableFeatures)); 194 flagsToRemoveForRestore.add(DISABLE_FEATURES); 195 } 196 if (flagsToRemove == null) return; 197 for (String flag : flagsToRemove) { 198 if (CommandLine.getInstance().hasSwitch(flag)) { 199 CommandLine.getInstance().removeSwitch(flag); 200 flagsToAddForRestore.put(flag, null); 201 } 202 } 203 } 204 updateFlagsForClass(Class<?> clazz, Set<String> flags)205 private static void updateFlagsForClass(Class<?> clazz, Set<String> flags) { 206 // Get flags from rules within the class. 207 for (Field field : clazz.getFields()) { 208 if (field.isAnnotationPresent(Rule.class)) { 209 // The order in which fields are returned is undefined, so, for consistency, 210 // a rule must only ever add flags. 211 updateFlagsForClass(field.getType(), flags); 212 } 213 } 214 for (Method method : clazz.getMethods()) { 215 Assert.assertFalse("@Rule annotations on methods are unsupported. Cause: " 216 + method.toGenericString(), 217 method.isAnnotationPresent(Rule.class)); 218 } 219 220 // Add the flags from the parent. Override any flags defined by the rules. 221 Class<?> parent = clazz.getSuperclass(); 222 if (parent != null) updateFlagsForClass(parent, flags); 223 224 // Flags on the element itself override all other flag sources. 225 if (clazz.isAnnotationPresent(CommandLineFlags.Add.class)) { 226 flags.addAll(Arrays.asList(clazz.getAnnotation(CommandLineFlags.Add.class).value())); 227 } 228 } 229 updateFlagsForMethod( Method method, Set<String> flagsToAdd, Set<String> flagsToRemove)230 private static void updateFlagsForMethod( 231 Method method, Set<String> flagsToAdd, Set<String> flagsToRemove) { 232 if (method.isAnnotationPresent(CommandLineFlags.Add.class)) { 233 flagsToAdd.addAll( 234 Arrays.asList(method.getAnnotation(CommandLineFlags.Add.class).value())); 235 } 236 if (method.isAnnotationPresent(CommandLineFlags.Remove.class)) { 237 flagsToRemove.addAll( 238 Arrays.asList(method.getAnnotation(CommandLineFlags.Remove.class).value())); 239 } 240 } 241 getFeatureValues(String flag)242 private static List<String> getFeatureValues(String flag) { 243 String value = CommandLine.getInstance().getSwitchValue(flag); 244 if (value == null) return new ArrayList<>(); 245 return Arrays.asList(value.split(",")); 246 } 247 CommandLineFlags()248 private CommandLineFlags() { 249 throw new AssertionError("CommandLineFlags is a non-instantiable class"); 250 } 251 getPreTestHook()252 public static TestHook getPreTestHook() { 253 return (targetContext, testMethod) -> CommandLineFlags.setUpMethod(testMethod.getMethod()); 254 } 255 getPreClassHook()256 public static ClassHook getPreClassHook() { 257 return (targetContext, testClass) -> CommandLineFlags.setUpClass(testClass); 258 } 259 getPostTestHook()260 public static TestHook getPostTestHook() { 261 return (targetContext, testMethod) -> CommandLineFlags.tearDownMethod(); 262 } 263 getPostClassHook()264 public static ClassHook getPostClassHook() { 265 return (targetContext, testClass) -> CommandLineFlags.tearDownClass(); 266 } 267 getTestCmdLineFile()268 public static String getTestCmdLineFile() { 269 return "test-cmdline-file"; 270 } 271 } 272