bluetooth: add a new bluetooth_AdapterPowerMeasure test

This patch adds a new test to measure the power consumption
during system suspension.

To run the test, a Raspberry Pi is required to act as the
chameleon host which installs the servo dependency as specified
in Cq-Depend:chromium:1981429

The only supported board is 'kukui' for now. The other boards will
be supported when a more generic method is figured out to specify the
config path for servod.

BUG=b:143862071
TEST=Follow the two steps below
Step 1: connect a servo micro between a DUT, i.e., Kukui, and a
Raspberry Pi.

Step 2: run the autotest as
(cros) test_that --autotest_dir ~/trunk/src/third_party/autotest/files/
       --args "chameleon_host=$CHAMELEON_IP" $DUT_IP
       bluetooth_AdapterPowerMeasure.suspension

       For a stress test of 100 iterations, use the control file:
       bluetooth_AdapterPowerMeasure.suspension_100

Cq-Depend:chromium:1981429, chromium:1980403
Change-Id: Id66f8b4dad911a462096fcada561a82113d78066
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/autotest/+/1900950
Reviewed-by: Abhishek Pandit-Subedi <abhishekpandit@chromium.org>
Reviewed-by: Yun-Hao Chung <howardchung@google.com>
Commit-Queue: Shyh-In Hwang <josephsih@chromium.org>
Tested-by: Shyh-In Hwang <josephsih@chromium.org>
diff --git a/client/cros/chameleon/chameleon.py b/client/cros/chameleon/chameleon.py
index 37bd6a1..4f14c8c 100644
--- a/client/cros/chameleon/chameleon.py
+++ b/client/cros/chameleon/chameleon.py
@@ -400,6 +400,16 @@
         return self._usb_ctrl
 
 
+    def get_bluetooth_base(self):
+        """Gets the Bluetooth base object on Chameleon.
+
+        This is a base object that does not emulate any Bluetooth device.
+
+        @return: A BluetoothBaseFlow object.
+        """
+        return self._chameleond_proxy.bluetooth_base
+
+
     def get_bluetooth_hid_mouse(self):
         """Gets the emulated Bluetooth (BR/EDR) HID mouse on Chameleon.
 
diff --git a/server/cros/bluetooth/bluetooth_adapter_tests.py b/server/cros/bluetooth/bluetooth_adapter_tests.py
index bb2d4c5..780d270 100644
--- a/server/cros/bluetooth/bluetooth_adapter_tests.py
+++ b/server/cros/bluetooth/bluetooth_adapter_tests.py
@@ -40,6 +40,11 @@
     'BLE_MOUSE': lambda chameleon: chameleon.get_ble_mouse,
     'BLE_KEYBOARD': lambda chameleon: chameleon.get_ble_keyboard,
     'A2DP_SINK': lambda chameleon: chameleon.get_bluetooth_a2dp_sink,
+
+    # This is a base object that does not emulate any Bluetooth device.
+    # This object is preferred when only a pure XMLRPC server is needed
+    # on the chameleon host, e.g., to perform servod methods.
+    'BLUETOOTH_BASE': lambda chameleon: chameleon.get_bluetooth_base,
 }
 
 
@@ -546,8 +551,7 @@
 
 
     def group_chameleons_type(self):
-        """Group all chameleons by the type of their detected device
-        """
+        """Group all chameleons by the type of their detected device."""
 
         # Use previously created chameleon_group instead of creating new
         if len(self.chameleon_group_copy) > 0:
@@ -2779,6 +2783,27 @@
 
 
     # -------------------------------------------------------------------
+    # Servod related tests
+    # -------------------------------------------------------------------
+
+    @_test_retry_and_log
+    def test_power_consumption(self, max_power_mw):
+        """Test the average power consumption."""
+        power_mw = self.device.servod.MeasurePowerConsumption()
+        self.results = {'power_mw': power_mw}
+
+        if (power_mw is None):
+            logging.error('Failed to measure power consumption')
+            return False
+
+        power_mw = float(power_mw)
+        logging.info('power consumption (mw): %f (max allowed: %f)',
+                     power_mw, max_power_mw)
+
+        return power_mw <= max_power_mw
+
+
+    # -------------------------------------------------------------------
     # Autotest methods
     # -------------------------------------------------------------------
 
diff --git a/server/hosts/cros_label.py b/server/hosts/cros_label.py
index a25b12a..186a2e1 100644
--- a/server/hosts/cros_label.py
+++ b/server/hosts/cros_label.py
@@ -366,6 +366,14 @@
                 logging.error('Error with initializing bt_a2dp_sink on '
                               'chameleon %s', chameleon_host.hostname)
 
+            try:
+                bt_base_device = chameleon.get_bluetooth_base()
+                if bt_base_device.IsDetected():
+                    labels.append('bt_base')
+            except:
+                logging.error('Error in detecting bt_base on '
+                              'chameleon %s', chameleon_host.hostname)
+
             if labels != []:
                 labels.append('bt_peer')
 
diff --git a/server/site_tests/bluetooth_AdapterPowerMeasure/bluetooth_AdapterPowerMeasure.py b/server/site_tests/bluetooth_AdapterPowerMeasure/bluetooth_AdapterPowerMeasure.py
new file mode 100644
index 0000000..14073eb
--- /dev/null
+++ b/server/site_tests/bluetooth_AdapterPowerMeasure/bluetooth_AdapterPowerMeasure.py
@@ -0,0 +1,172 @@
+# Copyright 2019 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Server side bluetooth adapter stress tests involving power consumption."""
+
+import logging
+import multiprocessing
+import time
+
+from autotest_lib.client.common_lib import error
+from autotest_lib.server.cros.bluetooth import bluetooth_adapter_tests
+from autotest_lib.server.cros.multimedia import remote_facade_factory
+
+
+test_case_log = bluetooth_adapter_tests.test_case_log
+
+
+class bluetooth_AdapterPowerMeasure(
+        bluetooth_adapter_tests.BluetoothAdapterTests):
+    """Server side bluetooth adapter power consumption test."""
+
+    # ---------------------------------------------------------------
+    # Definitions of all test cases
+    # ---------------------------------------------------------------
+
+    @test_case_log
+    def test_case_suspend_power_measurement(self, host, max_power_mw,
+                                            suspend_time_secs,
+                                            resume_network_timeout_secs=60):
+        """Test Case: measure the Bluetooth chip power consumption on suspend"""
+
+        def print_debug_count():
+            """Print the debug message about count values."""
+            logging.debug('count_fail_to_sleep: %d', self.count_fail_to_sleep)
+            logging.debug('count_fail_to_resume: %d', self.count_fail_to_resume)
+            logging.debug('count_system_resume_prematurely: %d',
+                          self.count_system_resume_prematurely)
+            logging.debug('count_success: %d', self.count_success)
+
+        def action_suspend():
+            """Calls the host method suspend."""
+            host.suspend(suspend_time=suspend_time_secs,
+                         allow_early_resume=True)
+
+        boot_id = host.get_boot_id()
+        proc = multiprocessing.Process(target=action_suspend)
+        proc.daemon = True
+        start_time = time.time()
+        proc.start()
+
+        # Block waiting until the system has suspended.
+        try:
+            host.test_wait_for_sleep(suspend_time_secs)
+        except Exception as e:
+            logging.error('host.test_wait_for_sleep failed: %s', e)
+            self.count_fail_to_sleep += 1
+            print_debug_count()
+            # Skip this time since the system failed to suspend.
+            proc.join()
+            return
+
+        # Test the Bluetooth chip power consumption.
+        if self.test_power_consumption(max_power_mw):
+            self.count_success += 1
+
+        # Block waiting until the system has resumed.
+        try:
+            host.test_wait_for_resume(
+                    boot_id, suspend_time_secs + resume_network_timeout_secs)
+        except Exception as e:
+            logging.error('host.test_wait_for_resume failed: %s', e)
+            self.count_fail_to_resume += 1
+
+        # If the system resumes prematurely, do not conduct the test in
+        # this iteration.
+        actual_suspend_time_secs = time.time() - start_time
+        if actual_suspend_time_secs < suspend_time_secs:
+            logging.error('actual suspension time %f is less than expected %f',
+                          actual_suspend_time_secs, suspend_time_secs)
+            self.count_system_resume_prematurely += 1
+
+        print_debug_count()
+        proc.join()
+
+
+    def initialize_servod(self):
+        """Peform initialize for servod task."""
+        self.count_fail_to_sleep = 0
+        self.count_fail_to_resume = 0
+        self.count_system_resume_prematurely = 0
+        self.count_success = 0
+
+        # When the autotest restarts ui, chrome would issue some Bluetooth
+        # commands which may prevent the system from suspending properly.
+        # Hence, let's stop ui for now.
+        self.host.run_short('stop ui')
+
+        board = self.host.get_board().split(':')[1]
+        logging.info('board: %s', board)
+
+        # TODO: figure out a way to support other boards.
+        if board != 'kukui':
+            raise error.TestError('Only kukui is supported for now.')
+
+        # self.device is a pure XMLRPC server running as chameleond
+        # on the chameleon host. We need to enable Servod.
+        if not self.device.EnableServod(board):
+            raise error.TestError('Failed to enable Servod.')
+
+        # Start the Servod process on the chameleon host.
+        if not self.device.servod.Start():
+            raise error.TestError('Failed to start Servod on chameleon host.')
+
+
+    def cleanup_servod(self):
+        """Peform cleanup for servod."""
+        if not self.device.servod.Stop():
+            logging.error('Failed to stop Servod on chameleon host.')
+
+        self.host.run_short('start ui')
+
+        logging.info('count_fail_to_sleep: %d', self.count_fail_to_sleep)
+        logging.info('count_fail_to_resume: %d', self.count_fail_to_resume)
+        logging.info('count_system_resume_prematurely: %d',
+                     self.count_system_resume_prematurely)
+        logging.info('count_success: %d', self.count_success)
+
+
+    def run_once(self, host, max_power_mw=3, device_type='BLUETOOTH_BASE',
+                 num_iterations=1, suspend_time_secs=30,
+                 test_category='suspension'):
+        """Running Bluetooth adapter power consumption autotest during system
+        suspension.
+
+        @param host: the DUT host.
+        @param max_power_mw: max power allowed in milli-watt
+        @param device_type: the device type emulated by the chameleon host
+        @param num_iterations: number of times to perform the tests.
+        @param suspend_time_secs: the system suspension duration in seconds
+        @param test_category: the test category
+
+        """
+
+        self.host = host
+        factory = remote_facade_factory.RemoteFacadeFactory(host)
+        self.bluetooth_facade = factory.create_bluetooth_hid_facade()
+
+        self.check_chameleon()
+        self.device = self.get_device(device_type)
+        self.initialize_servod()
+        self.test_power_on_adapter()
+        self.test_bluetoothd_running()
+
+        for i in xrange(1, num_iterations + 1):
+            logging.info('Starting iteration: %d / %d', i, num_iterations)
+
+            if test_category == 'suspension':
+                self.test_case_suspend_power_measurement(host, max_power_mw,
+                                                         suspend_time_secs)
+            else:
+                logging.error('Do not support the test category: %s',
+                              test_category)
+
+        if device_type == 'BLUETOOTH_BASE':
+            self.cleanup_servod()
+
+        if self.count_success == 0:
+            raise error.TestError('System failed to suspend/resume.')
+
+        if self.fails:
+            raise error.TestFail(self.fails)
diff --git a/server/site_tests/bluetooth_AdapterPowerMeasure/control.suspension b/server/site_tests/bluetooth_AdapterPowerMeasure/control.suspension
new file mode 100644
index 0000000..d74679b
--- /dev/null
+++ b/server/site_tests/bluetooth_AdapterPowerMeasure/control.suspension
@@ -0,0 +1,36 @@
+# Copyright 2019 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from autotest_lib.server import utils
+from autotest_lib.server.cros.bluetooth import advertisements_data
+
+
+AUTHOR = 'Chrome OS Team'
+NAME = 'bluetooth_AdapterPowerMeasure.suspension'
+PURPOSE = 'Test power consumption of Bluetooth chip during system suspension.'
+CRITERIA = 'Bluetooth chip should consume power less than specified.'
+ATTRIBUTES = 'suite:bluetooth'
+TIME = 'SHORT'   # Takes about 2 mins
+TEST_CATEGORY = 'Functional'
+TEST_CLASS = 'bluetooth'
+TEST_TYPE = 'server'
+DEPENDENCIES = 'bluetooth, chameleon:bt_base'
+
+DOC = """
+This test case verifies that the Bluetooth chip of the DUT does
+not consume power more than specified during system suspension.
+
+This autotest include the following test cases:
+     self.test_case_suspend_power_measurement()
+"""
+
+args_dict = utils.args_to_dict(args)
+chameleon_args = hosts.CrosHost.get_chameleon_arguments(args_dict)
+
+def run(machine):
+    host = hosts.create_host(machine, chameleon_args=chameleon_args)
+    job.run_test('bluetooth_AdapterPowerMeasure', host=host, max_power_mw=3,
+                 num_iterations=1)
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/bluetooth_AdapterPowerMeasure/control.suspension_100 b/server/site_tests/bluetooth_AdapterPowerMeasure/control.suspension_100
new file mode 100644
index 0000000..79c557d
--- /dev/null
+++ b/server/site_tests/bluetooth_AdapterPowerMeasure/control.suspension_100
@@ -0,0 +1,36 @@
+# Copyright 2019 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from autotest_lib.server import utils
+from autotest_lib.server.cros.bluetooth import advertisements_data
+
+
+AUTHOR = 'Chrome OS Team'
+NAME = 'bluetooth_AdapterPowerMeasure.suspension_100'
+PURPOSE = 'Test power consumption of Bluetooth chip during system suspension.'
+CRITERIA = 'Bluetooth chip should consume power less than specified.'
+ATTRIBUTES = 'suite:bluetooth'
+TIME = 'LONG'   # Takes about 60 mins
+TEST_CATEGORY = 'Functional'
+TEST_CLASS = 'bluetooth'
+TEST_TYPE = 'server'
+DEPENDENCIES = 'bluetooth, chameleon:bt_base'
+
+DOC = """
+This test case verifies that the Bluetooth chip of the DUT does
+not consume power more than specified during system suspension.
+
+This autotest include the following test cases:
+     self.test_case_suspend_power_measurement()
+"""
+
+args_dict = utils.args_to_dict(args)
+chameleon_args = hosts.CrosHost.get_chameleon_arguments(args_dict)
+
+def run(machine):
+    host = hosts.create_host(machine, chameleon_args=chameleon_args)
+    job.run_test('bluetooth_AdapterPowerMeasure', host=host, max_power_mw=3,
+                 num_iterations=100)
+
+parallel_simple(run, machines)