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__.py6
-rw-r--r--tests/functional/qemu_test/asset.py58
-rw-r--r--tests/functional/qemu_test/config.py12
-rw-r--r--tests/functional/qemu_test/decorators.py92
-rw-r--r--tests/functional/qemu_test/linuxkernel.py26
-rw-r--r--tests/functional/qemu_test/ports.py55
-rw-r--r--tests/functional/qemu_test/testcase.py32
-rw-r--r--tests/functional/qemu_test/tuxruntest.py34
-rw-r--r--tests/functional/qemu_test/uncompress.py24
9 files changed, 256 insertions, 83 deletions
diff --git a/tests/functional/qemu_test/__init__.py b/tests/functional/qemu_test/__init__.py
index da18302..6e666a0 100644
--- a/tests/functional/qemu_test/__init__.py
+++ b/tests/functional/qemu_test/__init__.py
@@ -7,14 +7,14 @@
from .asset import Asset
-from .config import BUILD_DIR
+from .config import BUILD_DIR, dso_suffix
from .cmd import is_readable_executable_file, \
interrupt_interactive_console_until_pattern, wait_for_console_pattern, \
exec_command, exec_command_and_wait_for_pattern, get_qemu_img, which
from .testcase import QemuBaseTest, QemuUserTest, QemuSystemTest
from .linuxkernel import LinuxKernelTest
from .decorators import skipIfMissingCommands, skipIfNotMachine, \
- skipFlakyTest, skipUntrustedTest, skipBigDataTest, \
- skipIfMissingImports
+ skipFlakyTest, skipUntrustedTest, skipBigDataTest, skipSlowTest, \
+ skipIfMissingImports, skipIfOperatingSystem, skipLockedMemoryTest
from .archive import archive_extract
from .uncompress import uncompress
diff --git a/tests/functional/qemu_test/asset.py b/tests/functional/qemu_test/asset.py
index f073069..704b84d 100644
--- a/tests/functional/qemu_test/asset.py
+++ b/tests/functional/qemu_test/asset.py
@@ -17,6 +17,14 @@ from pathlib import Path
from shutil import copyfileobj
from urllib.error import HTTPError
+class AssetError(Exception):
+ def __init__(self, asset, msg, transient=False):
+ self.url = asset.url
+ self.msg = msg
+ self.transient = transient
+
+ def __str__(self):
+ return "%s: %s" % (self.url, self.msg)
# Instances of this class must be declared as class level variables
# starting with a name "ASSET_". This enables the pre-caching logic
@@ -51,7 +59,7 @@ class Asset:
elif len(self.hash) == 128:
hl = hashlib.sha512()
else:
- raise Exception("unknown hash type")
+ raise AssetError(self, "unknown hash type")
# Calculate the hash of the file:
with open(cache_file, 'rb') as file:
@@ -111,7 +119,8 @@ class Asset:
return str(self.cache_file)
if not self.fetchable():
- raise Exception("Asset cache is invalid and downloads disabled")
+ raise AssetError(self,
+ "Asset cache is invalid and downloads disabled")
self.log.info("Downloading %s to %s...", self.url, self.cache_file)
tmp_cache_file = self.cache_file.with_suffix(".download")
@@ -121,6 +130,20 @@ class Asset:
with tmp_cache_file.open("xb") as dst:
with urllib.request.urlopen(self.url) as resp:
copyfileobj(resp, dst)
+ length_hdr = resp.getheader("Content-Length")
+
+ # Verify downloaded file size against length metadata, if
+ # available.
+ if length_hdr is not None:
+ length = int(length_hdr)
+ fsize = tmp_cache_file.stat().st_size
+ if fsize != length:
+ self.log.error("Unable to download %s: "
+ "connection closed before "
+ "transfer complete (%d/%d)",
+ self.url, fsize, length)
+ tmp_cache_file.unlink()
+ continue
break
except FileExistsError:
self.log.debug("%s already exists, "
@@ -133,10 +156,23 @@ class Asset:
tmp_cache_file)
tmp_cache_file.unlink()
continue
+ except HTTPError as e:
+ tmp_cache_file.unlink()
+ self.log.error("Unable to download %s: HTTP error %d",
+ self.url, e.code)
+ # Treat 404 as fatal, since it is highly likely to
+ # indicate a broken test rather than a transient
+ # server or networking problem
+ if e.code == 404:
+ raise AssetError(self, "Unable to download: "
+ "HTTP error %d" % e.code)
+ continue
except Exception as e:
- self.log.error("Unable to download %s: %s", self.url, e)
tmp_cache_file.unlink()
- raise
+ raise AssetError(self, "Unable to download: " % e)
+
+ if not os.path.exists(tmp_cache_file):
+ raise AssetError(self, "Download retries exceeded", transient=True)
try:
# Set these just for informational purposes
@@ -150,8 +186,7 @@ class Asset:
if not self._check(tmp_cache_file):
tmp_cache_file.unlink()
- raise Exception("Hash of %s does not match %s" %
- (self.url, self.hash))
+ raise AssetError(self, "Hash does not match %s" % self.hash)
tmp_cache_file.replace(self.cache_file)
# Remove write perms to stop tests accidentally modifying them
os.chmod(self.cache_file, stat.S_IRUSR | stat.S_IRGRP)
@@ -173,15 +208,10 @@ class Asset:
log.info("Attempting to cache '%s'" % asset)
try:
asset.fetch()
- except HTTPError as e:
- # Treat 404 as fatal, since it is highly likely to
- # indicate a broken test rather than a transient
- # server or networking problem
- if e.code == 404:
+ except AssetError as e:
+ if not e.transient:
raise
-
- log.debug(f"HTTP error {e.code} from {asset.url} " +
- "skipping asset precache")
+ log.error("%s: skipping asset precache" % e)
log.removeHandler(handler)
diff --git a/tests/functional/qemu_test/config.py b/tests/functional/qemu_test/config.py
index edd75b7..6d4c9c3 100644
--- a/tests/functional/qemu_test/config.py
+++ b/tests/functional/qemu_test/config.py
@@ -13,6 +13,7 @@
import os
from pathlib import Path
+import platform
def _source_dir():
@@ -34,3 +35,14 @@ def _build_dir():
raise Exception("Cannot identify build dir, set QEMU_BUILD_ROOT")
BUILD_DIR = _build_dir()
+
+def dso_suffix():
+ '''Return the dynamic libraries suffix for the current platform'''
+
+ if platform.system() == "Darwin":
+ return "dylib"
+
+ if platform.system() == "Windows":
+ return "dll"
+
+ return "so"
diff --git a/tests/functional/qemu_test/decorators.py b/tests/functional/qemu_test/decorators.py
index df088bc..c0d1567 100644
--- a/tests/functional/qemu_test/decorators.py
+++ b/tests/functional/qemu_test/decorators.py
@@ -2,9 +2,11 @@
#
# Decorators useful in functional tests
+import importlib
import os
import platform
-from unittest import skipUnless
+import resource
+from unittest import skipIf, skipUnless
from .cmd import which
@@ -16,15 +18,27 @@ Example:
@skipIfMissingCommands("mkisofs", "losetup")
'''
def skipIfMissingCommands(*args):
- def has_cmds(cmdlist):
- for cmd in cmdlist:
- if not which(cmd):
- return False
- return True
-
- return skipUnless(lambda: has_cmds(args),
- 'required command(s) "%s" not installed' %
- ", ".join(args))
+ has_cmds = True
+ for cmd in args:
+ if not which(cmd):
+ has_cmds = False
+ break
+
+ return skipUnless(has_cmds, 'required command(s) "%s" not installed' %
+ ", ".join(args))
+
+'''
+Decorator to skip execution of a test if the current
+host operating system does match one of the prohibited
+ones.
+Example
+
+ @skipIfOperatingSystem("Linux", "Darwin")
+'''
+def skipIfOperatingSystem(*args):
+ return skipIf(platform.system() in args,
+ 'running on an OS (%s) that is not able to run this test' %
+ ", ".join(args))
'''
Decorator to skip execution of a test if the current
@@ -35,9 +49,9 @@ Example
@skipIfNotMachine("x86_64", "aarch64")
'''
def skipIfNotMachine(*args):
- return skipUnless(lambda: platform.machine() in args,
- 'not running on one of the required machine(s) "%s"' %
- ", ".join(args))
+ return skipUnless(platform.machine() in args,
+ 'not running on one of the required machine(s) "%s"' %
+ ", ".join(args))
'''
Decorator to skip execution of flaky tests, unless
@@ -87,6 +101,20 @@ def skipBigDataTest():
'Test requires large host storage space')
'''
+Decorator to skip execution of tests which have a really long
+runtime (and might e.g. time out if QEMU has been compiled with
+debugging enabled) unless the $QEMU_TEST_ALLOW_SLOW
+environment variable is set
+
+Example:
+
+ @skipSlowTest()
+'''
+def skipSlowTest():
+ return skipUnless(os.getenv('QEMU_TEST_ALLOW_SLOW'),
+ 'Test has a very long runtime and might time out')
+
+'''
Decorator to skip execution of a test if the list
of python imports is not available.
Example:
@@ -94,14 +122,30 @@ Example:
@skipIfMissingImports("numpy", "cv2")
'''
def skipIfMissingImports(*args):
- def has_imports(importlist):
- for impname in importlist:
- try:
- import impname
- except ImportError:
- return False
- return True
-
- return skipUnless(lambda: has_imports(args),
- 'required import(s) "%s" not installed' %
- ", ".join(args))
+ has_imports = True
+ for impname in args:
+ try:
+ importlib.import_module(impname)
+ except ImportError:
+ has_imports = False
+ break
+
+ 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/linuxkernel.py b/tests/functional/qemu_test/linuxkernel.py
index 2c95981..2aca0ee 100644
--- a/tests/functional/qemu_test/linuxkernel.py
+++ b/tests/functional/qemu_test/linuxkernel.py
@@ -3,8 +3,12 @@
# This work is licensed under the terms of the GNU GPL, version 2 or
# later. See the COPYING file in the top-level directory.
+import hashlib
+import urllib.request
+
+from .cmd import wait_for_console_pattern, exec_command_and_wait_for_pattern
from .testcase import QemuSystemTest
-from .cmd import wait_for_console_pattern
+from .utils import get_usernet_hostfwd_port
class LinuxKernelTest(QemuSystemTest):
@@ -26,3 +30,23 @@ class LinuxKernelTest(QemuSystemTest):
self.vm.launch()
if wait_for:
self.wait_for_console_pattern(wait_for)
+
+ def check_http_download(self, filename, hashsum, guestport=8080,
+ pythoncmd='python3 -m http.server'):
+ exec_command_and_wait_for_pattern(self,
+ f'{pythoncmd} {guestport} & sleep 1',
+ f'Serving HTTP on 0.0.0.0 port {guestport}')
+ hl = hashlib.sha256()
+ hostport = get_usernet_hostfwd_port(self.vm)
+ url = f'http://localhost:{hostport}{filename}'
+ self.log.info(f'Downloading {url} ...')
+ with urllib.request.urlopen(url) as response:
+ while True:
+ chunk = response.read(1 << 20)
+ if not chunk:
+ break
+ hl.update(chunk)
+
+ digest = hl.hexdigest()
+ self.log.info(f'sha256sum of download is {digest}.')
+ self.assertEqual(digest, hashsum)
diff --git a/tests/functional/qemu_test/ports.py b/tests/functional/qemu_test/ports.py
new file mode 100644
index 0000000..631b77a
--- /dev/null
+++ b/tests/functional/qemu_test/ports.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+#
+# Simple functional tests for VNC functionality
+#
+# Copyright 2018, 2024 Red Hat, Inc.
+#
+# This work is licensed under the terms of the GNU GPL, version 2 or
+# later. See the COPYING file in the top-level directory.
+
+import fcntl
+import os
+import socket
+
+from .config import BUILD_DIR
+from typing import List
+
+
+class Ports():
+
+ PORTS_ADDR = '127.0.0.1'
+ PORTS_RANGE_SIZE = 1024
+ PORTS_START = 49152 + ((os.getpid() * PORTS_RANGE_SIZE) % 16384)
+ 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)
+ fcntl.flock(self.lock_fh, fcntl.LOCK_EX)
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ fcntl.flock(self.lock_fh, fcntl.LOCK_UN)
+ os.close(self.lock_fh)
+
+ def check_bind(self, port: int) -> bool:
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
+ try:
+ sock.bind((self.PORTS_ADDR, port))
+ except OSError:
+ return False
+
+ return True
+
+ def find_free_ports(self, count: int) -> List[int]:
+ result = []
+ for port in range(self.PORTS_START, self.PORTS_END):
+ if self.check_bind(port):
+ result.append(port)
+ if len(result) >= count:
+ break
+ assert len(result) == count
+ return result
+
+ def find_free_port(self) -> int:
+ return self.find_free_ports(1)[0]
diff --git a/tests/functional/qemu_test/testcase.py b/tests/functional/qemu_test/testcase.py
index 869f394..2082c6f 100644
--- a/tests/functional/qemu_test/testcase.py
+++ b/tests/functional/qemu_test/testcase.py
@@ -23,23 +23,16 @@ 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
-from .config import BUILD_DIR
+from .config import BUILD_DIR, dso_suffix
from .uncompress import uncompress
class QemuBaseTest(unittest.TestCase):
- qemu_bin = os.getenv('QEMU_TEST_QEMU_BINARY')
- arch = None
-
- workdir = None
- log = None
- logdir = None
-
'''
@params compressed: filename, Asset, or file-like object to uncompress
@params format: optional compression format (gzip, lzma)
@@ -184,6 +177,16 @@ class QemuBaseTest(unittest.TestCase):
def log_file(self, *args):
return str(Path(self.outputdir, *args))
+ '''
+ @params plugin name
+
+ Return the full path to the plugin taking into account any host OS
+ specific suffixes.
+ '''
+ def plugin_file(self, plugin_name):
+ sfx = dso_suffix()
+ return os.path.join('tests', 'tcg', 'plugins', f'{plugin_name}.{sfx}')
+
def assets_available(self):
for name, asset in vars(self.__class__).items():
if name.startswith("ASSET_") and type(asset) == Asset:
@@ -192,7 +195,8 @@ class QemuBaseTest(unittest.TestCase):
return False
return True
- def setUp(self, bin_prefix):
+ def setUp(self):
+ self.qemu_bin = os.getenv('QEMU_TEST_QEMU_BINARY')
self.assertIsNotNone(self.qemu_bin, 'QEMU_TEST_QEMU_BINARY must be set')
self.arch = self.qemu_bin.split('-')[-1]
self.socketdir = None
@@ -254,7 +258,7 @@ class QemuBaseTest(unittest.TestCase):
class QemuUserTest(QemuBaseTest):
def setUp(self):
- super().setUp('qemu-')
+ super().setUp()
self._ldpath = []
def add_ldpath(self, ldpath):
@@ -277,7 +281,7 @@ class QemuSystemTest(QemuBaseTest):
def setUp(self):
self._vms = {}
- super().setUp('qemu-system-')
+ super().setUp()
console_log = logging.getLogger('console')
console_log.setLevel(logging.DEBUG)
@@ -313,7 +317,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)
diff --git a/tests/functional/qemu_test/tuxruntest.py b/tests/functional/qemu_test/tuxruntest.py
index 7227a83..6c442ff 100644
--- a/tests/functional/qemu_test/tuxruntest.py
+++ b/tests/functional/qemu_test/tuxruntest.py
@@ -10,8 +10,6 @@
# SPDX-License-Identifier: GPL-2.0-or-later
import os
-import stat
-from subprocess import check_call, DEVNULL
from qemu_test import QemuSystemTest
from qemu_test import exec_command_and_wait_for_pattern
@@ -24,17 +22,6 @@ class TuxRunBaselineTest(QemuSystemTest):
# Tests are ~10-40s, allow for --debug/--enable-gcov overhead
timeout = 100
- def get_tag(self, tagname, default=None):
- """
- Get the metadata tag or return the default.
- """
- utag = self._get_unique_tag_val(tagname)
- print(f"{tagname}/{default} -> {utag}")
- if utag:
- return utag
-
- return default
-
def setUp(self):
super().setUp()
@@ -73,17 +60,7 @@ class TuxRunBaselineTest(QemuSystemTest):
Fetch the TuxBoot assets.
"""
kernel_image = kernel_asset.fetch()
- disk_image_zst = rootfs_asset.fetch()
-
- disk_image = self.scratch_file("rootfs.ext4")
-
- check_call(['zstd', "-f", "-d", disk_image_zst,
- "-o", disk_image],
- stdout=DEVNULL, stderr=DEVNULL)
- # zstd copies source archive permissions for the output
- # file, so must make this writable for QEMU
- os.chmod(disk_image, stat.S_IRUSR | stat.S_IWUSR)
-
+ disk_image = self.uncompress(rootfs_asset)
dtb = dtb_asset.fetch() if dtb_asset is not None else None
return (kernel_image, disk_image, dtb)
@@ -98,12 +75,12 @@ class TuxRunBaselineTest(QemuSystemTest):
blockdev = "driver=raw,file.driver=file," \
+ f"file.filename={disk},node-name=hd0"
- kcmd_line = self.KERNEL_COMMON_COMMAND_LINE
- kcmd_line += f" root=/dev/{self.root}"
- kcmd_line += f" console={self.console}"
+ self.kcmd_line = self.KERNEL_COMMON_COMMAND_LINE
+ self.kcmd_line += f" root=/dev/{self.root}"
+ self.kcmd_line += f" console={self.console}"
self.vm.add_args('-kernel', kernel,
- '-append', kcmd_line,
+ '-append', self.kcmd_line,
'-blockdev', blockdev)
# Sometimes we need extra devices attached
@@ -124,6 +101,7 @@ class TuxRunBaselineTest(QemuSystemTest):
wait to exit cleanly.
"""
ps1='root@tuxtest:~#'
+ self.wait_for_console_pattern(self.kcmd_line)
self.wait_for_console_pattern('tuxtest login:')
exec_command_and_wait_for_pattern(self, 'root', ps1)
exec_command_and_wait_for_pattern(self, 'cat /proc/interrupts', ps1)
diff --git a/tests/functional/qemu_test/uncompress.py b/tests/functional/qemu_test/uncompress.py
index 6d02ded..b7ef8f7 100644
--- a/tests/functional/qemu_test/uncompress.py
+++ b/tests/functional/qemu_test/uncompress.py
@@ -10,8 +10,10 @@
import gzip
import lzma
import os
+import stat
import shutil
from urllib.parse import urlparse
+from subprocess import run, CalledProcessError
from .asset import Asset
@@ -38,6 +40,24 @@ def lzma_uncompress(xz_path, output_path):
os.remove(output_path)
raise
+
+def zstd_uncompress(zstd_path, output_path):
+ if os.path.exists(output_path):
+ return
+
+ try:
+ run(['zstd', "-f", "-d", zstd_path,
+ "-o", output_path], capture_output=True, check=True)
+ except CalledProcessError as e:
+ os.remove(output_path)
+ raise Exception(
+ f"Unable to decompress zstd file {zstd_path} with {e}") from e
+
+ # zstd copies source archive permissions for the output
+ # file, so must make this writable for QEMU
+ os.chmod(output_path, stat.S_IRUSR | stat.S_IWUSR)
+
+
'''
@params compressed: filename, Asset, or file-like object to uncompress
@params uncompressed: filename to uncompress into
@@ -59,6 +79,8 @@ def uncompress(compressed, uncompressed, format=None):
lzma_uncompress(str(compressed), uncompressed)
elif format == "gz":
gzip_uncompress(str(compressed), uncompressed)
+ elif format == "zstd":
+ zstd_uncompress(str(compressed), uncompressed)
else:
raise Exception(f"Unknown compression format {format}")
@@ -79,5 +101,7 @@ def guess_uncompress_format(compressed):
return "xz"
elif ext == ".gz":
return "gz"
+ elif ext in [".zstd", ".zst"]:
+ return 'zstd'
else:
raise Exception(f"Unknown compression format for {compressed}")