1 // Copyright 2022 The Chromium Authors 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 androidx.annotation.Nullable; 10 11 import org.chromium.base.CommandLine; 12 import org.chromium.base.FeatureList; 13 14 import java.util.Collections; 15 import java.util.HashMap; 16 import java.util.HashSet; 17 import java.util.Map; 18 import java.util.Set; 19 20 /** 21 * Base class to help with setting Feature flags during tests. Relies on registering the 22 * appropriate {@link Processor} rule on the test class. 23 * 24 * Subclasses should introduce a {@code EnableFeatures} and {@code DisableFeatures} 25 * annotation and register them in classes that extend the {@link BaseJUnitProcessor} or 26 * {@link BaseInstrumentationProcessor} 27 * 28 * See {@link org.chromium.chrome.test.util.browser.Features} for an example of this. 29 * 30 * Subclasses should offer Singleton access to enable and disable features, letting other rules 31 * affect the final configuration before the start of the test. 32 */ 33 public class FeaturesBase { 34 protected static @Nullable FeaturesBase sInstance; 35 protected final Map<String, Boolean> mRegisteredState = new HashMap<>(); 36 37 /** 38 * Explicitly applies features collected so far to the command line. 39 * Note: This is only valid during instrumentation tests. 40 * TODO(dgn): remove once we have the compound test rule is available to enable a deterministic 41 * rule execution order. 42 */ ensureCommandLineIsUpToDate()43 public void ensureCommandLineIsUpToDate() { 44 sInstance.applyForInstrumentation(); 45 } 46 47 /** Collects the provided features to be registered as enabled. */ enable(String... featureNames)48 public void enable(String... featureNames) { 49 // TODO(dgn): assert that it's not being called too late and will be able to be applied. 50 for (String featureName : featureNames) mRegisteredState.put(featureName, true); 51 } 52 53 /** Collects the provided features to be registered as disabled. */ disable(String... featureNames)54 public void disable(String... featureNames) { 55 // TODO(dgn): assert that it's not being called too late and will be able to be applied. 56 for (String featureName : featureNames) mRegisteredState.put(featureName, false); 57 } 58 applyForJUnit()59 protected void applyForJUnit() { 60 FeatureList.setTestFeatures(mRegisteredState); 61 } 62 applyForInstrumentation()63 protected void applyForInstrumentation() { 64 FeatureList.setTestCanUseDefaultsForTesting(); 65 mergeFeatureLists("enable-features", true); 66 mergeFeatureLists("disable-features", false); 67 } 68 69 /** 70 * Feature processor intended to be used in unit tests. The collected feature states would be 71 * applied to {@link FeatureList}'s internal test-only feature map. 72 */ 73 public abstract static class BaseJUnitProcessor extends Processor { BaseJUnitProcessor(Class enabledFeatures, Class disabledFeatures)74 public BaseJUnitProcessor(Class enabledFeatures, Class disabledFeatures) { 75 super(enabledFeatures, disabledFeatures); 76 } 77 78 @Override applyFeatures()79 protected void applyFeatures() { 80 sInstance.applyForJUnit(); 81 } 82 83 @Override after()84 protected void after() { 85 super.after(); 86 sInstance = null; 87 } 88 } 89 90 /** 91 * Feature processor intended to be used in instrumentation tests with native library. The 92 * collected feature states would be applied to {@link CommandLine}. 93 */ 94 public abstract static class BaseInstrumentationProcessor extends Processor { BaseInstrumentationProcessor(Class enableFeatures, Class disableFeatures)95 public BaseInstrumentationProcessor(Class enableFeatures, Class disableFeatures) { 96 super(enableFeatures, disableFeatures); 97 } 98 99 @Override applyFeatures()100 protected void applyFeatures() { 101 sInstance.applyForInstrumentation(); 102 } 103 } 104 105 /** Resets Features-related state that might persist in between tests. */ reset()106 private static void reset() { 107 FeatureList.setTestFeatures(null); 108 FeatureList.resetTestCanUseDefaultsForTesting(); 109 } 110 clearRegisteredState()111 private void clearRegisteredState() { 112 mRegisteredState.clear(); 113 } 114 115 /** 116 * Add this rule to tests to activate the {@link Features} annotations and choose flags 117 * to enable, or get rid of exceptions when the production code tries to check for enabled 118 * features. 119 */ 120 private abstract static class Processor extends AnnotationRule { Processor(Class enableFeatures, Class disableFeatures)121 public Processor(Class enableFeatures, Class disableFeatures) { 122 super(enableFeatures, disableFeatures); 123 } 124 125 @Override before()126 protected void before() { 127 assert sInstance != null 128 : "Classes extending BaseProcessor need to create an instance."; 129 collectFeatures(); 130 applyFeatures(); 131 } 132 133 @Override after()134 protected void after() { 135 reset(); 136 137 // sInstance may already be null if there are nested usages. 138 if (sInstance == null) return; 139 140 sInstance.clearRegisteredState(); 141 } 142 applyFeatures()143 protected abstract void applyFeatures(); 144 collectFeatures()145 protected abstract void collectFeatures(); 146 } 147 148 /** 149 * Updates the reference list of features held by the CommandLine by merging it with the feature 150 * state registered via this utility. 151 * @param switchName Name of the command line switch that is the reference feature state. 152 * @param enabled Whether the feature list being modified is the enabled or disabled one. 153 */ mergeFeatureLists(String switchName, boolean enabled)154 private void mergeFeatureLists(String switchName, boolean enabled) { 155 CommandLine commandLine = CommandLine.getInstance(); 156 String switchValue = commandLine.getSwitchValue(switchName); 157 Set<String> existingFeatures = new HashSet<>(); 158 if (switchValue != null) { 159 Collections.addAll(existingFeatures, switchValue.split(",")); 160 } 161 for (String additionalFeature : mRegisteredState.keySet()) { 162 if (mRegisteredState.get(additionalFeature) != enabled) continue; 163 existingFeatures.add(additionalFeature); 164 } 165 166 // Not really append, it puts the value in a map so we can override values that way too. 167 commandLine.appendSwitchWithValue(switchName, TextUtils.join(",", existingFeatures)); 168 } 169 } 170