Merge "Add test interfaces to iperf and allow parimiko"
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/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)