1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.flags;
18 
19 import android.annotation.NonNull;
20 
21 import java.util.HashMap;
22 import java.util.HashSet;
23 import java.util.Map;
24 import java.util.Set;
25 
26 /**
27  * An implementation of {@link FeatureFlags} for testing.
28  *
29  * Before you read a flag from using this Fake, you must set that flag using
30  * {@link #setFlagValue(BooleanFlagBase, boolean)}. This ensures that your tests are deterministic.
31  *
32  * If you are relying on {@link FeatureFlags#getInstance()} to access FeatureFlags in your code
33  * under test, (instead of dependency injection), you can pass an instance of this fake to
34  * {@link FeatureFlags#setInstance(FeatureFlags)}. Be sure to call that method again, passing null,
35  * to ensure hermetic testing - you don't want static state persisting between your test methods.
36  *
37  * @hide
38  */
39 public class FeatureFlagsFake extends FeatureFlags {
40     private final Map<BooleanFlagBase, Boolean> mFlagValues = new HashMap<>();
41     private final Set<BooleanFlagBase> mReadFlags = new HashSet<>();
42 
FeatureFlagsFake(IFeatureFlags iFeatureFlags)43     public FeatureFlagsFake(IFeatureFlags iFeatureFlags) {
44         super(iFeatureFlags);
45     }
46 
47     @Override
isEnabled(@onNull BooleanFlag flag)48     public boolean isEnabled(@NonNull BooleanFlag flag) {
49         return requireFlag(flag);
50     }
51 
52     @Override
isEnabled(@onNull FusedOffFlag flag)53     public boolean isEnabled(@NonNull FusedOffFlag flag) {
54         return requireFlag(flag);
55     }
56 
57     @Override
isEnabled(@onNull FusedOnFlag flag)58     public boolean isEnabled(@NonNull FusedOnFlag flag) {
59         return requireFlag(flag);
60     }
61 
62     @Override
isCurrentlyEnabled(@onNull DynamicBooleanFlag flag)63     public boolean isCurrentlyEnabled(@NonNull DynamicBooleanFlag flag) {
64         return requireFlag(flag);
65     }
66 
67     @Override
syncInternal(Set<Flag<?>> dirtyFlags)68     protected void syncInternal(Set<Flag<?>> dirtyFlags) {
69     }
70 
71     /**
72      * Explicitly set a flag's value for reading in tests.
73      *
74      * You _must_ call this for every flag your code-under-test will read. Otherwise, an
75      * {@link IllegalStateException} will be thrown.
76      *
77      * You are able to set values for {@link FusedOffFlag} and {@link FusedOnFlag}, despite those
78      * flags having a fixed value at compile time, since unit tests should still test the state of
79      * those flags as both true and false. I.e. a flag that is off might be turned on in a future
80      * build or vice versa.
81      *
82      * You can not call this method _after_ a non-dynamic flag has been read. Non-dynamic flags
83      * are held stable in the system, so changing a value after reading would not match
84      * real-implementation behavior.
85      *
86      * Calling this method will trigger any {@link android.flags.FeatureFlags.ChangeListener}s that
87      * are registered for the supplied flag if the flag is a {@link DynamicFlag}.
88      *
89      * @param flag  The BooleanFlag that you want to set a value for.
90      * @param value The value that the flag should return when accessed.
91      */
setFlagValue(@onNull BooleanFlagBase flag, boolean value)92     public void setFlagValue(@NonNull BooleanFlagBase flag, boolean value) {
93         if (!(flag instanceof DynamicBooleanFlag) && mReadFlags.contains(flag)) {
94             throw new RuntimeException(
95                     "You can not set the value of a flag after it has been read. Tried to set "
96                             + flag + " to " + value + " but it already " + mFlagValues.get(flag));
97         }
98         mFlagValues.put(flag, value);
99         if (flag instanceof DynamicBooleanFlag) {
100             onFlagChange((DynamicFlag<?>) flag);
101         }
102     }
103 
requireFlag(BooleanFlagBase flag)104     private boolean requireFlag(BooleanFlagBase flag) {
105         if (!mFlagValues.containsKey(flag)) {
106             throw new IllegalStateException(
107                     "Tried to access " + flag + " in test but no overrided specified. You must "
108                             + "call #setFlagValue for each flag read in a test.");
109         }
110         mReadFlags.add(flag);
111 
112         return mFlagValues.get(flag);
113     }
114 
115 }
116