Merge "Fixed a couple Fuchsia Device excemptions and add get_info to attenuator"
diff --git a/acts/framework/acts/controllers/access_point.py b/acts/framework/acts/controllers/access_point.py
index 4b89de6..38db6a7 100755
--- a/acts/framework/acts/controllers/access_point.py
+++ b/acts/framework/acts/controllers/access_point.py
@@ -150,6 +150,7 @@
         self.lan = self.interfaces.get_lan_interface()
         self.__initial_ap()
         self.scapy_install_path = None
+        self.setup_bridge = False
 
     def __initial_ap(self):
         """Initial AP interfaces.
@@ -179,7 +180,10 @@
                 self.ssh.run(BRIDGE_DOWN)
                 self.ssh.run(BRIDGE_DEL)
 
-    def start_ap(self, hostapd_config, additional_parameters=None):
+    def start_ap(self,
+                 hostapd_config,
+                 setup_bridge=False,
+                 additional_parameters=None):
         """Starts as an ap using a set of configurations.
 
         This will start an ap on this host. To start an ap the controller
@@ -190,11 +194,13 @@
 
         Args:
             hostapd_config: hostapd_config.HostapdConfig, The configurations
-                            to use when starting up the ap.
+                to use when starting up the ap.
+            setup_bridge: Whether to bridge the LAN interface WLAN interface.
+                Only one WLAN interface can be bridged with the LAN interface
+                and none of the guest networks can be bridged.
             additional_parameters: A dictionary of parameters that can sent
-                                   directly into the hostapd config file.  This
-                                   can be used for debugging and or adding one
-                                   off parameters into the config.
+                directly into the hostapd config file.  This can be used for
+                debugging and or adding one off parameters into the config.
 
         Returns:
             An identifier for each ssid being started. These identifiers can be
@@ -203,7 +209,6 @@
         Raises:
             Error: When the ap can't be brought up.
         """
-
         if hostapd_config.frequency < 5000:
             interface = self.wlan_2g
             subnet = self._AP_2G_SUBNET
@@ -275,7 +280,12 @@
         # the server will come up.
         interface_ip = ipaddress.ip_interface(
             '%s/%s' % (subnet.router, subnet.network.netmask))
-        self._ip_cmd.set_ipv4_address(interface, interface_ip)
+        if setup_bridge is True:
+            bridge_interface_name = 'br_lan'
+            self.create_bridge(bridge_interface_name, [interface, self.lan])
+            self._ip_cmd.set_ipv4_address(bridge_interface_name, interface_ip)
+        else:
+            self._ip_cmd.set_ipv4_address(interface, interface_ip)
         if hostapd_config.bss_lookup:
             # This loop goes through each interface that was setup for
             # hostapd and assigns the DHCP scopes that were defined but
@@ -442,6 +452,13 @@
         del self._aps[identifier]
         if configured_subnets:
             self.start_dhcp(subnets=configured_subnets)
+        bridge_interfaces = self.interfaces.get_bridge_interface()
+        if bridge_interfaces:
+            for iface in bridge_interfaces:
+                BRIDGE_DOWN = 'ifconfig {} down'.format(iface)
+                BRIDGE_DEL = 'brctl delbr {}'.format(iface)
+                self.ssh.run(BRIDGE_DOWN)
+                self.ssh.run(BRIDGE_DEL)
 
     def stop_all_aps(self):
         """Stops all running aps on this device."""
diff --git a/acts/framework/acts/controllers/iperf_client.py b/acts/framework/acts/controllers/iperf_client.py
index 40c6993..046025f 100644
--- a/acts/framework/acts/controllers/iperf_client.py
+++ b/acts/framework/acts/controllers/iperf_client.py
@@ -23,6 +23,8 @@
 from acts import utils
 from acts.controllers.android_device import AndroidDevice
 from acts.controllers.iperf_server import _AndroidDeviceBridge
+from acts.controllers.fuchsia_lib.utils_lib import create_ssh_connection
+from acts.controllers.fuchsia_lib.utils_lib import SshResults
 from acts.controllers.utils_lib.ssh import connection
 from acts.controllers.utils_lib.ssh import settings
 from acts.event import event_bus
@@ -51,12 +53,24 @@
         if type(c) is dict and 'AndroidDevice' in c:
             results.append(IPerfClientOverAdb(c['AndroidDevice']))
         elif type(c) is dict and 'ssh_config' in c:
-            results.append(IPerfClientOverSsh(c['ssh_config']))
+            results.append(
+                IPerfClientOverSsh(c['ssh_config'],
+                                   use_paramiko=c.get('use_paramiko'),
+                                   test_interface=c.get('test_interface')))
         else:
             results.append(IPerfClient())
     return results
 
 
+def get_info(iperf_clients):
+    """Placeholder for info about iperf clients
+
+    Returns:
+        None
+    """
+    return None
+
+
 def destroy(_):
     # No cleanup needed.
     pass
@@ -98,7 +112,7 @@
 
         return os.path.join(full_out_dir, out_file_name)
 
-    def start(self, ip, iperf_args, tag, timeout=3600):
+    def start(self, ip, iperf_args, tag, iperf_binary=None, timeout=3600):
         """Starts iperf client, and waits for completion.
 
         Args:
@@ -106,6 +120,8 @@
             iperf_args: A string representing arguments to start iperf
                 client. Eg: iperf_args = "-t 10 -p 5001 -w 512k/-u -b 200M -J".
             tag: A string to further identify iperf results file
+            iperf_binary: Location of iperf3 binary. If none, it is assumed the
+                the binary is in the path.
             timeout: the maximum amount of time the iperf client can run.
 
         Returns:
@@ -116,8 +132,7 @@
 
 class IPerfClient(IPerfClientBase):
     """Class that handles iperf3 client operations."""
-
-    def start(self, ip, iperf_args, tag, timeout=3600):
+    def start(self, ip, iperf_args, tag, iperf_binary=None, timeout=3600):
         """Starts iperf client, and waits for completion.
 
         Args:
@@ -125,12 +140,20 @@
             iperf_args: A string representing arguments to start iperf
             client. Eg: iperf_args = "-t 10 -p 5001 -w 512k/-u -b 200M -J".
             tag: tag to further identify iperf results file
+            iperf_binary: Location of iperf3 binary. If none, it is assumed the
+                the binary is in the path.
             timeout: unused.
 
         Returns:
             full_out_path: iperf result path.
         """
-        iperf_cmd = ['iperf3', '-c', ip] + iperf_args.split(' ')
+        if not iperf_binary:
+            logging.debug('No iperf3 binary specified.  '
+                          'Assuming iperf3 is in the path.')
+            iperf_binary = 'iperf3'
+        else:
+            logging.debug('Using iperf3 binary located at %s' % iperf_binary)
+        iperf_cmd = [str(iperf_binary), '-c', ip] + iperf_args.split(' ')
         full_out_path = self._get_full_file_path(tag)
 
         with open(full_out_path, 'w') as out_file:
@@ -141,12 +164,21 @@
 
 class IPerfClientOverSsh(IPerfClientBase):
     """Class that handles iperf3 client operations on remote machines."""
-
-    def __init__(self, ssh_config):
+    def __init__(self, ssh_config, use_paramiko=False, test_interface=None):
         self._ssh_settings = settings.from_config(ssh_config)
-        self._ssh_session = connection.SshConnection(self._ssh_settings)
+        self._use_paramiko = use_paramiko
+        if str(self._use_paramiko) == 'True':
+            self._ssh_session = create_ssh_connection(
+                ip_address=ssh_config['host'],
+                ssh_username=ssh_config['user'],
+                ssh_config=ssh_config['ssh_config'])
+        else:
+            self._ssh_session = connection.SshConnection(self._ssh_settings)
 
-    def start(self, ip, iperf_args, tag, timeout=3600):
+        self.hostname = self._ssh_settings.hostname
+        self.test_interface = test_interface
+
+    def start(self, ip, iperf_args, tag, iperf_binary=None, timeout=3600):
         """Starts iperf client, and waits for completion.
 
         Args:
@@ -154,16 +186,34 @@
             iperf_args: A string representing arguments to start iperf
             client. Eg: iperf_args = "-t 10 -p 5001 -w 512k/-u -b 200M -J".
             tag: tag to further identify iperf results file
+            iperf_binary: Location of iperf3 binary. If none, it is assumed the
+                the binary is in the path.
             timeout: the maximum amount of time to allow the iperf client to run
 
         Returns:
             full_out_path: iperf result path.
         """
-        iperf_cmd = 'iperf3 -c {} {}'.format(ip, iperf_args)
+        if not iperf_binary:
+            logging.debug('No iperf3 binary specified.  '
+                          'Assuming iperf3 is in the path.')
+            iperf_binary = 'iperf3'
+        else:
+            logging.debug('Using iperf3 binary located at %s' % iperf_binary)
+        iperf_cmd = '{} -c {} {}'.format(iperf_binary, ip, iperf_args)
         full_out_path = self._get_full_file_path(tag)
 
         try:
-            iperf_process = self._ssh_session.run(iperf_cmd, timeout=timeout)
+            if self._use_paramiko:
+                cmd_result_stdin, cmd_result_stdout, cmd_result_stderr = (
+                    self._ssh_session.exec_command(iperf_cmd, timeout=timeout))
+                cmd_result_exit_status = (
+                    cmd_result_stdout.channel.recv_exit_status())
+                iperf_process = SshResults(cmd_result_stdin, cmd_result_stdout,
+                                           cmd_result_stderr,
+                                           cmd_result_exit_status)
+            else:
+                iperf_process = self._ssh_session.run(iperf_cmd,
+                                                      timeout=timeout)
             iperf_output = iperf_process.stdout
             with open(full_out_path, 'w') as out_file:
                 out_file.write(iperf_output)
@@ -175,7 +225,6 @@
 
 class IPerfClientOverAdb(IPerfClientBase):
     """Class that handles iperf3 operations over ADB devices."""
-
     def __init__(self, android_device_or_serial):
         """Creates a new IPerfClientOverAdb object.
 
@@ -195,7 +244,7 @@
             return _AndroidDeviceBridge.android_devices()[
                 self._android_device_or_serial]
 
-    def start(self, ip, iperf_args, tag, timeout=3600):
+    def start(self, ip, iperf_args, tag, iperf_binary=None, timeout=3600):
         """Starts iperf client, and waits for completion.
 
         Args:
@@ -203,6 +252,8 @@
             iperf_args: A string representing arguments to start iperf
             client. Eg: iperf_args = "-t 10 -p 5001 -w 512k/-u -b 200M -J".
             tag: tag to further identify iperf results file
+            iperf_binary: Location of iperf3 binary. If none, it is assumed the
+                the binary is in the path.
             timeout: the maximum amount of time to allow the iperf client to run
 
         Returns:
@@ -210,8 +261,19 @@
         """
         iperf_output = ''
         try:
-            iperf_status, iperf_output = self._android_device.run_iperf_client(
-                ip, iperf_args, timeout=timeout)
+            if not iperf_binary:
+                logging.debug('No iperf3 binary specified.  '
+                              'Assuming iperf3 is in the path.')
+                iperf_binary = 'iperf3'
+            else:
+                logging.debug('Using iperf3 binary located at %s' %
+                              iperf_binary)
+            iperf_cmd = '{} -c {} {}'.format(iperf_binary, ip, iperf_args)
+            out = self._android_device.adb.shell(str(iperf_cmd),
+                                                 timeout=timeout)
+            clean_out = out.split('\n')
+            if "error" in clean_out[0].lower():
+                raise Exception('clean_out')
         except job.TimeoutError:
             logging.warning('TimeoutError: Iperf measurement timed out.')
 
diff --git a/acts/framework/acts/controllers/iperf_server.py b/acts/framework/acts/controllers/iperf_server.py
index 039f143..3243e70 100755
--- a/acts/framework/acts/controllers/iperf_server.py
+++ b/acts/framework/acts/controllers/iperf_server.py
@@ -17,6 +17,7 @@
 import json
 import logging
 import math
+import IPy
 import os
 import shlex
 import subprocess
@@ -36,6 +37,10 @@
 
 ACTS_CONTROLLER_CONFIG_NAME = 'IPerfServer'
 ACTS_CONTROLLER_REFERENCE_NAME = 'iperf_servers'
+KILOBITS = 1024
+MEGABITS = KILOBITS * 1024
+GIGABITS = MEGABITS * 1024
+BITS_IN_BYTE = 8
 
 
 def create(configs):
@@ -56,7 +61,10 @@
         elif type(c) is dict and 'AndroidDevice' in c and 'port' in c:
             results.append(IPerfServerOverAdb(c['AndroidDevice'], c['port']))
         elif type(c) is dict and 'ssh_config' in c and 'port' in c:
-            results.append(IPerfServerOverSsh(c['ssh_config'], c['port']))
+            results.append(
+                IPerfServerOverSsh(c['ssh_config'],
+                                   c['port'],
+                                   test_interface=c.get('test_interface')))
         else:
             raise ValueError(
                 'Config entry %s in %s is not a valid IPerfServer '
@@ -64,6 +72,15 @@
     return results
 
 
+def get_info(iperf_servers):
+    """Placeholder for info about iperf servers
+
+    Returns:
+        None
+    """
+    return None
+
+
 def destroy(iperf_server_list):
     for iperf_server in iperf_server_list:
         try:
@@ -73,7 +90,7 @@
 
 
 class IPerfResult(object):
-    def __init__(self, result_path):
+    def __init__(self, result_path, reporting_speed_units='Mbytes'):
         """Loads iperf result from file.
 
         Loads iperf result from JSON formatted server log. File can be accessed
@@ -82,6 +99,7 @@
         containing multiple iperf client runs.
         """
         # if result_path isn't a path, treat it as JSON
+        self.reporting_speed_units = reporting_speed_units
         if not os.path.exists(result_path):
             self.result = json.loads(result_path)
         else:
@@ -89,8 +107,8 @@
                 with open(result_path, 'r') as f:
                     iperf_output = f.readlines()
                     if '}\n' in iperf_output:
-                        iperf_output = iperf_output[:iperf_output.index('}\n')
-                                                    + 1]
+                        iperf_output = iperf_output[:iperf_output.index('}\n'
+                                                                        ) + 1]
                     iperf_string = ''.join(iperf_output)
                     iperf_string = iperf_string.replace('nan', '0')
                     self.result = json.loads(iperf_string)
@@ -110,6 +128,33 @@
         return ('end' in self.result) and ('sum_received' in self.result['end']
                                            or 'sum' in self.result['end'])
 
+    def _get_reporting_speed(self, network_speed_in_bits_per_second):
+        """Sets the units for the network speed reporting based on how the
+        object was initiated.  Defaults to Megabytes per second.  Currently
+        supported, bits per second (bits), kilobits per second (kbits), megabits
+        per second (mbits), gigabits per second (gbits), bytes per second
+        (bytes), kilobits per second (kbytes), megabits per second (mbytes),
+        gigabytes per second (gbytes).
+
+        Args:
+            network_speed_in_bits_per_second: The network speed from iperf in
+                bits per second.
+
+        Returns:
+            The value of the throughput in the appropriate units.
+        """
+        speed_divisor = 1
+        print(self.reporting_speed_units)
+        if self.reporting_speed_units[1:].lower() == 'bytes':
+            speed_divisor = speed_divisor * BITS_IN_BYTE
+        if self.reporting_speed_units[0:1].lower() == 'k':
+            speed_divisor = speed_divisor * KILOBITS
+        if self.reporting_speed_units[0:1].lower() == 'm':
+            speed_divisor = speed_divisor * MEGABITS
+        if self.reporting_speed_units[0:1].lower() == 'g':
+            speed_divisor = speed_divisor * GIGABITS
+        return network_speed_in_bits_per_second / speed_divisor
+
     def get_json(self):
         """Returns the raw json output from iPerf."""
         return self.result
@@ -131,7 +176,7 @@
         if not self._has_data() or 'sum' not in self.result['end']:
             return None
         bps = self.result['end']['sum']['bits_per_second']
-        return bps / 8 / 1024 / 1024
+        return self._get_reporting_speed(bps)
 
     @property
     def avg_receive_rate(self):
@@ -143,7 +188,7 @@
         if not self._has_data() or 'sum_received' not in self.result['end']:
             return None
         bps = self.result['end']['sum_received']['bits_per_second']
-        return bps / 8 / 1024 / 1024
+        return self._get_reporting_speed(bps)
 
     @property
     def avg_send_rate(self):
@@ -155,7 +200,7 @@
         if not self._has_data() or 'sum_sent' not in self.result['end']:
             return None
         bps = self.result['end']['sum_sent']['bits_per_second']
-        return bps / 8 / 1024 / 1024
+        return self._get_reporting_speed(bps)
 
     @property
     def instantaneous_rates(self):
@@ -167,7 +212,7 @@
         if not self._has_data():
             return None
         intervals = [
-            interval['sum']['bits_per_second'] / 8 / 1024 / 1024
+            self._get_reporting_speed(interval['sum']['bits_per_second'])
             for interval in self.result['intervals']
         ]
         return intervals
@@ -199,10 +244,11 @@
         """
         if not self._has_data():
             return None
-        instantaneous_rates = self.instantaneous_rates[iperf_ignored_interval:
-                                                       -1]
+        instantaneous_rates = self.instantaneous_rates[
+            iperf_ignored_interval:-1]
         avg_rate = math.fsum(instantaneous_rates) / len(instantaneous_rates)
-        sqd_deviations = [(rate - avg_rate) ** 2 for rate in instantaneous_rates]
+        sqd_deviations = ([(rate - avg_rate)**2
+                           for rate in instantaneous_rates])
         std_dev = math.sqrt(
             math.fsum(sqd_deviations) / (len(sqd_deviations) - 1))
         return std_dev
@@ -256,7 +302,7 @@
         Note: If the directory for the file path does not exist, it will be
         created.
 
-        Args:
+        Args:s
             tag: The tag passed in to the server run.
         """
         out_dir = self.log_path
@@ -297,7 +343,6 @@
 
 class IPerfServer(IPerfServerBase):
     """Class that handles iperf server commands on localhost."""
-
     def __init__(self, port=5201):
         super().__init__(port)
         self._hinted_port = port
@@ -335,8 +380,9 @@
         if self._last_opened_file:
             self._last_opened_file.close()
         self._last_opened_file = open(self._current_log_file, 'w')
-        self._iperf_process = subprocess.Popen(
-            command, stdout=self._last_opened_file, stderr=subprocess.DEVNULL)
+        self._iperf_process = subprocess.Popen(command,
+                                               stdout=self._last_opened_file,
+                                               stderr=subprocess.DEVNULL)
         for attempts_left in reversed(range(3)):
             try:
                 self._port = int(
@@ -374,15 +420,22 @@
 
 class IPerfServerOverSsh(IPerfServerBase):
     """Class that handles iperf3 operations on remote machines."""
-
-    def __init__(self, ssh_config, port):
+    def __init__(self, ssh_config, port, test_interface=None):
         super().__init__(port)
         ssh_settings = settings.from_config(ssh_config)
         self._ssh_session = connection.SshConnection(ssh_settings)
 
-        self._iperf_command = 'iperf3 -s -J -p {}'.format(self.port)
         self._iperf_pid = None
         self._current_tag = None
+        self.hostname = ssh_settings.hostname
+        try:
+            # A test interface can only be found if an ip address is specified.
+            # A fully qualified hostname will return None for the
+            # test_interface.
+            self.test_interface = self._get_test_interface_based_on_ip(
+                test_interface)
+        except Exception:
+            self.test_interface = None
 
     @property
     def port(self):
@@ -395,20 +448,69 @@
     def _get_remote_log_path(self):
         return 'iperf_server_port%s.log' % self.port
 
-    def start(self, extra_args='', tag=''):
+    def _get_test_interface_based_on_ip(self, test_interface):
+        """Gets the test interface for a particular IP if the test interface
+            passed in test_interface is None
+
+        Args:
+            test_interface: Either a interface name, ie eth0, or None
+
+        Returns:
+            The name of the test interface.
+        """
+        if test_interface:
+            return test_interface
+        return utils.get_interface_based_on_ip(self._ssh_session,
+                                               self.hostname)
+
+    def get_interface_ip_addresses(self, interface):
+        """Gets all of the ip addresses, ipv4 and ipv6, associated with a
+           particular interface name.
+
+        Args:
+            interface: The interface name on the device, ie eth0
+
+    Returns:
+        A list of dictionaries of the the various IP addresses:
+            ipv4_private_local_addresses: Any 192.168, 172.16, or 10
+                addresses
+            ipv4_public_addresses: Any IPv4 public addresses
+            ipv6_link_local_addresses: Any fe80:: addresses
+            ipv6_private_local_addresses: Any fd00:: addresses
+            ipv6_public_addresses: Any publicly routable addresses
+    """
+        return utils.get_interface_ip_addresses(self._ssh_session, interface)
+
+    def renew_test_interface_ip_address(self):
+        """Renews the test interface's IP address.  Necessary for changing
+           DHCP scopes during a test.
+        """
+        utils.renew_linux_ip_address(self._ssh_session, self.test_interface)
+
+    def start(self, extra_args='', iperf_binary=None, tag=''):
         """Starts iperf server on specified machine and port.
 
         Args:
             extra_args: A string representing extra arguments to start iperf
                 server with.
+            iperf_binary: Location of iperf3 binary. If none, it is assumed the
+                the binary is in the path.
             tag: Appended to log file name to identify logs from different
                 iperf runs.
         """
         if self.started:
             return
 
+        if not iperf_binary:
+            logging.debug('No iperf3 binary specified.  '
+                          'Assuming iperf3 is in the path.')
+            iperf_binary = 'iperf3'
+        else:
+            logging.debug('Using iperf3 binary located at %s' % iperf_binary)
+        iperf_command = '{} -s -J -p {}'.format(iperf_binary, self.port)
+
         cmd = '{cmd} {extra_flags} > {log_file}'.format(
-            cmd=self._iperf_command,
+            cmd=iperf_command,
             extra_flags=extra_args,
             log_file=self._get_remote_log_path())
 
@@ -463,8 +565,10 @@
         """
         if not _AndroidDeviceBridge._test_class:
             return {}
-        return {device.serial: device
-                for device in _AndroidDeviceBridge._test_class.android_devices}
+        return {
+            device.serial: device
+            for device in _AndroidDeviceBridge._test_class.android_devices
+        }
 
 
 event_bus.register_subscription(
@@ -474,7 +578,6 @@
 
 class IPerfServerOverAdb(IPerfServerBase):
     """Class that handles iperf3 operations over ADB devices."""
-
     def __init__(self, android_device_or_serial, port):
         """Creates a new IPerfServerOverAdb object.
 
@@ -488,7 +591,6 @@
         super().__init__(port)
         self._android_device_or_serial = android_device_or_serial
 
-        self._iperf_command = 'iperf3 -s -J -p {}'.format(self.port)
         self._iperf_process = None
         self._current_tag = ''
 
@@ -511,21 +613,31 @@
     def _get_device_log_path(self):
         return '~/data/iperf_server_port%s.log' % self.port
 
-    def start(self, extra_args='', tag=''):
+    def start(self, extra_args='', iperf_binary=None, tag=''):
         """Starts iperf server on an ADB device.
 
         Args:
             extra_args: A string representing extra arguments to start iperf
                 server with.
+            iperf_binary: Location of iperf3 binary. If none, it is assumed the
+                the binary is in the path.
             tag: Appended to log file name to identify logs from different
                 iperf runs.
         """
         if self._iperf_process is not None:
             return
 
+        if not iperf_binary:
+            logging.debug('No iperf3 binary specified.  '
+                          'Assuming iperf3 is in the path.')
+            iperf_binary = 'iperf3'
+        else:
+            logging.debug('Using iperf3 binary located at %s' % iperf_binary)
+        iperf_command = '{} -s -J -p {}'.format(iperf_binary, self.port)
+
         self._iperf_process = self._android_device.adb.shell_nb(
             '{cmd} {extra_flags} > {log_file}'.format(
-                cmd=self._iperf_command,
+                cmd=iperf_command,
                 extra_flags=extra_args,
                 log_file=self._get_device_log_path()))
         self._iperf_process_adb_pid = ''
diff --git a/acts/framework/acts/test_utils/abstract_devices/utils_lib/wlan_utils.py b/acts/framework/acts/test_utils/abstract_devices/utils_lib/wlan_utils.py
index 745d35f..d4af3c3 100644
--- a/acts/framework/acts/test_utils/abstract_devices/utils_lib/wlan_utils.py
+++ b/acts/framework/acts/test_utils/abstract_devices/utils_lib/wlan_utils.py
@@ -25,8 +25,8 @@
 
        Args: Args match setup_ap_and_associate
     """
-    asserts.assert_true(
-        setup_ap_and_associate(*args, **kwargs), 'Failed to associate.')
+    asserts.assert_true(setup_ap_and_associate(*args, **kwargs),
+                        'Failed to associate.')
     asserts.explicit_pass('Successfully associated.')
 
 
@@ -49,7 +49,8 @@
                            check_connectivity=False,
                            n_capabilities=None,
                            ac_capabilities=None,
-                           vht_bandwidth=None):
+                           vht_bandwidth=None,
+                           setup_bridge=False):
     """Sets up the AP and associates a client.
 
     Args:
@@ -73,14 +74,13 @@
              beacon_interval, dtim_period, frag_threshold, rts_threshold,
              force_wmm, hidden, security, additional_ap_parameters, password,
              check_connectivity, n_capabilities, ac_capabilities,
-             vht_bandwidth)
+             vht_bandwidth, setup_bridge)
 
-    return associate(
-        client,
-        ssid,
-        password,
-        check_connectivity=check_connectivity,
-        hidden=hidden)
+    return associate(client,
+                     ssid,
+                     password,
+                     check_connectivity=check_connectivity,
+                     hidden=hidden)
 
 
 def setup_ap(access_point,
@@ -101,7 +101,8 @@
              check_connectivity=False,
              n_capabilities=None,
              ac_capabilities=None,
-             vht_bandwidth=None):
+             vht_bandwidth=None,
+             setup_bridge=False):
     """Sets up the AP.
 
     Args:
@@ -120,27 +121,27 @@
         password: Password to connect to WLAN if necessary.
         check_connectivity: Whether to check for internet connectivity.
     """
-    ap = hostapd_ap_preset.create_ap_preset(
-        profile_name=profile_name,
-        iface_wlan_2g=access_point.wlan_2g,
-        iface_wlan_5g=access_point.wlan_5g,
-        channel=channel,
-        ssid=ssid,
-        mode=mode,
-        short_preamble=preamble,
-        beacon_interval=beacon_interval,
-        dtim_period=dtim_period,
-        frag_threshold=frag_threshold,
-        rts_threshold=rts_threshold,
-        force_wmm=force_wmm,
-        hidden=hidden,
-        bss_settings=[],
-        security=security,
-        n_capabilities=n_capabilities,
-        ac_capabilities=ac_capabilities,
-        vht_bandwidth=vht_bandwidth)
-    access_point.start_ap(
-        hostapd_config=ap, additional_parameters=additional_ap_parameters)
+    ap = hostapd_ap_preset.create_ap_preset(profile_name=profile_name,
+                                            iface_wlan_2g=access_point.wlan_2g,
+                                            iface_wlan_5g=access_point.wlan_5g,
+                                            channel=channel,
+                                            ssid=ssid,
+                                            mode=mode,
+                                            short_preamble=preamble,
+                                            beacon_interval=beacon_interval,
+                                            dtim_period=dtim_period,
+                                            frag_threshold=frag_threshold,
+                                            rts_threshold=rts_threshold,
+                                            force_wmm=force_wmm,
+                                            hidden=hidden,
+                                            bss_settings=[],
+                                            security=security,
+                                            n_capabilities=n_capabilities,
+                                            ac_capabilities=ac_capabilities,
+                                            vht_bandwidth=vht_bandwidth)
+    access_point.start_ap(hostapd_config=ap,
+                          setup_bridge=setup_bridge,
+                          additional_parameters=additional_ap_parameters)
 
 
 def associate(client,
@@ -157,8 +158,10 @@
         check_connectivity: Whether to check internet connectivity.
         hidden: If the WLAN is hidden or not.
     """
-    return client.associate(
-        ssid, password, check_connectivity=check_connectivity, hidden=hidden)
+    return client.associate(ssid,
+                            password,
+                            check_connectivity=check_connectivity,
+                            hidden=hidden)
 
 
 def status(client):
diff --git a/acts/framework/acts/test_utils/coex/audio_capture.py b/acts/framework/acts/test_utils/coex/audio_capture.py
deleted file mode 100644
index bb29dcc..0000000
--- a/acts/framework/acts/test_utils/coex/audio_capture.py
+++ /dev/null
@@ -1,159 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-
-import argparse
-import json
-import logging
-import os
-import pyaudio
-import wave
-
-RECORD_FILE_TEMPLATE = 'recorded_audio_%s.wav'
-
-
-class DeviceNotFound(Exception):
-    """Raises exception if audio capture device is not found."""
-
-# TODO: (@sairamganesh) This class will be deprecated for
-# ../acts/test_utils/coex/audio_capture_device.py
-
-
-class AudioCapture:
-
-    def __init__(self, test_params, path):
-        """Creates object to pyaudio and defines audio parameters.
-
-        Args:
-            test_params: Audio parameters fetched from config.
-            path: Result path.
-        """
-        self.audio = pyaudio.PyAudio()
-        self.audio_format = pyaudio.paInt16
-        self.audio_params = test_params
-        self.channels = self.audio_params["channel"]
-        self.chunk = self.audio_params["chunk"]
-        self.sample_rate = self.audio_params["sample_rate"]
-        self.file_counter = 0
-        self.__input_device = None
-        self.record_file_template = os.path.join(path, RECORD_FILE_TEMPLATE)
-        if not self.audio_params["ssh_config"]:
-            self.__input_device = self.__get_input_device()
-
-    @property
-    def name(self):
-        try:
-            return self.audio_params["ssh_config"]["host"]
-        except KeyError:
-            return self.__input_device["name"]
-
-    def __get_input_device(self):
-        """Checks for the audio capture device."""
-        if self.__input_device is None:
-            for i in range(self.audio.get_device_count()):
-                device_info = self.audio.get_device_info_by_index(i)
-                logging.info("Device Information {}".format(device_info))
-                if self.audio_params['input_device'] in device_info['name']:
-                    self.__input_device = device_info
-                    break
-            else:
-                logging.error("Audio Capture device {} not found.".format(
-                    self.audio_params["input_device"]))
-                raise DeviceNotFound("Audio Capture Input device not found")
-        return self.__input_device
-
-    def capture_and_store_audio(self, trim_beginning=0, trim_end=0):
-        """Records the A2DP streaming.
-
-        Args:
-            trim_beginning: how many seconds to trim from the beginning
-            trim_end: how many seconds to trim from the end
-        """
-        if self.audio_params['ssh_config']:
-            self.__input_device = self.__get_input_device()
-        stream = self.audio.open(
-            format=self.audio_format,
-            channels=self.channels,
-            rate=self.sample_rate,
-            input=True,
-            frames_per_buffer=self.chunk,
-            input_device_index=self.__input_device['index'])
-        frames = []
-        b_chunks = trim_beginning * (self.sample_rate // self.chunk)
-        e_chunks = trim_end * (self.sample_rate // self.chunk)
-        total_chunks = self.sample_rate // self.chunk * self.audio_params[
-            'record_duration']
-        for i in range(total_chunks):
-            try:
-                data = stream.read(self.chunk, exception_on_overflow=False)
-            except IOError as ex:
-                logging.error("Cannot record audio :{}".format(ex))
-                return False
-            if b_chunks <= i < total_chunks - e_chunks:
-                frames.append(data)
-
-        stream.stop_stream()
-        stream.close()
-        status = self.write_record_file(frames)
-        return status
-
-    def last_fileno(self):
-        return self.next_fileno() - 1
-
-    def next_fileno(self):
-        counter = 0
-        while os.path.exists(self.record_file_template % counter):
-            counter += 1
-        return counter
-
-    def write_record_file(self, frames):
-        """Writes the recorded audio into the file.
-
-        Args:
-            frames: Recorded audio frames.
-        """
-        file_name = self.record_file_template % self.next_fileno()
-        logging.info('writing to %s' % file_name)
-        wf = wave.open(file_name, 'wb')
-        wf.setnchannels(self.channels)
-        wf.setsampwidth(self.audio.get_sample_size(self.audio_format))
-        wf.setframerate(self.sample_rate)
-        wf.writeframes(b''.join(frames))
-        wf.close()
-        return True
-
-    def terminate_audio(self):
-        """Terminates the pulse audio instance."""
-        self.audio.terminate()
-
-
-if __name__ == '__main__':
-    parser = argparse.ArgumentParser()
-    parser.add_argument(
-        '-p',
-        '--path',
-        type=str,
-        help="Contains path where the recorded files to be stored")
-    parser.add_argument(
-        '-t',
-        '--test_params',
-        type=json.loads,
-        help="Contains sample rate, channels,"
-             " chunk and device index for recording.")
-    args = parser.parse_args()
-    audio = AudioCapture(args.test_params, args.path)
-    audio.capture_and_store_audio(args.test_params['trim_beginning'],
-                                  args.test_params['trim_end'])
-    audio.terminate_audio()
diff --git a/acts/framework/acts/test_utils/coex/audio_capture_device.py b/acts/framework/acts/test_utils/coex/audio_capture_device.py
index 924bf4a..f99f6a8 100644
--- a/acts/framework/acts/test_utils/coex/audio_capture_device.py
+++ b/acts/framework/acts/test_utils/coex/audio_capture_device.py
@@ -57,6 +57,21 @@
     def last_fileno(self):
         return self.next_fileno - 1
 
+    @property
+    def get_last_record_duration_millis(self):
+        """Get duration of most recently recorded file.
+
+        Returns:
+            duration (float): duration of recorded file in milliseconds.
+        """
+        latest_file_path = self.wave_file % self.last_fileno
+        print (latest_file_path)
+        with wave.open(latest_file_path, 'r') as f:
+            frames = f.getnframes()
+            rate = f.getframerate()
+            duration = (frames / float(rate)) * 1000
+        return duration
+
     def write_record_file(self, audio_params, frames):
         """Writes the recorded audio into the file.
 
diff --git a/acts/framework/acts/test_utils/coex/audio_test_utils.py b/acts/framework/acts/test_utils/coex/audio_test_utils.py
index b514712..02b99ca 100644
--- a/acts/framework/acts/test_utils/coex/audio_test_utils.py
+++ b/acts/framework/acts/test_utils/coex/audio_test_utils.py
@@ -16,16 +16,12 @@
 
 import logging
 import os
-import wave
 
+from acts.test_utils.coex.audio_capture_device import AudioCaptureBase
 from acts.test_utils.coex.audio_capture_device import CaptureAudioOverAdb
 from acts.test_utils.coex.audio_capture_device import CaptureAudioOverLocal
-from acts.controllers.utils_lib.ssh import connection
-from acts.controllers.utils_lib.ssh import settings
 from acts.test_utils.audio_analysis_lib import audio_analysis
 from acts.test_utils.audio_analysis_lib.check_quality import quality_analysis
-from acts.test_utils.coex.audio_capture import AudioCapture
-from acts.test_utils.coex.audio_capture import RECORD_FILE_TEMPLATE
 
 ANOMALY_DETECTION_BLOCK_SIZE = audio_analysis.ANOMALY_DETECTION_BLOCK_SIZE
 ANOMALY_GROUPING_TOLERANCE = audio_analysis.ANOMALY_GROUPING_TOLERANCE
@@ -67,58 +63,18 @@
 class FileNotFound(Exception):
     """Raises Exception if file is not present"""
 
-# TODO @sairamganesh Rename this class to AudioCaptureResult and
-# remove duplicates which are in ../test_utils/coex/audio_capture_device.py.
 
+class AudioCaptureResult(AudioCaptureBase):
 
-class SshAudioCapture(AudioCapture):
-
-    def __init__(self, test_params, path):
-        super(SshAudioCapture, self).__init__(test_params, path)
-        self.remote_path = path
-        self.ssh_session = None
-
-    def capture_audio(self, trim_beginning=0, trim_end=0):
-        """Captures audio and store results.
+    def __init__(self, path):
+        """Initializes Audio Capture Result class.
 
         Args:
-            trim_beginning: To trim audio at the start in seconds.
-            trim_end: To trim audio at the end in seconds.
-
-        Returns:
-            Returns exit status of ssh connection.
+            path: Path of audio capture result.
         """
-        if not trim_beginning:
-            trim_beginning = self.audio_params.get('trim_beginning', 0)
-        if not trim_end:
-            trim_end = self.audio_params.get('trim_end', 0)
-        if self.audio_params["ssh_config"]:
-            ssh_settings = settings.from_config(
-                self.audio_params["ssh_config"])
-            self.ssh_session = connection.SshConnection(ssh_settings)
-            cur_path = os.path.dirname(os.path.realpath(__file__))
-            local_path = os.path.join(cur_path, "audio_capture.py")
-            self.ssh_session.send_file(local_path,
-                                       self.audio_params["dest_path"])
-            path = self.audio_params["dest_path"]
-            test_params = str(self.audio_params).replace("\'", "\"")
-            self.cmd = "python3 audio_capture.py -p '{}' -t '{}'".format(
-                path, test_params)
-            job_result = self.ssh_session.run(self.cmd)
-            logging.debug("Job Result {}".format(job_result.stdout))
-            self.ssh_session.pull_file(
-                self.remote_path, os.path.join(
-                    self.audio_params["dest_path"], "*.wav"))
-            return bool(not job_result.exit_status)
-        else:
-            return self.capture_and_store_audio(trim_beginning, trim_end)
-
-    def terminate_and_store_audio_results(self):
-        """Terminates audio and stores audio files."""
-        if self.audio_params["ssh_config"]:
-            self.ssh_session.run('rm *.wav', ignore_status=True)
-        else:
-            self.terminate_audio()
+        super().__init__()
+        self.path = path
+        self.analysis_path = os.path.join(self.log_path, ANALYSIS_FILE_TEMPLATE)
 
     def THDN(self, win_size=None, step_size=None, q=1, freq=None):
         """Calculate THD+N value for most recently recorded file.
@@ -137,13 +93,12 @@
             channel_results (list): THD+N value for each channel's signal.
                 List index corresponds to channel index.
         """
-        latest_file_path = self.record_file_template % self.last_fileno()
         if not (win_size and step_size):
-            return audio_analysis.get_file_THDN(filename=latest_file_path,
+            return audio_analysis.get_file_THDN(filename=self.path,
                                                 q=q,
                                                 freq=freq)
         else:
-            return audio_analysis.get_file_max_THDN(filename=latest_file_path,
+            return audio_analysis.get_file_max_THDN(filename=self.path,
                                                     step_size=step_size,
                                                     window_size=win_size,
                                                     q=q,
@@ -172,28 +127,22 @@
             channel_results (list): anomaly durations for each channel's signal.
                 List index corresponds to channel index.
         """
-        latest_file_path = self.record_file_template % self.last_fileno()
         return audio_analysis.get_file_anomaly_durations(
-            filename=latest_file_path,
+            filename=self.path,
             freq=freq,
             block_size=block_size,
             threshold=threshold,
             tolerance=tolerance)
 
-    def get_last_record_duration_millis(self):
-        """Get duration of most recently recorded file.
+    @property
+    def analysis_fileno(self):
+        """Returns the file number to dump audio analysis results."""
+        counter = 0
+        while os.path.exists(self.analysis_path % counter):
+            counter += 1
+        return counter
 
-        Returns:
-            duration (float): duration of recorded file in milliseconds.
-        """
-        latest_file_path = self.record_file_template % self.last_fileno()
-        with wave.open(latest_file_path, 'r') as f:
-            frames = f.getnframes()
-            rate = f.getframerate()
-            duration = (frames / float(rate)) * 1000
-        return duration
-
-    def audio_quality_analysis(self, path):
+    def audio_quality_analysis(self, audio_params):
         """Measures audio quality based on the audio file given as input.
 
         Args:
@@ -202,19 +151,16 @@
         Returns:
             analysis_path on success.
         """
-        dest_file_path = os.path.join(path,
-                RECORD_FILE_TEMPLATE % self.last_fileno())
-        analysis_path = os.path.join(path,
-                ANALYSIS_FILE_TEMPLATE % self.last_fileno())
-        if not os.path.exists(dest_file_path):
+        analysis_path = self.analysis_path % self.analysis_fileno
+        if not os.path.exists(self.path):
             raise FileNotFound("Recorded file not found")
         try:
             quality_analysis(
-                filename=dest_file_path,
+                filename=self.path,
                 output_file=analysis_path,
                 bit_width=bits_per_sample,
-                rate=self.audio_params["sample_rate"],
-                channel=self.audio_params["channel"],
+                rate=audio_params["sample_rate"],
+                channel=audio_params["channel"],
                 spectral_only=False)
         except Exception as err:
             logging.exception("Failed to analyze raw audio: %s" % err)
diff --git a/acts/framework/acts/utils.py b/acts/framework/acts/utils.py
index 99c961a..b42ea21 100755
--- a/acts/framework/acts/utils.py
+++ b/acts/framework/acts/utils.py
@@ -19,6 +19,7 @@
 import copy
 import datetime
 import functools
+import IPy
 import json
 import logging
 import os
@@ -1381,3 +1382,97 @@
 def ascii_string(uc_string):
     """Converts unicode string to ascii"""
     return str(uc_string).encode('ASCII')
+
+
+def get_interface_ip_addresses(comm_channel, interface):
+    """Gets all of the ip addresses, ipv4 and ipv6, associated with a
+       particular interface name.
+
+    Args:
+        comm_channel: How to send commands to a device.  Can be ssh, adb serial,
+            etc.  Must have the run function implemented.
+        interface: The interface name on the device, ie eth0
+
+    Returns:
+        A list of dictionaries of the the various IP addresses:
+            ipv4_private_local_addresses: Any 192.168, 172.16, or 10
+                addresses
+            ipv4_public_addresses: Any IPv4 public addresses
+            ipv6_link_local_addresses: Any fe80:: addresses
+            ipv6_private_local_addresses: Any fd00:: addresses
+            ipv6_public_addresses: Any publicly routable addresses
+    """
+    ipv4_private_local_addresses = []
+    ipv4_public_addresses = []
+    ipv6_link_local_addresses = []
+    ipv6_private_local_addresses = []
+    ipv6_public_addresses = []
+    all_interfaces_and_addresses = comm_channel.run(
+        'ip -o addr | awk \'!/^[0-9]*: ?lo|link\/ether/ {gsub("/", " "); '
+        'print $2" "$4}\'').stdout
+    ifconfig_output = comm_channel.run('ifconfig %s' % interface).stdout
+    for interface_line in all_interfaces_and_addresses.split('\n'):
+        if interface != interface_line.split()[0]:
+            continue
+        on_device_ip = IPy.IP(interface_line.split()[1])
+        if on_device_ip.version() is 4:
+            if on_device_ip.iptype() == 'PRIVATE':
+                if str(on_device_ip) in ifconfig_output:
+                    ipv4_private_local_addresses.append(
+                        on_device_ip.strNormal())
+            elif on_device_ip.iptype() == 'PUBLIC':
+                if str(on_device_ip) in ifconfig_output:
+                    ipv4_public_addresses.append(on_device_ip.strNormal())
+        elif on_device_ip.version() is 6:
+            if on_device_ip.iptype() == 'LINKLOCAL':
+                if str(on_device_ip) in ifconfig_output:
+                    ipv6_link_local_addresses.append(on_device_ip.strNormal())
+            elif on_device_ip.iptype() == 'ULA':
+                if str(on_device_ip) in ifconfig_output:
+                    ipv6_private_local_addresses.append(
+                        on_device_ip.strNormal())
+            elif 'ALLOCATED' in on_device_ip.iptype():
+                if str(on_device_ip) in ifconfig_output:
+                    ipv6_public_addresses.append(on_device_ip.strNormal())
+    return {
+        'ipv4_private': ipv4_private_local_addresses,
+        'ipv4_public': ipv4_public_addresses,
+        'ipv6_link_local': ipv6_link_local_addresses,
+        'ipv6_private_local': ipv6_private_local_addresses,
+        'ipv6_public': ipv6_public_addresses
+    }
+
+
+def get_interface_based_on_ip(comm_channel, desired_ip_address):
+    """Gets the interface for a particular IP
+
+    Args:
+        comm_channel: How to send commands to a device.  Can be ssh, adb serial,
+            etc.  Must have the run function implemented.
+        desired_ip_address: The IP address that is being looked for on a device.
+
+    Returns:
+        The name of the test interface.
+    """
+
+    desired_ip_address = desired_ip_address.split('%', 1)[0]
+    all_ips_and_interfaces = comm_channel.run(
+        '(ip -o -4 addr show; ip -o -6 addr show) | '
+        'awk \'{print $2" "$4}\'').stdout
+    #ipv4_addresses = comm_channel.run(
+    #    'ip -o -4 addr show| awk \'{print $2": "$4}\'').stdout
+    #ipv6_addresses = comm_channel._ssh_session.run(
+    #    'ip -o -6 addr show| awk \'{print $2": "$4}\'').stdout
+    #if desired_ip_address in ipv4_addresses:
+    #    ip_addresses_to_search = ipv4_addresses
+    #elif desired_ip_address in ipv6_addresses:
+    #    ip_addresses_to_search = ipv6_addresses
+    for ip_address_and_interface in all_ips_and_interfaces.split('\n'):
+        if desired_ip_address in ip_address_and_interface:
+            return ip_address_and_interface.split()[1][:-1]
+    return None
+
+
+def renew_linux_ip_address(comm_channel, interface):
+    comm_channel.run('sudo dhclient -r %s' % interface)
+    comm_channel.run('sudo dhclient %s' % interface)
diff --git a/acts/framework/setup.py b/acts/framework/setup.py
index f2d84c0..59210f9 100755
--- a/acts/framework/setup.py
+++ b/acts/framework/setup.py
@@ -40,6 +40,7 @@
     'xlsxwriter',
     'mobly',
     'grpcio',
+    'IPy',
     'Monsoon',
     # paramiko-ng is needed vs paramiko as currently paramiko does not support
     # ed25519 ssh keys, which is what Fuchsia uses.
diff --git a/acts/tests/google/coex/performance_tests/CoexBasicPerformanceTest.py b/acts/tests/google/coex/performance_tests/CoexBasicPerformanceTest.py
index 00cb735..c5c1879 100644
--- a/acts/tests/google/coex/performance_tests/CoexBasicPerformanceTest.py
+++ b/acts/tests/google/coex/performance_tests/CoexBasicPerformanceTest.py
@@ -14,153 +14,60 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
+import itertools
+
+from acts.test_utils.bt.bt_test_utils import enable_bluetooth
 from acts.test_utils.coex.CoexPerformanceBaseTest import CoexPerformanceBaseTest
 from acts.test_utils.coex.coex_test_utils import perform_classic_discovery
 
 
 class CoexBasicPerformanceTest(CoexPerformanceBaseTest):
 
-    def setup_class(self):
-        super().setup_class()
+    def __init__(self, controllers):
+        super().__init__(controllers)
+        req_params = [
+            # A dict containing:
+            #     protocol: A list containing TCP/UDP. Ex: protocol: ['tcp'].
+            #     stream: A list containing ul/dl. Ex: stream: ['ul']
+            'standalone_params'
+        ]
+        self.unpack_userparams(req_params)
+        self.tests = self.generated_test_cases(['bt_on', 'perform_discovery'])
 
-    def run_iperf_and_perform_discovery(self):
-        """Starts iperf client on host machine and bluetooth discovery
+    def perform_discovery(self):
+        """ Starts iperf client on host machine and bluetooth discovery
         simultaneously.
 
         Returns:
             True if successful, False otherwise.
         """
         tasks = [(perform_classic_discovery,
-                  (self.pri_ad, self.iperf["duration"], self.json_file,
-                   self.dev_list)), (self.run_iperf_and_get_result, ())]
-        if not self.set_attenuation_and_run_iperf(tasks):
-            return False
-        return self.teardown_result()
+                  (self.pri_ad, self.iperf['duration'], self.json_file,
+                   self.dev_list)),
+                 (self.run_iperf_and_get_result, ())]
+        return self.set_attenuation_and_run_iperf(tasks)
 
-    def test_performance_with_bt_on_tcp_ul(self):
-        """Check throughput when bluetooth on.
-
-        This test is to start TCP-Uplink traffic between host machine and
-        android device and check the throughput when bluetooth is on.
-
-        Steps:
-        1. Start TCP-uplink traffic when bluetooth is on.
-
-        Test Id: Bt_CoEx_kpi_005
-        """
-        self.set_attenuation_and_run_iperf()
-        return self.teardown_result()
-
-    def test_performance_with_bt_on_tcp_dl(self):
-        """Check throughput when bluetooth on.
-
-        This test is to start TCP-downlink traffic between host machine and
-        android device and check the throughput when bluetooth is on.
-
-        Steps:
-        1. Start TCP-downlink traffic when bluetooth is on.
-
-        Test Id: Bt_CoEx_kpi_006
-        """
-        self.set_attenuation_and_run_iperf()
-        return self.teardown_result()
-
-    def test_performance_with_bt_on_udp_ul(self):
-        """Check throughput when bluetooth on.
-
-        This test is to start UDP-uplink traffic between host machine and
-        android device and check the throughput when bluetooth is on.
-
-        Steps:
-        1. Start UDP-uplink traffic when bluetooth is on.
-
-        Test Id: Bt_CoEx_kpi_007
-        """
-        self.set_attenuation_and_run_iperf()
-        return self.teardown_result()
-
-    def test_performance_with_bt_on_udp_dl(self):
-        """Check throughput when bluetooth on.
-
-        This test is to start UDP-downlink traffic between host machine and
-        android device and check the throughput when bluetooth is on.
-
-        Steps:
-        1. Start UDP-downlink traffic when bluetooth is on.
-
-        Test Id: Bt_CoEx_kpi_008
-        """
-        self.set_attenuation_and_run_iperf()
-        return self.teardown_result()
-
-    def test_performance_with_bluetooth_discovery_tcp_ul(self):
-        """Check throughput when bluetooth discovery is ongoing.
-
-        This test is to start TCP-uplink traffic between host machine and
-        android device and bluetooth discovery and checks throughput.
-
-        Steps:
-        1. Start TCP-uplink traffic and bluetooth discovery parallelly.
+    def bt_on(self):
+        """ Turns on bluetooth and runs iperf.
 
         Returns:
-            True if successful, False otherwise.
-
-        Test Id: Bt_CoEx_kpi_009
+            True on success, False otherwise.
         """
-        if not self.run_iperf_and_perform_discovery():
+        if not enable_bluetooth(self.pri_ad.droid, self.pri_ad.ed):
             return False
-        return True
+        return self.set_attenuation_and_run_iperf()
 
-    def test_performance_with_bluetooth_discovery_tcp_dl(self):
-        """Check throughput when bluetooth discovery is ongoing.
+    def generated_test_cases(self, test_types):
+        """ Auto generates tests for basic coex tests. """
+        test_cases = []
+        for protocol, stream, test_type in itertools.product(
+                self.standalone_params['protocol'],
+                self.standalone_params['stream'], test_types):
 
-        This test is to start TCP-downlink traffic between host machine and
-        android device and bluetooth discovery and checks throughput.
+            test_name = 'test_performance_with_{}_{}_{}'.format(
+                test_type, protocol, stream)
 
-        Steps:
-        1. Start TCP-downlink traffic and bluetooth discovery parallelly.
-
-        Returns:
-            True if successful, False otherwise.
-
-        Test Id: Bt_CoEx_kpi_010
-        """
-        if not self.run_iperf_and_perform_discovery():
-            return False
-        return True
-
-    def test_performance_with_bluetooth_discovery_udp_ul(self):
-        """Check throughput when bluetooth discovery is ongoing.
-
-        This test is to start UDP-uplink traffic between host machine and
-        android device and bluetooth discovery and checks throughput.
-
-        Steps:
-        1. Start UDP-uplink traffic and bluetooth discovery parallelly.
-
-        Returns:
-            True if successful, False otherwise.
-
-        Test Id: Bt_CoEx_kpi_011
-        """
-        if not self.run_iperf_and_perform_discovery():
-            return False
-        return True
-
-    def test_performance_with_bluetooth_discovery_udp_dl(self):
-        """Check throughput when bluetooth discovery is ongoing.
-
-        This test is to start UDP-downlink traffic between host machine and
-        android device and bluetooth discovery and checks throughput.
-
-        Steps:
-        1. Start UDP-downlink traffic and bluetooth discovery parallelly.
-
-        Returns:
-            True if successful, False otherwise.
-
-        Test Id: Bt_CoEx_kpi_012
-        """
-        if not self.run_iperf_and_perform_discovery():
-            return False
-        return True
+            test_function = getattr(self, test_type)
+            setattr(self, test_name, test_function)
+            test_cases.append(test_name)
+        return test_cases
diff --git a/acts/tests/google/net/DataCostTest.py b/acts/tests/google/net/DataCostTest.py
index 2b7bd50..617626f 100644
--- a/acts/tests/google/net/DataCostTest.py
+++ b/acts/tests/google/net/DataCostTest.py
@@ -65,6 +65,34 @@
 
     """ Helper functions """
 
+    def _clear_netstats(self, ad):
+        """ Clear netstats stored on device
+
+        Args:
+            ad: Android device object
+        """
+        ad.log.info("Clear netstats record.")
+        ad.adb.shell("rm /data/system/netstats/*")
+        asserts.assert_equal("", ad.adb.shell("ls /data/system/netstats/"),
+                             "Fail to clear netstats.")
+        ad.reboot()
+        time.sleep(10)
+        self._check_multipath_preference_from_dumpsys(ad)
+
+    def _check_multipath_preference_from_dumpsys(self, ad):
+        """ Check cell multipath_preference from dumpsys
+
+        Args:
+            ad: Android device object
+        """
+        out = ad.adb.shell("dumpsys connectivity | grep budget")
+        asserts.assert_true(out, "Fail to get status from dumpsys.")
+        ad.log.info("MultipathPolicyTracker: %s" % out)
+        asserts.assert_true(
+            "HANDOVER|RELIABILITY" in out,
+            "Cell multipath preference should be HANDOVER|RELIABILITY."
+        )
+
     def _get_total_data_usage_for_device(self, ad, conn_type, sub_id):
         """ Get total data usage in MB for device
 
@@ -138,6 +166,8 @@
         """
         # set vars
         ad = self.android_devices[0]
+        self._clear_netstats(ad)
+
         sub_id = str(ad.droid.telephonyGetSubscriberId())
         cell_network = ad.droid.connectivityGetActiveNetwork()
         self.log.info("cell network %s" % cell_network)
@@ -182,6 +212,8 @@
         """
         # set vars
         ad = self.android_devices[1]
+        self._clear_netstats(ad)
+
         cell_network = ad.droid.connectivityGetActiveNetwork()
         self.log.info("cell network %s" % cell_network)
         wutils.wifi_connect(ad, self.wifi_network)