diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-02-03 00:03:49 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-02-03 00:03:49 +0000 |
commit | c165d01c0c5d76b510c496f2cdc673bd540a8e16 (patch) | |
tree | fe799a88a6e346981d38d7db36dfc9a8b6422194 | |
parent | e9b2ea17ece3f222479083269fc76ae36ded350e (diff) | |
parent | 4b2857d70f1876fe4eb318e361a726917d3a32db (diff) | |
download | connectivity-simpleperf-release.tar.gz |
Snap for 11400057 from 4b2857d70f1876fe4eb318e361a726917d3a32db to simpleperf-releasesimpleperf-release
Change-Id: Ib51e80d96b63fd311f875a2558fe2a4a5f4355c1
15 files changed, 1176 insertions, 77 deletions
diff --git a/acts/framework/acts/controllers/android_device.py b/acts/framework/acts/controllers/android_device.py index 33741af26..e7f594d80 100755 --- a/acts/framework/acts/controllers/android_device.py +++ b/acts/framework/acts/controllers/android_device.py @@ -539,7 +539,9 @@ class AndroidDevice: info = { "build_id": build_id, "incremental_build_id": incremental_build_id, - "build_type": self.adb.getprop("ro.build.type") + "build_type": self.adb.getprop("ro.build.type"), + "build_date": self.adb.getprop("ro.build.date.utc"), + "baseband": self.adb.getprop("gsm.version.baseband") } return info @@ -555,7 +557,8 @@ class AndroidDevice: 'model': self.model, 'build_info': self.build_info, 'user_added_info': self._user_added_device_info, - 'flavor': self.flavor + 'flavor': self.flavor, + 'revision': self.adb.getprop('ro.revision') } return info diff --git a/acts/framework/acts/controllers/cellular_lib/LteCellConfig.py b/acts/framework/acts/controllers/cellular_lib/LteCellConfig.py index 8af943f2c..be7782b54 100644 --- a/acts/framework/acts/controllers/cellular_lib/LteCellConfig.py +++ b/acts/framework/acts/controllers/cellular_lib/LteCellConfig.py @@ -114,6 +114,7 @@ class LteCellConfig(base_cell.BaseCellConfig): self.drx_retransmission_timer = None self.drx_long_cycle = None self.drx_long_cycle_offset = None + self.tracking_area = None self.disable_all_ul_subframes = None def __str__(self): @@ -387,8 +388,6 @@ class LteCellConfig(base_cell.BaseCellConfig): if self.PARAM_TA in parameters: self.tracking_area = int(parameters[self.PARAM_TA]) - else: - self.tracking_area = None def get_duplex_mode(self): """ Determines if the cell uses FDD or TDD duplex mode diff --git a/acts/framework/acts/controllers/cellular_lib/NrCellConfig.py b/acts/framework/acts/controllers/cellular_lib/NrCellConfig.py index a38f1c630..01f612527 100644 --- a/acts/framework/acts/controllers/cellular_lib/NrCellConfig.py +++ b/acts/framework/acts/controllers/cellular_lib/NrCellConfig.py @@ -40,6 +40,7 @@ class NrCellConfig(base_cell.BaseCellConfig): PARAM_UL_RBS = "ul_rbs" PARAM_TA = "tracking_area" PARAM_DRX = "drx" + PARAM_DISABLE_ALL_UL_SLOTS = "disable_all_ul_slots" PARAM_CONFIG_FLEXIBLE_SLOTS = "config_flexible_slots" @@ -62,6 +63,15 @@ class NrCellConfig(base_cell.BaseCellConfig): self.drx_connected_mode = None self.disable_all_ul_slots = None self.config_flexible_slots = None + self.drx_on_duration_timer = None + self.drx_inactivity_timer = None + self.drx_retransmission_timer_dl = None + self.drx_retransmission_timer_ul = None + self.drx_long_cycle = None + self.harq_rtt_timer_dl = 0 + self.harq_rtt_timer_ul = 0 + self.slot_offset = 0 + def configure(self, parameters): """ Configures an NR cell using a dictionary of parameters. @@ -152,5 +162,61 @@ class NrCellConfig(base_cell.BaseCellConfig): self.config_flexible_slots = parameters.get( self.PARAM_CONFIG_FLEXIBLE_SLOTS, False) + if self.PARAM_DRX in parameters and len( + parameters[self.PARAM_DRX]) >= 6: + self.drx_connected_mode = True + param_drx = parameters[self.PARAM_DRX] + self.drx_on_duration_timer = param_drx[0] + self.drx_inactivity_timer = param_drx[1] + self.drx_retransmission_timer_dl = param_drx[2] + self.drx_retransmission_timer_ul = param_drx[3] + self.drx_long_cycle = param_drx[4] + try: + long_cycle = int(param_drx[4]) + long_cycle_offset = int(param_drx[5]) + if long_cycle_offset in range(0, long_cycle): + self.drx_long_cycle_offset = long_cycle_offset + else: + self.log.error( + ("The cDRX long cycle offset must be in the " + "range 0 to (long cycle - 1). Setting " + "long cycle offset to 0")) + self.drx_long_cycle_offset = 0 + + self.harq_rtt_timer_dl = ( + int(param_drx[6]) + if len(param_drx) >= 7 + else 0 + ) + self.harq_rtt_timer_ul = ( + int(param_drx[7]) + if len(param_drx) >= 8 + else 0 + ) + self.slot_offset = ( + int(param_drx[8]) + if len(param_drx) >= 9 + else 0 + ) + + except ValueError: + self.log.error(("cDRX long cycle and long cycle offset " + "must be integers. Disabling cDRX mode.")) + self.drx_connected_mode = False + else: + self.log.warning( + "DRX mode was not configured properly.\n" + "Please provide a list with the following values:\n" + "1) DRX on duration timer\n" + "2) Inactivity timer\n" + "3) Retransmission timer dl\n" + "4) Retransmission timer ul\n" + "5) Long DRX cycle duration\n" + "6) Long DRX cycle offset\n" + "7) harq RTT timer dl\n" + "8) harq RTT timer ul\n" + "9) slot offset\n" + "Example: [2, 6, 1, 1, 160, 0, 0, 0, 0].") + def __str__(self): return str(vars(self)) diff --git a/acts/framework/acts/controllers/openwrt_ap.py b/acts/framework/acts/controllers/openwrt_ap.py index a9d1ca3b3..f4750d8da 100644 --- a/acts/framework/acts/controllers/openwrt_ap.py +++ b/acts/framework/acts/controllers/openwrt_ap.py @@ -181,6 +181,7 @@ class OpenWrtAP(object): def start_ap(self): """Starts the AP with the settings in /etc/config/wireless.""" + self.log.info("wifi up") self.ssh.run("wifi up") curr_time = time.time() while time.time() < curr_time + WAIT_TIME: @@ -192,6 +193,7 @@ class OpenWrtAP(object): def stop_ap(self): """Stops the AP.""" + self.log.info("wifi down") self.ssh.run("wifi down") curr_time = time.time() while time.time() < curr_time + WAIT_TIME: diff --git a/acts/framework/acts/controllers/openwrt_lib/openwrt_authentication.py b/acts/framework/acts/controllers/openwrt_lib/openwrt_authentication.py index a300fff4d..5cab36f6c 100644 --- a/acts/framework/acts/controllers/openwrt_lib/openwrt_authentication.py +++ b/acts/framework/acts/controllers/openwrt_lib/openwrt_authentication.py @@ -1,20 +1,30 @@ +"""Module for OpenWrt SSH authentication. + +This module provides a class, OpenWrtAuth, for managing SSH authentication for +OpenWrt devices. It allows you to generate RSA key pairs, save them in a +specified directory, and upload the public key to a remote host. + +Usage: + 1. Create an instance of OpenWrtAuth with the required parameters. + 2. Call generate_rsa_key() to generate RSA key pairs and save them. + 3. Call send_public_key_to_remote_host() to upload the public key + to the remote host. +""" + import logging import os import paramiko import scp -import subprocess -_REMOTE_PATH = '/etc/dropbear/authorized_keys' +_REMOTE_PATH = "/etc/dropbear/authorized_keys" class OpenWrtAuth: - """ - A class for managing SSH authentication for OpenWrt devices. - """ - def __init__(self, hostname, username='root', password='root', port=22): - """ - Initializes a new instance of the OpenWrtAuth class. + """Class for managing SSH authentication for OpenWrt devices.""" + + def __init__(self, hostname, username="root", password="root", port=22): + """Initializes a new instance of the OpenWrtAuth class. Args: hostname (str): The hostname or IP address of the remote device. @@ -32,23 +42,30 @@ class OpenWrtAuth: self.password = password self.port = port self.public_key = None - self.key_dir = '/tmp/openwrt/' - self.public_key_file = f'{self.key_dir}id_rsa_{self.hostname}.pub' - self.private_key_file = f'{self.key_dir}id_rsa_{self.hostname}' + self.key_dir = "/tmp/openwrt/" + self.public_key_file = f"{self.key_dir}id_rsa_{self.hostname}.pub" + self.private_key_file = f"{self.key_dir}id_rsa_{self.hostname}" def generate_rsa_key(self): - """ - Generates an RSA key pair and saves it to the specified directory. + """Generates an RSA key pair and saves it to the specified directory. Raises: - ValueError: If an error occurs while generating the RSA key pair. - paramiko.SSHException: If an error occurs while generating the RSA key pair. - FileNotFoundError: If the directory for saving the private or public key does not exist. - PermissionError: If there is a permission error while creating the directory for saving the keys. - Exception: If an unexpected error occurs while generating the RSA key pair. + ValueError: + If an error occurs while generating the RSA key pair. + paramiko.SSHException: + If an error occurs while generating the RSA key pair. + FileNotFoundError: + If the directory for saving the private or public key does not exist. + PermissionError: + If there is a permission error while creating the directory + for saving the keys. + Exception: + If an unexpected error occurs while generating the RSA key pair. """ # Checks if the private and public key files already exist. - if os.path.exists(self.private_key_file) and os.path.exists(self.public_key_file): + if os.path.exists(self.private_key_file) and os.path.exists( + self.public_key_file + ): logging.warning("RSA key pair already exists, skipping key generation.") return @@ -57,38 +74,42 @@ class OpenWrtAuth: logging.info("Generating RSA key pair...") key = paramiko.RSAKey.generate(bits=2048) self.public_key = f"ssh-rsa {key.get_base64()}" - logging.debug(f"Public key: {self.public_key}") + logging.debug("Public key: %s", self.public_key) # Create /tmp/openwrt/ directory if it doesn't exist. - logging.info(f"Creating {self.key_dir} directory...") + logging.info("Creating %s directory...", self.key_dir) os.makedirs(self.key_dir, exist_ok=True) # Saves the private key to a file. key.write_private_key_file(self.private_key_file) - logging.debug(f"Saved private key to file: {self.private_key_file}") + logging.debug("Saved private key to file: %s", self.private_key_file) # Saves the public key to a file. with open(self.public_key_file, "w") as f: - f.write(self.public_key) - logging.debug(f"Saved public key to file: {self.public_key_file}") + f.write(self.public_key) + logging.debug("Saved public key to file: %s", self.public_key_file) except (ValueError, paramiko.SSHException, PermissionError) as e: - logging.error(f"An error occurred while generating the RSA key pair: {e}") + logging.error("An error occurred while generating " + "the RSA key pair: %s", e) except Exception as e: - logging.error(f"An unexpected error occurred while generating the RSA key pair: {e}") + logging.error("An unexpected error occurred while generating " + "the RSA key pair: %s", e) def send_public_key_to_remote_host(self): - """ - Uploads the public key to the remote host. + """Uploads the public key to the remote host. Raises: - paramiko.AuthenticationException: If authentication to the remote host fails. - paramiko.SSHException: If an SSH-related error occurs during the connection. - FileNotFoundError: If the public key file or the private key file does not exist. + paramiko.AuthenticationException: + If authentication to the remote host fails. + paramiko.SSHException: + If an SSH-related error occurs during the connection. + FileNotFoundError: + If the public key file or the private key file does not exist. Exception: If an unexpected error occurs while sending the public key. """ try: # Connects to the remote host and uploads the public key. - logging.info(f"Uploading public key to remote host {self.hostname}...") + logging.info("Uploading public key to remote host %s...", self.hostname) with paramiko.SSHClient() as ssh: ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect(hostname=self.hostname, @@ -97,11 +118,11 @@ class OpenWrtAuth: password=self.password) scp_client = scp.SCPClient(ssh.get_transport()) scp_client.put(self.public_key_file, _REMOTE_PATH) - logging.info('Public key uploaded successfully.') + logging.info("Public key uploaded successfully.") except (paramiko.AuthenticationException, paramiko.SSHException, FileNotFoundError) as e: - logging.error(f"An error occurred while sending the public key: {e}") + logging.error("An error occurred while sending the public key: %s", e) except Exception as e: - logging.error(f"An unexpected error occurred while " - f"sending the public key: {e}") + logging.error("An unexpected error occurred while " + "sending the public key: %s", e) diff --git a/acts/framework/acts/controllers/rohdeschwarz_lib/cmw500_iperf_measurement.py b/acts/framework/acts/controllers/rohdeschwarz_lib/cmw500_iperf_measurement.py new file mode 100644 index 000000000..3ac828cc8 --- /dev/null +++ b/acts/framework/acts/controllers/rohdeschwarz_lib/cmw500_iperf_measurement.py @@ -0,0 +1,498 @@ +#!/usr/bin/env python3 +# +# Copyright 2024 - 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. +"""Provides classes for managing IPerf sessions on CMW500 callboxes.""" + +from enum import Enum +import time + +_MEASUREMENT_SIZE = 5 +_SERVER_LOSS_INDEX = 4 +_CLIENT_THROUGHPUT_INDEX = 5 +_SERVER_THROUGHPUT_INDEX = 3 +_CLIENT_COUNT_INDEX = 2 +_SERVER_COUNT_INDEX = 1 + + +class IPerfMode(Enum): + """Supported IPerf directions.""" + + CLIENT = 'CLI' + SERVER = 'SERV' + + +class IPerfType(Enum): + """Supported Iperf applications.""" + + IPERF = 'IPER' + IPERF3 = 'IP3' + # NAT = 'NAT' # unsupported ATM + + +class IPerfProtocol(Enum): + """Supported protocol types.""" + + TCP = 'TCP' + UDP = 'UDP' + + +class IPerfMeasurementState(Enum): + """Possible measurement states.""" + + OFF = 'OFF' + READY = 'RDY' + RUN = 'RUN' + + +def _try_parse(s, fun): + """Attempt to parse the value with the provided function or return None + if an error is encountered. + + Args + s: the string to parse + fun: requested parse function. + """ + try: + return fun(s) + except ValueError: + return None + + +class Cmw500IPerfMeasurement(object): + """Class for managing IPerf measurement sessions on CMX500.""" + def __init__(self, cmw, meas_id=1): + """Initializes a new CMW500 Iperf Measurement. + + Examples:: + + Using just a single client: + measurement = Cmw500IPerfMeasurement(cmw) + measurement.configure_services(0, 1) + measurement.test_time = 10 + ... + measurement.start() + + client = measurement.clients[0] + last_count = None + while measurement.state == IPerfMeasurementState.RUN: + count = client.count + throughput = client.throughput + if count != last_count: + last_count = count + print(f"Took measurement: {throughput}") + # Results are updated every 1 second, so wait 0.5 for margin + time.sleep(0.5) + """ + self._cmw = cmw + self._meas_id = meas_id + + # CMW can run up to a max of 8 client and 8 server instances in parallel + # track of how many servers/cients have been requested. + self._server_count = 0 + self._client_count = 0 + + # CMW always has 8 clients and 8 servers, privately initialize all of + # them so we can disable the ones that aren't requested. + self._clients = [ + CMW500IPerfService(cmw, meas_id, i + 1, IPerfMode.CLIENT) + for i in range(8) + ] + self._servers = [ + CMW500IPerfService(cmw, meas_id, i + 1, IPerfMode.SERVER) + for i in range(8) + ] + + def configure_services(self, server_count, client_count): + """Configures the IPerf services. + + Args: + server_count: the number of servers to initialize. + client_count: the number of clients to initialize. + """ + if server_count > 8: + raise CmwIPerfError( + 'CMW500 supports a maximum of 8 active servers, {} requested'. + format(server_count)) + if client_count > 8: + raise CmwIPerfError( + 'CMW500 supports a maximum of 8 active clients, {} requested'. + format(client_count)) + + self._server_count = server_count + self._client_count = client_count + + # CMW500 contains 8 servers/client applications, enable only requested + # services and disable the rest. + for i in range(server_count): + self._servers[i].enabled = True + for i in range(server_count, 8): + self._servers[i].enabled = False + + for i in range(client_count): + self._clients[i].enabled = True + for i in range(client_count, 8): + self._clients[i].enabled = False + + def start(self): + """Starts the performance measurement on the callbox.""" + if self.test_type != IPerfType.IPERF: + raise CmwIPerfError( + 'Unable to run performance test, test type {} not supported'. + format(self.test_type)) + + cmd = 'INITiate:DATA:MEAS{}:IPERf'.format(self._meas_id) + self._cmw.send_and_recv(cmd) + self._wait_for_state( + {IPerfMeasurementState.RUN, IPerfMeasurementState.READY}) + + def stop(self): + """Halts the measurement immediately and puts it the RDY state.""" + cmd = 'STOP:DATA:MEAS{}:IPERf'.format(self._meas_id) + self._cmw.send_and_recv(cmd) + self._wait_for_state( + {IPerfMeasurementState.OFF, IPerfMeasurementState.READY}) + + def close(self): + """Halts the measurement immediately and puts it the OFF state.""" + cmd = 'ABORt:DATA:MEAS{}:IPERf'.format(self._meas_id) + self._cmw.send_and_recv(cmd) + self._wait_for_state({IPerfMeasurementState.OFF}) + + @property + def servers(self): + """Gets all enabled servers.""" + return self._servers[:self._server_count] + + @property + def clients(self): + """Gets all enabled clients.""" + return self._clients[:self._client_count] + + @property + def ipv4_address(self): + """Gets the current DAU IPv4 address.""" + cmd = 'SENSe:DATA:CONTrol:IPVFour:CURRent:IPADdress?' + return self._cmw.send_and_recv(cmd).strip('"\'') + + @property + def test_time(self): + """Gets the duration of the IPerf measurement.""" + cmd = 'CONFigure:DATA:MEAS{}:IPERf:TDURation?'.format(self._meas_id) + return int(self._cmw.send_and_recv(cmd)) + + @test_time.setter + def test_time(self, duration): + """Gets the duration of the IPerf measurement. + + Args: + duration: the length of the IPerf measurement (in s). + """ + cmd = 'CONFigure:DATA:MEAS{}:IPERf:TDURation {}'.format( + self._meas_id, duration) + self._cmw.send_and_recv(cmd) + + @property + def test_type(self): + """Gets the type of IPerf application to use.""" + cmd = 'CONFigure:DATA:MEAS{}:IPERf:TYPE?'.format(self._meas_id) + return IPerfType(self._cmw.send_and_recv(cmd)) + + @test_type.setter + def test_type(self, mode): + """Sets the type of IPerf application to use. + + Args: + mode: IPER/IP3 + """ + if not isinstance(mode, IPerfType): + raise ValueError('mode should be the instance of IPerfType') + + cmd = 'CONFigure:DATA:MEAS{}:IPERf:TYPE {}'.format( + self._meas_id, mode.value) + self._cmw.send_and_recv(cmd) + + @property + def packet_size(self): + """Gets the IPerf session packet size.""" + cmd = 'CONFigure:DATA:MEAS{}:IPERf:PSIZe?'.format(self._meas_id) + return int(self._cmw.send_and_recv(cmd)) + + @packet_size.setter + def packet_size(self, size): + """Sets the IPerf session packet size. + + Args: + size: the packet size in B + """ + cmd = 'CONFigure:DATA:MEAS{}:IPERf:PSIZe {}'.format( + self._meas_id, size) + self._cmw.send_and_recv(cmd) + + @property + def state(self): + """Gets the current state of the measurement""" + cmd = 'FETCh:DATA:MEAS{}:IPERf:STATe?'.format(self._meas_id) + return IPerfMeasurementState(self._cmw.send_and_recv(cmd)) + + def _wait_for_state(self, states, timeout=10): + """Polls the measurement state until it reaches an allowable state + + Args: + states: the allowed states + timeout: the maximum amount time to wait + """ + while timeout > 0: + if self.state in states: + return + + time.sleep(1) + timeout -= 1 + + raise CmwIPerfError('Failed enter IPerf state: {}.'.format(states)) + + +class CMW500IPerfService(object): + """Class for controlling a single IPerf measurement instance.""" + def __init__(self, cmw, meas_id, service_id, mode): + """Initializes a CMW500 IPerf service instance. + + Args: + cmw: the cmw500 instrument controller. + meas_idx: the cmw500 measurement instance to use (always 1). + service_id: the client/sever id [1 - 8]. + mode: the IPerf mode to use (client/server). + """ + self._cmw = cmw + self._meas_id = meas_id + self._service_id = service_id + self._mode = mode + + @property + def enabled(self): + """Gets if the measurement is enabled.""" + cmd = 'CONFigure:DATA:MEAS{}:IPERf:{}{}:ENABle?'.format( + self._meas_id, self._mode.value, self._service_id) + return self._cmw.send_and_recv(cmd) == 'ON' + + @enabled.setter + def enabled(self, enabled): + """Sets if the measurement is enabled. + + Args: + enabled: True/False + """ + status = 'ON' if enabled else 'OFF' + cmd = 'CONFigure:DATA:MEAS{}:IPERf:{}{}:ENABle {}'.format( + self._meas_id, self._mode.value, self._service_id, status) + self._cmw.send_and_recv(cmd) + + @property + def protocol(self): + """Gets the IPerf protocol.""" + cmd = 'CONFigure:DATA:MEAS{}:IPERf:{}{}:PROTocol?'.format( + self._meas_id, self._mode.value, self._service_id) + return IPerfProtocol(self._cmw.send_and_recv(cmd)) + + @protocol.setter + def protocol(self, protocol): + """Sets the IPerf protocol. + + Args: + protocol: TCP/UDP + """ + if not isinstance(protocol, IPerfProtocol): + raise ValueError( + 'protocol should be the instance of IPerfProtocolType') + + cmd = 'CONFigure:DATA:MEAS{}:IPERf:{}{}:PROTocol {}'.format( + self._meas_id, self._mode.value, self._service_id, protocol.value) + self._cmw.send_and_recv(cmd) + + @property + def ip_address(self): + """Gets the service IP address (clients only).""" + cmd = 'CONFigure:DATA:MEAS{}:IPERf:{}{}:IPADdress?'.format( + self._meas_id, self._mode.value, self._service_id) + return self._cmw.send_and_recv(cmd).strip('"\'') + + @ip_address.setter + def ip_address(self, address): + """Sets the service IP address (clients only). + + Args: + address: ip address of the IPerf server + """ + cmd = 'CONFigure:DATA:MEAS{}:IPERf:{}{}:IPADdress "{}"'.format( + self._meas_id, self._mode.value, self._service_id, address) + self._cmw.send_and_recv(cmd) + + @property + def port(self): + """Gets the IPerf client/server port.""" + cmd = 'CONFigure:DATA:MEAS{}:IPERf:{}{}:PORT?'.format( + self._meas_id, self._mode.value, self._service_id) + return int(self._cmw.send_and_recv(cmd)) + + @port.setter + def port(self, port): + """Gets the IPerf client/server port. + + Args: + port: the port number to use + """ + cmd = 'CONFigure:DATA:MEAS{}:IPERf:{}{}:PORT {}'.format( + self._meas_id, self._mode.value, self._service_id, port) + self._cmw.send_and_recv(cmd) + + @property + def parallel_connections(self): + """Gets the number of parallel connections (TCP only)""" + cmd = 'CONFigure:DATA:MEAS{}:IPERf:{}{}:PCONnection?'.format( + self._meas_id, self._mode.value, self._service_id) + return int(self._cmw.send_and_recv(cmd)) + + @parallel_connections.setter + def parallel_connections(self, parallel_count): + """Sets the number of parallel connections (TCP only). + + Args: + parallel_count: number of parallel connections to use + """ + cmd = 'CONFigure:DATA:MEAS{}:IPERf:{}{}:PCONnection {}'.format( + self._meas_id, self._mode.value, self._service_id, parallel_count) + self._cmw.send_and_recv(cmd) + + @property + def window_size(self): + """Gets the IPerf window size.""" + cmd = 'CONFigure:DATA:MEAS{}:IPERf:{}{}:SBSize?'.format( + self._meas_id, self._mode.value, self._service_id) + return int(self._cmw.send_and_recv(cmd)) + + @window_size.setter + def window_size(self, size): + """Sets the IPerf window size. + + Args: + size: window size in kB + """ + cmd = 'CONFigure:DATA:MEAS{}:IPERf:{}{}:SBSize {}'.format( + self._meas_id, self._mode.value, self._service_id, size) + self._cmw.send_and_recv(cmd) + + @property + def max_bitrate(self): + """Gets the maximum bitrate (UDP client only).""" + cmd = 'CONFigure:DATA:MEAS{}:IPERf:{}{}:BITRate?'.format( + self._meas_id, self._mode.value, self._service_id) + return float(self._cmw.send_and_recv(cmd)) + + @max_bitrate.setter + def max_bitrate(self, bitrate): + """Sets the maximum bitrate (UDP client only). + + Args: + bitrate: the maximum bitrate in bps + """ + cmd = 'CONFigure:DATA:MEAS{}:IPERf:{}{}:BITRate {}'.format( + self._meas_id, self._mode.value, self._service_id, bitrate) + self._cmw.send_and_recv(cmd) + + def _query_results(self): + """Queries results for all IPerf services. + + Returns: a flat list of strings in the format: + [reliability, + server1_count, client1_count, server1_throughput, server1_loss, + client1_throughput + ..., + server8_count, server8_throughput, server8_loss, client8_throughput + ] + + Note: + - The reliability field is not used + - Missing values are set to "NAV" and their "count" will be 0 + + Examples:: + + Results from a single client on sample 5: + ["0", + "0","5","NAV","NAV","2.791470E+08", + "0","0","NAV","NAV","NAV", + "0","0","NAV","NAV","NAV", + "0","0","NAV","NAV","NAV", + "0","0","NAV","NAV","NAV", + "0","0","NAV","NAV","NAV", + "0","0","NAV","NAV","NAV", + "0","0","NAV","NAV","NAV", + ] + """ + return self._cmw.send_and_recv('FETCh:DATA:MEAS{}:IPERf:ALL?'.format( + self._service_id)).split(',') + + @property + def count(self): + """Gets the result sample ID or None if no samples are available. + + Note: CMW500 does not return all IPerf results, it only returns + the most recent result. Results are differentiated with different "IDs" + which is just the sample count number. + """ + results = self._query_results() + if self._mode == IPerfMode.CLIENT: + index = (self._service_id - + 1) * _MEASUREMENT_SIZE + _CLIENT_COUNT_INDEX + else: + index = (self._service_id - + 1) * _MEASUREMENT_SIZE + _SERVER_COUNT_INDEX + return _try_parse(results[index], int) + + @property + def throughput(self): + """Gets the current throughput, or None if no samples are available. + + Returns: + The maximum throughput in bps. + """ + results = self._query_results() + if self._mode == IPerfMode.CLIENT: + index = (self._service_id - + 1) * _MEASUREMENT_SIZE + _CLIENT_THROUGHPUT_INDEX + else: + index = (self._service_id - + 1) * _MEASUREMENT_SIZE + _SERVER_THROUGHPUT_INDEX + return _try_parse(results[index], float) + + @property + def loss(self): + """Gets the current loss rate, or None if no samples are available. + + Note: Only applicable for UDP servers, otherwise will be 0. + + Returns: + The loss rate in %. + """ + results = self._query_results() + if self._mode == IPerfMode.CLIENT: + raise CmwIPerfError( + 'Loss is not available on IPerf Client measurements.') + else: + index = (self._service_id - + 1) * _MEASUREMENT_SIZE + _SERVER_LOSS_INDEX + return _try_parse(results[index], float) + + +class CmwIPerfError(Exception): + """Class to raise exceptions related to cmx IPerf measurements.""" diff --git a/acts/framework/acts/controllers/rohdeschwarz_lib/cmx500.py b/acts/framework/acts/controllers/rohdeschwarz_lib/cmx500.py index cfa80794d..0d3a9a3c3 100644 --- a/acts/framework/acts/controllers/rohdeschwarz_lib/cmx500.py +++ b/acts/framework/acts/controllers/rohdeschwarz_lib/cmx500.py @@ -1397,7 +1397,75 @@ class NrBaseStation(BaseStation): config: The NrCellConfig for current base station. """ - logger.warning('Not implement yet') + logger.info( + f'Configure Nr drx with\n' + f'drx_on_duration_timer: {config.drx_on_duration_timer}\n' + f'drx_inactivity_timer: {config.drx_inactivity_timer}\n' + f'drx_retransmission_timer_dl: {config.drx_retransmission_timer_dl}\n' + f'drx_retransmission_timer_ul: {config.drx_retransmission_timer_ul}\n' + f'drx_long_cycle: {config.drx_long_cycle}\n' + f'drx_long_cycle_offset: {config.drx_long_cycle_offset}\n' + f'harq_rtt_timer_dl: {config.harq_rtt_timer_dl}\n' + f'harq_rtt_timer_ul: {config.harq_rtt_timer_ul}\n' + f'slot_offset: {config.slot_offset}\n' + ) + + from mrtype.nr.drx import ( + NrDrxConfig, + NrDrxInactivityTimer, + NrDrxOnDurationTimer, + NrDrxRetransmissionTimer, + NrDrxHarqRttTimer, + NrDrxSlotOffset, + ) + + from mrtype.nr.drx import NrDrxLongCycleStartOffset as longCycle + + long_cycle_mapping = { + 10: longCycle.ms10, 20: longCycle.ms20, 32: longCycle.ms32, + 40: longCycle.ms40, 60: longCycle.ms60, 64: longCycle.ms64, + 70: longCycle.ms70, 80: longCycle.ms80, 128: longCycle.ms128, + 160: longCycle.ms160, 256: longCycle.ms256, 320: longCycle.ms320, + 512: longCycle.ms512, 640: longCycle.ms640, 1024: longCycle.ms1024, + 1280: longCycle.ms1280, 2048: longCycle.ms2048, + 2560: longCycle.ms2560, + } + + drx_on_duration_timer = NrDrxOnDurationTimer( + int(config.drx_on_duration_timer) + ) + drx_inactivity_timer = NrDrxInactivityTimer( + int(config.drx_inactivity_timer) + ) + drx_retransmission_timer_dl = NrDrxRetransmissionTimer( + int(config.drx_retransmission_timer_dl) + ) + drx_retransmission_timer_ul = NrDrxRetransmissionTimer( + int(config.drx_retransmission_timer_ul) + ) + drx_long_cycle = long_cycle_mapping[int(config.drx_long_cycle)] + drx_long_cycle_offset = drx_long_cycle( + int(config.drx_long_cycle_offset) + ) + harq_rtt_timer_dl = NrDrxHarqRttTimer(config.harq_rtt_timer_dl) + harq_rtt_timer_ul = NrDrxHarqRttTimer(config.harq_rtt_timer_ul) + slot_offset=NrDrxSlotOffset(config.slot_offset) + + nr_drx_config = NrDrxConfig( + on_duration_timer=drx_on_duration_timer, + inactivity_timer=drx_inactivity_timer, + retransmission_timer_dl=drx_retransmission_timer_dl, + retransmission_timer_ul=drx_retransmission_timer_ul, + long_cycle_start_offset=drx_long_cycle_offset, + harq_rtt_timer_dl=harq_rtt_timer_dl, + harq_rtt_timer_ul=harq_rtt_timer_ul, + slot_offset=slot_offset, + ) + + self._cmx.dut.nr_cell_group().set_drx_and_adjust_scheduler( + nr_drx_config + ) + self._network.apply_changes() def set_dl_channel(self, channel): """Sets the downlink channel number of cell. diff --git a/acts/framework/acts/controllers/rohdeschwarz_lib/cmx500_iperf_measurement.py b/acts/framework/acts/controllers/rohdeschwarz_lib/cmx500_iperf_measurement.py new file mode 100644 index 000000000..438f6e62a --- /dev/null +++ b/acts/framework/acts/controllers/rohdeschwarz_lib/cmx500_iperf_measurement.py @@ -0,0 +1,350 @@ +#!/usr/bin/env python3 +# +# Copyright 2024 - 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. +"""Provides classes for managing IPerf sessions on CMX500 callboxes.""" + +from enum import Enum +from acts import logger + + +class IPerfType(Enum): + """Supported performance measurement types.""" + + IPERF = "IPERF" + IPERF3 = "IPERF3" + NAT = "NAT" + + +class IPerfProtocol(Enum): + """Supported IPerf protocol types.""" + + TCP = "TCP" + UDP = "UDP" + + +def delete_all_iperfs(): + from xlapi import meas + meas.delete_all_iperfs() + + +class Cmx500IPerfMeasurement(object): + + DEFAULT_TEST_TIME = 30 + """Class for managing IPerf measurement sessions on CMX500.""" + + def __init__(self): + """Initializes a new CMW500 Iperf Measurement. + + Examples:: + + Using just a single client: + measurement = Cmx500IPerfMeasurement() + measurement.configure_services(0, 1) + measurement.test_time = 10 + ... + measurement.start() + time.sleep(10) + client = measurement.clients[0] + for result in client.all_results: + print(f"took measurement: {result}") + """ + + self.clients = [] + self.servers = [] + + # Unlike with CMW500s, test_time is set per-service, keep track of it + # in the measurement and just apply to all services at start to keep + # things consistent. + self.test_time = self.DEFAULT_TEST_TIME + + def configure_services(self, server_count, client_count): + """Configures the IPerf services. + + Args: + server_count: the number of servers to initialize. + client_count: the number of clients to initialize. + """ + from xlapi.meas import IPerfMode + + # If configuration matches, don't do anything. + if len(self.clients) == client_count and len( + self.servers) == server_count: + return + + self.close() + self.servers = [ + Cmx500IPerfService(IPerfMode.SERVER) for _ in range(server_count) + ] + self.clients = [ + Cmx500IPerfService(IPerfMode.CLIENT) for _ in range(client_count) + ] + + def start(self): + """Starts the performance measurement on the callbox.""" + from mrtype import Time + test_time = Time.s(self.test_time) + for service in self.servers + self.clients: + service.start_single_shot(test_time) + + def stop(self): + """Halts the measurement.""" + for service in self.clients + self.servers: + service.stop() + + def close(self): + """Halts the measurement and releases all resources.""" + self.stop() + for service in self.clients + self.servers: + service.close() + + self.clients.clear() + self.servers.clear() + + @property + def ipv4_address(self): + """Gets the current DAU IPv4 address.""" + from xlapi import platform_manager + + data_service = platform_manager.get_default().mrt_data_service_stub + addresses = data_service.GetStaticDauIpV4Addresses() + if not addresses: + raise CmxIPerfError('No DAU IP address available') + # DAU can have multiple addresses, always use first one. + return addresses[0].value + + @property + def test_time(self): + """Gets the duration of the IPerf measurement (in s).""" + return self._time + + @test_time.setter + def test_time(self, duration): + """Sets the performance test duration. + + Args: + duration: the length of the IPerf measurement (in s). + """ + self._time = duration + + +class Cmx500IPerfService(object): + """Class for controlling a single IPerf measurement instance.""" + + def __init__(self, mode): + from xlapi import meas + + self.logger = logger.create_logger() + self._perf = meas.create_iperf() + self._perf.mode = mode + self._init_xlapi() + + def _init_xlapi(self): + """Initialize xlapi types.""" + from mrtype import DataSize, DataRate + self._data_size = DataSize + self._data_rate = DataRate + + def start_single_shot(self, time): + """Starts the IPerf client/server.""" + self._perf.start_single_shot(time) + + def stop(self): + """Waits for current measurement session to finish.""" + if self._perf.is_running(): + self._perf.stop() + + def close(self): + """Halts the measurement and releases all resources.""" + self.stop() + self._perf.delete() + + @property + def test_type(self): + """Gets the type of IPerf application to use.""" + return self._perf.application + + @test_type.setter + def test_type(self, mode): + """Sets the type of IPerf application to use + + Args: + mode: IPER/IP3 + """ + if not isinstance(mode, IPerfType): + raise ValueError("mode should be the instance of IPerfType") + + from xlapi.meas import IPerfApplication + + if mode == IPerfType.IPERF: + self._perf.application = IPerfApplication.IPERF + elif mode == IPerfType.IPERF3: + self._perf.application = IPerfApplication.IPERF3 + elif mode == IPerfType.NAT: + self._perf.application = IPerfApplication.IPERF_NAT_ + else: + raise CmxIPerfError( + "Unsupported IPerf application: {}".format(mode)) + + @property + def protocol(self): + """Gets the IPerf protocol.""" + return self._perf.protocol + + @protocol.setter + def protocol(self, protocol): + """Sets the IPerf protocol. + + Args: + protocol: TCP/UDP + """ + if not isinstance(protocol, IPerfProtocol): + raise ValueError( + "protocol should be the instance of IPerfProtocol") + from xlapi.meas import IPerfProtocol as _IPerfProtocol + + if protocol == IPerfProtocol.TCP: + self._perf.protocol = _IPerfProtocol.TCP + else: + self._perf.protocol = _IPerfProtocol.UDP + + @property + def ip_address(self): + """Gets the IPerf client/server port.""" + return self._perf.ip_address + + @ip_address.setter + def ip_address(self, address): + """Sets the service IP address (clients only). + + Args: + address: ip address of the IPerf server + """ + self._perf.ip_address = address + + @property + def port(self): + """Gets the IPerf client/server port.""" + return self._perf.port + + @port.setter + def port(self, port): + """Gets the IPerf client/server port. + + Args: + port: the port number to use + """ + self._perf.port = port + + @property + def parallel_connections(self): + """Gets the number of parallel connections (TCP only)""" + return self._perf.parallel_connections + + @parallel_connections.setter + def parallel_connections(self, parallel_count): + """Sets the number of parallel connections (TCP only) + + Args: + parallel_count: number of parallel connections to use + """ + self._perf.parallel_connections = parallel_count + + @property + def packet_size(self): + """Gets the IPerf packet size.""" + return self._perf.packet_size.in_B() + + @packet_size.setter + def packet_size(self, size): + """Sets the IPerf packet size. + + Args: + size: the packet size in B + """ + self._perf.packet_size = self._data_size.B(size) + + @property + def window_size(self): + """Gets the IPerf window size.""" + return self._perf.tcp_window_size.in_kB() + + @window_size.setter + def window_size(self, size): + """Sets the IPerf window size. + + Args: + size: the window size in kB + """ + self._perf.tcp_window_size = self._data_size.kB(size) + + @property + def max_bitrate(self): + """Gets the maximum bitrate (UDP client only).""" + return self._perf.bandwidth.in_bps() + + @max_bitrate.setter + def max_bitrate(self, bitrate): + """Sets the maximum bitrate (UDP client only). + + Args: + bitrate: the maximum bitrate in bps + """ + self._perf.bandwidth = self._data_rate.bps(bitrate) + + @property + def all_results(self): + """Gets all throughput results in bps. + + Returns: + A list of chronological floating point throughput measurements. + """ + from xlapi.meas import IPerfMode + + try: + if self._perf.mode == IPerfMode.CLIENT: + result = self._perf.result.dl.raw_data + else: + result = self._perf.result.ul.raw_data + + # xlapi returns results in reversed chronological order + return [d.in_bps() for d in result][::-1] + except Exception as e: + # xlapi will raise an error if no results are available yet. + self.logger.error("Failed to get results: {}".format(e)) + return [] + + @property + def count(self): + """Gets the available result sample count.""" + results = self.all_results + if results: + return len(results) + return 0 + + @property + def throughput(self): + """Gets the most recent throughput or None if no data is available. + + Returns: + The maximum throughput in bps. + """ + results = self.all_results + if results: + return results[-1] + return None + + +class CmxIPerfError(Exception): + """Class to raise exceptions related to cmx IPerf measurements.""" diff --git a/acts_tests/acts_contrib/test_utils/wifi/WifiBaseTest.py b/acts_tests/acts_contrib/test_utils/wifi/WifiBaseTest.py index f57fab9f3..b9bbf093d 100644 --- a/acts_tests/acts_contrib/test_utils/wifi/WifiBaseTest.py +++ b/acts_tests/acts_contrib/test_utils/wifi/WifiBaseTest.py @@ -17,7 +17,9 @@ Base Class for Defining Common WiFi Test Functionality """ +import contextlib import copy +import logging import os import time @@ -41,6 +43,27 @@ AP_1 = 0 AP_2 = 1 MAX_AP_COUNT = 2 +@contextlib.contextmanager +def logged_suppress(message: str, allow_test_fail: bool = False): + """Suppresses any Exceptions and logs the outcome. + + This is to make sure all steps in every test class's teardown_test/on_fail + are executed even if super().teardown_test() or super().on_fail() + is called at the very beginning. + + Args: + message: message to describe the error. + allow_test_fail: True to re-raise the exception, False to suppress it. + + Yields: + None + """ + try: + yield + except signals.TestFailure: + if allow_test_fail: + raise + logging.exception(message) class WifiBaseTest(BaseTestClass): def __init__(self, configs): @@ -85,11 +108,21 @@ class WifiBaseTest(BaseTestClass): for ad in self.android_devices: proc = nutils.start_tcpdump(ad, self.test_name) self.tcpdump_proc.append((ad, proc)) + + # Delete any existing ssrdumps. + ad.log.info("Deleting existing ssrdumps") + ad.adb.shell("find /data/vendor/ssrdump/ -type f -delete", + ignore_status=True) + if hasattr(self, "packet_logger"): self.packet_log_pid = wutils.start_pcap(self.packet_logger, 'dual', self.test_name) def teardown_test(self): + with logged_suppress("SubSystem Restart(SSR) Exception.", + allow_test_fail=True): + self._check_ssrdumps() + if (hasattr(self, "android_devices")): wutils.stop_all_wlan_logs(self.android_devices) for proc in self.tcpdump_proc: @@ -115,7 +148,6 @@ class WifiBaseTest(BaseTestClass): for ad in self.android_devices: ad.take_bug_report(test_name, begin_time) ad.cat_adb_log(test_name, begin_time) - wutils.get_ssrdumps(ad) wutils.stop_all_wlan_logs(self.android_devices) for ad in self.android_devices: wutils.get_wlan_logs(ad) @@ -132,6 +164,20 @@ class WifiBaseTest(BaseTestClass): for device in getattr(self, "fuchsia_devices", []): self.on_device_fail(device, test_name, begin_time) + def _check_ssrdumps(self): + """Failed the test if SubSystem Restart occurred on any device.""" + is_ramdump_happened = False + if (hasattr(self, "android_devices")): + for ad in self.android_devices: + wutils.get_ssrdumps(ad) + if wutils.has_ssrdumps(ad): + is_ramdump_happened = True + + if is_ramdump_happened: + raise signals.TestFailure( + f"SubSystem Restart(SSR) occurred on " + f"{self.TAG}:{self.current_test_name}") + def on_device_fail(self, device, test_name, begin_time): """Gets a generic device DUT bug report. diff --git a/acts_tests/acts_contrib/test_utils/wifi/wifi_test_utils.py b/acts_tests/acts_contrib/test_utils/wifi/wifi_test_utils.py index 5af197101..7f939f23a 100755 --- a/acts_tests/acts_contrib/test_utils/wifi/wifi_test_utils.py +++ b/acts_tests/acts_contrib/test_utils/wifi/wifi_test_utils.py @@ -2534,6 +2534,15 @@ def get_current_softap_capability(ad, callbackId, need_to_wait): return capability +def has_ssrdumps(ad) -> bool: + """Checks if ssrdumps files are present in ssrdump dir + + Returns: + True if ssrdumps are present, False otherwise. + """ + files = ad.get_file_names("/data/vendor/ssrdump/") + return bool(files) + def get_ssrdumps(ad): """Pulls dumps in the ssrdump dir Args: @@ -2545,9 +2554,6 @@ def get_ssrdumps(ad): log_path = os.path.join(ad.device_log_path, "SSRDUMPS_%s" % ad.serial) os.makedirs(log_path, exist_ok=True) ad.pull_files(logs, log_path) - ad.adb.shell("find /data/vendor/ssrdump/ -type f -delete", - ignore_status=True) - def start_pcap(pcap, wifi_band, test_name): """Start packet capture in monitor mode. @@ -2653,7 +2659,7 @@ def start_wlan_logs(ad): def stop_all_wlan_logs(ads): for ad in ads: stop_wlan_logs(ad) - ad.log.info("Wait 30s for the createion of zip file for wlan logs") + ad.log.info("Wait 30s for the creation of zip file for wlan logs") time.sleep(30) def stop_wlan_logs(ad): diff --git a/acts_tests/tests/google/power/tel/PowerTelPdcch_Modem_Test.py b/acts_tests/tests/google/power/tel/PowerTelPdcch_Modem_Test.py index d2623b977..805dd6533 100644 --- a/acts_tests/tests/google/power/tel/PowerTelPdcch_Modem_Test.py +++ b/acts_tests/tests/google/power/tel/PowerTelPdcch_Modem_Test.py @@ -54,3 +54,11 @@ class PowerTelPdcch_Modem_Test(cppt.PowerTelPDCCHTest): def test_nr_1_n78_pdcch(self): self.display_name_test_case = 'PDCCH 5G Sub6 NSA' self.power_pdcch_test() + + def test_nr_1_n48_cdrx(self): + self.display_name_test_case = 'CDRx 5G Sub6 NSA' + self.power_pdcch_test() + + def test_nr_1_n78_cdrx(self): + self.display_name_test_case = 'CDRx 5G Sub6 NSA' + self.power_pdcch_test() diff --git a/acts_tests/tests/google/wifi/WifiBridgedApTest.py b/acts_tests/tests/google/wifi/WifiBridgedApTest.py index d98a7cbf3..392fdc122 100644 --- a/acts_tests/tests/google/wifi/WifiBridgedApTest.py +++ b/acts_tests/tests/google/wifi/WifiBridgedApTest.py @@ -57,10 +57,7 @@ class WifiBridgedApTest(WifiBaseTest): self.client1 = self.android_devices[1] self.client2 = self.android_devices[2] else: - raise signals.TestAbortClass("WifiBridgedApTest requires 3 DUTs") - - if not self.dut.droid.wifiIsBridgedApConcurrencySupported(): - raise signals.TestAbortClass("Legacy phone is not supported") + raise signals.TestFailure("WifiBridgedApTest requires 3 DUTs") req_params = ["dbs_supported_models"] opt_param = [] @@ -70,6 +67,7 @@ class WifiBridgedApTest(WifiBaseTest): def setup_test(self): super().setup_test() + asserts.skip_if(not self.dut.droid.wifiIsBridgedApConcurrencySupported(), "Phone %s doesn't support bridged AP." % (self.dut.model)) for ad in self.android_devices: wutils.reset_wifi(ad) wutils.wifi_toggle_state(self.dut, False) @@ -1332,4 +1330,4 @@ class WifiBridgedApTest(WifiBaseTest): self.dut, [WifiEnums.WIFI_CONFIG_SOFTAP_BAND_2G, WifiEnums.WIFI_CONFIG_SOFTAP_BAND_5G], False) # Restore config - wutils.save_wifi_soft_ap_config(self.dut, original_softap_config)
\ No newline at end of file + wutils.save_wifi_soft_ap_config(self.dut, original_softap_config) diff --git a/acts_tests/tests/google/wifi/WifiCrashStressTest.py b/acts_tests/tests/google/wifi/WifiCrashStressTest.py index 9982cd899..4b0a8a56d 100644 --- a/acts_tests/tests/google/wifi/WifiCrashStressTest.py +++ b/acts_tests/tests/google/wifi/WifiCrashStressTest.py @@ -74,6 +74,10 @@ class WifiCrashStressTest(WifiBaseTest): wutils.wifi_toggle_state(self.dut_client, True) def teardown_test(self): + # Deletes all ssrdump files in every DUTs. + for ad in self.android_devices: + ad.adb.shell("find /data/vendor/ssrdump/ -type f -delete", + ignore_status=True) super().teardown_test() if self.dut.droid.wifiIsApEnabled(): wutils.stop_wifi_tethering(self.dut) diff --git a/acts_tests/tests/google/wifi/WifiPnoTest.py b/acts_tests/tests/google/wifi/WifiPnoTest.py index 70a9eca08..05337f872 100644 --- a/acts_tests/tests/google/wifi/WifiPnoTest.py +++ b/acts_tests/tests/google/wifi/WifiPnoTest.py @@ -25,6 +25,7 @@ WifiEnums = wutils.WifiEnums MAX_ATTN = 95 WAIT_WIFI_SCAN_RESULTS_SEC = 10 WAIT_ATTENUATION_SEC = 5 +WAIT_WIFI_DISCONNECT_SEC = 60 class WifiPnoTest(WifiBaseTest): @@ -103,25 +104,39 @@ class WifiPnoTest(WifiBaseTest): raise def trigger_pno_and_assert_connect(self, ad, attn_val_name, expected_con): - """Sets attenuators to disconnect current connection to trigger PNO. - Validate that the DUT connected to the new SSID as expected after PNO. + """Trigger PNO and verify the connection after PNO. Args: ad: Android Device to trigger PNO on. attn_val_name: Name of the attenuation value pair to use. expected_con: The expected info of the network to we expect the DUT - to roam to. + to connect to. """ connection_info = ad.droid.wifiGetConnectionInfo() - ad.log.info("Triggering PNO connect from %s to %s", - connection_info[WifiEnums.SSID_KEY], - expected_con[WifiEnums.SSID_KEY]) + + # Stops APs to force DUT to disconnect and restart APs. + for i in range(len(self.user_params["OpenWrtAP"])): + self.openwrt = self.access_points[i] + self.openwrt.stop_ap() + + wutils.wait_for_disconnect(self.dut, + timeout=WAIT_WIFI_DISCONNECT_SEC) + + for i in range(len(self.user_params["OpenWrtAP"])): + self.openwrt = self.access_points[i] + self.openwrt.start_ap() + self.set_attns(attn_val_name) - ad.log.info("Wait %ss for triggering PNO.", self.pno_interval) + + ad.log.info("Wait %ss for triggering PNO scan, connect from %s to %s.", + self.pno_interval, + connection_info[WifiEnums.SSID_KEY], + expected_con[WifiEnums.SSID_KEY]) time.sleep(self.pno_interval) + try: ad.log.info("Expect it's connected to %s after PNO interval" - % ad.droid.wifiGetConnectionInfo()) + % ad.droid.wifiGetConnectionInfo()[WifiEnums.SSID_KEY]) expected_ssid = expected_con[WifiEnums.SSID_KEY] verify_con = {WifiEnums.SSID_KEY: expected_ssid} wutils.verify_wifi_connection_info(ad, verify_con) @@ -164,12 +179,14 @@ class WifiPnoTest(WifiBaseTest): """Test PNO triggered autoconnect to a network. Steps: - 1. Switch off the screen on the device. - 2. Save 2 valid network configurations (a & b) in the device. - 3. Attenuate 5Ghz network and wait for a few seconds to trigger PNO. - 4. Check the device connected to 2Ghz network automatically. + 1. Puts the DUT to sleep. + 2. DUT connects to a 2G(a) DUT and a 5G(b) network so they will be + saved network and won't be excluded from PNO scan. + 3. Stops APs to force DUT to disconnect and restart APs. + 4. Attenuates to (2G in range, 5G out of range). + 5. Waits for 120 seconds PNO interval. + 6. Checks the device connected to 2G network automatically. """ - # DUT connects to the saved networks so they won't be excluded from PNO scan. wutils.connect_to_wifi_network(self.dut, self.pno_network_a) wutils.connect_to_wifi_network(self.dut, self.pno_network_b) self.trigger_pno_and_assert_connect(self.dut, @@ -181,10 +198,13 @@ class WifiPnoTest(WifiBaseTest): """Test PNO triggered autoconnect to a network. Steps: - 1. Switch off the screen on the device. - 2. Save 2 valid network configurations (a & b) in the device. - 3. Attenuate 2Ghz network and wait for a few seconds to trigger PNO. - 4. Check the device connected to 5Ghz network automatically. + 1. Puts the DUT to sleep. + 2. DUT connects to a 5G(b) DUT and a 2G(a) network so they will be + saved network and won't be excluded from PNO scan. + 3. Stops APs to force DUT to disconnect from WiFi and restart APs. + 4. Attenuates to (5G in range, 2G out of range). + 5. Waits for 120 seconds PNO interval. + 6. Checks the device connected to 5G network automatically. """ # DUT connects to the saved networks so they won't be excluded from PNO scan. wutils.connect_to_wifi_network(self.dut, self.pno_network_b) @@ -195,17 +215,25 @@ class WifiPnoTest(WifiBaseTest): @test_tracker_info(uuid="844b15be-ff45-4b09-a11b-0b2b4bb13b22") def test_pno_connection_with_multiple_saved_networks(self): - """Test PNO triggered autoconnect to a network when there are more + """Test autoconnect with multiple saved networks after PNO. + + Test PNO triggered autoconnect to a network when there are more than 16 networks saved in the device. - 16 is the max list size of PNO watch list for most devices. The device should automatically - pick the 16 most recently connected networks. For networks that were never connected, the - networks seen in the previous scan result would have higher priority. + 16 is the max list size of PNO watch list for most devices. The device + should automatically pick the 16 most recently connected networks. + For networks that were never connected, the networks seen in the + previous scan result would have higher priority. Steps: - 1. Save 16 test network configurations in the device. - 2. Add 2 connectable networks and do a normal scan. - 3. Trigger PNO scan. + 1. Puts the DUt to sleep. + 2. Saves 16 test network configurations in the device. + 3. DUT connects to a 5G(b) DUT and a 2G(a) network so they will be + saved network and won't be excluded from PNO scan. + 4. Stops APs to force DUT to disconnect from WiFi and restart APs. + 5. Attenuates to (5G in range, 2G out of range). + 6. Waits for 120 seconds PNO interval. + 7. Checks the device connected to 5G network automatically. """ self.add_and_enable_test_networks(16) # DUT connects to the saved networks so they won't be excluded from PNO scan. diff --git a/acts_tests/tests/google/wifi/WifiStressTest.py b/acts_tests/tests/google/wifi/WifiStressTest.py index 37033ce55..6928d2516 100644 --- a/acts_tests/tests/google/wifi/WifiStressTest.py +++ b/acts_tests/tests/google/wifi/WifiStressTest.py @@ -230,6 +230,8 @@ class WifiStressTest(WifiBaseTest): connection_info[WifiEnums.SSID_KEY], expected_con[WifiEnums.SSID_KEY]) else: + logging.info("Move the DUT in WiFi range.") + self.attenuators[0].set_atten(MIN_ATTN) # force start a single scan so we don't have to wait for the scheduled scan. wutils.start_wifi_connection_scan_and_return_status(self.dut) self.log.info("Wait 60s for network selection.") @@ -415,7 +417,7 @@ class WifiStressTest(WifiBaseTest): "https://www.youtube.com/watch?v=WNCl-69POro", "https://www.youtube.com/watch?v=dVkK36KOcqs", "https://www.youtube.com/watch?v=0wCC3aLXdOw", - "https://www.youtube.com/watch?v=rN6nlNC9WQA", + "https://www.youtube.com/watch?v=QpyGNwnEmKo", "https://www.youtube.com/watch?v=RK1K2bCg4J8" ] try: @@ -591,7 +593,7 @@ class WifiStressTest(WifiBaseTest): for count in range(self.stress_count): self.connect_and_verify_connected_ssid( self.reference_networks[0]['2g']) - # move the DUT out of range + logging.info("Move the DUT out of WiFi range and wait 10 seconds.") self.attenuators[0].set_atten(95) time.sleep(10) wutils.set_attns(self.attenuators, "default") |