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.BaseTestResult.PreTestHook; 15 16 import java.lang.annotation.ElementType; 17 import java.lang.annotation.Inherited; 18 import java.lang.annotation.Retention; 19 import java.lang.annotation.RetentionPolicy; 20 import java.lang.annotation.Target; 21 import java.lang.reflect.AnnotatedElement; 22 import java.lang.reflect.Field; 23 import java.lang.reflect.Method; 24 import java.util.Arrays; 25 import java.util.Collections; 26 import java.util.HashSet; 27 import java.util.List; 28 import java.util.Set; 29 30 /** 31 * Provides annotations related to command-line flag handling. 32 * 33 * Uses of these annotations on a derived class will take precedence over uses on its base classes, 34 * so a derived class can add a command-line flag that a base class has removed (or vice versa). 35 * Similarly, uses of these annotations on a test method will take precedence over uses on the 36 * containing class. 37 * <p> 38 * These annonations may also be used on Junit4 Rule classes and on their base classes. Note, 39 * however that the annotation processor only looks at the declared type of the Rule, not its actual 40 * type, so in, for example: 41 * 42 * <pre> 43 * @Rule 44 * TestRule mRule = new ChromeActivityTestRule(); 45 * </pre> 46 * 47 * will only look for CommandLineFlags annotations on TestRule, not for CommandLineFlags annotations 48 * on ChromeActivityTestRule. 49 * <p> 50 * In addition a rule may not remove flags added by an independently invoked rule, although it may 51 * remove flags added by its base classes. 52 * <p> 53 * Uses of these annotations on the test class or methods take precedence over uses on Rule classes. 54 * <p> 55 * Note that this class should never be instantiated. 56 */ 57 public final class CommandLineFlags { 58 private static final String DISABLE_FEATURES = "disable-features"; 59 private static final String ENABLE_FEATURES = "enable-features"; 60 61 /** 62 * Adds command-line flags to the {@link org.chromium.base.CommandLine} for this test. 63 */ 64 @Inherited 65 @Retention(RetentionPolicy.RUNTIME) 66 @Target({ElementType.METHOD, ElementType.TYPE}) 67 public @interface Add { value()68 String[] value(); 69 } 70 71 /** 72 * Removes command-line flags from the {@link org.chromium.base.CommandLine} from this test. 73 * 74 * Note that this can only remove flags added via {@link Add} above. 75 */ 76 @Inherited 77 @Retention(RetentionPolicy.RUNTIME) 78 @Target({ElementType.METHOD, ElementType.TYPE}) 79 public @interface Remove { value()80 String[] value(); 81 } 82 83 /** 84 * Sets up the CommandLine with the appropriate flags. 85 * 86 * This will add the difference of the sets of flags specified by {@link CommandLineFlags.Add} 87 * and {@link CommandLineFlags.Remove} to the {@link org.chromium.base.CommandLine}. Note that 88 * trying to remove a flag set externally, i.e. by the command-line flags file, will not work. 89 */ setUp(AnnotatedElement element)90 public static void setUp(AnnotatedElement element) { 91 CommandLine.reset(); 92 CommandLineInitUtil.initCommandLine(getTestCmdLineFile()); 93 Set<String> enableFeatures = new HashSet<String>(); 94 Set<String> disableFeatures = new HashSet<String>(); 95 Set<String> flags = getFlags(element); 96 for (String flag : flags) { 97 String[] parsedFlags = flag.split("=", 2); 98 if (parsedFlags.length == 1) { 99 CommandLine.getInstance().appendSwitch(flag); 100 } else if (ENABLE_FEATURES.equals(parsedFlags[0])) { 101 // We collect enable/disable features flags separately and aggregate them because 102 // they may be specified multiple times, in which case the values will trample each 103 // other. 104 Collections.addAll(enableFeatures, parsedFlags[1].split(",")); 105 } else if (DISABLE_FEATURES.equals(parsedFlags[0])) { 106 Collections.addAll(disableFeatures, parsedFlags[1].split(",")); 107 } else { 108 CommandLine.getInstance().appendSwitchWithValue(parsedFlags[0], parsedFlags[1]); 109 } 110 } 111 112 if (enableFeatures.size() > 0) { 113 CommandLine.getInstance().appendSwitchWithValue( 114 ENABLE_FEATURES, TextUtils.join(",", enableFeatures)); 115 } 116 if (disableFeatures.size() > 0) { 117 CommandLine.getInstance().appendSwitchWithValue( 118 DISABLE_FEATURES, TextUtils.join(",", disableFeatures)); 119 } 120 } 121 getFlags(AnnotatedElement type)122 private static Set<String> getFlags(AnnotatedElement type) { 123 Set<String> rule_flags = new HashSet<>(); 124 updateFlagsForElement(type, rule_flags); 125 return rule_flags; 126 } 127 updateFlagsForElement(AnnotatedElement element, Set<String> flags)128 private static void updateFlagsForElement(AnnotatedElement element, Set<String> flags) { 129 if (element instanceof Class<?>) { 130 // Get flags from rules within the class. 131 for (Field field : ((Class<?>) element).getFields()) { 132 if (field.isAnnotationPresent(Rule.class)) { 133 // The order in which fields are returned is undefined, so, for consistency, 134 // a rule must not remove a flag added by a different rule. Ensure this by 135 // initially getting the flags into a new set. 136 Set<String> rule_flags = getFlags(field.getType()); 137 flags.addAll(rule_flags); 138 } 139 } 140 for (Method method : ((Class<?>) element).getMethods()) { 141 if (method.isAnnotationPresent(Rule.class)) { 142 // The order in which methods are returned is undefined, so, for consistency, 143 // a rule must not remove a flag added by a different rule. Ensure this by 144 // initially getting the flags into a new set. 145 Set<String> rule_flags = getFlags(method.getReturnType()); 146 flags.addAll(rule_flags); 147 } 148 } 149 } 150 151 // Add the flags from the parent. Override any flags defined by the rules. 152 AnnotatedElement parent = (element instanceof Method) 153 ? ((Method) element).getDeclaringClass() 154 : ((Class<?>) element).getSuperclass(); 155 if (parent != null) updateFlagsForElement(parent, flags); 156 157 // Flags on the element itself override all other flag sources. 158 if (element.isAnnotationPresent(CommandLineFlags.Add.class)) { 159 flags.addAll( 160 Arrays.asList(element.getAnnotation(CommandLineFlags.Add.class).value())); 161 } 162 163 if (element.isAnnotationPresent(CommandLineFlags.Remove.class)) { 164 List<String> flagsToRemove = 165 Arrays.asList(element.getAnnotation(CommandLineFlags.Remove.class).value()); 166 for (String flagToRemove : flagsToRemove) { 167 // If your test fails here, you have tried to remove a command-line flag via 168 // CommandLineFlags.Remove that was loaded into CommandLine via something other 169 // than CommandLineFlags.Add (probably the command-line flag file). 170 Assert.assertFalse("Unable to remove command-line flag \"" + flagToRemove + "\".", 171 CommandLine.getInstance().hasSwitch(flagToRemove)); 172 } 173 flags.removeAll(flagsToRemove); 174 } 175 } 176 CommandLineFlags()177 private CommandLineFlags() { 178 throw new AssertionError("CommandLineFlags is a non-instantiable class"); 179 } 180 getRegistrationHook()181 public static PreTestHook getRegistrationHook() { 182 return (targetContext, testMethod) -> CommandLineFlags.setUp(testMethod); 183 } 184 getTestCmdLineFile()185 public static String getTestCmdLineFile() { 186 return "test-cmdline-file"; 187 } 188 } 189