1 /*
2  * Copyright (C) 2019 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 com.android.modules.utils.testing;
18 
19 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
20 
21 import static org.mockito.ArgumentMatchers.any;
22 import static org.mockito.ArgumentMatchers.anyBoolean;
23 import static org.mockito.ArgumentMatchers.anyFloat;
24 import static org.mockito.ArgumentMatchers.anyInt;
25 import static org.mockito.ArgumentMatchers.anyLong;
26 import static org.mockito.ArgumentMatchers.anyString;
27 import static org.mockito.ArgumentMatchers.nullable;
28 import static org.mockito.Mockito.when;
29 import static org.mockito.Mockito.spy;
30 
31 import android.provider.DeviceConfig;
32 import android.provider.DeviceConfig.Properties;
33 import android.util.ArrayMap;
34 import android.util.Pair;
35 
36 import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;
37 import com.android.modules.utils.testing.AbstractExtendedMockitoRule.AbstractBuilder;
38 
39 import com.android.modules.utils.build.SdkLevel;
40 
41 import org.junit.rules.TestRule;
42 import org.mockito.ArgumentMatchers;
43 import org.mockito.Mockito;
44 import org.mockito.stubbing.Answer;
45 
46 import java.util.Collections;
47 import java.util.HashMap;
48 import java.util.Map;
49 import java.util.concurrent.ConcurrentHashMap;
50 import java.util.concurrent.Executor;
51 
52 /**
53  * TestableDeviceConfig is a {@link StaticMockFixture} that uses ExtendedMockito to replace the real
54  * implementation of DeviceConfig with essentially a local HashMap in the callers process. This
55  * allows for unit testing that do not modify the real DeviceConfig on the device at all.
56  */
57 public final class TestableDeviceConfig implements StaticMockFixture {
58 
59     private Map<DeviceConfig.OnPropertiesChangedListener, Pair<String, Executor>>
60             mOnPropertiesChangedListenerMap = new HashMap<>();
61     private Map<String, String> mKeyValueMap = new ConcurrentHashMap<>();
62 
63     /**
64      * Clears out all local overrides.
65      */
clearDeviceConfig()66     public void clearDeviceConfig() {
67         mKeyValueMap.clear();
68     }
69 
70     /**
71      * {@inheritDoc}
72      */
73     @Override
setUpMockedClasses( StaticMockitoSessionBuilder sessionBuilder)74     public StaticMockitoSessionBuilder setUpMockedClasses(
75             StaticMockitoSessionBuilder sessionBuilder) {
76         sessionBuilder.spyStatic(DeviceConfig.class);
77         return sessionBuilder;
78     }
79 
80     /**
81      * {@inheritDoc}
82      */
83     @Override
setUpMockBehaviors()84     public void setUpMockBehaviors() {
85         doAnswer((Answer<Void>) invocationOnMock -> {
86             String namespace = invocationOnMock.getArgument(0);
87             Executor executor = invocationOnMock.getArgument(1);
88             DeviceConfig.OnPropertiesChangedListener onPropertiesChangedListener =
89                     invocationOnMock.getArgument(2);
90             mOnPropertiesChangedListenerMap.put(
91                     onPropertiesChangedListener, new Pair<>(namespace, executor));
92             return null;
93         }).when(() -> DeviceConfig.addOnPropertiesChangedListener(
94                 anyString(), any(Executor.class),
95                 any(DeviceConfig.OnPropertiesChangedListener.class)));
96 
97         doAnswer((Answer<Boolean>) invocationOnMock -> {
98             String namespace = invocationOnMock.getArgument(0);
99             String name = invocationOnMock.getArgument(1);
100             String value = invocationOnMock.getArgument(2);
101             mKeyValueMap.put(getKey(namespace, name), value);
102             invokeListeners(namespace, getProperties(namespace, name, value));
103             return true;
104         }).when(() -> DeviceConfig.setProperty(
105                 anyString(), anyString(), anyString(), anyBoolean()));
106 
107         if (SdkLevel.isAtLeastT()) {
108             doAnswer((Answer<Boolean>) invocationOnMock -> {
109                 String namespace = invocationOnMock.getArgument(0);
110                 String name = invocationOnMock.getArgument(1);
111                 mKeyValueMap.remove(getKey(namespace, name));
112                 invokeListeners(namespace, getProperties(namespace, name, null));
113                 return true;
114             }).when(() -> DeviceConfig.deleteProperty(anyString(), anyString()));
115 
116             doAnswer((Answer<Boolean>) invocationOnMock -> {
117                 Properties properties = invocationOnMock.getArgument(0);
118                 String namespace = properties.getNamespace();
119                 Map<String, String> keyValues = new ArrayMap<>();
120                 for (String name : properties.getKeyset()) {
121                     String value = properties.getString(name, /* defaultValue= */ "");
122                     mKeyValueMap.put(getKey(namespace, name), value);
123                     keyValues.put(name.toLowerCase(), value);
124                 }
125                 invokeListeners(namespace, getProperties(namespace, keyValues));
126                 return true;
127             }).when(() -> DeviceConfig.setProperties(any(Properties.class)));
128         }
129 
130         doAnswer((Answer<String>) invocationOnMock -> {
131             String namespace = invocationOnMock.getArgument(0);
132             String name = invocationOnMock.getArgument(1);
133             return mKeyValueMap.get(getKey(namespace, name));
134         }).when(() -> DeviceConfig.getProperty(anyString(), anyString()));
135         if (SdkLevel.isAtLeastR()) {
136             doAnswer((Answer<Properties>) invocationOnMock -> {
137                 String namespace = invocationOnMock.getArgument(0);
138                 final int varargStartIdx = 1;
139                 Map<String, String> keyValues = new ArrayMap<>();
140                 if (invocationOnMock.getArguments().length == varargStartIdx) {
141                     mKeyValueMap.entrySet().forEach(entry -> {
142                         Pair<String, String> nameSpaceAndName = getNameSpaceAndName(entry.getKey());
143                         if (!nameSpaceAndName.first.equals(namespace)) {
144                             return;
145                         }
146                         keyValues.put(nameSpaceAndName.second.toLowerCase(), entry.getValue());
147                     });
148                 } else {
149                     for (int i = varargStartIdx; i < invocationOnMock.getArguments().length; ++i) {
150                         String name = invocationOnMock.getArgument(i);
151                         keyValues.put(name.toLowerCase(),
152                             mKeyValueMap.get(getKey(namespace, name)));
153                     }
154                 }
155                 return getProperties(namespace, keyValues);
156             }).when(() -> DeviceConfig.getProperties(anyString(), ArgumentMatchers.<String>any()));
157         }
158     }
159 
160     /**
161      * {@inheritDoc}
162      */
163     @Override
tearDown()164     public void tearDown() {
165         clearDeviceConfig();
166         mOnPropertiesChangedListenerMap.clear();
167     }
168 
getKey(String namespace, String name)169     private static String getKey(String namespace, String name) {
170         return namespace + "/" + name;
171     }
172 
getNameSpaceAndName(String key)173     private Pair<String, String> getNameSpaceAndName(String key) {
174         final String[] values = key.split("/");
175         return Pair.create(values[0], values[1]);
176     }
177 
invokeListeners(String namespace, Properties properties)178     private void invokeListeners(String namespace, Properties properties) {
179         for (DeviceConfig.OnPropertiesChangedListener listener :
180                 mOnPropertiesChangedListenerMap.keySet()) {
181             if (namespace.equals(mOnPropertiesChangedListenerMap.get(listener).first)) {
182                 mOnPropertiesChangedListenerMap.get(listener).second.execute(
183                         () -> listener.onPropertiesChanged(properties));
184             }
185         }
186     }
187 
getProperties(String namespace, String name, String value)188     private Properties getProperties(String namespace, String name, String value) {
189         return getProperties(namespace, Collections.singletonMap(name.toLowerCase(), value));
190     }
191 
getProperties(String namespace, Map<String, String> keyValues)192     private Properties getProperties(String namespace, Map<String, String> keyValues) {
193         Properties.Builder builder = new Properties.Builder(namespace);
194         keyValues.forEach((k, v) -> {
195             builder.setString(k, v);
196         });
197         Properties properties = spy(builder.build());
198         when(properties.getNamespace()).thenReturn(namespace);
199         when(properties.getKeyset()).thenReturn(keyValues.keySet());
200         when(properties.getBoolean(anyString(), anyBoolean())).thenAnswer(
201                 invocation -> {
202                     String key = invocation.getArgument(0);
203                     boolean defaultValue = invocation.getArgument(1);
204                     final String value = keyValues.get(key.toLowerCase());
205                     if (value != null) {
206                         return Boolean.parseBoolean(value);
207                     } else {
208                         return defaultValue;
209                     }
210                 }
211         );
212         when(properties.getFloat(anyString(), anyFloat())).thenAnswer(
213                 invocation -> {
214                     String key = invocation.getArgument(0);
215                     float defaultValue = invocation.getArgument(1);
216                     final String value = keyValues.get(key.toLowerCase());
217                     if (value != null) {
218                         try {
219                             return Float.parseFloat(value);
220                         } catch (NumberFormatException e) {
221                             return defaultValue;
222                         }
223                     } else {
224                         return defaultValue;
225                     }
226                 }
227         );
228         when(properties.getInt(anyString(), anyInt())).thenAnswer(
229                 invocation -> {
230                     String key = invocation.getArgument(0);
231                     int defaultValue = invocation.getArgument(1);
232                     final String value = keyValues.get(key.toLowerCase());
233                     if (value != null) {
234                         try {
235                             return Integer.parseInt(value);
236                         } catch (NumberFormatException e) {
237                             return defaultValue;
238                         }
239                     } else {
240                         return defaultValue;
241                     }
242                 }
243         );
244         when(properties.getLong(anyString(), anyLong())).thenAnswer(
245                 invocation -> {
246                     String key = invocation.getArgument(0);
247                     long defaultValue = invocation.getArgument(1);
248                     final String value = keyValues.get(key.toLowerCase());
249                     if (value != null) {
250                         try {
251                             return Long.parseLong(value);
252                         } catch (NumberFormatException e) {
253                             return defaultValue;
254                         }
255                     } else {
256                         return defaultValue;
257                     }
258                 }
259         );
260         when(properties.getString(anyString(), nullable(String.class))).thenAnswer(
261                 invocation -> {
262                     String key = invocation.getArgument(0);
263                     String defaultValue = invocation.getArgument(1);
264                     final String value = keyValues.get(key.toLowerCase());
265                     if (value != null) {
266                         return value;
267                     } else {
268                         return defaultValue;
269                     }
270                 }
271         );
272 
273         return properties;
274     }
275 
276     /**
277      * <p>TestableDeviceConfigRule is a {@link TestRule} that wraps a {@link TestableDeviceConfig}
278      * to set it up and tear it down automatically. This works well when you have no other static
279      * mocks.</p>
280      *
281      * <p>TestableDeviceConfigRule should be defined as a rule on your test so it can clean up after
282      * itself. Like the following:</p>
283      * <pre class="prettyprint">
284      * &#064;Rule
285      * public final TestableDeviceConfigRule mTestableDeviceConfigRule =
286      *     new TestableDeviceConfigRule(this);
287      * </pre>
288      */
289     public static final class TestableDeviceConfigRule extends
290             AbstractExtendedMockitoRule<TestableDeviceConfigRule, TestableDeviceConfigRuleBuilder> {
291 
292         /**
293          * Creates the rule, initializing the mocks for the given test.
294          */
TestableDeviceConfigRule(Object testClassInstance)295         public TestableDeviceConfigRule(Object testClassInstance) {
296             this(new TestableDeviceConfigRuleBuilder(testClassInstance)
297                     .addStaticMockFixtures(TestableDeviceConfig::new));
298         }
299 
300         /**
301          * Creates the rule, without initializing the mocks.
302          */
TestableDeviceConfigRule()303         public TestableDeviceConfigRule() {
304             this(new TestableDeviceConfigRuleBuilder()
305                     .addStaticMockFixtures(TestableDeviceConfig::new));
306         }
307 
TestableDeviceConfigRule(TestableDeviceConfigRuleBuilder builder)308         private TestableDeviceConfigRule(TestableDeviceConfigRuleBuilder builder) {
309             super(builder);
310         }
311     }
312 
313     private static final class TestableDeviceConfigRuleBuilder extends
314             AbstractBuilder<TestableDeviceConfigRule, TestableDeviceConfigRuleBuilder> {
315 
TestableDeviceConfigRuleBuilder(Object testClassInstance)316         TestableDeviceConfigRuleBuilder(Object testClassInstance) {
317             super(testClassInstance);
318         }
319 
TestableDeviceConfigRuleBuilder()320         TestableDeviceConfigRuleBuilder() {
321             super();
322         }
323 
324         @Override
build()325         public TestableDeviceConfigRule build() {
326             return new TestableDeviceConfigRule(this);
327         }
328     }
329 }
330