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