• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Lint as: python2, python3
2# Copyright 2020 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5""" Server-side bluetooth adapter tests that involve suspend/resume with peers
6
7paired and/or connected.
8
9Single btpeer tests:
10  - Reconnect on resume test
11    - Classic HID
12    - LE HID
13    - A2DP
14  - Wake from suspend test
15    - Classic HID
16    - LE HID
17    - A2DP shouldn't wake from suspend
18  - Suspend while discovering (discovering should pause and unpause)
19  - Suspend while advertising (advertising should pause and unpause)
20
21Multiple btpeer tests:
22  - Reconnect on resume test
23    - One classic HID, One LE HID
24    - Two classic HID
25    - Two LE HID
26  - Wake from suspend test
27    - Two classic HID
28    - Two classic LE
29"""
30from __future__ import absolute_import
31from __future__ import division
32from __future__ import print_function
33
34import logging
35import time
36
37from autotest_lib.client.cros.bluetooth.bluetooth_audio_test_data import A2DP
38from autotest_lib.server.cros.bluetooth.bluetooth_adapter_tests import (
39        TABLET_MODELS, SUSPEND_POWER_DOWN_CHIPSETS)
40from autotest_lib.server.cros.bluetooth.bluetooth_adapter_audio_tests import (
41        BluetoothAdapterAudioTests)
42from autotest_lib.server.cros.bluetooth.bluetooth_adapter_quick_tests import (
43        BluetoothAdapterQuickTests)
44
45from six.moves import range
46
47test_wrapper = BluetoothAdapterQuickTests.quick_test_test_decorator
48batch_wrapper = BluetoothAdapterQuickTests.quick_test_batch_decorator
49
50PROFILE_CONNECT_WAIT = 15
51SUSPEND_SEC = 15
52EXPECT_NO_WAKE_SUSPEND_SEC = 30
53EXPECT_PEER_WAKE_SUSPEND_SEC = 60
54
55STRESS_ITERATIONS = 25
56
57
58class bluetooth_AdapterSRHealth(BluetoothAdapterQuickTests,
59                                BluetoothAdapterAudioTests):
60    """Server side bluetooth adapter suspend resume test with peer."""
61
62    def _test_keyboard_with_string(self, device):
63        self.test_hid_device_created(device.address)
64        return self.test_keyboard_input_from_trace(device, "simple_text")
65
66    def _test_mouse_left_click(self, device):
67        self.test_hid_device_created(device.address)
68        return self.test_mouse_left_click(device)
69
70    # ---------------------------------------------------------------
71    # Reconnect after suspend tests
72    # ---------------------------------------------------------------
73
74    def run_reconnect_device(self,
75                             devtuples,
76                             iterations=1,
77                             auto_reconnect=False):
78        """ Reconnects a device after suspend/resume.
79
80        @param devtuples: array of tuples consisting of the following
81                            * device_type: MOUSE, BLE_MOUSE, etc.
82                            * device: meta object for peer device
83                            * device_test: Optional; test function to run w/
84                                           device (for example, mouse click)
85        @params iterations: number of suspend/resume + reconnect iterations
86        @params auto_reconnect: Expect host to automatically reconnect to peer
87        """
88        boot_id = self.host.get_boot_id()
89
90        try:
91            # Set up the device; any failures should assert
92            for _, device, device_test in devtuples:
93                self.assert_discover_and_pair(device)
94                self.assert_on_fail(
95                        self.test_device_set_discoverable(device, False))
96                self.assert_on_fail(
97                        self.test_connection_by_adapter(device.address))
98
99                # Profile connection may not have completed yet and this will
100                # race with a subsequent disconnection (due to suspend). Use the
101                # device test to force profile connect or wait if no test was
102                # given.
103                if device_test is not None:
104                    self.assert_on_fail(device_test(device))
105                else:
106                    time.sleep(PROFILE_CONNECT_WAIT)
107
108            for it in range(iterations):
109                logging.info('Running iteration {}/{} of suspend reconnection'.
110                             format(it + 1, iterations))
111
112                # Start the suspend process
113                suspend = self.suspend_async(suspend_time=SUSPEND_SEC)
114                start_time = self.bluetooth_facade.get_device_time()
115
116                # Trigger suspend, wait for regular resume, verify we can reconnect
117                # and run device specific test
118                self.test_suspend_and_wait_for_sleep(suspend,
119                                                     sleep_timeout=SUSPEND_SEC)
120                self.test_wait_for_resume(boot_id,
121                                          suspend,
122                                          resume_timeout=SUSPEND_SEC,
123                                          test_start_time=start_time)
124
125                for device_type, device, device_test in devtuples:
126                    # Only reconnect if we don't expect automatic reconnect
127                    if not auto_reconnect:
128                        if 'BLE' in device_type:
129                            # LE can't reconnect without advertising/discoverable
130                            self.test_device_set_discoverable(device, True)
131                            # Make sure we're actually connected
132                            self.test_device_is_connected(device.address)
133                        else:
134                            # Classic requires peer to initiate a connection to
135                            # wake up the dut
136                            self.test_connection_by_device(device)
137
138                    if device_test is not None:
139                        device_test(device)
140
141        finally:
142            for _, device, __ in devtuples:
143                self.test_remove_pairing(device.address)
144
145    @test_wrapper('Reconnect Classic HID', devices={'MOUSE': 1})
146    def sr_reconnect_classic_hid(self):
147        """ Reconnects a classic HID device after suspend/resume. """
148        device_type = 'MOUSE'
149        device = self.devices[device_type][0]
150        self.run_reconnect_device([(device_type, device,
151                                    self._test_mouse_left_click)])
152
153    @test_wrapper('Reconnect LE HID', devices={'BLE_MOUSE': 1})
154    def sr_reconnect_le_hid(self):
155        """ Reconnects a LE HID device after suspend/resume. """
156        device_type = 'BLE_MOUSE'
157        device = self.devices[device_type][0]
158        self.run_reconnect_device([(device_type, device,
159                                    self._test_mouse_left_click)])
160
161    # TODO(b/163143005) - Hana can't handle two concurrent HID connections
162    @test_wrapper('Reconnect Multiple Classic HID',
163                  devices={
164                          'MOUSE': 1,
165                          'KEYBOARD': 1
166                  },
167                  skip_models=['hana'])
168    def sr_reconnect_multiple_classic_hid(self):
169        """ Reconnects multiple classic HID devices after suspend/resume. """
170        devices = [('MOUSE', self.devices['MOUSE'][0],
171                    self._test_mouse_left_click),
172                   ('KEYBOARD', self.devices['KEYBOARD'][0],
173                    self._test_keyboard_with_string)]
174        self.run_reconnect_device(devices)
175
176    @test_wrapper('Reconnect Multiple LE HID',
177                  devices={
178                          'BLE_MOUSE': 1,
179                          'BLE_KEYBOARD': 1
180                  })
181    def sr_reconnect_multiple_le_hid(self):
182        """ Reconnects multiple LE HID devices after suspend/resume. """
183        devices = [('BLE_MOUSE', self.devices['BLE_MOUSE'][0],
184                    self._test_mouse_left_click),
185                   ('BLE_KEYBOARD', self.devices['BLE_KEYBOARD'][0],
186                    self._test_keyboard_with_string)]
187        self.run_reconnect_device(devices)
188
189    @test_wrapper('Reconnect one of each classic+LE HID',
190                  devices={
191                          'BLE_MOUSE': 1,
192                          'KEYBOARD': 1
193                  })
194    def sr_reconnect_multiple_classic_le_hid(self):
195        """ Reconnects one of each classic and LE HID devices after
196            suspend/resume.
197        """
198        devices = [('BLE_MOUSE', self.devices['BLE_MOUSE'][0],
199                    self._test_mouse_left_click),
200                   ('KEYBOARD', self.devices['KEYBOARD'][0],
201                    self._test_keyboard_with_string)]
202        self.run_reconnect_device(devices)
203
204    @test_wrapper('Reconnect Classic HID Stress Test', devices={'MOUSE': 1})
205    def sr_reconnect_classic_hid_stress(self):
206        """ Reconnects a classic HID device after suspend/resume. """
207        device_type = 'MOUSE'
208        device = self.devices[device_type][0]
209        self.run_reconnect_device(
210                [(device_type, device, self._test_mouse_left_click)],
211                iterations=STRESS_ITERATIONS)
212
213    @test_wrapper('Reconnect LE HID Stress Test', devices={'BLE_MOUSE': 1})
214    def sr_reconnect_le_hid_stress(self):
215        """ Reconnects a LE HID device after suspend/resume. """
216        device_type = 'BLE_MOUSE'
217        device = self.devices[device_type][0]
218        self.run_reconnect_device(
219                [(device_type, device, self._test_mouse_left_click)],
220                iterations=STRESS_ITERATIONS)
221
222    @test_wrapper('Reconnect A2DP',
223                  devices={'BLUETOOTH_AUDIO': 1},
224                  skip_chipsets=SUSPEND_POWER_DOWN_CHIPSETS)
225    def sr_reconnect_a2dp(self):
226        """ Reconnects an A2DP device after suspend/resume. """
227        device_type = 'BLUETOOTH_AUDIO'
228        device = self.devices[device_type][0]
229        self.initialize_bluetooth_audio(device, A2DP)
230        self.run_reconnect_device(
231                [(device_type, device, self.test_device_a2dp_connected)],
232                auto_reconnect=True)
233
234    # ---------------------------------------------------------------
235    # Wake from suspend tests
236    # ---------------------------------------------------------------
237
238    def run_peer_wakeup_device(self,
239                               device_type,
240                               device,
241                               device_test=None,
242                               iterations=1,
243                               should_wake=True):
244        """ Uses paired peer device to wake the device from suspend.
245
246        @param device_type: the device type (used to determine if it's LE)
247        @param device: the meta device with the paired device
248        @param device_test: What to test to run after waking and connecting the
249                            adapter/host
250        @param iterations: Number of suspend + peer wake loops to run
251        @param should_wake: Whether wakeup should occur on this test. With HID
252                            peers, this should be True. With non-HID peers, this
253                            should be false.
254        """
255        boot_id = self.host.get_boot_id()
256
257        if should_wake:
258            sleep_time = EXPECT_PEER_WAKE_SUSPEND_SEC
259            resume_time = SUSPEND_SEC
260            resume_slack = 5  # Allow 5s slack for resume timeout
261        else:
262            sleep_time = EXPECT_NO_WAKE_SUSPEND_SEC
263            resume_time = EXPECT_NO_WAKE_SUSPEND_SEC
264            # Negative resume slack lets us wake a bit earlier than expected
265            # If suspend takes a while to enter, this may be necessary to get
266            # the timings right.
267            resume_slack = -5
268
269        # Clear wake before testing
270        self.test_adapter_set_wake_disabled()
271
272        try:
273            self.assert_discover_and_pair(device)
274            self.assert_on_fail(
275                    self.test_device_set_discoverable(device, False))
276
277            # Confirm connection completed
278            self.assert_on_fail(self.test_device_is_connected(device.address))
279
280            # Profile connection may not have completed yet and this will
281            # race with a subsequent disconnection (due to suspend). Use the
282            # device test to force profile connect or wait if no test was
283            # given.
284            if device_test is not None:
285                self.assert_on_fail(device_test(device))
286            else:
287                time.sleep(PROFILE_CONNECT_WAIT)
288
289            for it in range(iterations):
290                logging.info(
291                        'Running iteration {}/{} of suspend peer wake'.format(
292                                it + 1, iterations))
293
294                # Start a new suspend instance
295                suspend = self.suspend_async(suspend_time=sleep_time,
296                                             expect_bt_wake=should_wake)
297                start_time = self.bluetooth_facade.get_device_time()
298
299                if should_wake:
300                    self.test_device_wake_allowed(device.address)
301                    # Also wait until powerd marks adapter as wake enabled
302                    self.test_adapter_wake_enabled()
303                else:
304                    self.test_device_wake_not_allowed(device.address)
305
306                # Trigger suspend, asynchronously wake and wait for resume
307                self.test_suspend_and_wait_for_sleep(suspend, sleep_timeout=5)
308
309                # Trigger peer wakeup
310                adapter_address = self.bluetooth_facade.address
311                peer_wake = self.device_connect_async(device_type,
312                                                      device,
313                                                      adapter_address,
314                                                      delay_wake=5,
315                                                      should_wake=should_wake)
316                peer_wake.start()
317
318                # Expect a quick resume. If a timeout occurs, test fails. Since
319                # we delay sending the wake signal, we should accommodate that
320                # in our expected timeout.
321                self.test_wait_for_resume(boot_id,
322                                          suspend,
323                                          resume_timeout=resume_time,
324                                          test_start_time=start_time,
325                                          resume_slack=resume_slack,
326                                          fail_on_timeout=should_wake,
327                                          fail_early_wake=not should_wake)
328
329                # Finish peer wake process
330                peer_wake.join()
331
332                # Only check peer device connection state if we expected to wake
333                # from it. Otherwise, we may or may not be connected based on
334                # the specific profile's reconnection policy.
335                if should_wake:
336                    # Make sure we're actually connected
337                    self.test_device_is_connected(device.address)
338
339                    # Verify the profile is working
340                    if device_test is not None:
341                        device_test(device)
342
343        finally:
344            self.test_remove_pairing(device.address)
345
346    # TODO(b/151332866) - Bob can't wake from suspend due to wrong power/wakeup
347    # TODO(b/150897528) - Dru is powered down during suspend, won't wake up
348    @test_wrapper('Peer wakeup Classic HID',
349                  devices={'MOUSE': 1},
350                  skip_models=TABLET_MODELS + ['bob', 'dru'],
351                  skip_chipsets=SUSPEND_POWER_DOWN_CHIPSETS)
352    def sr_peer_wake_classic_hid(self):
353        """ Use classic HID device to wake from suspend. """
354        device = self.devices['MOUSE'][0]
355        self.run_peer_wakeup_device('MOUSE',
356                                    device,
357                                    device_test=self._test_mouse_left_click)
358
359    # TODO(b/151332866) - Bob can't wake from suspend due to wrong power/wakeup
360    # TODO(b/150897528) - Dru is powered down during suspend, won't wake up
361    @test_wrapper('Peer wakeup LE HID',
362                  devices={'BLE_MOUSE': 1},
363                  skip_models=TABLET_MODELS + ['bob', 'dru'],
364                  skip_chipsets=SUSPEND_POWER_DOWN_CHIPSETS)
365    def sr_peer_wake_le_hid(self):
366        """ Use LE HID device to wake from suspend. """
367        device = self.devices['BLE_MOUSE'][0]
368        self.run_peer_wakeup_device('BLE_MOUSE',
369                                    device,
370                                    device_test=self._test_mouse_left_click)
371
372    # TODO(b/151332866) - Bob can't wake from suspend due to wrong power/wakeup
373    # TODO(b/150897528) - Dru is powered down during suspend, won't wake up
374    @test_wrapper('Peer wakeup Classic HID',
375                  devices={'MOUSE': 1},
376                  skip_models=TABLET_MODELS + ['bob', 'dru'],
377                  skip_chipsets=SUSPEND_POWER_DOWN_CHIPSETS)
378    def sr_peer_wake_classic_hid_stress(self):
379        """ Use classic HID device to wake from suspend. """
380        device = self.devices['MOUSE'][0]
381        self.run_peer_wakeup_device('MOUSE',
382                                    device,
383                                    device_test=self._test_mouse_left_click,
384                                    iterations=STRESS_ITERATIONS)
385
386    # TODO(b/151332866) - Bob can't wake from suspend due to wrong power/wakeup
387    # TODO(b/150897528) - Dru is powered down during suspend, won't wake up
388    @test_wrapper('Peer wakeup LE HID',
389                  devices={'BLE_MOUSE': 1},
390                  skip_models=TABLET_MODELS + ['bob', 'dru'],
391                  skip_chipsets=SUSPEND_POWER_DOWN_CHIPSETS)
392    def sr_peer_wake_le_hid_stress(self):
393        """ Use LE HID device to wake from suspend. """
394        device = self.devices['BLE_MOUSE'][0]
395        self.run_peer_wakeup_device('BLE_MOUSE',
396                                    device,
397                                    device_test=self._test_mouse_left_click,
398                                    iterations=STRESS_ITERATIONS)
399
400    @test_wrapper('Peer wakeup with A2DP should fail',
401                  devices={'BLUETOOTH_AUDIO': 1},
402                  skip_chipsets=SUSPEND_POWER_DOWN_CHIPSETS)
403    def sr_peer_wake_a2dp_should_fail(self):
404        """ Use A2DP device to wake from suspend and fail. """
405        device_type = 'BLUETOOTH_AUDIO'
406        device = self.devices[device_type][0]
407        self.initialize_bluetooth_audio(device, A2DP)
408        self.run_peer_wakeup_device(
409                device_type,
410                device,
411                device_test=self.test_device_a2dp_connected,
412                should_wake=False)
413
414    # ---------------------------------------------------------------
415    # Suspend while discovering and advertising
416    # ---------------------------------------------------------------
417
418    # TODO(b/150897528) - Scarlet Dru loses firmware around suspend
419    @test_wrapper('Suspend while discovering',
420                  devices={'BLE_MOUSE': 1},
421                  skip_models=['dru'],
422                  skip_chipsets=SUSPEND_POWER_DOWN_CHIPSETS)
423    def sr_while_discovering(self):
424        """ Suspend while discovering. """
425        device = self.devices['BLE_MOUSE'][0]
426        boot_id = self.host.get_boot_id()
427
428        self.test_device_set_discoverable(device, True)
429
430        # Test discovery without setting discovery filter
431        # ----------------------------------------------------------------------
432        suspend = self.suspend_async(suspend_time=EXPECT_NO_WAKE_SUSPEND_SEC)
433        start_time = self.bluetooth_facade.get_device_time()
434
435        # We don't pair to the peer device because we don't want it in the
436        # allowlist. However, we want an advertising peer in this test
437        # responding to the discovery requests.
438        self.test_start_discovery()
439        self.test_suspend_and_wait_for_sleep(suspend,
440                                             sleep_timeout=SUSPEND_SEC)
441
442        # If discovery events wake us early, we will raise and suspend.exitcode
443        # will be non-zero
444        self.test_wait_for_resume(boot_id,
445                                  suspend,
446                                  resume_timeout=EXPECT_NO_WAKE_SUSPEND_SEC,
447                                  test_start_time=start_time)
448
449        # Discovering should restore after suspend
450        self.test_is_discovering()
451        self.test_stop_discovery()
452
453        # Test discovery with discovery filter set
454        # ----------------------------------------------------------------------
455        suspend = self.suspend_async(suspend_time=EXPECT_NO_WAKE_SUSPEND_SEC)
456        start_time = self.bluetooth_facade.get_device_time()
457
458        self.test_set_discovery_filter({'Transport': 'auto'})
459        self.test_start_discovery()
460        self.test_suspend_and_wait_for_sleep(suspend,
461                                             sleep_timeout=SUSPEND_SEC)
462
463        # If discovery events wake us early, we will raise and suspend.exitcode
464        # will be non-zero
465        self.test_wait_for_resume(boot_id,
466                                  suspend,
467                                  resume_timeout=EXPECT_NO_WAKE_SUSPEND_SEC,
468                                  test_start_time=start_time)
469
470        # Discovering should restore after suspend
471        self.test_is_discovering()
472        self.test_stop_discovery()
473
474    # TODO(b/150897528) - Scarlet Dru loses firmware around suspend
475    @test_wrapper('Suspend while advertising',
476                  devices={'MOUSE': 1},
477                  skip_models=['dru'],
478                  skip_chipsets=SUSPEND_POWER_DOWN_CHIPSETS)
479    def sr_while_advertising(self):
480        """ Suspend while advertising. """
481        device = self.devices['MOUSE'][0]
482        boot_id = self.host.get_boot_id()
483        suspend = self.suspend_async(suspend_time=EXPECT_NO_WAKE_SUSPEND_SEC)
484        start_time = self.bluetooth_facade.get_device_time()
485
486        self.test_discoverable()
487        self.test_suspend_and_wait_for_sleep(suspend,
488                                             sleep_timeout=SUSPEND_SEC)
489
490        # Peer device should not be able to discover us in suspend
491        self.test_discover_by_device_fails(device)
492
493        self.test_wait_for_resume(boot_id,
494                                  suspend,
495                                  resume_timeout=EXPECT_NO_WAKE_SUSPEND_SEC,
496                                  test_start_time=start_time)
497
498        # Test that we are properly discoverable again
499        self.test_is_discoverable()
500        self.test_discover_by_device(device)
501
502        self.test_nondiscoverable()
503
504    # ---------------------------------------------------------------
505    # Health checks
506    # ---------------------------------------------------------------
507
508    @test_wrapper('Suspend while powered off', devices={'MOUSE': 1})
509    def sr_while_powered_off(self):
510        """ Suspend while adapter is powered off. """
511        device = self.devices['MOUSE'][0]
512        boot_id = self.host.get_boot_id()
513        suspend = self.suspend_async(suspend_time=SUSPEND_SEC)
514        start_time = self.bluetooth_facade.get_device_time()
515
516        # Pair device so we have something to do in suspend
517        self.assert_discover_and_pair(device)
518
519        # Trigger power down and quickly suspend
520        self.test_power_off_adapter()
521        self.test_suspend_and_wait_for_sleep(suspend,
522                                             sleep_timeout=SUSPEND_SEC)
523        # Suspend and resume should succeed
524        self.test_wait_for_resume(boot_id,
525                                  suspend,
526                                  resume_timeout=SUSPEND_SEC,
527                                  test_start_time=start_time)
528
529        # We should be able to power it back on
530        self.test_power_on_adapter()
531
532        # Test that we can reconnect to the device after powering back on
533        self.test_connection_by_device(device)
534
535    @batch_wrapper('SR with Peer Health')
536    def sr_health_batch_run(self, num_iterations=1, test_name=None):
537        """ Batch of suspend/resume peer health tests. """
538        self.sr_reconnect_classic_hid()
539        self.sr_reconnect_le_hid()
540        self.sr_peer_wake_classic_hid()
541        self.sr_peer_wake_le_hid()
542        self.sr_while_discovering()
543        self.sr_while_advertising()
544        self.sr_reconnect_multiple_classic_hid()
545        self.sr_reconnect_multiple_le_hid()
546        self.sr_reconnect_multiple_classic_le_hid()
547
548    def run_once(self,
549                 host,
550                 num_iterations=1,
551                 args_dict=None,
552                 test_name=None,
553                 flag='Quick Health'):
554        """Running Bluetooth adapter suspend resume with peer autotest.
555
556        @param host: the DUT, usually a chromebook
557        @param num_iterations: the number of times to execute the test
558        @param test_name: the test to run or None for all tests
559        @param flag: run tests with this flag (default: Quick Health)
560
561        """
562
563        # Initialize and run the test batch or the requested specific test
564        self.quick_test_init(host,
565                             use_btpeer=True,
566                             flag=flag,
567                             args_dict=args_dict)
568        self.sr_health_batch_run(num_iterations, test_name)
569        self.quick_test_cleanup()
570