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