1 /* 2 * Copyright 2018 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 package com.android.bluetooth; 17 18 import static org.mockito.ArgumentMatchers.eq; 19 import static org.mockito.Mockito.*; 20 21 import android.bluetooth.BluetoothAdapter; 22 import android.bluetooth.BluetoothDevice; 23 import android.content.Intent; 24 import android.os.Handler; 25 import android.os.Looper; 26 import android.support.test.InstrumentationRegistry; 27 import android.support.test.rule.ServiceTestRule; 28 29 import com.android.bluetooth.btservice.AdapterService; 30 import com.android.bluetooth.btservice.ProfileService; 31 32 import org.junit.Assert; 33 import org.mockito.ArgumentCaptor; 34 import org.mockito.internal.util.MockUtil; 35 36 import java.lang.reflect.Field; 37 import java.lang.reflect.InvocationTargetException; 38 import java.lang.reflect.Method; 39 import java.util.concurrent.BlockingQueue; 40 import java.util.concurrent.TimeUnit; 41 import java.util.concurrent.TimeoutException; 42 43 /** 44 * A set of methods useful in Bluetooth instrumentation tests 45 */ 46 public class TestUtils { 47 private static final int SERVICE_TOGGLE_TIMEOUT_MS = 1000; // 1s 48 49 /** 50 * Utility method to replace obj.fieldName with newValue where obj is of type c 51 * 52 * @param c type of obj 53 * @param fieldName field name to be replaced 54 * @param obj instance of type c whose fieldName is to be replaced, null for static fields 55 * @param newValue object used to replace fieldName 56 * @return the old value of fieldName that got replaced, caller is responsible for restoring 57 * it back to obj 58 * @throws NoSuchFieldException when fieldName is not found in type c 59 * @throws IllegalAccessException when fieldName cannot be accessed in type c 60 */ replaceField(final Class c, final String fieldName, final Object obj, final Object newValue)61 public static Object replaceField(final Class c, final String fieldName, final Object obj, 62 final Object newValue) throws NoSuchFieldException, IllegalAccessException { 63 Field field = c.getDeclaredField(fieldName); 64 field.setAccessible(true); 65 66 Object oldValue = field.get(obj); 67 field.set(obj, newValue); 68 return oldValue; 69 } 70 71 /** 72 * Set the return value of {@link AdapterService#getAdapterService()} to a test specified value 73 * 74 * @param adapterService the designated {@link AdapterService} in test, must not be null, can 75 * be mocked or spied 76 * @throws NoSuchMethodException when setAdapterService method is not found 77 * @throws IllegalAccessException when setAdapterService method cannot be accessed 78 * @throws InvocationTargetException when setAdapterService method cannot be invoked, which 79 * should never happen since setAdapterService is a static method 80 */ setAdapterService(AdapterService adapterService)81 public static void setAdapterService(AdapterService adapterService) 82 throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { 83 Assert.assertNull("AdapterService.getAdapterService() must be null before setting another" 84 + " AdapterService", AdapterService.getAdapterService()); 85 Assert.assertNotNull(adapterService); 86 // We cannot mock AdapterService.getAdapterService() with Mockito. 87 // Hence we need to use reflection to call a private method to 88 // initialize properly the AdapterService.sAdapterService field. 89 Method method = 90 AdapterService.class.getDeclaredMethod("setAdapterService", AdapterService.class); 91 method.setAccessible(true); 92 method.invoke(null, adapterService); 93 } 94 95 /** 96 * Clear the return value of {@link AdapterService#getAdapterService()} to null 97 * 98 * @param adapterService the {@link AdapterService} used when calling 99 * {@link TestUtils#setAdapterService(AdapterService)} 100 * @throws NoSuchMethodException when clearAdapterService method is not found 101 * @throws IllegalAccessException when clearAdapterService method cannot be accessed 102 * @throws InvocationTargetException when clearAdappterService method cannot be invoked, 103 * which should never happen since clearAdapterService is a static method 104 */ clearAdapterService(AdapterService adapterService)105 public static void clearAdapterService(AdapterService adapterService) 106 throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { 107 Assert.assertSame("AdapterService.getAdapterService() must return the same object as the" 108 + " supplied adapterService in this method", adapterService, 109 AdapterService.getAdapterService()); 110 Assert.assertNotNull(adapterService); 111 Method method = 112 AdapterService.class.getDeclaredMethod("clearAdapterService", AdapterService.class); 113 method.setAccessible(true); 114 method.invoke(null, adapterService); 115 } 116 117 /** 118 * Start a profile service using the given {@link ServiceTestRule} and verify through 119 * {@link AdapterService#getAdapterService()} that the service is actually started within 120 * {@link TestUtils#SERVICE_TOGGLE_TIMEOUT_MS} milliseconds. 121 * {@link #setAdapterService(AdapterService)} must be called with a mocked 122 * {@link AdapterService} before calling this method 123 * 124 * @param serviceTestRule the {@link ServiceTestRule} used to execute the service start request 125 * @param profileServiceClass a class from one of {@link ProfileService}'s child classes 126 * @throws TimeoutException when service failed to start within either default timeout of 127 * {@link ServiceTestRule#DEFAULT_TIMEOUT} (normally 5s) or user specified time when creating 128 * {@link ServiceTestRule} through {@link ServiceTestRule#withTimeout(long, TimeUnit)} method 129 */ startService(ServiceTestRule serviceTestRule, Class<T> profileServiceClass)130 public static <T extends ProfileService> void startService(ServiceTestRule serviceTestRule, 131 Class<T> profileServiceClass) throws TimeoutException { 132 AdapterService adapterService = AdapterService.getAdapterService(); 133 Assert.assertNotNull(adapterService); 134 Assert.assertTrue("AdapterService.getAdapterService() must return a mocked or spied object" 135 + " before calling this method", MockUtil.isMock(adapterService)); 136 Intent startIntent = 137 new Intent(InstrumentationRegistry.getTargetContext(), profileServiceClass); 138 startIntent.putExtra(AdapterService.EXTRA_ACTION, 139 AdapterService.ACTION_SERVICE_STATE_CHANGED); 140 startIntent.putExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_ON); 141 serviceTestRule.startService(startIntent); 142 ArgumentCaptor<ProfileService> profile = ArgumentCaptor.forClass(profileServiceClass); 143 verify(adapterService, timeout(SERVICE_TOGGLE_TIMEOUT_MS)).onProfileServiceStateChanged( 144 profile.capture(), eq(BluetoothAdapter.STATE_ON)); 145 Assert.assertEquals(profileServiceClass.getName(), profile.getValue().getClass().getName()); 146 } 147 148 /** 149 * Stop a profile service using the given {@link ServiceTestRule} and verify through 150 * {@link AdapterService#getAdapterService()} that the service is actually stopped within 151 * {@link TestUtils#SERVICE_TOGGLE_TIMEOUT_MS} milliseconds. 152 * {@link #setAdapterService(AdapterService)} must be called with a mocked 153 * {@link AdapterService} before calling this method 154 * 155 * @param serviceTestRule the {@link ServiceTestRule} used to execute the service start request 156 * @param profileServiceClass a class from one of {@link ProfileService}'s child classes 157 * @throws TimeoutException when service failed to start within either default timeout of 158 * {@link ServiceTestRule#DEFAULT_TIMEOUT} (normally 5s) or user specified time when creating 159 * {@link ServiceTestRule} through {@link ServiceTestRule#withTimeout(long, TimeUnit)} method 160 */ stopService(ServiceTestRule serviceTestRule, Class<T> profileServiceClass)161 public static <T extends ProfileService> void stopService(ServiceTestRule serviceTestRule, 162 Class<T> profileServiceClass) throws TimeoutException { 163 AdapterService adapterService = AdapterService.getAdapterService(); 164 Assert.assertNotNull(adapterService); 165 Assert.assertTrue("AdapterService.getAdapterService() must return a mocked or spied object" 166 + " before calling this method", MockUtil.isMock(adapterService)); 167 Intent stopIntent = 168 new Intent(InstrumentationRegistry.getTargetContext(), profileServiceClass); 169 stopIntent.putExtra(AdapterService.EXTRA_ACTION, 170 AdapterService.ACTION_SERVICE_STATE_CHANGED); 171 stopIntent.putExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF); 172 serviceTestRule.startService(stopIntent); 173 ArgumentCaptor<ProfileService> profile = ArgumentCaptor.forClass(profileServiceClass); 174 verify(adapterService, timeout(SERVICE_TOGGLE_TIMEOUT_MS)).onProfileServiceStateChanged( 175 profile.capture(), eq(BluetoothAdapter.STATE_OFF)); 176 Assert.assertEquals(profileServiceClass.getName(), profile.getValue().getClass().getName()); 177 } 178 179 /** 180 * Create a test device. 181 * 182 * @param bluetoothAdapter the Bluetooth adapter to use 183 * @param id the test device ID. It must be an integer in the interval [0, 0xFF]. 184 * @return {@link BluetoothDevice} test device for the device ID 185 */ getTestDevice(BluetoothAdapter bluetoothAdapter, int id)186 public static BluetoothDevice getTestDevice(BluetoothAdapter bluetoothAdapter, int id) { 187 Assert.assertTrue(id <= 0xFF); 188 Assert.assertNotNull(bluetoothAdapter); 189 BluetoothDevice testDevice = 190 bluetoothAdapter.getRemoteDevice(String.format("00:01:02:03:04:%02X", id)); 191 Assert.assertNotNull(testDevice); 192 return testDevice; 193 } 194 195 /** 196 * Wait and verify that an intent has been received. 197 * 198 * @param timeoutMs the time (in milliseconds) to wait for the intent 199 * @param queue the queue for the intent 200 * @return the received intent 201 */ waitForIntent(int timeoutMs, BlockingQueue<Intent> queue)202 public static Intent waitForIntent(int timeoutMs, BlockingQueue<Intent> queue) { 203 try { 204 Intent intent = queue.poll(timeoutMs, TimeUnit.MILLISECONDS); 205 Assert.assertNotNull(intent); 206 return intent; 207 } catch (InterruptedException e) { 208 Assert.fail("Cannot obtain an Intent from the queue: " + e.getMessage()); 209 } 210 return null; 211 } 212 213 /** 214 * Wait and verify that no intent has been received. 215 * 216 * @param timeoutMs the time (in milliseconds) to wait and verify no intent 217 * has been received 218 * @param queue the queue for the intent 219 * @return the received intent. Should be null under normal circumstances 220 */ waitForNoIntent(int timeoutMs, BlockingQueue<Intent> queue)221 public static Intent waitForNoIntent(int timeoutMs, BlockingQueue<Intent> queue) { 222 try { 223 Intent intent = queue.poll(timeoutMs, TimeUnit.MILLISECONDS); 224 Assert.assertNull(intent); 225 return intent; 226 } catch (InterruptedException e) { 227 Assert.fail("Cannot obtain an Intent from the queue: " + e.getMessage()); 228 } 229 return null; 230 } 231 232 /** 233 * Wait for looper to finish its current task and all tasks schedule before this 234 * 235 * @param looper looper of interest 236 */ waitForLooperToFinishScheduledTask(Looper looper)237 public static void waitForLooperToFinishScheduledTask(Looper looper) { 238 runOnLooperSync(looper, () -> { 239 // do nothing, just need to make sure looper finishes current task 240 }); 241 } 242 243 /** 244 * Run synchronously a runnable action on a looper. 245 * The method will return after the action has been execution to completion. 246 * 247 * Example: 248 * <pre> 249 * {@code 250 * TestUtils.runOnMainSync(new Runnable() { 251 * public void run() { 252 * Assert.assertTrue(mA2dpService.stop()); 253 * } 254 * }); 255 * } 256 * </pre> 257 * 258 * @param looper the looper used to run the action 259 * @param action the action to run 260 */ runOnLooperSync(Looper looper, Runnable action)261 public static void runOnLooperSync(Looper looper, Runnable action) { 262 if (Looper.myLooper() == looper) { 263 // requested thread is the same as the current thread. call directly. 264 action.run(); 265 } else { 266 Handler handler = new Handler(looper); 267 SyncRunnable sr = new SyncRunnable(action); 268 handler.post(sr); 269 sr.waitForComplete(); 270 } 271 } 272 273 /** 274 * Helper class used to run synchronously a runnable action on a looper. 275 */ 276 private static final class SyncRunnable implements Runnable { 277 private final Runnable mTarget; 278 private volatile boolean mComplete = false; 279 SyncRunnable(Runnable target)280 SyncRunnable(Runnable target) { 281 mTarget = target; 282 } 283 284 @Override run()285 public void run() { 286 mTarget.run(); 287 synchronized (this) { 288 mComplete = true; 289 notifyAll(); 290 } 291 } 292 waitForComplete()293 public void waitForComplete() { 294 synchronized (this) { 295 while (!mComplete) { 296 try { 297 wait(); 298 } catch (InterruptedException e) { 299 } 300 } 301 } 302 } 303 } 304 } 305