Merge "Refactor: clean up unused and replicated code."
diff --git a/acts/framework/acts/controllers/access_point.py b/acts/framework/acts/controllers/access_point.py
index 4f3f3b2..687ac94 100755
--- a/acts/framework/acts/controllers/access_point.py
+++ b/acts/framework/acts/controllers/access_point.py
@@ -36,6 +36,7 @@
from acts.controllers.ap_lib import radvd
from acts.controllers.ap_lib import radvd_config
from acts.controllers.ap_lib.extended_capabilities import ExtendedCapabilities
+from acts.controllers.ap_lib.wireless_network_management import BssTransitionManagementRequest
from acts.controllers.utils_lib.commands import ip
from acts.controllers.utils_lib.commands import route
from acts.controllers.utils_lib.commands import shell
@@ -907,3 +908,13 @@
raise ValueError(f'Invalid identifier {identifier} given')
instance = self._aps.get(identifier)
return instance.hostapd.get_sta_extended_capabilities(sta_mac)
+
+ def send_bss_transition_management_req(
+ self, identifier, sta_mac: str,
+ request: BssTransitionManagementRequest):
+ """Send a BSS Transition Management request to an associated STA."""
+ if identifier not in list(self._aps.keys()):
+ raise ValueError('Invalid identifier {identifier} given')
+ instance = self._aps.get(identifier)
+ return instance.hostapd.send_bss_transition_management_req(
+ sta_mac, request)
diff --git a/acts/framework/acts/controllers/ap_lib/hostapd.py b/acts/framework/acts/controllers/ap_lib/hostapd.py
index 995d3e2..c2cfc54 100644
--- a/acts/framework/acts/controllers/ap_lib/hostapd.py
+++ b/acts/framework/acts/controllers/ap_lib/hostapd.py
@@ -22,6 +22,7 @@
from acts.controllers.ap_lib import hostapd_config
from acts.controllers.ap_lib import hostapd_constants
from acts.controllers.ap_lib.extended_capabilities import ExtendedCapabilities
+from acts.controllers.ap_lib.wireless_network_management import BssTransitionManagementRequest
from acts.controllers.utils_lib.commands import shell
from acts.libs.proc.job import Result
@@ -153,7 +154,7 @@
"""Return MAC addresses of all associated STAs."""
list_sta_result = self._list_sta()
stas = set()
- for line in list_sta_result.stdout:
+ for line in list_sta_result.stdout.splitlines():
# Each line must be a valid MAC address. Capture it.
m = re.match(r'((?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2})', line)
if m:
@@ -196,6 +197,59 @@
raise Error(
f'ext_capab contains invalid hex string repr {raw_ext_capab}')
+ def _bss_tm_req(self, client_mac: str,
+ request: BssTransitionManagementRequest) -> Result:
+ """Send a hostapd BSS Transition Management request command to a STA.
+
+ Args:
+ client_mac: MAC address that will receive the request.
+ request: BSS Transition Management request that will be sent.
+ Returns:
+ acts.libs.proc.job.Result containing the results of the command.
+ Raises: See _run_hostapd_cli_cmd
+ """
+ bss_tm_req_cmd = f'bss_tm_req {client_mac}'
+
+ if request.abridged:
+ bss_tm_req_cmd += ' abridged=1'
+ if request.bss_termination_included and request.bss_termination_duration:
+ bss_tm_req_cmd += f' bss_term={request.bss_termination_duration.duration}'
+ if request.disassociation_imminent:
+ bss_tm_req_cmd += ' disassoc_imminent=1'
+ if request.disassociation_timer is not None:
+ bss_tm_req_cmd += f' disassoc_timer={request.disassociation_timer}'
+ if request.preferred_candidate_list_included:
+ bss_tm_req_cmd += ' pref=1'
+ if request.session_information_url:
+ bss_tm_req_cmd += f' url={request.session_information_url}'
+ if request.validity_interval:
+ bss_tm_req_cmd += f' valid_int={request.validity_interval}'
+
+ # neighbor= can appear multiple times, so it requires special handling.
+ for neighbor in request.candidate_list:
+ bssid = neighbor.bssid
+ bssid_info = hex(neighbor.bssid_information)
+ op_class = neighbor.operating_class
+ chan_num = neighbor.channel_number
+ phy_type = int(neighbor.phy_type)
+ bss_tm_req_cmd += f' neighbor={bssid},{bssid_info},{op_class},{chan_num},{phy_type}'
+
+ return self._run_hostapd_cli_cmd(bss_tm_req_cmd)
+
+ def send_bss_transition_management_req(
+ self, sta_mac: str,
+ request: BssTransitionManagementRequest) -> Result:
+ """Send a BSS Transition Management request to an associated STA.
+
+ Args:
+ sta_mac: MAC address of the STA in question.
+ request: BSS Transition Management request that will be sent.
+ Returns:
+ acts.libs.proc.job.Result containing the results of the command.
+ Raises: See _run_hostapd_cli_cmd
+ """
+ return self._bss_tm_req(sta_mac, request)
+
def is_alive(self):
"""
Returns:
diff --git a/acts/framework/acts/controllers/fuchsia_device.py b/acts/framework/acts/controllers/fuchsia_device.py
index 2bc64d1..8a36c85 100644
--- a/acts/framework/acts/controllers/fuchsia_device.py
+++ b/acts/framework/acts/controllers/fuchsia_device.py
@@ -14,6 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+from typing import Optional
import backoff
import json
import logging
@@ -198,25 +199,26 @@
self.conf_data = fd_conf_data
if "ip" not in fd_conf_data:
raise FuchsiaDeviceError(FUCHSIA_DEVICE_NO_IP_MSG)
- self.ip = fd_conf_data["ip"]
- self.orig_ip = fd_conf_data["ip"]
- self.sl4f_port = fd_conf_data.get("sl4f_port", 80)
- self.ssh_port = fd_conf_data.get("ssh_port", DEFAULT_SSH_PORT)
- self.ssh_config = fd_conf_data.get("ssh_config", None)
- self.ssh_priv_key = fd_conf_data.get("ssh_priv_key", None)
- self.authorized_file = fd_conf_data.get("authorized_file_loc", None)
- self.serial_number = fd_conf_data.get("serial_number", None)
- self.device_type = fd_conf_data.get("device_type", None)
- self.product_type = fd_conf_data.get("product_type", None)
- self.board_type = fd_conf_data.get("board_type", None)
- self.build_number = fd_conf_data.get("build_number", None)
- self.build_type = fd_conf_data.get("build_type", None)
- self.server_path = fd_conf_data.get("server_path", None)
- self.specific_image = fd_conf_data.get("specific_image", None)
- self.ffx_binary_path = fd_conf_data.get("ffx_binary_path", None)
- self.pm_binary_path = fd_conf_data.get("pm_binary_path", None)
- self.packages_path = fd_conf_data.get("packages_path", None)
- self.mdns_name = fd_conf_data.get("mdns_name", None)
+ self.ip: str = fd_conf_data["ip"]
+ self.orig_ip: str = fd_conf_data["ip"]
+ self.sl4f_port: int = fd_conf_data.get("sl4f_port", 80)
+ self.ssh_port: int = fd_conf_data.get("ssh_port", DEFAULT_SSH_PORT)
+ self.ssh_config: Optional[str] = fd_conf_data.get("ssh_config", None)
+ self.ssh_priv_key: Optional[str] = fd_conf_data.get("ssh_priv_key", None)
+ self.authorized_file: Optional[str] = fd_conf_data.get("authorized_file_loc", None)
+ self.serial_number: Optional[str] = fd_conf_data.get("serial_number", None)
+ self.device_type: Optional[str] = fd_conf_data.get("device_type", None)
+ self.product_type: Optional[str] = fd_conf_data.get("product_type", None)
+ self.board_type: Optional[str] = fd_conf_data.get("board_type", None)
+ self.build_number: Optional[str] = fd_conf_data.get("build_number", None)
+ self.build_type: Optional[str] = fd_conf_data.get("build_type", None)
+ self.server_path: Optional[str] = fd_conf_data.get("server_path", None)
+ self.specific_image: Optional[str] = fd_conf_data.get("specific_image", None)
+ self.ffx_binary_path: Optional[str] = fd_conf_data.get("ffx_binary_path", None)
+ # Path to a tar.gz archive with pm and amber-files, as necessary for
+ # starting a package server.
+ self.packages_archive_path: Optional[str] = fd_conf_data.get("packages_archive_path", None)
+ self.mdns_name: Optional[str] = fd_conf_data.get("mdns_name", None)
# Instead of the input ssh_config, a new config is generated with proper
# ControlPath to the test output directory.
@@ -429,9 +431,9 @@
self.wlan_policy_controller = WlanPolicyController(self)
def start_package_server(self):
- if not self.pm_binary_path or not self.packages_path:
+ if not self.packages_archive_path:
self.log.warn(
- "Either pm_binary_path or packages_path is not specified. "
+ "packages_archive_path is not specified. "
"Assuming a package server is already running and configured on "
"the DUT. If this is not the case, either run your own package "
"server, or configure these fields appropriately. "
@@ -444,8 +446,7 @@
)
return
- self.package_server = PackageServer(self.pm_binary_path,
- self.packages_path)
+ self.package_server = PackageServer(self.packages_archive_path)
self.package_server.start()
self.package_server.configure_device(self.ssh)
diff --git a/acts/framework/acts/controllers/fuchsia_lib/package_server.py b/acts/framework/acts/controllers/fuchsia_lib/package_server.py
index 96fe901..7c763e6 100644
--- a/acts/framework/acts/controllers/fuchsia_lib/package_server.py
+++ b/acts/framework/acts/controllers/fuchsia_lib/package_server.py
@@ -16,14 +16,16 @@
import json
import os
+import shutil
import socket
import subprocess
+import tarfile
+import tempfile
import time
from dataclasses import dataclass
from datetime import datetime
-from io import FileIO
-from typing import List, Optional
+from typing import TextIO, List, Optional
from acts import context
from acts import logger
@@ -112,44 +114,49 @@
class PackageServer:
- """Package manager for Fuchsia; an interface to the "pm" CLI tool.
+ """Package manager for Fuchsia; an interface to the "pm" CLI tool."""
- Attributes:
- log: Logger for the device-specific instance of ffx.
- binary_path: Path to the pm binary.
- packages_path: Path to amber-files.
- port: Port to listen on for package serving.
- """
-
- def __init__(self, binary_path: str, packages_path: str) -> None:
+ def __init__(self, packages_archive_path: str) -> None:
"""
Args:
- binary_path: Path to ffx binary.
- packages_path: Path to amber-files.
+ packages_archive_path: Path to an archive containing the pm binary
+ and amber-files.
"""
- self.log: TraceLogger = logger.create_tagged_trace_logger(f"pm")
- self.binary_path = binary_path
- self.packages_path = packages_path
- self.port = random_port()
+ self.log: TraceLogger = logger.create_tagged_trace_logger("pm")
- self._server_log: Optional[FileIO] = None
+ self._server_log: Optional[TextIO] = None
self._server_proc: Optional[subprocess.Popen] = None
+ self._log_path: Optional[str] = None
+
+ self._tmp_dir = tempfile.mkdtemp(prefix="packages-")
+ tar = tarfile.open(packages_archive_path, "r:gz")
+ tar.extractall(self._tmp_dir)
+
+ self._binary_path = os.path.join(self._tmp_dir, "pm")
+ self._packages_path = os.path.join(self._tmp_dir, "amber-files")
+ self._port = random_port()
self._assert_repo_has_not_expired()
+ def clean_up(self) -> None:
+ if self._server_proc:
+ self.stop_server()
+ if self._tmp_dir:
+ shutil.rmtree(self._tmp_dir)
+
def _assert_repo_has_not_expired(self) -> None:
"""Abort if the repository metadata has expired.
Raises:
TestAbortClass: when the timestamp.json file has expired
"""
- with open(f'{self.packages_path}/repository/timestamp.json', 'r') as f:
+ with open(f'{self._packages_path}/repository/timestamp.json', 'r') as f:
data = json.load(f)
expiresAtRaw = data["signed"]["expires"]
expiresAt = datetime.strptime(expiresAtRaw, '%Y-%m-%dT%H:%M:%SZ')
if expiresAt <= datetime.now():
raise signals.TestAbortClass(
- f'{self.packages_path}/repository/timestamp.json has expired on {expiresAtRaw}'
+ f'{self._packages_path}/repository/timestamp.json has expired on {expiresAtRaw}'
)
def start(self) -> None:
@@ -163,7 +170,7 @@
)
return
- pm_command = f'{self.binary_path} serve -c 2 -repo {self.packages_path} -l :{self.port}'
+ pm_command = f'{self._binary_path} serve -c 2 -repo {self._packages_path} -l :{self._port}'
root_dir = context.get_current_context().get_full_output_path()
epoch = utils.get_current_epoch_time()
@@ -177,7 +184,7 @@
stdout=self._server_log,
stderr=subprocess.STDOUT)
self._wait_for_server()
- self.log.info(f'Serving packages on port {self.port}')
+ self.log.info(f'Serving packages on port {self._port}')
def configure_device(self,
device_ssh: SSHProvider,
@@ -197,7 +204,7 @@
# Configure the device with the new repository.
host_ip = find_host_ip(device_ssh.ip)
- repo_url = f"http://{host_ip}:{self.port}"
+ repo_url = f"http://{host_ip}:{self._port}"
device_ssh.run(
f"pkgctl repo add url -f 2 -n {repo_name} {repo_url}/config.json")
self.log.info(
@@ -220,18 +227,20 @@
timeout = time.perf_counter() + timeout_sec
while True:
try:
- socket.create_connection(('127.0.0.1', self.port),
+ socket.create_connection(('127.0.0.1', self._port),
timeout=timeout)
return
except ConnectionRefusedError:
continue
finally:
if time.perf_counter() > timeout:
- self._server_log.close()
- with open(self._log_path, 'r') as f:
- logs = f.read()
+ if self._server_log:
+ self._server_log.close()
+ if self._log_path:
+ with open(self._log_path, 'r') as f:
+ logs = f.read()
raise TimeoutError(
- f"pm serve failed to expose port {self.port} after {timeout_sec}s. Logs:\n{logs}"
+ f"pm serve failed to expose port {self._port} after {timeout_sec}s. Logs:\n{logs}"
)
def stop_server(self) -> None:
@@ -251,12 +260,9 @@
self._server_proc.kill()
self._server_proc.wait(timeout=PM_SERVE_STOP_TIMEOUT_SEC)
finally:
- self._server_log.close()
+ if self._server_log:
+ self._server_log.close()
self._server_proc = None
self._log_path = None
self._server_log = None
-
- def clean_up(self) -> None:
- if self._server_proc:
- self.stop_server()
diff --git a/acts/framework/setup.py b/acts/framework/setup.py
index cc09235..81eb251 100755
--- a/acts/framework/setup.py
+++ b/acts/framework/setup.py
@@ -26,7 +26,6 @@
'backoff',
# Future needs to have a newer version that contains urllib.
'future>=0.16.0',
- 'grpcio',
'mobly==1.12.0',
# Latest version of mock (4.0.0b) causes a number of compatibility issues with ACTS unit tests
# b/148695846, b/148814743
@@ -46,7 +45,8 @@
versioned_deps = {
'numpy': 'numpy',
'scipy': 'scipy',
- 'protobuf': 'protobuf==4.21.5'
+ 'protobuf': 'protobuf==4.21.5',
+ 'grpcio': 'grpcio',
}
# numpy and scipy version matrix per:
@@ -58,6 +58,7 @@
versioned_deps['numpy'] = 'numpy<1.20'
versioned_deps['scipy'] = 'scipy<1.6'
versioned_deps['protobuf'] = 'protobuf==3.20.1'
+ versioned_deps['grpcio'] = 'grpcio==1.48.2'
versioned_deps['typing_extensions'] = 'typing_extensions==4.1.1'
if (sys.version_info.major, sys.version_info.minor) == (3, 6):
versioned_deps['dataclasses'] = 'dataclasses==0.8'