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