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)