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  *     &#64Rule
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