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:
+17
-18
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user