From patchwork Tue Jul 12 10:28:25 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Peter Hoyes X-Patchwork-Id: 10095 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id DA1A0C433EF for ; Tue, 12 Jul 2022 10:28:48 +0000 (UTC) Received: from foss.arm.com (foss.arm.com [217.140.110.172]) by mx.groups.io with SMTP id smtpd.web11.7387.1657621724519170596 for ; Tue, 12 Jul 2022 03:28:44 -0700 Authentication-Results: mx.groups.io; dkim=missing; spf=pass (domain: arm.com, ip: 217.140.110.172, mailfrom: peter.hoyes@arm.com) Received: from usa-sjc-imap-foss1.foss.arm.com (unknown [10.121.207.14]) by usa-sjc-mx-foss1.foss.arm.com (Postfix) with ESMTP id 83F0B1515; Tue, 12 Jul 2022 03:28:44 -0700 (PDT) Received: from e125920.cambridge.arm.com (unknown [10.1.199.64]) by usa-sjc-imap-foss1.foss.arm.com (Postfix) with ESMTPSA id 3872D3F792; Tue, 12 Jul 2022 03:28:43 -0700 (PDT) From: Peter Hoyes To: meta-arm@lists.yoctoproject.org Cc: diego.sueiro@arm.com, robbie.cao@arm.com, Peter Hoyes Subject: [PATCH 1/6] scripts,arm/lib: Refactor runfvp into FVPRunner Date: Tue, 12 Jul 2022 11:28:25 +0100 Message-Id: <20220712102830.625090-2-peter.hoyes@arm.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20220712102830.625090-1-peter.hoyes@arm.com> References: <20220712102830.625090-1-peter.hoyes@arm.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from li982-79.members.linode.com [45.33.32.79] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Tue, 12 Jul 2022 10:28:48 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/meta-arm/message/3555 From: Peter Hoyes Refactor runfvp into a "fvp" library inside meta-arm. Split into terminal, conffile and runner. Issue-Id: SCM-4957 Signed-off-by: Peter Hoyes Change-Id: I797f9a4eab810f3cc331b7db140f59c9911231fd --- meta-arm/lib/fvp/__init__.py | 0 meta-arm/lib/fvp/conffile.py | 58 +++++++++ meta-arm/lib/fvp/runner.py | 115 +++++++++++++++++ meta-arm/lib/fvp/terminal.py | 59 +++++++++ scripts/runfvp | 240 +++++------------------------------ 5 files changed, 262 insertions(+), 210 deletions(-) create mode 100644 meta-arm/lib/fvp/__init__.py create mode 100644 meta-arm/lib/fvp/conffile.py create mode 100644 meta-arm/lib/fvp/runner.py create mode 100644 meta-arm/lib/fvp/terminal.py diff --git a/meta-arm/lib/fvp/__init__.py b/meta-arm/lib/fvp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/meta-arm/lib/fvp/conffile.py b/meta-arm/lib/fvp/conffile.py new file mode 100644 index 0000000..77afca1 --- /dev/null +++ b/meta-arm/lib/fvp/conffile.py @@ -0,0 +1,58 @@ +import json +import pathlib +import os + + +def get_image_directory(machine=None): + """ + Get the DEPLOY_DIR_IMAGE for the specified machine + (or the configured machine if not set). + """ + try: + import bb.tinfoil + except ImportError as e: + raise RuntimeError("Cannot connect to BitBake, did you oe-init-build-env?") from e + + if machine: + os.environ["MACHINE"] = machine + + with bb.tinfoil.Tinfoil() as tinfoil: + tinfoil.prepare(config_only=True) + image_dir = tinfoil.config_data.getVar("DEPLOY_DIR_IMAGE") + return pathlib.Path(image_dir) + +def find(machine): + image_dir = get_image_directory(machine) + # All .fvpconf configuration files + configs = image_dir.glob("*.fvpconf") + # Just the files + configs = [p for p in configs if p.is_file() and not p.is_symlink()] + if not configs: + print(f"Cannot find any .fvpconf in {image_dir}") + raise RuntimeError() + # Sorted by modification time + configs = sorted(configs, key=lambda p: p.stat().st_mtime) + return configs[-1] + + +def load(config_file): + with open(config_file) as f: + config = json.load(f) + + # Ensure that all expected keys are present + def sanitise(key, value): + if key not in config or config[key] is None: + config[key] = value + sanitise("fvp-bindir", "") + sanitise("exe", "") + sanitise("parameters", {}) + sanitise("data", {}) + sanitise("applications", {}) + sanitise("terminals", {}) + sanitise("args", []) + sanitise("console", "") + + if not config["exe"]: + raise ValueError("Required value FVP_EXE not set in machine configuration") + + return config diff --git a/meta-arm/lib/fvp/runner.py b/meta-arm/lib/fvp/runner.py new file mode 100644 index 0000000..e7983c6 --- /dev/null +++ b/meta-arm/lib/fvp/runner.py @@ -0,0 +1,115 @@ +import asyncio +import re +import subprocess +import os +import shutil +import sys + +from .terminal import terminals + + +def cli_from_config(config, terminal_choice): + cli = [] + if config["fvp-bindir"]: + cli.append(os.path.join(config["fvp-bindir"], config["exe"])) + else: + cli.append(config["exe"]) + + for param, value in config["parameters"].items(): + cli.extend(["--parameter", f"{param}={value}"]) + + for value in config["data"]: + cli.extend(["--data", value]) + + for param, value in config["applications"].items(): + cli.extend(["--application", f"{param}={value}"]) + + for terminal, name in config["terminals"].items(): + # If terminals are enabled and this terminal has been named + if terminal_choice != "none" and name: + # TODO if raw mode + # cli.extend(["--parameter", f"{terminal}.mode=raw"]) + # TODO put name into terminal title + cli.extend(["--parameter", f"{terminal}.terminal_command={terminals[terminal_choice].command}"]) + else: + # Disable terminal + cli.extend(["--parameter", f"{terminal}.start_telnet=0"]) + + cli.extend(config["args"]) + + return cli + +def check_telnet(): + # Check that telnet is present + if not bool(shutil.which("telnet")): + raise RuntimeError("Cannot find telnet, this is needed to connect to the FVP.") + +class FVPRunner: + def __init__(self, logger): + self._terminal_ports = {} + self._line_callbacks = [] + self._logger = logger + self._fvp_process = None + self._telnets = [] + + def add_line_callback(self, callback): + self._line_callbacks.append(callback) + + async def start(self, config, extra_args=[], terminal_choice="none"): + cli = cli_from_config(config, terminal_choice) + cli += extra_args + self._logger.debug(f"Constructed FVP call: {cli}") + self._fvp_process = await asyncio.create_subprocess_exec(*cli, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + + def detect_terminals(line): + m = re.match(r"^(\S+): Listening for serial connection on port (\d+)$", line) + if m: + terminal = m.group(1) + port = int(m.group(2)) + self._terminal_ports[terminal] = port + self.add_line_callback(detect_terminals) + + async def stop(self): + if self._fvp_process: + self._logger.debug(f"Killing FVP PID {self._fvp_process.pid}") + try: + self._fvp_process.terminate() + except ProcessLookupError: + pass + + if await self._fvp_process.wait() != 0: + self._logger.info(f"FVP quit with code {self._fvp_process.returncode}") + return self._fvp_process.returncode + else: + return 0 + + for telnet in self._telnets: + await telnet.terminate() + await telnet.wait() + + async def run(self, until=None): + if until and until(): + return + + async for line in self._fvp_process.stdout: + line = line.strip().decode("utf-8", errors="replace") + for callback in self._line_callbacks: + callback(line) + if until and until(): + return + + async def _get_terminal_port(self, terminal, timeout): + def terminal_exists(): + return terminal in self._terminal_ports + await asyncio.wait_for(self.run(terminal_exists), timeout) + return self._terminal_ports[terminal] + + async def create_telnet(self, terminal, timeout=15.0): + check_telnet() + port = await self._get_terminal_port(terminal, timeout) + telnet = await asyncio.create_subprocess_exec("telnet", "localhost", str(port), stdin=sys.stdin, stdout=sys.stdout) + self._telnets.append(telnet) + return telnet + + def pid(self): + return self._fvp_process.pid diff --git a/meta-arm/lib/fvp/terminal.py b/meta-arm/lib/fvp/terminal.py new file mode 100644 index 0000000..6f40815 --- /dev/null +++ b/meta-arm/lib/fvp/terminal.py @@ -0,0 +1,59 @@ +import shutil +import collections +import pathlib +import os + +from typing import List, Optional + + +def get_config_dir() -> pathlib.Path: + value = os.environ.get("XDG_CONFIG_HOME") + if value and os.path.isabs(value): + return pathlib.Path(value) + else: + return pathlib.Path.home() / ".config" + +class Terminals: + Terminal = collections.namedtuple("Terminal", ["priority", "name", "command"]) + + def __init__(self): + self.terminals = [] + + def add_terminal(self, priority, name, command): + self.terminals.append(Terminals.Terminal(priority, name, command)) + # Keep this list sorted by priority + self.terminals.sort(reverse=True, key=lambda t: t.priority) + self.name_map = {t.name: t for t in self.terminals} + + def configured_terminal(self) -> Optional[str]: + import configparser + + config = configparser.ConfigParser() + config.read(get_config_dir() / "runfvp.conf") + return config.get("RunFVP", "Terminal", fallback=None) + + def preferred_terminal(self) -> str: + import shlex + + preferred = self.configured_terminal() + if preferred: + return preferred + + for t in self.terminals: + if t.command and shutil.which(shlex.split(t.command)[0]): + return t.name + return self.terminals[-1].name + + def all_terminals(self) -> List[str]: + return self.name_map.keys() + + def __getitem__(self, name: str): + return self.name_map[name] + +terminals = Terminals() +# TODO: option to switch between telnet and netcat +connect_command = "telnet localhost %port" +terminals.add_terminal(2, "tmux", f"tmux new-window -n \"%title\" \"{connect_command}\""), +terminals.add_terminal(2, "gnome-terminal", f"gnome-terminal --window --title \"%title\" --command \"{connect_command}\""), +terminals.add_terminal(1, "xterm", f"xterm -title \"%title\" -e {connect_command}"), +terminals.add_terminal(0, "none", None) diff --git a/scripts/runfvp b/scripts/runfvp index 7a3c239..30eae40 100755 --- a/scripts/runfvp +++ b/scripts/runfvp @@ -1,96 +1,23 @@ #! /usr/bin/env python3 import asyncio -import collections -import json import os -import re -import shutil +import pathlib import signal import sys -import subprocess -import pathlib import logging logger = logging.getLogger("RunFVP") -from typing import List, Optional - -def get_config_dir() -> pathlib.Path: - value = os.environ.get("XDG_CONFIG_HOME") - if value and os.path.isabs(value): - return pathlib.Path(value) - else: - return pathlib.Path.home() / ".config" - -class Terminals: - Terminal = collections.namedtuple("Terminal", ["priority", "name", "command"]) - - def __init__(self): - self.terminals = [] - - def add_terminal(self, priority, name, command): - self.terminals.append(Terminals.Terminal(priority, name, command)) - # Keep this list sorted by priority - self.terminals.sort(reverse=True, key=lambda t: t.priority) - self.name_map = {t.name: t for t in self.terminals} - - def configured_terminal(self) -> Optional[str]: - import configparser - - config = configparser.ConfigParser() - config.read(get_config_dir() / "runfvp.conf") - return config.get("RunFVP", "Terminal", fallback=None) - - def preferred_terminal(self) -> str: - import shlex - - preferred = self.configured_terminal() - if preferred: - return preferred - - for t in self.terminals: - if t.command and shutil.which(shlex.split(t.command)[0]): - return t.name - return self.terminals[-1].name - - def all_terminals(self) -> List[str]: - return self.name_map.keys() - - def __getitem__(self, name: str): - return self.name_map[name] - -terminals = Terminals() -# TODO: option to switch between telnet and netcat -connect_command = "telnet localhost %port" -terminals.add_terminal(2, "tmux", f"tmux new-window -n \"%title\" \"{connect_command}\""), -terminals.add_terminal(2, "gnome-terminal", f"gnome-terminal --window --title \"%title\" --command \"{connect_command}\""), -terminals.add_terminal(1, "xterm", f"xterm -title \"%title\" -e {connect_command}"), -terminals.add_terminal(0, "none", None) - -def get_image_directory(machine=None): - """ - Get the DEPLOY_DIR_IMAGE for the specified machine - (or the configured machine if not set). - """ - try: - import bb.tinfoil - except ImportError: - logger.error("Cannot connect to BitBake, did you oe-init-build-env?") - sys.exit(1) - - if machine: - os.environ["MACHINE"] = machine - - with bb.tinfoil.Tinfoil() as tinfoil: - tinfoil.prepare(config_only=True) - image_dir = tinfoil.config_data.getVar("DEPLOY_DIR_IMAGE") - logger.debug(f"Got DEPLOY_DIR_IMAGE {image_dir}") - return pathlib.Path(image_dir) +# Add meta-arm/lib/ to path +libdir = pathlib.Path(__file__).parents[1] / "meta-arm" / "lib" +sys.path.insert(0, str(libdir)) +from fvp import terminal, runner, conffile def parse_args(arguments): import argparse + terminals = terminal.terminals parser = argparse.ArgumentParser(description="Run images in a FVP") parser.add_argument("config", nargs="?", help="Machine name or path to .fvpconf file") @@ -120,148 +47,41 @@ def parse_args(arguments): logger.debug(f"FVP arguments: {fvp_args}") return args, fvp_args -def find_config(args): - if args.config and os.path.exists(args.config): - return args.config - else: - image_dir = get_image_directory(args.config) - # All .fvpconf configuration files - configs = image_dir.glob("*.fvpconf") - # Just the files - configs = [p for p in configs if p.is_file() and not p.is_symlink()] - if not configs: - print(f"Cannot find any .fvpconf in {image_dir}") - sys.exit(1) - # Sorted by modification time - configs = sorted(configs, key=lambda p: p.stat().st_mtime) - return configs[-1] - - -def load_config(config_file): - logger.debug(f"Loading {config_file}") - with open(config_file) as f: - config = json.load(f) - - # Ensure that all expected keys are present - def sanitise(key, value): - if key not in config or config[key] is None: - config[key] = value - sanitise("fvp-bindir", "") - sanitise("exe", "") - sanitise("parameters", {}) - sanitise("data", {}) - sanitise("applications", {}) - sanitise("terminals", {}) - sanitise("args", []) - sanitise("console", "") - - if not config["exe"]: - logger.error("Required value FVP_EXE not set in machine configuration") - sys.exit(1) - - return config -def parse_config(args, config): - cli = [] - if config["fvp-bindir"]: - cli.append(os.path.join(config["fvp-bindir"], config["exe"])) - else: - cli.append(config["exe"]) - - for param, value in config["parameters"].items(): - cli.extend(["--parameter", f"{param}={value}"]) - - for value in config["data"]: - cli.extend(["--data", value]) - - for param, value in config["applications"].items(): - cli.extend(["--application", f"{param}={value}"]) - - for terminal, name in config["terminals"].items(): - # If terminals are enabled and this terminal has been named - if args.terminals != "none" and name: - # TODO if raw mode - # cli.extend(["--parameter", f"{terminal}.mode=raw"]) - # TODO put name into terminal title - cli.extend(["--parameter", f"{terminal}.terminal_command={terminals[args.terminals].command}"]) - else: - # Disable terminal - cli.extend(["--parameter", f"{terminal}.start_telnet=0"]) - - cli.extend(config["args"]) - - return cli - -async def start_fvp(cli, console_cb): +async def start_fvp(args, config, extra_args): + fvp = runner.FVPRunner(logger) try: - fvp_process = await asyncio.create_subprocess_exec(*cli, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - - async for line in fvp_process.stdout: - line = line.strip().decode("utf-8", errors="replace") - if console_cb: - logger.debug(f"FVP output: {line}") - else: - print(line) + await fvp.start(config, extra_args, args.terminals) + + if args.console: + fvp.add_line_callback(lambda line: logger.debug(f"FVP output: {line}")) + expected_terminal = config["console"] + if not expected_terminal: + logger.error("--console used but FVP_CONSOLE not set in machine configuration") + return 1 + telnet = await fvp.create_telnet(expected_terminal) + await telnet.wait() + logger.debug(f"Telnet quit, cancelling tasks") + else: + fvp.add_line_callback(lambda line: print(line)) - # Look for serial connections opening - if console_cb: - m = re.match(r"^(\S+): Listening for serial connection on port (\d+)$", line) - if m: - terminal = m.group(1) - port = int(m.group(2)) - logger.debug(f"Console for {terminal} started on port {port}") - # When we can assume Py3.7+, this can be create_task - asyncio.ensure_future(console_cb(terminal, port)) + await fvp.run() finally: - # If we get cancelled or throw an exception, kill the FVP - logger.debug(f"Killing FVP PID {fvp_process.pid}") - try: - fvp_process.terminate() - except ProcessLookupError: - pass - - if await fvp_process.wait() != 0: - logger.info(f"{cli[0]} quit with code {fvp_process.returncode}") - return fvp_process.returncode - else: - return 0 + await fvp.stop() def runfvp(cli_args): - args, fvp_args = parse_args(cli_args) - config_file = find_config(args) - config = load_config(config_file) - cli = parse_config(args, config) - cli.extend(fvp_args) - logger.debug(f"Constructed FVP call: {cli}") - - # Check that telnet is present - if not bool(shutil.which("telnet")): - logger.error("Cannot find telnet, this is needed to connect to the FVP.") - return 1 - - if args.console: - expected_terminal = config["console"] - if not expected_terminal: - logger.error("--console used but FVP_CONSOLE not set in machine configuration") - return 1 + args, extra_args = parse_args(cli_args) + if args.config and pathlib.Path(args.config).exists(): + config_file = args.config else: - expected_terminal = None - - async def console_started(name, port): - if name == expected_terminal: - telnet = await asyncio.create_subprocess_exec("telnet", "localhost", str(port), stdin=sys.stdin, stdout=sys.stdout) - await telnet.wait() - logger.debug(f"Telnet quit, cancelling tasks") - # TODO: this is 3.7+ - for t in asyncio.all_tasks(): - logger.debug(f"Cancelling {t}") - t.cancel() + config_file = conffile.find(args.config) + logger.debug(f"Loading {config_file}") + config = conffile.load(config_file) try: # When we can assume Py3.7+, this can simply be asyncio.run() loop = asyncio.get_event_loop() - console_cb = expected_terminal and console_started or None - return loop.run_until_complete(start_fvp(cli, console_cb=console_cb)) + return loop.run_until_complete(start_fvp(args, config, extra_args)) except asyncio.CancelledError: # This means telnet exited, which isn't an error return 0 From patchwork Tue Jul 12 10:28:26 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Peter Hoyes X-Patchwork-Id: 10096 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id CCA05C433EF for ; Tue, 12 Jul 2022 10:28:58 +0000 (UTC) Received: from foss.arm.com (foss.arm.com [217.140.110.172]) by mx.groups.io with SMTP id smtpd.web12.7454.1657621730562113547 for ; Tue, 12 Jul 2022 03:28:50 -0700 Authentication-Results: mx.groups.io; dkim=missing; spf=pass (domain: arm.com, ip: 217.140.110.172, mailfrom: peter.hoyes@arm.com) Received: from usa-sjc-imap-foss1.foss.arm.com (unknown [10.121.207.14]) by usa-sjc-mx-foss1.foss.arm.com (Postfix) with ESMTP id 8120A1515; Tue, 12 Jul 2022 03:28:50 -0700 (PDT) Received: from e125920.cambridge.arm.com (unknown [10.1.199.64]) by usa-sjc-imap-foss1.foss.arm.com (Postfix) with ESMTPSA id 6BDA23F792; Tue, 12 Jul 2022 03:28:49 -0700 (PDT) From: Peter Hoyes To: meta-arm@lists.yoctoproject.org Cc: diego.sueiro@arm.com, robbie.cao@arm.com, Peter Hoyes Subject: [PATCH 2/6] arm/oeqa: Add selftests for FVP library Date: Tue, 12 Jul 2022 11:28:26 +0100 Message-Id: <20220712102830.625090-3-peter.hoyes@arm.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20220712102830.625090-1-peter.hoyes@arm.com> References: <20220712102830.625090-1-peter.hoyes@arm.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from li982-79.members.linode.com [45.33.32.79] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Tue, 12 Jul 2022 10:28:58 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/meta-arm/message/3556 From: Peter Hoyes Create basic tests for conffile and runner in meta-arm/lib/fvp Issue-Id: SCM-4957 Signed-off-by: Peter Hoyes Change-Id: I1684b0c99fb4fd5299df19f00abb30e8faab3495 --- meta-arm/lib/oeqa/selftest/cases/runfvp.py | 58 ++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/meta-arm/lib/oeqa/selftest/cases/runfvp.py b/meta-arm/lib/oeqa/selftest/cases/runfvp.py index 5b06ca8..aabd0b7 100644 --- a/meta-arm/lib/oeqa/selftest/cases/runfvp.py +++ b/meta-arm/lib/oeqa/selftest/cases/runfvp.py @@ -1,6 +1,9 @@ +import asyncio import os import pathlib import subprocess +import tempfile +import unittest.mock from oeqa.selftest.case import OESelftestTestCase @@ -49,3 +52,58 @@ class RunFVPTests(OESelftestTestCase): def test_fvp_options(self): # test-parameter sets one argument, add another manually self.run_fvp(testdir / "test-parameter.json", "--", "--parameter", "board.dog=woof") + +class ConfFileTests(OESelftestTestCase): + def test_no_exe(self): + from fvp import conffile + with tempfile.NamedTemporaryFile('w') as tf: + tf.write('{}') + tf.flush() + + with self.assertRaises(ValueError): + conffile.load(tf.name) + + def test_minimal(self): + from fvp import conffile + with tempfile.NamedTemporaryFile('w') as tf: + tf.write('{"exe": "FVP_Binary"}') + tf.flush() + + conf = conffile.load(tf.name) + self.assertTrue('fvp-bindir' in conf) + self.assertTrue('fvp-bindir' in conf) + self.assertTrue("exe" in conf) + self.assertTrue("parameters" in conf) + self.assertTrue("data" in conf) + self.assertTrue("applications" in conf) + self.assertTrue("terminals" in conf) + self.assertTrue("args" in conf) + self.assertTrue("console" in conf) + + +class RunnerTests(OESelftestTestCase): + def create_mock(self): + return unittest.mock.patch("asyncio.create_subprocess_exec") + + def test_start(self): + from fvp import runner + with self.create_mock() as m: + fvp = runner.FVPRunner(self.logger) + asyncio.run(fvp.start({ + "fvp-bindir": "/usr/bin", + "exe": "FVP_Binary", + "parameters": {'foo': 'bar'}, + "data": ['data1'], + "applications": {'a1': 'file'}, + "terminals": {}, + "args": ['--extra-arg'], + })) + + m.assert_called_once_with('/usr/bin/FVP_Binary', + '--parameter', 'foo=bar', + '--data', 'data1', + '--application', 'a1=file', + '--extra-arg', + stdin=unittest.mock.ANY, + stdout=unittest.mock.ANY, + stderr=unittest.mock.ANY) From patchwork Tue Jul 12 10:28:27 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Peter Hoyes X-Patchwork-Id: 10099 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id DCC97CCA47C for ; Tue, 12 Jul 2022 10:28:58 +0000 (UTC) Received: from foss.arm.com (foss.arm.com [217.140.110.172]) by mx.groups.io with SMTP id smtpd.web08.7534.1657621731985866493 for ; Tue, 12 Jul 2022 03:28:52 -0700 Authentication-Results: mx.groups.io; dkim=missing; spf=pass (domain: arm.com, ip: 217.140.110.172, mailfrom: peter.hoyes@arm.com) Received: from usa-sjc-imap-foss1.foss.arm.com (unknown [10.121.207.14]) by usa-sjc-mx-foss1.foss.arm.com (Postfix) with ESMTP id 1AA671596; Tue, 12 Jul 2022 03:28:52 -0700 (PDT) Received: from e125920.cambridge.arm.com (unknown [10.1.199.64]) by usa-sjc-imap-foss1.foss.arm.com (Postfix) with ESMTPSA id 05B763F792; Tue, 12 Jul 2022 03:28:50 -0700 (PDT) From: Peter Hoyes To: meta-arm@lists.yoctoproject.org Cc: diego.sueiro@arm.com, robbie.cao@arm.com, Peter Hoyes Subject: [PATCH 3/6] arm/oeqa: Refactor OEFVPTarget to use FVPRunner and pexpect Date: Tue, 12 Jul 2022 11:28:27 +0100 Message-Id: <20220712102830.625090-4-peter.hoyes@arm.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20220712102830.625090-1-peter.hoyes@arm.com> References: <20220712102830.625090-1-peter.hoyes@arm.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from li982-79.members.linode.com [45.33.32.79] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Tue, 12 Jul 2022 10:28:58 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/meta-arm/message/3557 From: Peter Hoyes Refactor OEFVPTarget to use the FVPRunner in meta-arm/lib instead of calling runfvp in a new process. Use pexpect to wait for the login prompt instead of parsing the FVP output manually. This patch introduces a dependency on pexpect for the meta-arm test targets. It is already in the Yocto host dependency list and the Kas container image, but may need to be installed on development machines. Issue-Id: SCM-4957 Signed-off-by: Peter Hoyes Change-Id: I7200e958c5701d82493287d021936afcf2f2bac9 --- meta-arm/lib/fvp/runner.py | 10 ++++ meta-arm/lib/oeqa/controllers/fvp.py | 75 +++++++--------------------- 2 files changed, 29 insertions(+), 56 deletions(-) diff --git a/meta-arm/lib/fvp/runner.py b/meta-arm/lib/fvp/runner.py index e7983c6..74ebc02 100644 --- a/meta-arm/lib/fvp/runner.py +++ b/meta-arm/lib/fvp/runner.py @@ -51,6 +51,7 @@ class FVPRunner: self._logger = logger self._fvp_process = None self._telnets = [] + self._pexpects = [] def add_line_callback(self, callback): self._line_callbacks.append(callback) @@ -87,6 +88,9 @@ class FVPRunner: await telnet.terminate() await telnet.wait() + for pexpect in self._pexpects: + pexpect.close() + async def run(self, until=None): if until and until(): return @@ -111,5 +115,11 @@ class FVPRunner: self._telnets.append(telnet) return telnet + async def create_pexpect(self, terminal, timeout=15.0, **kwargs): + check_telnet() + import pexpect + port = await self._get_terminal_port(terminal, timeout) + return pexpect.spawn(f"telnet localhost {port}", **kwargs) + def pid(self): return self._fvp_process.pid diff --git a/meta-arm/lib/oeqa/controllers/fvp.py b/meta-arm/lib/oeqa/controllers/fvp.py index 2913f78..c0a87ba 100644 --- a/meta-arm/lib/oeqa/controllers/fvp.py +++ b/meta-arm/lib/oeqa/controllers/fvp.py @@ -1,16 +1,11 @@ import asyncio -import os import pathlib -import signal -import subprocess +import pexpect import oeqa.core.target.ssh +from fvp import conffile, runner class OEFVPTarget(oeqa.core.target.ssh.OESSHTarget): - - # meta-arm/scripts isn't on PATH, so work out where it is - metaarm = pathlib.Path(__file__).parents[4] - def __init__(self, logger, target_ip, server_ip, timeout=300, user='root', port=None, server_port=0, dir_image=None, rootfs=None, bootlog=None, **kwargs): @@ -29,45 +24,24 @@ class OEFVPTarget(oeqa.core.target.ssh.OESSHTarget): self.logfile = bootlog and open(bootlog, "wb") or None async def boot_fvp(self): - cmd = [OEFVPTarget.metaarm / "scripts" / "runfvp", "--console", "--verbose", self.fvpconf] - # Python 3.7 needs the command items to be str - cmd = [str(c) for c in cmd] - self.logger.debug(f"Starting {cmd}") - - # TODO: refactor runfvp so this can import it and directly hook to the - # console callback, then use telnetlib directly to access the console. - - # As we're using --console, telnet expects stdin to be readable too. - self.fvp = await asyncio.create_subprocess_exec(*cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE) - self.logger.debug(f"Started runfvp PID {self.fvp.pid}") - - async def wait_for_login(bootlog): - while True: - line = await self.fvp.stdout.read(1024) - if not line: - self.logger.debug("runfvp terminated") - return False - - self.logger.debug(f"Read line [{line}]") - - bootlog += line - if self.logfile: - self.logfile.write(line) - - if b" login:" in bootlog: - self.logger.debug("Found login prompt") - return True - - bootlog = bytearray() + config = conffile.load(self.fvpconf) + self.fvp = runner.FVPRunner(self.logger) + await self.fvp.start(config) + self.logger.debug(f"Started FVP PID {self.fvp.pid()}") + console = await self.fvp.create_pexpect(config["console"]) try: - found = await asyncio.wait_for(wait_for_login(bootlog), self.boot_timeout) - if found: - return - except asyncio.TimeoutError: + console.expect("login\:", timeout=self.boot_timeout) + self.logger.debug("Found login prompt") + except pexpect.TIMEOUT: self.logger.info("Timed out waiting for login prompt.") - self.logger.info("Boot log follows:") - self.logger.info(b"\n".join(bootlog.splitlines()[-200:]).decode("utf-8", errors="replace")) - raise RuntimeError("Failed to start FVP.") + self.logger.info("Boot log follows:") + self.logger.info(b"\n".join(console.before.splitlines()[-200:]).decode("utf-8", errors="replace")) + raise RuntimeError("Failed to start FVP.") + + async def stop_fvp(self): + returncode = await self.fvp.stop() + + self.logger.debug(f"Stopped FVP with return code {returncode}") def start(self, **kwargs): # When we can assume Py3.7+, this can simply be asyncio.run() @@ -76,15 +50,4 @@ class OEFVPTarget(oeqa.core.target.ssh.OESSHTarget): def stop(self, **kwargs): loop = asyncio.get_event_loop() - - try: - # Kill the process group so that the telnet and FVP die too - gid = os.getpgid(self.fvp.pid) - self.logger.debug(f"Sending SIGTERM to {gid}") - os.killpg(gid, signal.SIGTERM) - loop.run_until_complete(asyncio.wait_for(self.fvp.wait(), 10)) - except TimeoutError: - self.logger.debug(f"Timed out, sending SIGKILL to {gid}") - os.killpg(gid, signal.SIGKILL) - except ProcessLookupError: - return + loop.run_until_complete(asyncio.gather(self.stop_fvp())) From patchwork Tue Jul 12 10:28:28 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Peter Hoyes X-Patchwork-Id: 10098 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id D9327CCA482 for ; Tue, 12 Jul 2022 10:28:58 +0000 (UTC) Received: from foss.arm.com (foss.arm.com [217.140.110.172]) by mx.groups.io with SMTP id smtpd.web08.7535.1657621733603167925 for ; Tue, 12 Jul 2022 03:28:53 -0700 Authentication-Results: mx.groups.io; dkim=missing; spf=pass (domain: arm.com, ip: 217.140.110.172, mailfrom: peter.hoyes@arm.com) Received: from usa-sjc-imap-foss1.foss.arm.com (unknown [10.121.207.14]) by usa-sjc-mx-foss1.foss.arm.com (Postfix) with ESMTP id 995B51A9A; Tue, 12 Jul 2022 03:28:53 -0700 (PDT) Received: from e125920.cambridge.arm.com (unknown [10.1.199.64]) by usa-sjc-imap-foss1.foss.arm.com (Postfix) with ESMTPSA id 8459F3F792; Tue, 12 Jul 2022 03:28:52 -0700 (PDT) From: Peter Hoyes To: meta-arm@lists.yoctoproject.org Cc: diego.sueiro@arm.com, robbie.cao@arm.com, Peter Hoyes Subject: [PATCH 4/6] arm/classes: Change FVP_CONSOLE to FVP_CONSOLES in fvpconf Date: Tue, 12 Jul 2022 11:28:28 +0100 Message-Id: <20220712102830.625090-5-peter.hoyes@arm.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20220712102830.625090-1-peter.hoyes@arm.com> References: <20220712102830.625090-1-peter.hoyes@arm.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from li982-79.members.linode.com [45.33.32.79] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Tue, 12 Jul 2022 10:28:58 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/meta-arm/message/3558 From: Peter Hoyes So that the test target can connect to the desired console(s) as soon as they appear in the FVP stdout, add the variable FVP_CONSOLES to the fvpconf as a replcaement for FVP_CONSOLE. The varflags of this variable define a mapping between FVP console names (e.g. terminal_0) and console names in the tests (e.g. 'zephyr'). The console defined in FVP_CONSOLE is automatically mapped as 'default' for backwards compatibility. This also enables greater reuse of test cases, as the "default" console name can be remapped on a per-machine basis. Issue-Id: SCM-4957 Signed-off-by: Peter Hoyes Change-Id: I9d88b172bfc5a5459b9f5132f287c70816d7fb55 --- meta-arm/classes/fvpboot.bbclass | 5 ++++- meta-arm/lib/fvp/conffile.py | 2 +- meta-arm/lib/oeqa/selftest/cases/runfvp.py | 2 +- scripts/runfvp | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/meta-arm/classes/fvpboot.bbclass b/meta-arm/classes/fvpboot.bbclass index d02742d..ec9d4f5 100644 --- a/meta-arm/classes/fvpboot.bbclass +++ b/meta-arm/classes/fvpboot.bbclass @@ -18,6 +18,9 @@ FVP_APPLICATIONS ?= "" FVP_TERMINALS ?= "" # What terminal should be considered the primary console FVP_CONSOLE ?= "" +# Flags for console names, as they appear in the FVP output. Flag name is an +# application-specific id for the console for use in test cases +FVP_CONSOLES[default] ?= "${FVP_CONSOLE}" # Arbitrary extra arguments FVP_EXTRA_ARGS ?= "" @@ -59,7 +62,7 @@ python do_write_fvpboot_conf() { data["parameters"] = getFlags("FVP_CONFIG") data["data"] = shlex.split(d.getVar("FVP_DATA") or "") data["applications"] = getFlags("FVP_APPLICATIONS") - data["console"] = d.getVar("FVP_CONSOLE") + data["consoles"] = getFlags("FVP_CONSOLES") data["terminals"] = getFlags("FVP_TERMINALS") data["args"] = shlex.split(d.getVar("FVP_EXTRA_ARGS") or "") diff --git a/meta-arm/lib/fvp/conffile.py b/meta-arm/lib/fvp/conffile.py index 77afca1..acede40 100644 --- a/meta-arm/lib/fvp/conffile.py +++ b/meta-arm/lib/fvp/conffile.py @@ -50,7 +50,7 @@ def load(config_file): sanitise("applications", {}) sanitise("terminals", {}) sanitise("args", []) - sanitise("console", "") + sanitise("consoles", {}) if not config["exe"]: raise ValueError("Required value FVP_EXE not set in machine configuration") diff --git a/meta-arm/lib/oeqa/selftest/cases/runfvp.py b/meta-arm/lib/oeqa/selftest/cases/runfvp.py index aabd0b7..d1e452f 100644 --- a/meta-arm/lib/oeqa/selftest/cases/runfvp.py +++ b/meta-arm/lib/oeqa/selftest/cases/runfvp.py @@ -78,7 +78,7 @@ class ConfFileTests(OESelftestTestCase): self.assertTrue("applications" in conf) self.assertTrue("terminals" in conf) self.assertTrue("args" in conf) - self.assertTrue("console" in conf) + self.assertTrue("consoles" in conf) class RunnerTests(OESelftestTestCase): diff --git a/scripts/runfvp b/scripts/runfvp index 30eae40..9fb77d3 100755 --- a/scripts/runfvp +++ b/scripts/runfvp @@ -55,7 +55,7 @@ async def start_fvp(args, config, extra_args): if args.console: fvp.add_line_callback(lambda line: logger.debug(f"FVP output: {line}")) - expected_terminal = config["console"] + expected_terminal = config["consoles"]["default"] if not expected_terminal: logger.error("--console used but FVP_CONSOLE not set in machine configuration") return 1 From patchwork Tue Jul 12 10:28:29 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Peter Hoyes X-Patchwork-Id: 10100 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id D0502CCA480 for ; Tue, 12 Jul 2022 10:28:58 +0000 (UTC) Received: from foss.arm.com (foss.arm.com [217.140.110.172]) by mx.groups.io with SMTP id smtpd.web09.7575.1657621736165982055 for ; Tue, 12 Jul 2022 03:28:56 -0700 Authentication-Results: mx.groups.io; dkim=missing; spf=pass (domain: arm.com, ip: 217.140.110.172, mailfrom: peter.hoyes@arm.com) Received: from usa-sjc-imap-foss1.foss.arm.com (unknown [10.121.207.14]) by usa-sjc-mx-foss1.foss.arm.com (Postfix) with ESMTP id C35491515; Tue, 12 Jul 2022 03:28:55 -0700 (PDT) Received: from e125920.cambridge.arm.com (unknown [10.1.199.64]) by usa-sjc-imap-foss1.foss.arm.com (Postfix) with ESMTPSA id ABE5F3F792; Tue, 12 Jul 2022 03:28:54 -0700 (PDT) From: Peter Hoyes To: meta-arm@lists.yoctoproject.org Cc: diego.sueiro@arm.com, robbie.cao@arm.com, Peter Hoyes Subject: [PATCH 5/6] arm/oeqa: Create new OEFVPSerialTarget with pexpect interface Date: Tue, 12 Jul 2022 11:28:29 +0100 Message-Id: <20220712102830.625090-6-peter.hoyes@arm.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20220712102830.625090-1-peter.hoyes@arm.com> References: <20220712102830.625090-1-peter.hoyes@arm.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from li982-79.members.linode.com [45.33.32.79] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Tue, 12 Jul 2022 10:28:58 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/meta-arm/message/3559 From: Peter Hoyes Refactor OEFVPTarget into new base class, OEFVPSSHTarget. OEFVPTarget extends OEFVPSSHTarget and additionally waits for a Linux login prompt for compatibility with tests in OE-core. OEFVPSerialTarget also extends OEFVPSSHTarget. It also exposes the entire API of pexpect, with the first argument being the FVP_TEST_CONSOLE varflag key. It logs each console output to separate files inside the core-image-minimal work directory. Issue-Id: SCM-4957 Signed-off-by: Peter Hoyes Change-Id: I1b93f94471c6311da9ee71a48239640ee37de0af --- meta-arm/lib/oeqa/controllers/fvp.py | 137 +++++++++++++++++++++++---- 1 file changed, 118 insertions(+), 19 deletions(-) diff --git a/meta-arm/lib/oeqa/controllers/fvp.py b/meta-arm/lib/oeqa/controllers/fvp.py index c0a87ba..ad01c11 100644 --- a/meta-arm/lib/oeqa/controllers/fvp.py +++ b/meta-arm/lib/oeqa/controllers/fvp.py @@ -1,45 +1,45 @@ import asyncio import pathlib import pexpect +import os -import oeqa.core.target.ssh +from oeqa.core.target.ssh import OESSHTarget from fvp import conffile, runner -class OEFVPTarget(oeqa.core.target.ssh.OESSHTarget): + +class OEFVPSSHTarget(OESSHTarget): + """ + Base class for meta-arm FVP targets. + Contains common logic to start and stop an FVP. + """ def __init__(self, logger, target_ip, server_ip, timeout=300, user='root', - port=None, server_port=0, dir_image=None, rootfs=None, bootlog=None, - **kwargs): + port=None, dir_image=None, rootfs=None, **kwargs): super().__init__(logger, target_ip, server_ip, timeout, user, port) image_dir = pathlib.Path(dir_image) # rootfs may have multiple extensions so we need to strip *all* suffixes basename = pathlib.Path(rootfs) basename = basename.name.replace("".join(basename.suffixes), "") self.fvpconf = image_dir / (basename + ".fvpconf") + self.config = conffile.load(self.fvpconf) if not self.fvpconf.exists(): raise FileNotFoundError(f"Cannot find {self.fvpconf}") - # FVPs boot slowly, so allow ten minutes - self.boot_timeout = 10 * 60 - - self.logfile = bootlog and open(bootlog, "wb") or None async def boot_fvp(self): - config = conffile.load(self.fvpconf) self.fvp = runner.FVPRunner(self.logger) - await self.fvp.start(config) + await self.fvp.start(self.config) self.logger.debug(f"Started FVP PID {self.fvp.pid()}") - console = await self.fvp.create_pexpect(config["console"]) - try: - console.expect("login\:", timeout=self.boot_timeout) - self.logger.debug("Found login prompt") - except pexpect.TIMEOUT: - self.logger.info("Timed out waiting for login prompt.") - self.logger.info("Boot log follows:") - self.logger.info(b"\n".join(console.before.splitlines()[-200:]).decode("utf-8", errors="replace")) - raise RuntimeError("Failed to start FVP.") + await self._after_start() + + async def _after_start(self): + pass + + async def _after_stop(self): + pass async def stop_fvp(self): returncode = await self.fvp.stop() + await self._after_stop() self.logger.debug(f"Stopped FVP with return code {returncode}") @@ -51,3 +51,102 @@ class OEFVPTarget(oeqa.core.target.ssh.OESSHTarget): def stop(self, **kwargs): loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.gather(self.stop_fvp())) + + +class OEFVPTarget(OEFVPSSHTarget): + """ + For compatibility with OE-core test cases, this target's start() method + waits for a Linux shell before returning to ensure that SSH commands work + with the default test dependencies. + """ + def __init__(self, logger, target_ip, server_ip, bootlog=None, **kwargs): + super().__init__(logger, target_ip, server_ip, **kwargs) + self.logfile = bootlog and open(bootlog, "wb") or None + + # FVPs boot slowly, so allow ten minutes + self.boot_timeout = 10 * 60 + + async def _after_start(self): + self.logger.debug(f"Awaiting console on terminal {self.config['consoles']['default']}") + console = await self.fvp.create_pexpect(self.config['consoles']['default']) + try: + console.expect("login\\:", timeout=self.boot_timeout) + self.logger.debug("Found login prompt") + except pexpect.TIMEOUT: + self.logger.info("Timed out waiting for login prompt.") + self.logger.info("Boot log follows:") + self.logger.info(b"\n".join(console.before.splitlines()[-200:]).decode("utf-8", errors="replace")) + raise RuntimeError("Failed to start FVP.") + + +class OEFVPSerialTarget(OEFVPSSHTarget): + """ + This target is intended for interaction with the target over one or more + telnet consoles using pexpect. + + This still depends on OEFVPSSHTarget so SSH commands can still be run on + the target, but note that this class does not inherently guarantee that + the SSH server is running prior to running test cases. Test cases that use + SSH should first validate that SSH is available. + """ + DEFAULT_CONSOLE = "default" + + def __init__(self, logger, target_ip, server_ip, bootlog=None, **kwargs): + super().__init__(logger, target_ip, server_ip, **kwargs) + self.terminals = {} + + self.test_log_path = pathlib.Path(bootlog).parent + self.test_log_suffix = pathlib.Path(bootlog).suffix + self.bootlog = bootlog + + async def _add_terminal(self, name, fvp_name): + logfile = self._create_logfile(name) + self.logger.info(f'Creating terminal {name} on {fvp_name}') + self.terminals[name] = \ + await self.fvp.create_pexpect(fvp_name, logfile=logfile) + + def _create_logfile(self, name): + fvp_log_file = f"{name}_log{self.test_log_suffix}" + fvp_log_path = pathlib.Path(self.test_log_path, fvp_log_file) + fvp_log_symlink = pathlib.Path(self.test_log_path, f"{name}_log") + try: + os.remove(fvp_log_symlink) + except: + pass + os.symlink(fvp_log_file, fvp_log_symlink) + return open(fvp_log_path, 'wb') + + async def _after_start(self): + for name, console in self.config["consoles"].items(): + await self._add_terminal(name, console) + + # testimage.bbclass expects to see a log file at `bootlog`, + # so make a symlink to the 'default' log file + if name == 'default': + default_test_file = f"{name}_log{self.test_log_suffix}" + os.symlink(default_test_file, self.bootlog) + + async def _after_stop(self): + # Ensure pexpect logs all remaining output to the logfile + for terminal in self.terminals.values(): + terminal.expect(pexpect.EOF, timeout=5) + terminal.close() + + def _get_terminal(self, name): + return self.terminals[name] + + def __getattr__(self, name): + """ + Magic method which automatically exposes the whole pexpect API on the + target, with the first argument being the terminal name. + + e.g. self.target.expect(self.target.DEFAULT_CONSOLE, "login\\:") + """ + def call_pexpect(terminal, *args, **kwargs): + attr = getattr(self.terminals[terminal], name) + if callable(attr): + return attr(*args, **kwargs) + else: + return attr + + return call_pexpect From patchwork Tue Jul 12 10:28:30 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Peter Hoyes X-Patchwork-Id: 10097 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id CB4DFC43334 for ; Tue, 12 Jul 2022 10:28:58 +0000 (UTC) Received: from foss.arm.com (foss.arm.com [217.140.110.172]) by mx.groups.io with SMTP id smtpd.web09.7577.1657621737689324852 for ; Tue, 12 Jul 2022 03:28:57 -0700 Authentication-Results: mx.groups.io; dkim=missing; spf=pass (domain: arm.com, ip: 217.140.110.172, mailfrom: peter.hoyes@arm.com) Received: from usa-sjc-imap-foss1.foss.arm.com (unknown [10.121.207.14]) by usa-sjc-mx-foss1.foss.arm.com (Postfix) with ESMTP id B85D21596; Tue, 12 Jul 2022 03:28:57 -0700 (PDT) Received: from e125920.cambridge.arm.com (unknown [10.1.199.64]) by usa-sjc-imap-foss1.foss.arm.com (Postfix) with ESMTPSA id A388E3F792; Tue, 12 Jul 2022 03:28:56 -0700 (PDT) From: Peter Hoyes To: meta-arm@lists.yoctoproject.org Cc: diego.sueiro@arm.com, robbie.cao@arm.com, Peter Hoyes Subject: [PATCH 6/6] arm/oeqa: Use linuxboot and OEFVPSerialTarget instead of noop Date: Tue, 12 Jul 2022 11:28:30 +0100 Message-Id: <20220712102830.625090-7-peter.hoyes@arm.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20220712102830.625090-1-peter.hoyes@arm.com> References: <20220712102830.625090-1-peter.hoyes@arm.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from li982-79.members.linode.com [45.33.32.79] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Tue, 12 Jul 2022 10:28:58 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/meta-arm/message/3560 From: Peter Hoyes Create a new "linuxboot" test that uses the pexpect methods on OEFVPSerialTarget to wait for a Linux login shell. Switch to this test method for fvp-baser-aemv8r64, corstone500 and corstone1000. Issue-Id: SCM-4957 Signed-off-by: Peter Hoyes Change-Id: Idd749652ee72e244b7a3831dd2295e0bfaed3bfa --- .../conf/machine/corstone1000-fvp.conf | 4 ++-- meta-arm-bsp/conf/machine/corstone500.conf | 4 ++-- .../conf/machine/fvp-baser-aemv8r64.conf | 4 ++-- meta-arm/lib/fvp/runner.py | 4 +++- meta-arm/lib/oeqa/controllers/fvp.py | 3 ++- meta-arm/lib/oeqa/runtime/cases/linuxboot.py | 18 ++++++++++++++++++ meta-arm/lib/oeqa/runtime/cases/noop.py | 12 ------------ 7 files changed, 29 insertions(+), 20 deletions(-) create mode 100644 meta-arm/lib/oeqa/runtime/cases/linuxboot.py delete mode 100644 meta-arm/lib/oeqa/runtime/cases/noop.py diff --git a/meta-arm-bsp/conf/machine/corstone1000-fvp.conf b/meta-arm-bsp/conf/machine/corstone1000-fvp.conf index eb122f3..e79373d 100644 --- a/meta-arm-bsp/conf/machine/corstone1000-fvp.conf +++ b/meta-arm-bsp/conf/machine/corstone1000-fvp.conf @@ -8,8 +8,8 @@ TFA_TARGET_PLATFORM = "fvp" TFM_PLATFORM_IS_FVP = "TRUE" # testimage config -TEST_TARGET = "OEFVPTarget" -TEST_SUITES = "noop" +TEST_TARGET = "OEFVPSerialTarget" +TEST_SUITES = "linuxboot" # FVP Config FVP_PROVIDER ?= "fvp-corstone1000-native" diff --git a/meta-arm-bsp/conf/machine/corstone500.conf b/meta-arm-bsp/conf/machine/corstone500.conf index 6a3b36e..1d25471 100644 --- a/meta-arm-bsp/conf/machine/corstone500.conf +++ b/meta-arm-bsp/conf/machine/corstone500.conf @@ -33,8 +33,8 @@ WKS_FILE_DEPENDS:append = " ${EXTRA_IMAGEDEPENDS}" WKS_FILE ?= "core-image-minimal.corstone500.wks" -TEST_TARGET = "OEFVPTarget" -TEST_SUITES = "noop" +TEST_TARGET = "OEFVPSerialTarget" +TEST_SUITES = "linuxboot" FVP_PROVIDER ?= "fvp-corstone500-native" FVP_EXE ?= "FVP_Corstone-500" diff --git a/meta-arm-bsp/conf/machine/fvp-baser-aemv8r64.conf b/meta-arm-bsp/conf/machine/fvp-baser-aemv8r64.conf index ee85cc6..9c80059 100644 --- a/meta-arm-bsp/conf/machine/fvp-baser-aemv8r64.conf +++ b/meta-arm-bsp/conf/machine/fvp-baser-aemv8r64.conf @@ -33,8 +33,8 @@ PACKAGECONFIG:remove:pn-openssh = "rng-tools" MACHINE_EXTRA_RRECOMMENDS += "ssh-pregen-hostkeys" # testimage configuration -TEST_TARGET = "OEFVPTarget" -TEST_SUITES = "ping ssh" +TEST_TARGET = "OEFVPSerialTarget" +TEST_SUITES = "linuxboot" TEST_TARGET_IP ?= "127.0.0.1:8022" TEST_SERVER_IP ?= "127.0.1.1" diff --git a/meta-arm/lib/fvp/runner.py b/meta-arm/lib/fvp/runner.py index 74ebc02..3b3fd00 100644 --- a/meta-arm/lib/fvp/runner.py +++ b/meta-arm/lib/fvp/runner.py @@ -119,7 +119,9 @@ class FVPRunner: check_telnet() import pexpect port = await self._get_terminal_port(terminal, timeout) - return pexpect.spawn(f"telnet localhost {port}", **kwargs) + instance = pexpect.spawn(f"telnet localhost {port}", **kwargs) + self._pexpects.append(instance) + return instance def pid(self): return self._fvp_process.pid diff --git a/meta-arm/lib/oeqa/controllers/fvp.py b/meta-arm/lib/oeqa/controllers/fvp.py index ad01c11..30b6296 100644 --- a/meta-arm/lib/oeqa/controllers/fvp.py +++ b/meta-arm/lib/oeqa/controllers/fvp.py @@ -87,7 +87,8 @@ class OEFVPSerialTarget(OEFVPSSHTarget): This still depends on OEFVPSSHTarget so SSH commands can still be run on the target, but note that this class does not inherently guarantee that the SSH server is running prior to running test cases. Test cases that use - SSH should first validate that SSH is available. + SSH should first validate that SSH is available, e.g. by depending on the + "linuxboot" test case in meta-arm. """ DEFAULT_CONSOLE = "default" diff --git a/meta-arm/lib/oeqa/runtime/cases/linuxboot.py b/meta-arm/lib/oeqa/runtime/cases/linuxboot.py new file mode 100644 index 0000000..19e6e18 --- /dev/null +++ b/meta-arm/lib/oeqa/runtime/cases/linuxboot.py @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: MIT + +from oeqa.runtime.case import OERuntimeTestCase + + +class LinuxBootTest(OERuntimeTestCase): + """ + This test case is only compatible with the OEFVPSerialTarget as it uses + the pexpect interface. It waits for a Linux login prompt on the default + console. + """ + + def setUp(self): + self.console = self.target.DEFAULT_CONSOLE + + def test_linux_boot(self): + self.logger.info(f"{self.console}: Waiting for login prompt") + self.target.expect(self.console, "login\:", timeout=10*60) diff --git a/meta-arm/lib/oeqa/runtime/cases/noop.py b/meta-arm/lib/oeqa/runtime/cases/noop.py deleted file mode 100644 index b5fba7c..0000000 --- a/meta-arm/lib/oeqa/runtime/cases/noop.py +++ /dev/null @@ -1,12 +0,0 @@ -# SPDX-License-Identifier: MIT - -from oeqa.runtime.case import OERuntimeTestCase - -class NoopTest(OERuntimeTestCase): - """ - This is a test case which does nothing. Useful when you want to use - testimage to verify that an image boots, but you don't have networking so - none of the existing test cases are suitable. - """ - def test_no_op(self): - return