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