aboutsummaryrefslogtreecommitdiff
path: root/tests/functional/qemu_test
diff options
context:
space:
mode:
Diffstat (limited to 'tests/functional/qemu_test')
-rw-r--r--tests/functional/qemu_test/__init__.py4
-rw-r--r--tests/functional/qemu_test/asset.py25
-rw-r--r--tests/functional/qemu_test/cmd.py67
-rw-r--r--tests/functional/qemu_test/decorators.py36
-rw-r--r--tests/functional/qemu_test/gdb.py86
-rw-r--r--tests/functional/qemu_test/ports.py5
-rw-r--r--tests/functional/qemu_test/testcase.py29
7 files changed, 227 insertions, 25 deletions
diff --git a/tests/functional/qemu_test/__init__.py b/tests/functional/qemu_test/__init__.py
index af41c2c..3201935 100644
--- a/tests/functional/qemu_test/__init__.py
+++ b/tests/functional/qemu_test/__init__.py
@@ -15,6 +15,8 @@ from .testcase import QemuBaseTest, QemuUserTest, QemuSystemTest
from .linuxkernel import LinuxKernelTest
from .decorators import skipIfMissingCommands, skipIfNotMachine, \
skipFlakyTest, skipUntrustedTest, skipBigDataTest, skipSlowTest, \
- skipIfMissingImports, skipIfOperatingSystem
+ skipIfMissingImports, skipIfOperatingSystem, skipLockedMemoryTest, \
+ skipIfMissingEnv
from .archive import archive_extract
from .uncompress import uncompress
+from .gdb import GDB
diff --git a/tests/functional/qemu_test/asset.py b/tests/functional/qemu_test/asset.py
index 704b84d..f666125 100644
--- a/tests/functional/qemu_test/asset.py
+++ b/tests/functional/qemu_test/asset.py
@@ -15,7 +15,7 @@ import urllib.request
from time import sleep
from pathlib import Path
from shutil import copyfileobj
-from urllib.error import HTTPError
+from urllib.error import HTTPError, URLError
class AssetError(Exception):
def __init__(self, asset, msg, transient=False):
@@ -72,6 +72,10 @@ class Asset:
return self.hash == hl.hexdigest()
def valid(self):
+ if os.getenv("QEMU_TEST_REFRESH_CACHE", None) is not None:
+ self.log.info("Force refresh of asset %s", self.url)
+ return False
+
return self.cache_file.exists() and self._check(self.cache_file)
def fetchable(self):
@@ -167,9 +171,25 @@ class Asset:
raise AssetError(self, "Unable to download: "
"HTTP error %d" % e.code)
continue
+ except URLError as e:
+ # This is typically a network/service level error
+ # eg urlopen error [Errno 110] Connection timed out>
+ tmp_cache_file.unlink()
+ self.log.error("Unable to download %s: URL error %s",
+ self.url, e.reason)
+ raise AssetError(self, "Unable to download: URL error %s" %
+ e.reason, transient=True)
+ except ConnectionError as e:
+ # A socket connection failure, such as dropped conn
+ # or refused conn
+ tmp_cache_file.unlink()
+ self.log.error("Unable to download %s: Connection error %s",
+ self.url, e)
+ continue
except Exception as e:
tmp_cache_file.unlink()
- raise AssetError(self, "Unable to download: " % e)
+ raise AssetError(self, "Unable to download: %s" % e,
+ transient=True)
if not os.path.exists(tmp_cache_file):
raise AssetError(self, "Download retries exceeded", transient=True)
@@ -205,7 +225,6 @@ class Asset:
log.addHandler(handler)
for name, asset in vars(test.__class__).items():
if name.startswith("ASSET_") and type(asset) == Asset:
- log.info("Attempting to cache '%s'" % asset)
try:
asset.fetch()
except AssetError as e:
diff --git a/tests/functional/qemu_test/cmd.py b/tests/functional/qemu_test/cmd.py
index dc5f422..f544566 100644
--- a/tests/functional/qemu_test/cmd.py
+++ b/tests/functional/qemu_test/cmd.py
@@ -45,13 +45,16 @@ def is_readable_executable_file(path):
# If end of line is seen, with neither @success or @failure
# return False
#
+# In both cases, also return the contents of the line (in bytes)
+# up to that point.
+#
# If @failure is seen, then mark @test as failed
def _console_read_line_until_match(test, vm, success, failure):
msg = bytes([])
done = False
while True:
c = vm.console_socket.recv(1)
- if c is None:
+ if not c:
done = True
test.fail(
f"EOF in console, expected '{success}'")
@@ -76,10 +79,23 @@ def _console_read_line_until_match(test, vm, success, failure):
except:
console_logger.debug(msg)
- return done
+ return done, msg
def _console_interaction(test, success_message, failure_message,
send_string, keep_sending=False, vm=None):
+ """
+ Interact with the console until either message is seen.
+
+ :param success_message: if this message appears, finish interaction
+ :param failure_message: if this message appears, test fails
+ :param send_string: a string to send to the console before trying
+ to read a new line
+ :param keep_sending: keep sending the send string each time
+ :param vm: the VM to interact with
+
+ :return: The collected output (in bytes form).
+ """
+
assert not keep_sending or send_string
assert success_message or send_string
@@ -101,6 +117,8 @@ def _console_interaction(test, success_message, failure_message,
if failure_message is not None:
failure_message_b = failure_message.encode()
+ out = bytes([])
+
while True:
if send_string:
vm.console_socket.sendall(send_string.encode())
@@ -113,14 +131,21 @@ def _console_interaction(test, success_message, failure_message,
break
continue
- if _console_read_line_until_match(test, vm,
- success_message_b,
- failure_message_b):
+ done, line = _console_read_line_until_match(test, vm,
+ success_message_b,
+ failure_message_b)
+
+ out += line
+
+ if done:
break
+ return out
+
def interrupt_interactive_console_until_pattern(test, success_message,
failure_message=None,
- interrupt_string='\r'):
+ interrupt_string='\r',
+ vm=None):
"""
Keep sending a string to interrupt a console prompt, while logging the
console output. Typical use case is to break a boot loader prompt, such:
@@ -140,10 +165,13 @@ def interrupt_interactive_console_until_pattern(test, success_message,
:param failure_message: if this message appears, test fails
:param interrupt_string: a string to send to the console before trying
to read a new line
+ :param vm: VM to use
+
+ :return: The collected output (in bytes form).
"""
assert success_message
- _console_interaction(test, success_message, failure_message,
- interrupt_string, True)
+ return _console_interaction(test, success_message, failure_message,
+ interrupt_string, True, vm=vm)
def wait_for_console_pattern(test, success_message, failure_message=None,
vm=None):
@@ -155,11 +183,15 @@ def wait_for_console_pattern(test, success_message, failure_message=None,
:type test: :class:`qemu_test.QemuSystemTest`
:param success_message: if this message appears, test succeeds
:param failure_message: if this message appears, test fails
+ :param vm: VM to use
+
+ :return: The collected output (in bytes form).
"""
assert success_message
- _console_interaction(test, success_message, failure_message, None, vm=vm)
+ return _console_interaction(test, success_message, failure_message,
+ None, vm=vm)
-def exec_command(test, command):
+def exec_command(test, command, vm=None):
"""
Send a command to a console (appending CRLF characters), while logging
the content.
@@ -167,12 +199,16 @@ def exec_command(test, command):
:param test: a test containing a VM.
:type test: :class:`qemu_test.QemuSystemTest`
:param command: the command to send
+ :param vm: VM to use
:type command: str
+
+ :return: The collected output (in bytes form).
"""
- _console_interaction(test, None, None, command + '\r')
+ return _console_interaction(test, None, None, command + '\r', vm=vm)
def exec_command_and_wait_for_pattern(test, command,
- success_message, failure_message=None):
+ success_message, failure_message=None,
+ vm=None):
"""
Send a command to a console (appending CRLF characters), then wait
for success_message to appear on the console, while logging the.
@@ -184,9 +220,14 @@ def exec_command_and_wait_for_pattern(test, command,
:param command: the command to send
:param success_message: if this message appears, test succeeds
:param failure_message: if this message appears, test fails
+ :param vm: VM to use
+
+ :return: The collected output (in bytes form).
"""
assert success_message
- _console_interaction(test, success_message, failure_message, command + '\r')
+
+ return _console_interaction(test, success_message, failure_message,
+ command + '\r', vm=vm)
def get_qemu_img(test):
test.log.debug('Looking for and selecting a qemu-img binary')
diff --git a/tests/functional/qemu_test/decorators.py b/tests/functional/qemu_test/decorators.py
index 50d29de..b239295 100644
--- a/tests/functional/qemu_test/decorators.py
+++ b/tests/functional/qemu_test/decorators.py
@@ -5,11 +5,30 @@
import importlib
import os
import platform
+import resource
from unittest import skipIf, skipUnless
from .cmd import which
'''
+Decorator to skip execution of a test if the provided
+environment variables are not set.
+Example:
+
+ @skipIfMissingEnv("QEMU_ENV_VAR0", "QEMU_ENV_VAR1")
+'''
+def skipIfMissingEnv(*vars_):
+ missing_vars = []
+ for var in vars_:
+ if os.getenv(var) == None:
+ missing_vars.append(var)
+
+ has_vars = True if len(missing_vars) == 0 else False
+
+ return skipUnless(has_vars, f"Missing env var(s): {', '.join(missing_vars)}")
+
+'''
+
Decorator to skip execution of a test if the list
of command binaries is not available in $PATH.
Example:
@@ -131,3 +150,20 @@ def skipIfMissingImports(*args):
return skipUnless(has_imports, 'required import(s) "%s" not installed' %
", ".join(args))
+
+'''
+Decorator to skip execution of a test if the system's
+locked memory limit is below the required threshold.
+Takes required locked memory threshold in kB.
+Example:
+
+ @skipLockedMemoryTest(2_097_152)
+'''
+def skipLockedMemoryTest(locked_memory):
+ # get memlock hard limit in bytes
+ _, ulimit_memory = resource.getrlimit(resource.RLIMIT_MEMLOCK)
+
+ return skipUnless(
+ ulimit_memory == resource.RLIM_INFINITY or ulimit_memory >= locked_memory * 1024,
+ f'Test required {locked_memory} kB of available locked memory',
+ )
diff --git a/tests/functional/qemu_test/gdb.py b/tests/functional/qemu_test/gdb.py
new file mode 100644
index 0000000..558d476
--- /dev/null
+++ b/tests/functional/qemu_test/gdb.py
@@ -0,0 +1,86 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# A simple interface module built around pygdbmi for handling GDB commands.
+#
+# Copyright (c) 2025 Linaro Limited
+#
+# Author:
+# Gustavo Romero <gustavo.romero@linaro.org>
+#
+
+import re
+
+
+class GDB:
+ """Provides methods to run and capture GDB command output."""
+
+
+ def __init__(self, gdb_path, echo=True, suffix='# ', prompt="$ "):
+ from pygdbmi.gdbcontroller import GdbController
+ from pygdbmi.constants import GdbTimeoutError
+ type(self).TimeoutError = GdbTimeoutError
+
+ gdb_cmd = [gdb_path, "-q", "--interpreter=mi2"]
+ self.gdbmi = GdbController(gdb_cmd)
+ self.echo = echo
+ self.suffix = suffix
+ self.prompt = prompt
+ self.response = None
+ self.cmd_output = None
+
+
+ def get_payload(self, response, kind):
+ output = []
+ for o in response:
+ # Unpack payloads of the same type.
+ _type, _, payload, *_ = o.values()
+ if _type == kind:
+ output += [payload]
+
+ # Some output lines do not end with \n but begin with it,
+ # so remove the leading \n and merge them with the next line
+ # that ends with \n.
+ lines = [line.lstrip('\n') for line in output]
+ lines = "".join(lines)
+ lines = lines.splitlines(keepends=True)
+
+ return lines
+
+
+ def cli(self, cmd, timeout=32.0):
+ self.response = self.gdbmi.write(cmd, timeout_sec=timeout)
+ self.cmd_output = self.get_payload(self.response, kind="console")
+ if self.echo:
+ print(self.suffix + self.prompt + cmd)
+
+ if len(self.cmd_output) > 0:
+ cmd_output = self.suffix.join(self.cmd_output)
+ print(self.suffix + cmd_output, end="")
+
+ return self
+
+
+ def get_addr(self):
+ address_pattern = r"0x[0-9A-Fa-f]+"
+ cmd_output = "".join(self.cmd_output) # Concat output lines.
+
+ match = re.search(address_pattern, cmd_output)
+
+ return int(match[0], 16) if match else None
+
+
+ def get_log(self):
+ r = self.get_payload(self.response, kind="log")
+ r = "".join(r)
+
+ return r
+
+
+ def get_console(self):
+ r = "".join(self.cmd_output)
+
+ return r
+
+
+ def exit(self):
+ self.gdbmi.exit()
diff --git a/tests/functional/qemu_test/ports.py b/tests/functional/qemu_test/ports.py
index 631b77a..81174a6 100644
--- a/tests/functional/qemu_test/ports.py
+++ b/tests/functional/qemu_test/ports.py
@@ -23,8 +23,9 @@ class Ports():
PORTS_END = PORTS_START + PORTS_RANGE_SIZE
def __enter__(self):
- lock_file = os.path.join(BUILD_DIR, "tests", "functional", "port_lock")
- self.lock_fh = os.open(lock_file, os.O_CREAT)
+ lock_file = os.path.join(BUILD_DIR, "tests", "functional",
+ f".port_lock.{self.PORTS_START}")
+ self.lock_fh = os.open(lock_file, os.O_CREAT, mode=0o666)
fcntl.flock(self.lock_fh, fcntl.LOCK_EX)
return self
diff --git a/tests/functional/qemu_test/testcase.py b/tests/functional/qemu_test/testcase.py
index 50c401b..2c0abde 100644
--- a/tests/functional/qemu_test/testcase.py
+++ b/tests/functional/qemu_test/testcase.py
@@ -19,11 +19,12 @@ import shutil
from subprocess import run
import sys
import tempfile
+import warnings
import unittest
import uuid
from qemu.machine import QEMUMachine
-from qemu.utils import kvm_available, tcg_available
+from qemu.utils import hvf_available, kvm_available, tcg_available
from .archive import archive_extract
from .asset import Asset
@@ -204,6 +205,10 @@ class QemuBaseTest(unittest.TestCase):
self.outputdir = self.build_file('tests', 'functional',
self.arch, self.id())
self.workdir = os.path.join(self.outputdir, 'scratch')
+ if os.path.exists(self.workdir):
+ # Purge as safety net in case of unclean termination of
+ # previous test, or use of QEMU_TEST_KEEP_SCRATCH
+ shutil.rmtree(self.workdir)
os.makedirs(self.workdir, exist_ok=True)
self.log_filename = self.log_file('base.log')
@@ -232,8 +237,13 @@ class QemuBaseTest(unittest.TestCase):
self.socketdir = None
self.machinelog.removeHandler(self._log_fh)
self.log.removeHandler(self._log_fh)
+ self._log_fh.close()
+ @staticmethod
def main():
+ warnings.simplefilter("default")
+ os.environ["PYTHONWARNINGS"] = "default"
+
path = os.path.basename(sys.argv[0])[:-3]
cache = os.environ.get("QEMU_TEST_PRECACHE", None)
@@ -244,14 +254,15 @@ class QemuBaseTest(unittest.TestCase):
tr = pycotap.TAPTestRunner(message_log = pycotap.LogMode.LogToError,
test_output_log = pycotap.LogMode.LogToError)
res = unittest.main(module = None, testRunner = tr, exit = False,
- argv=["__dummy__", path])
+ argv=[sys.argv[0], path] + sys.argv[1:])
+ failed = {}
for (test, message) in res.result.errors + res.result.failures:
-
- if hasattr(test, "log_filename"):
+ if hasattr(test, "log_filename") and not test.id() in failed:
print('More information on ' + test.id() + ' could be found here:'
'\n %s' % test.log_filename, file=sys.stderr)
if hasattr(test, 'console_log_name'):
print(' %s' % test.console_log_name, file=sys.stderr)
+ failed[test.id()] = True
sys.exit(not res.result.wasSuccessful())
@@ -317,7 +328,9 @@ class QemuSystemTest(QemuBaseTest):
:type accelerator: str
"""
checker = {'tcg': tcg_available,
- 'kvm': kvm_available}.get(accelerator)
+ 'kvm': kvm_available,
+ 'hvf': hvf_available,
+ }.get(accelerator)
if checker is None:
self.skipTest("Don't know how to check for the presence "
"of accelerator %s" % accelerator)
@@ -395,6 +408,10 @@ class QemuSystemTest(QemuBaseTest):
def tearDown(self):
for vm in self._vms.values():
- vm.shutdown()
+ try:
+ vm.shutdown()
+ except Exception as ex:
+ self.log.error("Failed to teardown VM: %s" % ex)
logging.getLogger('console').removeHandler(self._console_log_fh)
+ self._console_log_fh.close()
super().tearDown()