• 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.server.testables;
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                     for (DeviceConfig.OnPropertiesChangedListener listener :
99                             mOnPropertiesChangedListenerMap.keySet()) {
100                         if (namespace.equals(mOnPropertiesChangedListenerMap.get(listener).first)) {
101                             mOnPropertiesChangedListenerMap.get(listener).second.execute(
102                                     () -> listener.onPropertiesChanged(
103                                             getProperties(namespace, name, value)));
104                         }
105                     }
106                     return true;
107                 }
108         ).when(() -> DeviceConfig.setProperty(anyString(), anyString(), anyString(), anyBoolean()));
109 
110         doAnswer((Answer<String>) invocationOnMock -> {
111             String namespace = invocationOnMock.getArgument(0);
112             String name = invocationOnMock.getArgument(1);
113             return mKeyValueMap.get(getKey(namespace, name));
114         }).when(() -> DeviceConfig.getProperty(anyString(), anyString()));
115 
116         doAnswer((Answer<Properties>) invocationOnMock -> {
117             String namespace = invocationOnMock.getArgument(0);
118             final int varargStartIdx = 1;
119             Map<String, String> keyValues = new ArrayMap<>();
120             if (invocationOnMock.getArguments().length == varargStartIdx) {
121                 mKeyValueMap.entrySet().forEach(entry -> {
122                     Pair<String, String> nameSpaceAndName = getNameSpaceAndName(entry.getKey());
123                     if (!nameSpaceAndName.first.equals(namespace)) {
124                         return;
125                     }
126                     keyValues.put(nameSpaceAndName.second.toLowerCase(), entry.getValue());
127                 });
128             } else {
129                 for (int i = varargStartIdx; i < invocationOnMock.getArguments().length; ++i) {
130                     String name = invocationOnMock.getArgument(i);
131                     keyValues.put(name.toLowerCase(), mKeyValueMap.get(getKey(namespace, name)));
132                 }
133             }
134             return getProperties(namespace, keyValues);
135         }).when(() -> DeviceConfig.getProperties(anyString(), ArgumentMatchers.<String>any()));
136     }
137 
138     /**
139      * {@inheritDoc}
140      */
141     @Override
tearDown()142     public void tearDown() {
143         clearDeviceConfig();
144         mOnPropertiesChangedListenerMap.clear();
145     }
146 
getKey(String namespace, String name)147     private static String getKey(String namespace, String name) {
148         return namespace + "/" + name;
149     }
150 
getNameSpaceAndName(String key)151     private Pair<String, String> getNameSpaceAndName(String key) {
152         final String[] values = key.split("/");
153         return Pair.create(values[0], values[1]);
154     }
155 
getProperties(String namespace, String name, String value)156     private Properties getProperties(String namespace, String name, String value) {
157         return getProperties(namespace, Collections.singletonMap(name.toLowerCase(), value));
158     }
159 
getProperties(String namespace, Map<String, String> keyValues)160     private Properties getProperties(String namespace, Map<String, String> keyValues) {
161         Properties properties = Mockito.mock(Properties.class);
162         when(properties.getNamespace()).thenReturn(namespace);
163         when(properties.getKeyset()).thenReturn(keyValues.keySet());
164         when(properties.getBoolean(anyString(), anyBoolean())).thenAnswer(
165                 invocation -> {
166                     String key = invocation.getArgument(0);
167                     boolean defaultValue = invocation.getArgument(1);
168                     final String value = keyValues.get(key.toLowerCase());
169                     if (value != null) {
170                         return Boolean.parseBoolean(value);
171                     } else {
172                         return defaultValue;
173                     }
174                 }
175         );
176         when(properties.getFloat(anyString(), anyFloat())).thenAnswer(
177                 invocation -> {
178                     String key = invocation.getArgument(0);
179                     float defaultValue = invocation.getArgument(1);
180                     final String value = keyValues.get(key.toLowerCase());
181                     if (value != null) {
182                         try {
183                             return Float.parseFloat(value);
184                         } catch (NumberFormatException e) {
185                             return defaultValue;
186                         }
187                     } else {
188                         return defaultValue;
189                     }
190                 }
191         );
192         when(properties.getInt(anyString(), anyInt())).thenAnswer(
193                 invocation -> {
194                     String key = invocation.getArgument(0);
195                     int defaultValue = invocation.getArgument(1);
196                     final String value = keyValues.get(key.toLowerCase());
197                     if (value != null) {
198                         try {
199                             return Integer.parseInt(value);
200                         } catch (NumberFormatException e) {
201                             return defaultValue;
202                         }
203                     } else {
204                         return defaultValue;
205                     }
206                 }
207         );
208         when(properties.getLong(anyString(), anyLong())).thenAnswer(
209                 invocation -> {
210                     String key = invocation.getArgument(0);
211                     long defaultValue = invocation.getArgument(1);
212                     final String value = keyValues.get(key.toLowerCase());
213                     if (value != null) {
214                         try {
215                             return Long.parseLong(value);
216                         } catch (NumberFormatException e) {
217                             return defaultValue;
218                         }
219                     } else {
220                         return defaultValue;
221                     }
222                 }
223         );
224         when(properties.getString(anyString(), nullable(String.class))).thenAnswer(
225                 invocation -> {
226                     String key = invocation.getArgument(0);
227                     String defaultValue = invocation.getArgument(1);
228                     final String value = keyValues.get(key.toLowerCase());
229                     if (value != null) {
230                         return value;
231                     } else {
232                         return defaultValue;
233                     }
234                 }
235         );
236 
237         return properties;
238     }
239 
240     /**
241      * <p>TestableDeviceConfigRule is a {@link TestRule} that wraps a {@link TestableDeviceConfig}
242      * to set it up and tear it down automatically. This works well when you have no other static
243      * mocks.</p>
244      *
245      * <p>TestableDeviceConfigRule should be defined as a rule on your test so it can clean up after
246      * itself. Like the following:</p>
247      * <pre class="prettyprint">
248      * &#064;Rule
249      * public final TestableDeviceConfigRule mTestableDeviceConfigRule =
250      *     new TestableDeviceConfigRule();
251      * </pre>
252      */
253     public static class TestableDeviceConfigRule extends StaticMockFixtureRule {
TestableDeviceConfigRule()254         public TestableDeviceConfigRule() {
255             super(TestableDeviceConfig::new);
256         }
257     }
258 }
259