• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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