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 * @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