diff mbox series

[3/5] arm/lib: Decouple console parsing from the FVPRunner

Message ID 20221115150116.314729-3-peter.hoyes@arm.com
State New
Headers show
Series [1/5] arm/fvp: Join cli arguments in verbose logging | expand

Commit Message

Peter Hoyes Nov. 15, 2022, 3:01 p.m. UTC
From: Peter Hoyes <Peter.Hoyes@arm.com>

To simplify the FVPRunner class, create a separate ConsolePortParser
class to handle reading an iterator of lines and parsing port numbers
for FVP consoles. Use this in runfvp and the test targets.

This refactor also allows the stream being monitored to be changed more
easily, e.g. to a log file.

Issue-Id: SCM-5314
Signed-off-by: Peter Hoyes <Peter.Hoyes@arm.com>
Change-Id: Iade3a4c803fb355b04af7afa298d0a41fe707d94
---
 meta-arm/lib/fvp/runner.py           | 66 ++++++++++++++--------------
 meta-arm/lib/oeqa/controllers/fvp.py | 17 +++----
 scripts/runfvp                       |  9 ++--
 3 files changed, 47 insertions(+), 45 deletions(-)
diff mbox series

Patch

diff --git a/meta-arm/lib/fvp/runner.py b/meta-arm/lib/fvp/runner.py
index 9b537e27..5c5ded28 100644
--- a/meta-arm/lib/fvp/runner.py
+++ b/meta-arm/lib/fvp/runner.py
@@ -44,18 +44,39 @@  def check_telnet():
     if not bool(shutil.which("telnet")):
         raise RuntimeError("Cannot find telnet, this is needed to connect to the FVP.")
 
+
+class ConsolePortParser:
+    def __init__(self, lines):
+        self._lines = lines
+        self._console_ports = {}
+
+    def parse_port(self, console):
+        if console in self._console_ports:
+            return self._console_ports[console]
+
+        while True:
+            try:
+                line = next(self._lines).strip().decode(errors='ignore')
+                m = re.match(r"^(\S+): Listening for serial connection on port (\d+)$", line)
+                if m:
+                    matched_console = m.group(1)
+                    matched_port = int(m.group(2))
+                    if matched_console == console:
+                        return matched_port
+                    else:
+                        self._console_ports[matched_console] = matched_port
+            except StopIteration:
+                # self._lines might be a growing log file
+                pass
+
+
 class FVPRunner:
     def __init__(self, logger):
-        self._terminal_ports = {}
-        self._line_callbacks = []
         self._logger = logger
         self._fvp_process = None
         self._telnets = []
         self._pexpects = []
 
-    def add_line_callback(self, callback):
-        self._line_callbacks.append(callback)
-
     def start(self, config, extra_args=[], terminal_choice="none"):
         cli = cli_from_config(config, terminal_choice)
         cli += extra_args
@@ -73,14 +94,6 @@  class FVPRunner:
             stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
             env=env)
 
-        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)
-
     def stop(self):
         if self._fvp_process:
             self._logger.debug(f"Terminating FVP PID {self._fvp_process.pid}")
@@ -117,34 +130,21 @@  class FVPRunner:
         else:
             return 0
 
-    def run(self, until=None):
-        if until and until():
-            return
+    def wait(self, timeout):
+        self._fvp_process.wait(timeout)
 
-        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
+    @property
+    def stdout(self):
+        return self._fvp_process.stdout
 
-    def _get_terminal_port(self, terminal):
-        def terminal_exists():
-            return terminal in self._terminal_ports
-        self.run(terminal_exists)
-        return self._terminal_ports[terminal]
-
-    def create_telnet(self, terminal):
+    def create_telnet(self, port):
         check_telnet()
-        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
 
-    def create_pexpect(self, terminal, **kwargs):
-        check_telnet()
+    def create_pexpect(self, port, **kwargs):
         import pexpect
-        port = self._get_terminal_port(terminal)
         instance = pexpect.spawn(f"telnet localhost {port}", **kwargs)
         self._pexpects.append(instance)
         return instance
diff --git a/meta-arm/lib/oeqa/controllers/fvp.py b/meta-arm/lib/oeqa/controllers/fvp.py
index dfc2b88b..0c3c2214 100644
--- a/meta-arm/lib/oeqa/controllers/fvp.py
+++ b/meta-arm/lib/oeqa/controllers/fvp.py
@@ -52,8 +52,10 @@  class OEFVPTarget(OEFVPSSHTarget):
         self.boot_timeout = 10 * 60
 
     def _after_start(self):
+        parser = runner.ConsolePortParser(self.fvp.stdout)
         self.logger.debug(f"Awaiting console on terminal {self.config['consoles']['default']}")
-        console = self.fvp.create_pexpect(self.config['consoles']['default'])
+        port = parser.parse_port(self.config['consoles']['default'])
+        console = self.fvp.create_pexpect(port)
         try:
             console.expect("login\\:", timeout=self.boot_timeout)
             self.logger.debug("Found login prompt")
@@ -85,12 +87,6 @@  class OEFVPSerialTarget(OEFVPSSHTarget):
         self.test_log_suffix = pathlib.Path(bootlog).suffix
         self.bootlog = bootlog
 
-    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] = \
-            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)
@@ -103,8 +99,13 @@  class OEFVPSerialTarget(OEFVPSSHTarget):
         return open(fvp_log_path, 'wb')
 
     def _after_start(self):
+        parser = runner.ConsolePortParser(self.fvp.stdout)
         for name, console in self.config["consoles"].items():
-            self._add_terminal(name, console)
+            logfile = self._create_logfile(name)
+            self.logger.info(f'Creating terminal {name} on {console}')
+            port = parser.parse_port(console)
+            self.terminals[name] = \
+                self.fvp.create_pexpect(port, logfile=logfile)
 
             # testimage.bbclass expects to see a log file at `bootlog`,
             # so make a symlink to the 'default' log file
diff --git a/scripts/runfvp b/scripts/runfvp
index 727223ca..454acb86 100755
--- a/scripts/runfvp
+++ b/scripts/runfvp
@@ -52,17 +52,18 @@  def start_fvp(args, config, extra_args):
         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["consoles"]["default"]
             if not expected_terminal:
                 logger.error("--console used but FVP_CONSOLE not set in machine configuration")
                 return 1
-            telnet = fvp.create_telnet(expected_terminal)
+            parser = runner.ConsolePortParser(fvp.stdout)
+            port = parser.parse_port(expected_terminal)
+            telnet = fvp.create_telnet(port)
             telnet.wait()
             logger.debug(f"Telnet quit, cancelling tasks")
         else:
-            fvp.add_line_callback(lambda line: print(line))
-            fvp.run()
+            for line in fvp.stdout:
+                print(line.strip().decode(errors='ignore'))
 
     finally:
         fvp.stop()