summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2024-02-03 00:03:49 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2024-02-03 00:03:49 +0000
commitc165d01c0c5d76b510c496f2cdc673bd540a8e16 (patch)
treefe799a88a6e346981d38d7db36dfc9a8b6422194
parente9b2ea17ece3f222479083269fc76ae36ded350e (diff)
parent4b2857d70f1876fe4eb318e361a726917d3a32db (diff)
downloadconnectivity-simpleperf-release.tar.gz
Snap for 11400057 from 4b2857d70f1876fe4eb318e361a726917d3a32db to simpleperf-releasesimpleperf-release
Change-Id: Ib51e80d96b63fd311f875a2558fe2a4a5f4355c1
-rwxr-xr-xacts/framework/acts/controllers/android_device.py7
-rw-r--r--acts/framework/acts/controllers/cellular_lib/LteCellConfig.py3
-rw-r--r--acts/framework/acts/controllers/cellular_lib/NrCellConfig.py66
-rw-r--r--acts/framework/acts/controllers/openwrt_ap.py2
-rw-r--r--acts/framework/acts/controllers/openwrt_lib/openwrt_authentication.py93
-rw-r--r--acts/framework/acts/controllers/rohdeschwarz_lib/cmw500_iperf_measurement.py498
-rw-r--r--acts/framework/acts/controllers/rohdeschwarz_lib/cmx500.py70
-rw-r--r--acts/framework/acts/controllers/rohdeschwarz_lib/cmx500_iperf_measurement.py350
-rw-r--r--acts_tests/acts_contrib/test_utils/wifi/WifiBaseTest.py48
-rwxr-xr-xacts_tests/acts_contrib/test_utils/wifi/wifi_test_utils.py14
-rw-r--r--acts_tests/tests/google/power/tel/PowerTelPdcch_Modem_Test.py8
-rw-r--r--acts_tests/tests/google/wifi/WifiBridgedApTest.py8
-rw-r--r--acts_tests/tests/google/wifi/WifiCrashStressTest.py4
-rw-r--r--acts_tests/tests/google/wifi/WifiPnoTest.py76
-rw-r--r--acts_tests/tests/google/wifi/WifiStressTest.py6
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")