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