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