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