Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

target: tests: Address review comments on PR#667 #677

Merged
merged 1 commit into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions devlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@
ChromeOsTarget,
)

from devlib.target_runner import QEMUTargetRunner

from devlib.host import (
PACKAGE_BIN_DIRECTORY,
LocalConnection,
Expand Down
181 changes: 99 additions & 82 deletions devlib/target_runner.py → devlib/_target_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@

import logging
import os
import signal
import subprocess
import time
from platform import machine

Expand All @@ -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.
Expand All @@ -62,44 +81,36 @@ def __init__(self,
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

if connect:
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()

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.
"""
Expand All @@ -109,10 +120,10 @@ def wait_boot_complete(self):
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)
Expand All @@ -123,33 +134,41 @@ def wait_boot_complete(self):

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:
Expand Down Expand Up @@ -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`.
Expand All @@ -196,72 +214,71 @@ 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,
'username': 'root',
'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)
12 changes: 6 additions & 6 deletions devlib/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -1131,14 +1131,14 @@ 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
Expand All @@ -1147,7 +1147,7 @@ def make_temp(self, is_directory=True, directory='', prefix='devlib-test'):
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'

Expand Down
11 changes: 7 additions & 4 deletions devlib/utils/ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,10 +392,13 @@ def __init__(self,
)
)

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:
Expand Down
5 changes: 0 additions & 5 deletions tests/target_configs.yaml

This file was deleted.

Loading