1
0
mirror of https://git.yoctoproject.org/meta-arm synced 2026-06-07 15:10:09 +00:00

arm/lib: Factor out asyncio in FVPRunner

FVPRunner relies heavily on asyncio, despite there being very little
concurrent work happening. Additionally, while the runfvp entry point
starts an asyncio runner, it is not practical to have a single asyncio
runtime during testimage, which is fully synchronous.

Refactor to use subprocess.Popen and related functionality. The process
object has a similar interface to its async equivalent.

Cascade the API changes to runfvp and the test target classes.

Issue-Id: SCM-5314
Signed-off-by: Peter Hoyes <Peter.Hoyes@arm.com>
Change-Id: I3e7517e8bcbb3b93c41405d43dbd8bd24a9e7eb8
Signed-off-by: Jon Mason <jon.mason@arm.com>
This commit is contained in:
Peter Hoyes
2022-11-15 15:01:13 +00:00
committed by Jon Mason
parent 721bec250d
commit 81e0cf090b
4 changed files with 46 additions and 69 deletions
+17 -18
View File
@@ -1,4 +1,3 @@
import asyncio
import re
import subprocess
import os
@@ -57,7 +56,7 @@ class FVPRunner:
def add_line_callback(self, callback):
self._line_callbacks.append(callback)
async def start(self, config, extra_args=[], terminal_choice="none"):
def start(self, config, extra_args=[], terminal_choice="none"):
cli = cli_from_config(config, terminal_choice)
cli += extra_args
@@ -69,8 +68,8 @@ class FVPRunner:
env[name] = os.environ[name]
self._logger.debug(f"Constructed FVP call: {shlex.join(cli)}")
self._fvp_process = await asyncio.create_subprocess_exec(
*cli,
self._fvp_process = subprocess.Popen(
cli,
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
env=env)
@@ -82,13 +81,13 @@ class FVPRunner:
self._terminal_ports[terminal] = port
self.add_line_callback(detect_terminals)
async def stop(self):
def stop(self):
if self._fvp_process:
self._logger.debug(f"Terminating FVP PID {self._fvp_process.pid}")
try:
self._fvp_process.terminate()
await asyncio.wait_for(self._fvp_process.wait(), 10.0)
except asyncio.TimeoutError:
self._fvp_process.wait(10.0)
except subprocess.TimeoutExpired:
self._logger.debug(f"Killing FVP PID {self._fvp_process.pid}")
self._fvp_process.kill()
except ProcessLookupError:
@@ -97,8 +96,8 @@ class FVPRunner:
for telnet in self._telnets:
try:
telnet.terminate()
await asyncio.wait_for(telnet.wait(), 10.0)
except asyncio.TimeoutError:
telnet.wait(10.0)
except subprocess.TimeoutExpired:
telnet.kill()
except ProcessLookupError:
pass
@@ -118,34 +117,34 @@ class FVPRunner:
else:
return 0
async def run(self, until=None):
def run(self, until=None):
if until and until():
return
async for line in self._fvp_process.stdout:
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 _get_terminal_port(self, terminal):
def terminal_exists():
return terminal in self._terminal_ports
await asyncio.wait_for(self.run(terminal_exists), timeout)
self.run(terminal_exists)
return self._terminal_ports[terminal]
async def create_telnet(self, terminal, timeout=15.0):
def create_telnet(self, terminal):
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)
port = self._get_terminal_port(terminal)
telnet = subprocess.Popen(["telnet", "localhost", str(port)], stdin=sys.stdin, stdout=sys.stdout)
self._telnets.append(telnet)
return telnet
async def create_pexpect(self, terminal, timeout=15.0, **kwargs):
def create_pexpect(self, terminal, **kwargs):
check_telnet()
import pexpect
port = await self._get_terminal_port(terminal, timeout)
port = self._get_terminal_port(terminal)
instance = pexpect.spawn(f"telnet localhost {port}", **kwargs)
self._pexpects.append(instance)
return instance
+13 -28
View File
@@ -1,4 +1,3 @@
import asyncio
import pathlib
import pexpect
import os
@@ -25,32 +24,18 @@ class OEFVPSSHTarget(OESSHTarget):
if not self.fvpconf.exists():
raise FileNotFoundError(f"Cannot find {self.fvpconf}")
async def boot_fvp(self):
self.fvp = runner.FVPRunner(self.logger)
await self.fvp.start(self.config)
self.logger.debug(f"Started FVP PID {self.fvp.pid()}")
await self._after_start()
async def _after_start(self):
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}")
def start(self, **kwargs):
# When we can assume Py3.7+, this can simply be asyncio.run()
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(self.boot_fvp()))
self.fvp = runner.FVPRunner(self.logger)
self.fvp.start(self.config)
self.logger.debug(f"Started FVP PID {self.fvp.pid()}")
self._after_start()
def stop(self, **kwargs):
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(self.stop_fvp()))
returncode = self.fvp.stop()
self.logger.debug(f"Stopped FVP with return code {returncode}")
class OEFVPTarget(OEFVPSSHTarget):
@@ -66,9 +51,9 @@ class OEFVPTarget(OEFVPSSHTarget):
# FVPs boot slowly, so allow ten minutes
self.boot_timeout = 10 * 60
async def _after_start(self):
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'])
console = self.fvp.create_pexpect(self.config['consoles']['default'])
try:
console.expect("login\\:", timeout=self.boot_timeout)
self.logger.debug("Found login prompt")
@@ -100,11 +85,11 @@ class OEFVPSerialTarget(OEFVPSSHTarget):
self.test_log_suffix = pathlib.Path(bootlog).suffix
self.bootlog = bootlog
async def _add_terminal(self, name, fvp_name):
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)
self.fvp.create_pexpect(fvp_name, logfile=logfile)
def _create_logfile(self, name):
fvp_log_file = f"{name}_log{self.test_log_suffix}"
@@ -117,9 +102,9 @@ class OEFVPSerialTarget(OEFVPSSHTarget):
os.symlink(fvp_log_file, fvp_log_symlink)
return open(fvp_log_path, 'wb')
async def _after_start(self):
def _after_start(self):
for name, console in self.config["consoles"].items():
await self._add_terminal(name, console)
self._add_terminal(name, console)
# testimage.bbclass expects to see a log file at `bootlog`,
# so make a symlink to the 'default' log file
+8 -8
View File
@@ -81,13 +81,13 @@ class ConfFileTests(OESelftestTestCase):
class RunnerTests(OESelftestTestCase):
def create_mock(self):
return unittest.mock.patch("asyncio.create_subprocess_exec")
return unittest.mock.patch("subprocess.Popen")
def test_start(self):
from fvp import runner
with self.create_mock() as m:
fvp = runner.FVPRunner(self.logger)
asyncio.run(fvp.start({
fvp.start({
"fvp-bindir": "/usr/bin",
"exe": "FVP_Binary",
"parameters": {'foo': 'bar'},
@@ -96,13 +96,13 @@ class RunnerTests(OESelftestTestCase):
"terminals": {},
"args": ['--extra-arg'],
"env": {"FOO": "BAR"}
}))
})
m.assert_called_once_with('/usr/bin/FVP_Binary',
m.assert_called_once_with(['/usr/bin/FVP_Binary',
'--parameter', 'foo=bar',
'--data', 'data1',
'--application', 'a1=file',
'--extra-arg',
'--extra-arg'],
stdin=unittest.mock.ANY,
stdout=unittest.mock.ANY,
stderr=unittest.mock.ANY,
@@ -113,7 +113,7 @@ class RunnerTests(OESelftestTestCase):
from fvp import runner
with self.create_mock() as m:
fvp = runner.FVPRunner(self.logger)
asyncio.run(fvp.start({
fvp.start({
"fvp-bindir": "/usr/bin",
"exe": "FVP_Binary",
"parameters": {},
@@ -122,9 +122,9 @@ class RunnerTests(OESelftestTestCase):
"terminals": {},
"args": [],
"env": {"FOO": "BAR"}
}))
})
m.assert_called_once_with('/usr/bin/FVP_Binary',
m.assert_called_once_with(['/usr/bin/FVP_Binary'],
stdin=unittest.mock.ANY,
stdout=unittest.mock.ANY,
stderr=unittest.mock.ANY,
+8 -15
View File
@@ -1,6 +1,5 @@
#! /usr/bin/env python3
import asyncio
import os
import pathlib
import signal
@@ -47,11 +46,10 @@ def parse_args(arguments):
logger.debug(f"FVP arguments: {fvp_args}")
return args, fvp_args
async def start_fvp(args, config, extra_args):
def start_fvp(args, config, extra_args):
fvp = runner.FVPRunner(logger)
try:
await fvp.start(config, extra_args, args.terminals)
fvp.start(config, extra_args, args.terminals)
if args.console:
fvp.add_line_callback(lambda line: logger.debug(f"FVP output: {line}"))
@@ -59,15 +57,16 @@ async def start_fvp(args, config, extra_args):
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()
telnet = fvp.create_telnet(expected_terminal)
telnet.wait()
logger.debug(f"Telnet quit, cancelling tasks")
else:
fvp.add_line_callback(lambda line: print(line))
await fvp.run()
fvp.run()
finally:
await fvp.stop()
fvp.stop()
def runfvp(cli_args):
args, extra_args = parse_args(cli_args)
@@ -77,14 +76,8 @@ def runfvp(cli_args):
config_file = conffile.find(args.config)
logger.debug(f"Loading {config_file}")
config = conffile.load(config_file)
start_fvp(args, config, extra_args)
try:
# When we can assume Py3.7+, this can simply be asyncio.run()
loop = asyncio.get_event_loop()
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
if __name__ == "__main__":
try: