diff --git a/external/devlib/LICENSE b/external/devlib/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..28f59a0ea2d061bf74869161b158a09bd4c96ae6 --- /dev/null +++ b/external/devlib/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 Arm Ltd. + + 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. diff --git a/external/devlib/devlib/__init__.py b/external/devlib/devlib/__init__.py index dceda0d6258146cc644d0b41c5d84855a67e17a7..e496299b15453a47ceef03577a24314159775b62 100644 --- a/external/devlib/devlib/__init__.py +++ b/external/devlib/devlib/__init__.py @@ -22,8 +22,6 @@ from devlib.target import ( ChromeOsTarget, ) -from devlib.target_runner import QEMUTargetRunner - from devlib.host import ( PACKAGE_BIN_DIRECTORY, LocalConnection, diff --git a/external/devlib/devlib/target_runner.py b/external/devlib/devlib/_target_runner.py similarity index 61% rename from external/devlib/devlib/target_runner.py rename to external/devlib/devlib/_target_runner.py index c08c3a1abb351f51e726b091bbcc6927668fb87a..a45612354560bcc64a706ec2404234b7b71ced6c 100644 --- a/external/devlib/devlib/target_runner.py +++ b/external/devlib/devlib/_target_runner.py @@ -19,8 +19,6 @@ Target runner and related classes are implemented here. import logging import os -import signal -import subprocess import time from platform import machine @@ -37,20 +35,41 @@ class TargetRunner: It mainly aims to provide framework support for QEMU like target runners (e.g., :class:`QEMUTargetRunner`). + :param target: Specifies type of target per :class:`Target` based classes. + :type target: Target + """ + + def __init__(self, + target): + self.target = target + + self.logger = logging.getLogger(self.__class__.__name__) + + def __enter__(self): + return self + + def __exit__(self, *_): + pass + + +class SubprocessTargetRunner(TargetRunner): + """ + Class for providing subprocess support to the target runners. + :param runner_cmd: The command to start runner process (e.g., ``qemu-system-aarch64 -kernel Image -append "console=ttyAMA0" ...``). - :type runner_cmd: str + :type runner_cmd: list(str) :param target: Specifies type of target per :class:`Target` based classes. :type target: Target :param connect: Specifies if :class:`TargetRunner` should try to connect target after launching it, defaults to True. - :type connect: bool, optional + :type connect: bool or None :param boot_timeout: Timeout for target's being ready for SSH access in seconds, defaults to 60. - :type boot_timeout: int, optional + :type boot_timeout: int or None :raises HostError: if it cannot execute runner command successfully. @@ -62,15 +81,14 @@ class TargetRunner: target, connect=True, boot_timeout=60): - self.boot_timeout = boot_timeout - self.target = target + super().__init__(target=target) - self.logger = logging.getLogger(self.__class__.__name__) + self.boot_timeout = boot_timeout self.logger.info('runner_cmd: %s', runner_cmd) try: - self.runner_process = get_subprocess(list(runner_cmd.split())) + self.runner_process = get_subprocess(runner_cmd) except Exception as ex: raise HostError(f'Error while running "{runner_cmd}": {ex}') from ex @@ -78,20 +96,13 @@ class TargetRunner: self.wait_boot_complete() def __enter__(self): - """ - Complementary method for contextmanager. - - :return: Self object. - :rtype: TargetRunner - """ - return self def __exit__(self, *_): """ Exit routine for contextmanager. - Ensure :attr:`TargetRunner.runner_process` is terminated on exit. + Ensure ``SubprocessTargetRunner.runner_process`` is terminated on exit. """ self.terminate() @@ -99,7 +110,7 @@ class TargetRunner: def wait_boot_complete(self): """ Wait for target OS to finish boot up and become accessible over SSH in at most - :attr:`TargetRunner.boot_timeout` seconds. + ``SubprocessTargetRunner.boot_timeout`` seconds. :raises TargetStableError: In case of timeout. """ @@ -109,10 +120,10 @@ class TargetRunner: while self.boot_timeout >= elapsed: try: self.target.connect(timeout=self.boot_timeout - elapsed) - self.logger.info('Target is ready.') + self.logger.debug('Target is ready.') return # pylint: disable=broad-except - except BaseException as ex: + except Exception as ex: self.logger.info('Cannot connect target: %s', ex) time.sleep(1) @@ -123,33 +134,41 @@ class TargetRunner: def terminate(self): """ - Terminate :attr:`TargetRunner.runner_process`. + Terminate ``SubprocessTargetRunner.runner_process``. """ - if self.runner_process is None: - return - - try: - self.runner_process.stdin.close() - self.runner_process.stdout.close() - self.runner_process.stderr.close() + self.logger.debug('Killing target runner...') + self.runner_process.kill() + self.runner_process.__exit__(None, None, None) - if self.runner_process.poll() is None: - self.logger.debug('Terminating target runner...') - os.killpg(self.runner_process.pid, signal.SIGTERM) - # Wait 3 seconds before killing the runner. - self.runner_process.wait(timeout=3) - except subprocess.TimeoutExpired: - self.logger.info('Killing target runner...') - os.killpg(self.runner_process.pid, signal.SIGKILL) +class NOPTargetRunner(TargetRunner): + """ + Class for implementing a target runner which does nothing except providing .target attribute. -class QEMUTargetRunner(TargetRunner): + :param target: Specifies type of target per :class:`Target` based classes. + :type target: Target """ - Class for interacting with QEMU runners. - :class:`QEMUTargetRunner` is a subclass of :class:`TargetRunner` which performs necessary - groundwork for launching a guest OS on QEMU. + def __init__(self, target): + super().__init__(target=target) + + def __enter__(self): + return self + + def __exit__(self, *_): + pass + + def terminate(self): + """ + Nothing to terminate for NOP target runners. + Defined to be compliant with other runners (e.g., ``SubprocessTargetRunner``). + """ + + +class QEMUTargetRunner(SubprocessTargetRunner): + """ + Class for preparing necessary groundwork for launching a guest OS on QEMU. :param qemu_settings: A dictionary which has QEMU related parameters. The full list of QEMU parameters is below: @@ -181,12 +200,11 @@ class QEMUTargetRunner(TargetRunner): :type qemu_settings: Dict :param connection_settings: the dictionary to store connection settings - of :attr:`Target.connection_settings`, defaults to None. - :type connection_settings: Dict, optional + of ``Target.connection_settings``, defaults to None. + :type connection_settings: Dict or None - :param make_target: Lambda function for creating :class:`Target` based - object, defaults to :func:`lambda **kwargs: LinuxTarget(**kwargs)`. - :type make_target: func, optional + :param make_target: Lambda function for creating :class:`Target` based object. + :type make_target: func or None :Variable positional arguments: Forwarded to :class:`TargetRunner`. @@ -196,9 +214,9 @@ class QEMUTargetRunner(TargetRunner): def __init__(self, qemu_settings, connection_settings=None, - # pylint: disable=unnecessary-lambda - make_target=lambda **kwargs: LinuxTarget(**kwargs), + make_target=LinuxTarget, **args): + self.connection_settings = { 'host': '127.0.0.1', 'port': 8022, @@ -206,62 +224,61 @@ class QEMUTargetRunner(TargetRunner): 'password': 'root', 'strict_host_check': False, } - - if connection_settings is not None: - self.connection_settings = self.connection_settings | connection_settings + self.connection_settings = {**self.connection_settings, **(connection_settings or {})} qemu_args = { - 'kernel_image': '', 'arch': 'aarch64', 'cpu_type': 'cortex-a72', - 'initrd_image': '', 'mem_size': 512, 'num_cores': 2, 'num_threads': 2, 'cmdline': 'console=ttyAMA0', 'enable_kvm': True, } - - qemu_args = qemu_args | qemu_settings + qemu_args = {**qemu_args, **qemu_settings} qemu_executable = f'qemu-system-{qemu_args["arch"]}' qemu_path = which(qemu_executable) if qemu_path is None: raise FileNotFoundError(f'Cannot find {qemu_executable} executable!') - if not os.path.exists(qemu_args["kernel_image"]): - raise FileNotFoundError(f'{qemu_args["kernel_image"]} does not exist!') - - # pylint: disable=consider-using-f-string - qemu_cmd = '''\ -{} -kernel {} -append "{}" -m {} -smp cores={},threads={} -netdev user,id=net0,hostfwd=tcp::{}-:22 \ --device virtio-net-pci,netdev=net0 --nographic\ -'''.format( - qemu_path, - qemu_args["kernel_image"], - qemu_args["cmdline"], - qemu_args["mem_size"], - qemu_args["num_cores"], - qemu_args["num_threads"], - self.connection_settings["port"], - ) - - if qemu_args["initrd_image"]: + if qemu_args.get("kernel_image"): + if not os.path.exists(qemu_args["kernel_image"]): + raise FileNotFoundError(f'{qemu_args["kernel_image"]} does not exist!') + else: + raise KeyError('qemu_settings must have kernel_image!') + + qemu_cmd = [qemu_path, + '-kernel', qemu_args["kernel_image"], + '-append', f"'{qemu_args['cmdline']}'", + '-m', str(qemu_args["mem_size"]), + '-smp', f'cores={qemu_args["num_cores"]},threads={qemu_args["num_threads"]}', + '-netdev', f'user,id=net0,hostfwd=tcp::{self.connection_settings["port"]}-:22', + '-device', 'virtio-net-pci,netdev=net0', + '--nographic', + ] + + if qemu_args.get("initrd_image"): if not os.path.exists(qemu_args["initrd_image"]): raise FileNotFoundError(f'{qemu_args["initrd_image"]} does not exist!') - qemu_cmd += f' -initrd {qemu_args["initrd_image"]}' + qemu_cmd.extend(['-initrd', qemu_args["initrd_image"]]) - if qemu_args["arch"] == machine(): - if qemu_args["enable_kvm"]: - qemu_cmd += ' --enable-kvm' - else: - qemu_cmd += f' -machine virt -cpu {qemu_args["cpu_type"]}' + if qemu_args["enable_kvm"]: + # Enable KVM accelerator if host and guest architectures match. + # Comparison is done based on x86 for the sake of simplicity. + if (qemu_args['arch'].startswith('x86') and machine().startswith('x86')) or ( + qemu_args['arch'].startswith('x86') and machine().startswith('x86')): + qemu_cmd.append('--enable-kvm') + + # qemu-system-x86_64 does not support -machine virt as of now. + if not qemu_args['arch'].startswith('x86'): + qemu_cmd.extend(['-machine', 'virt', '-cpu', qemu_args["cpu_type"]]) - self.target = make_target(connect=False, - conn_cls=SshConnection, - connection_settings=self.connection_settings) + target = make_target(connect=False, + conn_cls=SshConnection, + connection_settings=self.connection_settings) super().__init__(runner_cmd=qemu_cmd, - target=self.target, + target=target, **args) diff --git a/external/devlib/devlib/bin/scripts/shutils.in b/external/devlib/devlib/bin/scripts/shutils.in index 6607504e9bdf8d5b430128dd7bd506f0484f9e4f..2e72362e4d65f0e78343167bc226bb3a57a5891b 100755 --- a/external/devlib/devlib/bin/scripts/shutils.in +++ b/external/devlib/devlib/bin/scripts/shutils.in @@ -154,7 +154,16 @@ cgroups_run_into() { # Move this shell into that control group echo $$ > $CGPATH/cgroup.procs echo "Moving task into root CGroup ($CGPATH)" + # Check the move actually worked + $GREP -E "$$" $CGPATH/cgroup.procs >/dev/null 2>&1 + if [ $? -ne 0 ]; then + echo "ERROR: Process was not moved into $CGP" + exit 1 + fi done + if [ $? -ne 0 ]; then + exit 1 + fi # Execution under specified CGroup else @@ -173,8 +182,16 @@ cgroups_run_into() { # Move this shell into that control group echo $$ > $CGPATH/cgroup.procs echo "Moving task into $CGPATH" + # Check the move actually worked + $GREP -E "$$" $CGPATH/cgroup.procs >/dev/null 2>&1 + if [ $? -ne 0 ]; then + echo "ERROR: Process was not moved into $CGP" + exit 1 + fi done - + if [ $? -ne 0 ]; then + exit 1 + fi fi # Execute the command diff --git a/external/devlib/devlib/collector/dmesg.py b/external/devlib/devlib/collector/dmesg.py index 4d4ee60c08639268d07ca8d2a5c8861450f7a90b..617258c405748fe9a2ca15824f1e1975bc459ef3 100644 --- a/external/devlib/devlib/collector/dmesg.py +++ b/external/devlib/devlib/collector/dmesg.py @@ -70,7 +70,7 @@ class KernelLogEntry(object): def parse_raw_level(line): match = cls._RAW_LEVEL_REGEX.match(line) if not match: - raise ValueError('dmesg entry format not recognized: {}'.format(line)) + raise ValueError(f'dmesg entry format not recognized: {line}') level, remainder = match.groups() levels = DmesgCollector.LOG_LEVELS # BusyBox dmesg can output numbers that need to wrap around @@ -79,11 +79,15 @@ class KernelLogEntry(object): def parse_pretty_level(line): match = cls._PRETTY_LEVEL_REGEX.match(line) + if not match: + raise ValueError(f'dmesg entry pretty format not recognized: {line}') facility, level, remainder = match.groups() return facility, level, remainder def parse_timestamp_msg(line): match = cls._TIMESTAMP_MSG_REGEX.match(line) + if not match: + raise ValueError(f'dmesg entry timestamp format not recognized: {line}') timestamp, msg = match.groups() timestamp = timedelta(seconds=float(timestamp.strip())) return timestamp, msg diff --git a/external/devlib/devlib/target.py b/external/devlib/devlib/target.py index 5f2c595a0904856ce8c6d88373d368d6969256fd..753fb96a3620dcee19d32b3dc3b31d8a1858dabf 100644 --- a/external/devlib/devlib/target.py +++ b/external/devlib/devlib/target.py @@ -13,6 +13,7 @@ # limitations under the License. # +import atexit import asyncio from contextlib import contextmanager import io @@ -40,6 +41,7 @@ from past.builtins import long from past.types import basestring from numbers import Number from shlex import quote +from weakref import WeakMethod try: from collections.abc import Mapping except ImportError: @@ -413,6 +415,10 @@ class Target(object): )) self._modules = modules + atexit.register( + WeakMethod(self.disconnect, atexit.unregister) + ) + self._update_modules('early') if connect: self.connect(max_async=max_async) @@ -521,10 +527,32 @@ class Target(object): def disconnect(self): connections = self._conn.get_all_values() + # Now that we have all the connection objects, we simply reset the TLS + # property so that the connections we got will not be reused anywhere. + del self._conn + + unused_conns = self._unused_conns + self._unused_conns.clear() + for conn in itertools.chain(connections, self._unused_conns): conn.close() - if self._async_pool is not None: - self._async_pool.__exit__(None, None, None) + + pool = self._async_pool + self._async_pool = None + if pool is not None: + pool.__exit__(None, None, None) + + def __enter__(self): + return self + + def __exit__(self, *args, **kwargs): + self.disconnect() + + async def __aenter__(self): + return self.__enter__() + + async def __aexit__(self, *args, **kwargs): + return self.__exit__(*args, **kwargs) def get_connection(self, timeout=None): if self.conn_cls is None: @@ -1125,20 +1153,20 @@ fi else: raise - @contextmanager - def make_temp(self, is_directory=True, directory='', prefix='devlib-test'): + @asyn.asynccontextmanager + async def make_temp(self, is_directory=True, directory='', prefix='devlib-test'): """ Creates temporary file/folder on target and deletes it once it's done. :param is_directory: Specifies if temporary object is a directory, defaults to True. - :type is_directory: bool, optional + :type is_directory: bool or None :param directory: Temp object will be created under this directory, - defaults to :attr:`Target.working_directory`. - :type directory: str, optional + defaults to ``Target.working_directory``. + :type directory: str or None - :param prefix: Prefix of temp object's name, defaults to 'devlib-test'. - :type prefix: str, optional + :param prefix: Prefix of temp object's name. + :type prefix: str or None :yield: Full path of temp object. :rtype: str @@ -1147,15 +1175,15 @@ fi directory = directory or self.working_directory temp_obj = None try: - cmd = f'mktemp -p {directory} {prefix}-XXXXXX' + cmd = f'mktemp -p {quote(directory)} {quote(prefix)}-XXXXXX' if is_directory: cmd += ' -d' - temp_obj = self.execute(cmd).strip() + temp_obj = (await self.execute.asyn(cmd)).strip() yield temp_obj finally: if temp_obj is not None: - self.remove(temp_obj) + await self.remove.asyn(temp_obj) def reset(self): try: diff --git a/external/devlib/devlib/utils/android.py b/external/devlib/devlib/utils/android.py index 6f2eb87ae60ad331b95287d0e7ada9320e6d6744..81d70c28c0b1b20fe18a8b06893d141d36a63a4f 100755 --- a/external/devlib/devlib/utils/android.py +++ b/external/devlib/devlib/utils/android.py @@ -562,7 +562,7 @@ def adb_disconnect(device, adb_server=None, adb_port=None): adb_cmd = get_adb_command(None, 'disconnect', adb_server, adb_port) command = "{} {}".format(adb_cmd, device) logger.debug(command) - retval = subprocess.call(command, stdout=open(os.devnull, 'wb'), shell=True) + retval = subprocess.call(command, stdout=subprocess.DEVNULL, shell=True) if retval: raise TargetTransientError('"{}" returned {}'.format(command, retval)) @@ -571,11 +571,13 @@ def _ping(device, adb_server=None, adb_port=None): adb_cmd = get_adb_command(device, 'shell', adb_server, adb_port) command = "{} {}".format(adb_cmd, quote('ls /data/local/tmp > /dev/null')) logger.debug(command) - result = subprocess.call(command, stderr=subprocess.PIPE, shell=True) - if not result: # pylint: disable=simplifiable-if-statement - return True - else: + try: + subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True) + except subprocess.CalledProcessError as e: + logger.debug(f'ADB ping failed: {e.stdout}') return False + else: + return True # pylint: disable=too-many-locals diff --git a/external/devlib/devlib/utils/ssh.py b/external/devlib/devlib/utils/ssh.py index 0eb7db2ba81335a50ef00f79509ed7d0455c5713..bc130226e83a902c5aedf87f25644df4f55e8844 100644 --- a/external/devlib/devlib/utils/ssh.py +++ b/external/devlib/devlib/utils/ssh.py @@ -24,14 +24,12 @@ import tempfile import socket import sys import time -import atexit import contextlib import select import copy import functools import shutil from shlex import quote -from weakref import WeakMethod from paramiko.client import SSHClient, AutoAddPolicy, RejectPolicy import paramiko.ssh_exception @@ -372,8 +370,6 @@ class SshConnection(SshConnectionBase): self.client = None try: self.client = self._make_client() - weak_close = WeakMethod(self.close, atexit.unregister) - atexit.register(weak_close) # Use a marker in the output so that we will be able to differentiate # target connection issues with "password needed". @@ -392,10 +388,13 @@ class SshConnection(SshConnectionBase): ) ) - except BaseException: - if self.client is not None: - self.client.close() - raise + # pylint: disable=broad-except + except BaseException as e: + try: + if self.client is not None: + self.client.close() + finally: + raise e def _make_client(self): if self.strict_host_check: @@ -815,9 +814,6 @@ class TelnetConnection(SshConnectionBase): self.conn = telnet_get_shell(host, username, password, port, timeout, original_prompt) - weak_close = WeakMethod(self.close, atexit.unregister) - atexit.register(weak_close) - def fmt_remote_path(self, path): return '{}@{}:{}'.format(self.username, self.host, path) diff --git a/external/devlib/tests/target_configs.yaml b/external/devlib/tests/target_configs.yaml deleted file mode 100644 index 47e00ec4ebf4ea126c3899b12e3ea415dbaf92c3..0000000000000000000000000000000000000000 --- a/external/devlib/tests/target_configs.yaml +++ /dev/null @@ -1,5 +0,0 @@ -LocalLinuxTarget: - entry-0: - connection_settings: - unrooted: True - diff --git a/external/devlib/tests/target_configs.yaml.example b/external/devlib/tests/target_configs.yaml.example deleted file mode 100644 index 1154ea8b99ec607825e71680e1ee493f0f184fdd..0000000000000000000000000000000000000000 --- a/external/devlib/tests/target_configs.yaml.example +++ /dev/null @@ -1,39 +0,0 @@ -AndroidTarget: - entry-0: - timeout: 60 - connection_settings: - device: 'emulator-5554' - -ChromeOsTarget: - entry-0: - connection_settings: - device: 'emulator-5556' - host: 'example.com' - username: 'username' - password: 'password' - -LinuxTarget: - entry-0: - connection_settings: - host: 'example.com' - username: 'username' - password: 'password' - -LocalLinuxTarget: - entry-0: - connection_settings: - unrooted: True - -QEMUTargetRunner: - entry-0: - qemu_settings: - kernel_image: '/path/to/devlib/tools/buildroot/buildroot-v2023.11.1-aarch64/output/images/Image' - - entry-1: - connection_settings: - port : 8023 - - qemu_settings: - kernel_image: '/path/to/devlib/tools/buildroot/buildroot-v2023.11.1-x86_64/output/images/bzImage' - arch: 'x86_64' - cmdline: 'console=ttyS0' diff --git a/external/devlib/tests/test_config.yml b/external/devlib/tests/test_config.yml new file mode 100644 index 0000000000000000000000000000000000000000..6c5b53ba26fb3c2157f238d355d1bf22b87bd2a3 --- /dev/null +++ b/external/devlib/tests/test_config.yml @@ -0,0 +1,5 @@ +target-configs: + entry-0: + LocalLinuxTarget: + connection_settings: + unrooted: True diff --git a/external/devlib/tests/test_config.yml.example b/external/devlib/tests/test_config.yml.example new file mode 100644 index 0000000000000000000000000000000000000000..5e33a6c77216f07a01f5db50fb16adadc20a4de7 --- /dev/null +++ b/external/devlib/tests/test_config.yml.example @@ -0,0 +1,40 @@ +target-configs: + entry-0: + AndroidTarget: + timeout: 60 + connection_settings: + device: 'emulator-5554' + + entry-1: + ChromeOsTarget: + connection_settings: + device: 'emulator-5556' + host: 'example.com' + username: 'username' + password: 'password' + + entry-2: + LinuxTarget: + connection_settings: + host: 'example.com' + username: 'username' + password: 'password' + + entry-3: + LocalLinuxTarget: + connection_settings: + unrooted: True + + entry-4: + QEMUTargetRunner: + qemu_settings: + kernel_image: '/path/to/devlib/tools/buildroot/buildroot-v2023.11.1-aarch64/output/images/Image' + + entry-5: + QEMUTargetRunner: + connection_settings: + port: 8023 + qemu_settings: + kernel_image: '/path/to/devlib/tools/buildroot/buildroot-v2023.11.1-x86_64/output/images/bzImage' + arch: 'x86_64' + cmdline: 'console=ttyS0' diff --git a/external/devlib/tests/test_target.py b/external/devlib/tests/test_target.py index 2d59a2374c6cc49ddeb1bf706c99a9be552d8389..2d811321fcce6c8b9f6d7fc904cb09189dc53782 100644 --- a/external/devlib/tests/test_target.py +++ b/external/devlib/tests/test_target.py @@ -14,136 +14,168 @@ # limitations under the License. # -"""Module for testing targets.""" +""" +Module for testing targets. +Sample run with log level is set to DEBUG (see +https://docs.pytest.org/en/7.1.x/how-to/logging.html#live-logs for logging details): + +$ python -m pytest --log-cli-level DEBUG test_target.py +""" + +import logging import os -from pprint import pp import pytest -from devlib import AndroidTarget, ChromeOsTarget, LinuxTarget, LocalLinuxTarget, QEMUTargetRunner +from devlib import AndroidTarget, ChromeOsTarget, LinuxTarget, LocalLinuxTarget +from devlib._target_runner import NOPTargetRunner, QEMUTargetRunner from devlib.utils.android import AdbConnection from devlib.utils.misc import load_struct_from_yaml -def build_targets(): - """Read targets from a YAML formatted config file""" +logger = logging.getLogger('test_target') - config_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'target_configs.yaml') - target_configs = load_struct_from_yaml(config_file) - if target_configs is None: +def get_class_object(name): + """ + Get associated class object from string formatted class name + + :param name: Class name + :type name: str + :return: Class object + :rtype: object or None + """ + if globals().get(name) is None: + return None + + return globals()[name] if issubclass(globals()[name], object) else None + + +@pytest.fixture(scope="module") +# pylint: disable=too-many-branches +def build_target_runners(): + """Read targets from a YAML formatted config file and create runners for them""" + + logger.info("Initializing resources...") + + config_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'test_config.yml') + + test_config = load_struct_from_yaml(config_file) + if test_config is None: raise ValueError(f'{config_file} looks empty!') - targets = [] + target_configs = test_config.get('target-configs') + if target_configs is None: + raise ValueError('No targets found!') + + target_runners = [] - if target_configs.get('AndroidTarget') is not None: - print('> Android targets:') - for entry in target_configs['AndroidTarget'].values(): - pp(entry) + for entry in target_configs.values(): + key, target_info = entry.popitem() + + target_class = get_class_object(key) + if target_class is AndroidTarget: + logger.info('> Android target: %s', repr(target_info)) a_target = AndroidTarget( connect=False, - connection_settings=entry['connection_settings'], + connection_settings=target_info['connection_settings'], conn_cls=lambda **kwargs: AdbConnection(adb_as_root=True, **kwargs), ) - a_target.connect(timeout=entry.get('timeout', 60)) - targets.append((a_target, None)) - - if target_configs.get('LinuxTarget') is not None: - print('> Linux targets:') - for entry in target_configs['LinuxTarget'].values(): - pp(entry) - l_target = LinuxTarget(connection_settings=entry['connection_settings']) - targets.append((l_target, None)) - - if target_configs.get('ChromeOsTarget') is not None: - print('> ChromeOS targets:') - for entry in target_configs['ChromeOsTarget'].values(): - pp(entry) + a_target.connect(timeout=target_info.get('timeout', 60)) + target_runners.append(NOPTargetRunner(a_target)) + + elif target_class is ChromeOsTarget: + logger.info('> ChromeOS target: %s', repr(target_info)) c_target = ChromeOsTarget( - connection_settings=entry['connection_settings'], + connection_settings=target_info['connection_settings'], working_directory='/tmp/devlib-target', ) - targets.append((c_target, None)) - - if target_configs.get('LocalLinuxTarget') is not None: - print('> LocalLinux targets:') - for entry in target_configs['LocalLinuxTarget'].values(): - pp(entry) - ll_target = LocalLinuxTarget(connection_settings=entry['connection_settings']) - targets.append((ll_target, None)) - - if target_configs.get('QEMUTargetRunner') is not None: - print('> QEMU target runners:') - for entry in target_configs['QEMUTargetRunner'].values(): - pp(entry) - qemu_settings = entry.get('qemu_settings') and entry['qemu_settings'] - connection_settings = entry.get( - 'connection_settings') and entry['connection_settings'] + target_runners.append(NOPTargetRunner(c_target)) + + elif target_class is LinuxTarget: + logger.info('> Linux target: %s', repr(target_info)) + l_target = LinuxTarget(connection_settings=target_info['connection_settings']) + target_runners.append(NOPTargetRunner(l_target)) + + elif target_class is LocalLinuxTarget: + logger.info('> LocalLinux target: %s', repr(target_info)) + ll_target = LocalLinuxTarget(connection_settings=target_info['connection_settings']) + target_runners.append(NOPTargetRunner(ll_target)) + + elif target_class is QEMUTargetRunner: + logger.info('> QEMU target runner: %s', repr(target_info)) qemu_runner = QEMUTargetRunner( - qemu_settings=qemu_settings, - connection_settings=connection_settings, + qemu_settings=target_info.get('qemu_settings'), + connection_settings=target_info.get('connection_settings'), ) - if entry.get('ChromeOsTarget') is None: - targets.append((qemu_runner.target, qemu_runner)) - continue + if target_info.get('ChromeOsTarget') is not None: + # Leave termination of QEMU runner to ChromeOS target. + target_runners.append(NOPTargetRunner(qemu_runner.target)) - # Leave termination of QEMU runner to ChromeOS target. - targets.append((qemu_runner.target, None)) + logger.info('>> ChromeOS target: %s', repr(target_info["ChromeOsTarget"])) + qemu_runner.target = ChromeOsTarget( + connection_settings={ + **target_info['ChromeOsTarget']['connection_settings'], + **qemu_runner.target.connection_settings, + }, + working_directory='/tmp/devlib-target', + ) - print('> ChromeOS targets:') - pp(entry['ChromeOsTarget']) - c_target = ChromeOsTarget( - connection_settings={ - **entry['ChromeOsTarget']['connection_settings'], - **qemu_runner.target.connection_settings, - }, - working_directory='/tmp/devlib-target', - ) - targets.append((c_target, qemu_runner)) + target_runners.append(qemu_runner) + + else: + raise ValueError(f'Unknown target type {key}!') + + yield target_runners - return targets + logger.info("Destroying resources...") + for target_runner in target_runners: + target = target_runner.target -@pytest.mark.parametrize("target, target_runner", build_targets()) -def test_read_multiline_values(target, target_runner): + # TODO: Revisit per https://github.com/ARM-software/devlib/issues/680. + logger.debug('Removing %s...', target.working_directory) + target.remove(target.working_directory) + + target_runner.terminate() + + +# pylint: disable=redefined-outer-name +def test_read_multiline_values(build_target_runners): """ Test Target.read_tree_values_flat() - :param target: Type of target per :class:`Target` based classes. - :type target: Target - - :param target_runner: Target runner object to terminate target (if necessary). - :type target: TargetRunner + Runs tests around ``Target.read_tree_values_flat()`` for ``TargetRunner`` objects. """ + logger.info('Running test_read_multiline_values test...') + data = { 'test1': '1', 'test2': '2\n\n', 'test3': '3\n\n4\n\n', } - print(f'target={target.__class__.__name__} os={target.os} hostname={target.hostname}') + target_runners = build_target_runners + for target_runner in target_runners: + target = target_runner.target - with target.make_temp() as tempdir: - print(f'Created {tempdir}.') + logger.info('target=%s os=%s hostname=%s', + target.__class__.__name__, target.os, target.hostname) - for key, value in data.items(): - path = os.path.join(tempdir, key) - print(f'Writing {value!r} to {path}...') - target.write_value(path, value, verify=False, - as_root=target.conn.connected_as_root) + with target.make_temp() as tempdir: + logger.debug('Created %s.', tempdir) - print('Reading values from target...') - raw_result = target.read_tree_values_flat(tempdir) - result = {os.path.basename(k): v for k, v in raw_result.items()} + for key, value in data.items(): + path = os.path.join(tempdir, key) + logger.debug('Writing %s to %s...', repr(value), path) + target.write_value(path, value, verify=False, + as_root=target.conn.connected_as_root) - print(f'Removing {target.working_directory}...') - target.remove(target.working_directory) - - if target_runner is not None: - print('Terminating target runner...') - target_runner.terminate() + logger.debug('Reading values from target...') + raw_result = target.read_tree_values_flat(tempdir) + result = {os.path.basename(k): v for k, v in raw_result.items()} - assert {k: v.strip() for k, v in data.items()} == result + assert {k: v.strip() for k, v in data.items()} == result diff --git a/external/devlib/tools/android/install_base.sh b/external/devlib/tools/android/install_base.sh index 5335d5981aedb25e1be4565d89dee959b6ebd246..d1ea7612d944f0bd8d2fc3faa8e20cca0689d84e 100755 --- a/external/devlib/tools/android/install_base.sh +++ b/external/devlib/tools/android/install_base.sh @@ -37,21 +37,21 @@ test_os_release() { if [[ "$(read_os_release "${field_name}")" == "${value}" ]]; then return 0 - else - return 1 fi + return 1 } -function set_host_arch -{ - # Google ABI type for Arm platforms - HOST_ARCH="arm64-v8a" +get_android_sdk_host_arch() { + # Default to Google ABI type for Arm platforms + local arch="arm64-v8a" + local machine + + machine=$(uname -m) + if [[ "${machine}" == "x86"* ]]; then + arch=${machine} + fi - local machine - machine=$(uname -m) - if [[ "${machine}" == "x86"* ]]; then - HOST_ARCH=${machine} - fi + echo "${arch}" } ANDROID_HOME="$(dirname "${0}")/android-sdk-linux" @@ -60,7 +60,7 @@ export ANDROID_USER_HOME="${ANDROID_HOME}/.android" mkdir -p "${ANDROID_HOME}/cmdline-tools" -CMDLINE_VERSION=${CMDLINE_VERSION:-"11076708"} +ANDROID_CMDLINE_VERSION=${ANDROID_CMDLINE_VERSION:-"11076708"} cleanup_android_home() { echo "Cleaning up Android SDK: ${ANDROID_HOME}" @@ -72,7 +72,7 @@ install_android_sdk_manager() { echo "Installing Android SDK manager ..." # URL taken from "Command line tools only": https://developer.android.com/studio - local url="https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_VERSION}_latest.zip" + local url="https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CMDLINE_VERSION}_latest.zip" echo "Downloading Android SDK manager from: $url" wget -qO- "${url}" | bsdtar -xf- -C "${ANDROID_HOME}/cmdline-tools" @@ -117,29 +117,34 @@ call_android_avdmanager() { # Needs install_android_sdk_manager first install_android_tools() { + local android_sdk_host_arch + android_sdk_host_arch=$(get_android_sdk_host_arch) + yes | call_android_sdkmanager --verbose --channel=0 --install "platform-tools" yes | call_android_sdkmanager --verbose --channel=0 --install "platforms;android-31" yes | call_android_sdkmanager --verbose --channel=0 --install "platforms;android-33" yes | call_android_sdkmanager --verbose --channel=0 --install "platforms;android-34" - yes | call_android_sdkmanager --verbose --channel=0 --install "system-images;android-31;google_apis;${HOST_ARCH}" - yes | call_android_sdkmanager --verbose --channel=0 --install "system-images;android-33;android-desktop;${HOST_ARCH}" - yes | call_android_sdkmanager --verbose --channel=0 --install "system-images;android-34;google_apis;${HOST_ARCH}" + yes | call_android_sdkmanager --verbose --channel=0 --install "system-images;android-31;google_apis;${android_sdk_host_arch}" + yes | call_android_sdkmanager --verbose --channel=0 --install "system-images;android-33;android-desktop;${android_sdk_host_arch}" + yes | call_android_sdkmanager --verbose --channel=0 --install "system-images;android-34;google_apis;${android_sdk_host_arch}" } create_android_vds() { - local vd_name + local android_sdk_host_arch + android_sdk_host_arch=$(get_android_sdk_host_arch) + local vd_name vd_name="devlib-p6-12" echo "Creating virtual device \"${vd_name}\" (Pixel 6 - Android 12)..." - echo no | call_android_avdmanager -s create avd -n "${vd_name}" -k "system-images;android-31;google_apis;${HOST_ARCH}" --skin pixel_6 -b "${HOST_ARCH}" -f + echo no | call_android_avdmanager -s create avd -n "${vd_name}" -k "system-images;android-31;google_apis;${android_sdk_host_arch}" --skin pixel_6 -b "${android_sdk_host_arch}" -f vd_name="devlib-p6-14" echo "Creating virtual device \"${vd_name}\" (Pixel 6 - Android 14)..." - echo no | call_android_avdmanager -s create avd -n "${vd_name}" -k "system-images;android-34;google_apis;${HOST_ARCH}" --skin pixel_6 -b "${HOST_ARCH}" -f + echo no | call_android_avdmanager -s create avd -n "${vd_name}" -k "system-images;android-34;google_apis;${android_sdk_host_arch}" --skin pixel_6 -b "${android_sdk_host_arch}" -f vd_name="devlib-chromeos" echo "Creating virtual device \"${vd_name}\" (ChromeOS - Android 13, Pixel tablet)..." - echo no | call_android_avdmanager -s create avd -n "${vd_name}" -k "system-images;android-33;android-desktop;${HOST_ARCH}" --skin pixel_tablet -b "${HOST_ARCH}" -f + echo no | call_android_avdmanager -s create avd -n "${vd_name}" -k "system-images;android-33;android-desktop;${android_sdk_host_arch}" --skin pixel_tablet -b "${android_sdk_host_arch}" -f } install_apt() { @@ -183,7 +188,6 @@ if which apt-get &>/dev/null; then install_functions+=(install_apt) package_manager='apt-get' expected_distro="Ubuntu" - elif which pacman &>/dev/null; then install_functions+=(install_pacman) package_manager="pacman" @@ -223,8 +227,6 @@ else args=("$@") fi -set_host_arch - # Use conditional fall-through ;;& to all matching all branches with # --install-all for arg in "${args[@]}"; do diff --git a/external/devlib/tools/docker/Dockerfile b/external/devlib/tools/docker/Dockerfile index e52a7d613c257d88388322f64c54951a1a3aec2a..b96a90352bce282b185efaf4cc05fe88948b4ce7 100644 --- a/external/devlib/tools/docker/Dockerfile +++ b/external/devlib/tools/docker/Dockerfile @@ -63,9 +63,9 @@ RUN cd /devlib && \ pip install --upgrade pip setuptools wheel && \ pip install .[full] -# Set CMDLINE_VERSION environment variable if you want to use a specific -# version of Android command line tools rather than default which is -# ``11076708`` as of writing this comment. +# Set ANDROID_CMDLINE_VERSION environment variable if you want to use a +# specific version of Android command line tools rather than default +# which is ``11076708`` as of writing this comment. RUN cd /devlib/tools/android && ./install_base.sh # Set BUILDROOT_VERSION environment variable if you want to use a specific diff --git a/external/devlib/tools/docker/target_configs.yaml b/external/devlib/tools/docker/target_configs.yaml deleted file mode 100644 index d616822c30d363b41d48fb8c5f4ad464a19f0f92..0000000000000000000000000000000000000000 --- a/external/devlib/tools/docker/target_configs.yaml +++ /dev/null @@ -1,43 +0,0 @@ -AndroidTarget: - # Android-12, Pixel-6 - entry-0: - timeout: 60 - connection_settings: - device: 'emulator-5554' - - # Android-14, Pixel-6 - entry-1: - connection_settings: - device: 'emulator-5556' - - # Android-13, Pixel tablet - entry-2: - connection_settings: - device: 'emulator-5558' - -LocalLinuxTarget: - entry-0: - connection_settings: - unrooted: True - -QEMUTargetRunner: - entry-0: - qemu_settings: - kernel_image: '/devlib/tools/buildroot/buildroot-v2023.11.1-aarch64/output/images/Image' - - ChromeOsTarget: - connection_settings: - device: 'emulator-5558' - - entry-1: - connection_settings: - port: 8023 - - qemu_settings: - kernel_image: '/devlib/tools/buildroot/buildroot-v2023.11.1-x86_64/output/images/bzImage' - arch: 'x86_64' - cmdline: 'console=ttyS0' - - ChromeOsTarget: - connection_settings: - device: 'emulator-5558' diff --git a/external/devlib/tools/docker/target_configs.yml b/external/devlib/tools/docker/target_configs.yml new file mode 100644 index 0000000000000000000000000000000000000000..f6b74210f69373a48e88ed41a379f005374f580b --- /dev/null +++ b/external/devlib/tools/docker/target_configs.yml @@ -0,0 +1,46 @@ +target-configs: + entry-0: + # Android-12, Pixel-6 + AndroidTarget: + timeout: 60 + connection_settings: + device: 'emulator-5554' + + entry-1: + # Android-14, Pixel-6 + AndroidTarget: + connection_settings: + device: 'emulator-5556' + + entry-2: + # Android-13, Pixel tablet + AndroidTarget: + connection_settings: + device: 'emulator-5558' + + entry-3: + LocalLinuxTarget: + connection_settings: + unrooted: True + + entry-4: + # aarch64 target + QEMUTargetRunner: + qemu_settings: + kernel_image: '/devlib/tools/buildroot/buildroot-v2023.11.1-aarch64/output/images/Image' + ChromeOsTarget: + connection_settings: + device: 'emulator-5558' + + entry-5: + # x86_64 target + QEMUTargetRunner: + connection_settings: + port: 8023 + qemu_settings: + kernel_image: '/devlib/tools/buildroot/buildroot-v2023.11.1-x86_64/output/images/bzImage' + arch: 'x86_64' + cmdline: 'console=ttyS0' + ChromeOsTarget: + connection_settings: + device: 'emulator-5558' diff --git a/external/workload-automation/requirements.txt b/external/workload-automation/requirements.txt index aff5658588e3082393755ccbe91188984f9766cb..de0f3c5365a5f13aefc51e351bf0d7b3a900b0fb 100644 --- a/external/workload-automation/requirements.txt +++ b/external/workload-automation/requirements.txt @@ -6,7 +6,7 @@ colorama==0.4.6 cryptography==42.0.4 devlib==1.3.4 future==0.18.3 -idna==3.4 +idna==3.7 Louie-latest==1.3.1 lxml==4.9.2 nose==1.3.7 @@ -21,7 +21,7 @@ pyserial==3.5 python-dateutil==2.8.2 pytz==2023.3 PyYAML==6.0 -requests==2.31.0 +requests==2.32.0 scp==0.14.5 six==1.16.0 tzdata==2023.3 diff --git a/external/workload-automation/wa/workloads/speedometer/__init__.py b/external/workload-automation/wa/workloads/speedometer/__init__.py index 202554dc5b86c67cd889935bf80d1478dee07bc9..c225feecc776d548a4100bcf4713cb12124de4df 100755 --- a/external/workload-automation/wa/workloads/speedometer/__init__.py +++ b/external/workload-automation/wa/workloads/speedometer/__init__.py @@ -204,6 +204,9 @@ class Speedometer(Workload): '\n', '\n', '\n', + # Add a 'request count' value to dismiss the pop-up window on the screen. + # If the value is greater than 1, pop-up window will be dismissed. + '\n', ]: lines.insert(len(lines) - 1, line)