diff mbox series

[5/6] arm/oeqa: Create new OEFVPSerialTarget with pexpect interface

Message ID 20220712102830.625090-6-peter.hoyes@arm.com
State New
Headers show
Series Refactor runfvp for OEFVPSerialTarget | expand

Commit Message

Peter Hoyes July 12, 2022, 10:28 a.m. UTC
From: Peter Hoyes <Peter.Hoyes@arm.com>

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 <Peter.Hoyes@arm.com>
Change-Id: I1b93f94471c6311da9ee71a48239640ee37de0af
---
 meta-arm/lib/oeqa/controllers/fvp.py | 137 +++++++++++++++++++++++----
 1 file changed, 118 insertions(+), 19 deletions(-)
diff mbox series

Patch

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