aboutsummaryrefslogtreecommitdiffstats
path: root/tests/qemu-iotests/iotests.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/qemu-iotests/iotests.py')
-rw-r--r--tests/qemu-iotests/iotests.py1449
1 files changed, 1449 insertions, 0 deletions
diff --git a/tests/qemu-iotests/iotests.py b/tests/qemu-iotests/iotests.py
new file mode 100644
index 000000000..83bfedb90
--- /dev/null
+++ b/tests/qemu-iotests/iotests.py
@@ -0,0 +1,1449 @@
+# Common utilities and Python wrappers for qemu-iotests
+#
+# Copyright (C) 2012 IBM Corp.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+import atexit
+import bz2
+from collections import OrderedDict
+import faulthandler
+import json
+import logging
+import os
+import re
+import shutil
+import signal
+import struct
+import subprocess
+import sys
+import time
+from typing import (Any, Callable, Dict, Iterable, Iterator,
+ List, Optional, Sequence, TextIO, Tuple, Type, TypeVar)
+import unittest
+
+from contextlib import contextmanager
+
+from qemu.machine import qtest
+from qemu.qmp import QMPMessage
+
+# Use this logger for logging messages directly from the iotests module
+logger = logging.getLogger('qemu.iotests')
+logger.addHandler(logging.NullHandler())
+
+# Use this logger for messages that ought to be used for diff output.
+test_logger = logging.getLogger('qemu.iotests.diff_io')
+
+
+faulthandler.enable()
+
+# This will not work if arguments contain spaces but is necessary if we
+# want to support the override options that ./check supports.
+qemu_img_args = [os.environ.get('QEMU_IMG_PROG', 'qemu-img')]
+if os.environ.get('QEMU_IMG_OPTIONS'):
+ qemu_img_args += os.environ['QEMU_IMG_OPTIONS'].strip().split(' ')
+
+qemu_io_args = [os.environ.get('QEMU_IO_PROG', 'qemu-io')]
+if os.environ.get('QEMU_IO_OPTIONS'):
+ qemu_io_args += os.environ['QEMU_IO_OPTIONS'].strip().split(' ')
+
+qemu_io_args_no_fmt = [os.environ.get('QEMU_IO_PROG', 'qemu-io')]
+if os.environ.get('QEMU_IO_OPTIONS_NO_FMT'):
+ qemu_io_args_no_fmt += \
+ os.environ['QEMU_IO_OPTIONS_NO_FMT'].strip().split(' ')
+
+qemu_nbd_prog = os.environ.get('QEMU_NBD_PROG', 'qemu-nbd')
+qemu_nbd_args = [qemu_nbd_prog]
+if os.environ.get('QEMU_NBD_OPTIONS'):
+ qemu_nbd_args += os.environ['QEMU_NBD_OPTIONS'].strip().split(' ')
+
+qemu_prog = os.environ.get('QEMU_PROG', 'qemu')
+qemu_opts = os.environ.get('QEMU_OPTIONS', '').strip().split(' ')
+
+gdb_qemu_env = os.environ.get('GDB_OPTIONS')
+qemu_gdb = []
+if gdb_qemu_env:
+ qemu_gdb = ['gdbserver'] + gdb_qemu_env.strip().split(' ')
+
+qemu_print = os.environ.get('PRINT_QEMU', False)
+
+imgfmt = os.environ.get('IMGFMT', 'raw')
+imgproto = os.environ.get('IMGPROTO', 'file')
+output_dir = os.environ.get('OUTPUT_DIR', '.')
+
+try:
+ test_dir = os.environ['TEST_DIR']
+ sock_dir = os.environ['SOCK_DIR']
+ cachemode = os.environ['CACHEMODE']
+ aiomode = os.environ['AIOMODE']
+ qemu_default_machine = os.environ['QEMU_DEFAULT_MACHINE']
+except KeyError:
+ # We are using these variables as proxies to indicate that we're
+ # not being run via "check". There may be other things set up by
+ # "check" that individual test cases rely on.
+ sys.stderr.write('Please run this test via the "check" script\n')
+ sys.exit(os.EX_USAGE)
+
+qemu_valgrind = []
+if os.environ.get('VALGRIND_QEMU') == "y" and \
+ os.environ.get('NO_VALGRIND') != "y":
+ valgrind_logfile = "--log-file=" + test_dir
+ # %p allows to put the valgrind process PID, since
+ # we don't know it a priori (subprocess.Popen is
+ # not yet invoked)
+ valgrind_logfile += "/%p.valgrind"
+
+ qemu_valgrind = ['valgrind', valgrind_logfile, '--error-exitcode=99']
+
+luks_default_secret_object = 'secret,id=keysec0,data=' + \
+ os.environ.get('IMGKEYSECRET', '')
+luks_default_key_secret_opt = 'key-secret=keysec0'
+
+sample_img_dir = os.environ['SAMPLE_IMG_DIR']
+
+
+@contextmanager
+def change_log_level(
+ logger_name: str, level: int = logging.CRITICAL) -> Iterator[None]:
+ """
+ Utility function for temporarily changing the log level of a logger.
+
+ This can be used to silence errors that are expected or uninteresting.
+ """
+ _logger = logging.getLogger(logger_name)
+ current_level = _logger.level
+ _logger.setLevel(level)
+
+ try:
+ yield
+ finally:
+ _logger.setLevel(current_level)
+
+
+def unarchive_sample_image(sample, fname):
+ sample_fname = os.path.join(sample_img_dir, sample + '.bz2')
+ with bz2.open(sample_fname) as f_in, open(fname, 'wb') as f_out:
+ shutil.copyfileobj(f_in, f_out)
+
+
+def qemu_tool_pipe_and_status(tool: str, args: Sequence[str],
+ connect_stderr: bool = True) -> Tuple[str, int]:
+ """
+ Run a tool and return both its output and its exit code
+ """
+ stderr = subprocess.STDOUT if connect_stderr else None
+ with subprocess.Popen(args, stdout=subprocess.PIPE,
+ stderr=stderr, universal_newlines=True) as subp:
+ output = subp.communicate()[0]
+ if subp.returncode < 0:
+ cmd = ' '.join(args)
+ sys.stderr.write(f'{tool} received signal \
+ {-subp.returncode}: {cmd}\n')
+ return (output, subp.returncode)
+
+def qemu_img_pipe_and_status(*args: str) -> Tuple[str, int]:
+ """
+ Run qemu-img and return both its output and its exit code
+ """
+ full_args = qemu_img_args + list(args)
+ return qemu_tool_pipe_and_status('qemu-img', full_args)
+
+def qemu_img(*args: str) -> int:
+ '''Run qemu-img and return the exit code'''
+ return qemu_img_pipe_and_status(*args)[1]
+
+def ordered_qmp(qmsg, conv_keys=True):
+ # Dictionaries are not ordered prior to 3.6, therefore:
+ if isinstance(qmsg, list):
+ return [ordered_qmp(atom) for atom in qmsg]
+ if isinstance(qmsg, dict):
+ od = OrderedDict()
+ for k, v in sorted(qmsg.items()):
+ if conv_keys:
+ k = k.replace('_', '-')
+ od[k] = ordered_qmp(v, conv_keys=False)
+ return od
+ return qmsg
+
+def qemu_img_create(*args):
+ args = list(args)
+
+ # default luks support
+ if '-f' in args and args[args.index('-f') + 1] == 'luks':
+ if '-o' in args:
+ i = args.index('-o')
+ if 'key-secret' not in args[i + 1]:
+ args[i + 1].append(luks_default_key_secret_opt)
+ args.insert(i + 2, '--object')
+ args.insert(i + 3, luks_default_secret_object)
+ else:
+ args = ['-o', luks_default_key_secret_opt,
+ '--object', luks_default_secret_object] + args
+
+ args.insert(0, 'create')
+
+ return qemu_img(*args)
+
+def qemu_img_measure(*args):
+ return json.loads(qemu_img_pipe("measure", "--output", "json", *args))
+
+def qemu_img_check(*args):
+ return json.loads(qemu_img_pipe("check", "--output", "json", *args))
+
+def qemu_img_verbose(*args):
+ '''Run qemu-img without suppressing its output and return the exit code'''
+ exitcode = subprocess.call(qemu_img_args + list(args))
+ if exitcode < 0:
+ sys.stderr.write('qemu-img received signal %i: %s\n'
+ % (-exitcode, ' '.join(qemu_img_args + list(args))))
+ return exitcode
+
+def qemu_img_pipe(*args: str) -> str:
+ '''Run qemu-img and return its output'''
+ return qemu_img_pipe_and_status(*args)[0]
+
+def qemu_img_log(*args):
+ result = qemu_img_pipe(*args)
+ log(result, filters=[filter_testfiles])
+ return result
+
+def img_info_log(filename, filter_path=None, imgopts=False, extra_args=()):
+ args = ['info']
+ if imgopts:
+ args.append('--image-opts')
+ else:
+ args += ['-f', imgfmt]
+ args += extra_args
+ args.append(filename)
+
+ output = qemu_img_pipe(*args)
+ if not filter_path:
+ filter_path = filename
+ log(filter_img_info(output, filter_path))
+
+def qemu_io(*args):
+ '''Run qemu-io and return the stdout data'''
+ args = qemu_io_args + list(args)
+ return qemu_tool_pipe_and_status('qemu-io', args)[0]
+
+def qemu_io_log(*args):
+ result = qemu_io(*args)
+ log(result, filters=[filter_testfiles, filter_qemu_io])
+ return result
+
+def qemu_io_silent(*args):
+ '''Run qemu-io and return the exit code, suppressing stdout'''
+ if '-f' in args or '--image-opts' in args:
+ default_args = qemu_io_args_no_fmt
+ else:
+ default_args = qemu_io_args
+
+ args = default_args + list(args)
+ result = subprocess.run(args, stdout=subprocess.DEVNULL, check=False)
+ if result.returncode < 0:
+ sys.stderr.write('qemu-io received signal %i: %s\n' %
+ (-result.returncode, ' '.join(args)))
+ return result.returncode
+
+def qemu_io_silent_check(*args):
+ '''Run qemu-io and return the true if subprocess returned 0'''
+ args = qemu_io_args + list(args)
+ result = subprocess.run(args, stdout=subprocess.DEVNULL,
+ stderr=subprocess.STDOUT, check=False)
+ return result.returncode == 0
+
+class QemuIoInteractive:
+ def __init__(self, *args):
+ self.args = qemu_io_args_no_fmt + list(args)
+ # We need to keep the Popen objext around, and not
+ # close it immediately. Therefore, disable the pylint check:
+ # pylint: disable=consider-using-with
+ self._p = subprocess.Popen(self.args, stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ universal_newlines=True)
+ out = self._p.stdout.read(9)
+ if out != 'qemu-io> ':
+ # Most probably qemu-io just failed to start.
+ # Let's collect the whole output and exit.
+ out += self._p.stdout.read()
+ self._p.wait(timeout=1)
+ raise ValueError(out)
+
+ def close(self):
+ self._p.communicate('q\n')
+
+ def _read_output(self):
+ pattern = 'qemu-io> '
+ n = len(pattern)
+ pos = 0
+ s = []
+ while pos != n:
+ c = self._p.stdout.read(1)
+ # check unexpected EOF
+ assert c != ''
+ s.append(c)
+ if c == pattern[pos]:
+ pos += 1
+ else:
+ pos = 0
+
+ return ''.join(s[:-n])
+
+ def cmd(self, cmd):
+ # quit command is in close(), '\n' is added automatically
+ assert '\n' not in cmd
+ cmd = cmd.strip()
+ assert cmd not in ('q', 'quit')
+ self._p.stdin.write(cmd + '\n')
+ self._p.stdin.flush()
+ return self._read_output()
+
+
+def qemu_nbd(*args):
+ '''Run qemu-nbd in daemon mode and return the parent's exit code'''
+ return subprocess.call(qemu_nbd_args + ['--fork'] + list(args))
+
+def qemu_nbd_early_pipe(*args: str) -> Tuple[int, str]:
+ '''Run qemu-nbd in daemon mode and return both the parent's exit code
+ and its output in case of an error'''
+ full_args = qemu_nbd_args + ['--fork'] + list(args)
+ output, returncode = qemu_tool_pipe_and_status('qemu-nbd', full_args,
+ connect_stderr=False)
+ return returncode, output if returncode else ''
+
+def qemu_nbd_list_log(*args: str) -> str:
+ '''Run qemu-nbd to list remote exports'''
+ full_args = [qemu_nbd_prog, '-L'] + list(args)
+ output, _ = qemu_tool_pipe_and_status('qemu-nbd', full_args)
+ log(output, filters=[filter_testfiles, filter_nbd_exports])
+ return output
+
+@contextmanager
+def qemu_nbd_popen(*args):
+ '''Context manager running qemu-nbd within the context'''
+ pid_file = file_path("qemu_nbd_popen-nbd-pid-file")
+
+ assert not os.path.exists(pid_file)
+
+ cmd = list(qemu_nbd_args)
+ cmd.extend(('--persistent', '--pid-file', pid_file))
+ cmd.extend(args)
+
+ log('Start NBD server')
+ with subprocess.Popen(cmd) as p:
+ try:
+ while not os.path.exists(pid_file):
+ if p.poll() is not None:
+ raise RuntimeError(
+ "qemu-nbd terminated with exit code {}: {}"
+ .format(p.returncode, ' '.join(cmd)))
+
+ time.sleep(0.01)
+ yield
+ finally:
+ if os.path.exists(pid_file):
+ os.remove(pid_file)
+ log('Kill NBD server')
+ p.kill()
+ p.wait()
+
+def compare_images(img1, img2, fmt1=imgfmt, fmt2=imgfmt):
+ '''Return True if two image files are identical'''
+ return qemu_img('compare', '-f', fmt1,
+ '-F', fmt2, img1, img2) == 0
+
+def create_image(name, size):
+ '''Create a fully-allocated raw image with sector markers'''
+ with open(name, 'wb') as file:
+ i = 0
+ while i < size:
+ sector = struct.pack('>l504xl', i // 512, i // 512)
+ file.write(sector)
+ i = i + 512
+
+def image_size(img):
+ '''Return image's virtual size'''
+ r = qemu_img_pipe('info', '--output=json', '-f', imgfmt, img)
+ return json.loads(r)['virtual-size']
+
+def is_str(val):
+ return isinstance(val, str)
+
+test_dir_re = re.compile(r"%s" % test_dir)
+def filter_test_dir(msg):
+ return test_dir_re.sub("TEST_DIR", msg)
+
+win32_re = re.compile(r"\r")
+def filter_win32(msg):
+ return win32_re.sub("", msg)
+
+qemu_io_re = re.compile(r"[0-9]* ops; [0-9\/:. sec]* "
+ r"\([0-9\/.inf]* [EPTGMKiBbytes]*\/sec "
+ r"and [0-9\/.inf]* ops\/sec\)")
+def filter_qemu_io(msg):
+ msg = filter_win32(msg)
+ return qemu_io_re.sub("X ops; XX:XX:XX.X "
+ "(XXX YYY/sec and XXX ops/sec)", msg)
+
+chown_re = re.compile(r"chown [0-9]+:[0-9]+")
+def filter_chown(msg):
+ return chown_re.sub("chown UID:GID", msg)
+
+def filter_qmp_event(event):
+ '''Filter a QMP event dict'''
+ event = dict(event)
+ if 'timestamp' in event:
+ event['timestamp']['seconds'] = 'SECS'
+ event['timestamp']['microseconds'] = 'USECS'
+ return event
+
+def filter_qmp(qmsg, filter_fn):
+ '''Given a string filter, filter a QMP object's values.
+ filter_fn takes a (key, value) pair.'''
+ # Iterate through either lists or dicts;
+ if isinstance(qmsg, list):
+ items = enumerate(qmsg)
+ else:
+ items = qmsg.items()
+
+ for k, v in items:
+ if isinstance(v, (dict, list)):
+ qmsg[k] = filter_qmp(v, filter_fn)
+ else:
+ qmsg[k] = filter_fn(k, v)
+ return qmsg
+
+def filter_testfiles(msg):
+ pref1 = os.path.join(test_dir, "%s-" % (os.getpid()))
+ pref2 = os.path.join(sock_dir, "%s-" % (os.getpid()))
+ return msg.replace(pref1, 'TEST_DIR/PID-').replace(pref2, 'SOCK_DIR/PID-')
+
+def filter_qmp_testfiles(qmsg):
+ def _filter(_key, value):
+ if is_str(value):
+ return filter_testfiles(value)
+ return value
+ return filter_qmp(qmsg, _filter)
+
+def filter_virtio_scsi(output: str) -> str:
+ return re.sub(r'(virtio-scsi)-(ccw|pci)', r'\1', output)
+
+def filter_qmp_virtio_scsi(qmsg):
+ def _filter(_key, value):
+ if is_str(value):
+ return filter_virtio_scsi(value)
+ return value
+ return filter_qmp(qmsg, _filter)
+
+def filter_generated_node_ids(msg):
+ return re.sub("#block[0-9]+", "NODE_NAME", msg)
+
+def filter_img_info(output, filename):
+ lines = []
+ for line in output.split('\n'):
+ if 'disk size' in line or 'actual-size' in line:
+ continue
+ line = line.replace(filename, 'TEST_IMG')
+ line = filter_testfiles(line)
+ line = line.replace(imgfmt, 'IMGFMT')
+ line = re.sub('iters: [0-9]+', 'iters: XXX', line)
+ line = re.sub('uuid: [-a-f0-9]+',
+ 'uuid: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX',
+ line)
+ line = re.sub('cid: [0-9]+', 'cid: XXXXXXXXXX', line)
+ lines.append(line)
+ return '\n'.join(lines)
+
+def filter_imgfmt(msg):
+ return msg.replace(imgfmt, 'IMGFMT')
+
+def filter_qmp_imgfmt(qmsg):
+ def _filter(_key, value):
+ if is_str(value):
+ return filter_imgfmt(value)
+ return value
+ return filter_qmp(qmsg, _filter)
+
+def filter_nbd_exports(output: str) -> str:
+ return re.sub(r'((min|opt|max) block): [0-9]+', r'\1: XXX', output)
+
+
+Msg = TypeVar('Msg', Dict[str, Any], List[Any], str)
+
+def log(msg: Msg,
+ filters: Iterable[Callable[[Msg], Msg]] = (),
+ indent: Optional[int] = None) -> None:
+ """
+ Logs either a string message or a JSON serializable message (like QMP).
+ If indent is provided, JSON serializable messages are pretty-printed.
+ """
+ for flt in filters:
+ msg = flt(msg)
+ if isinstance(msg, (dict, list)):
+ # Don't sort if it's already sorted
+ do_sort = not isinstance(msg, OrderedDict)
+ test_logger.info(json.dumps(msg, sort_keys=do_sort, indent=indent))
+ else:
+ test_logger.info(msg)
+
+class Timeout:
+ def __init__(self, seconds, errmsg="Timeout"):
+ self.seconds = seconds
+ self.errmsg = errmsg
+ def __enter__(self):
+ if qemu_gdb or qemu_valgrind:
+ return self
+ signal.signal(signal.SIGALRM, self.timeout)
+ signal.setitimer(signal.ITIMER_REAL, self.seconds)
+ return self
+ def __exit__(self, exc_type, value, traceback):
+ if qemu_gdb or qemu_valgrind:
+ return False
+ signal.setitimer(signal.ITIMER_REAL, 0)
+ return False
+ def timeout(self, signum, frame):
+ raise Exception(self.errmsg)
+
+def file_pattern(name):
+ return "{0}-{1}".format(os.getpid(), name)
+
+class FilePath:
+ """
+ Context manager generating multiple file names. The generated files are
+ removed when exiting the context.
+
+ Example usage:
+
+ with FilePath('a.img', 'b.img') as (img_a, img_b):
+ # Use img_a and img_b here...
+
+ # a.img and b.img are automatically removed here.
+
+ By default images are created in iotests.test_dir. To create sockets use
+ iotests.sock_dir:
+
+ with FilePath('a.sock', base_dir=iotests.sock_dir) as sock:
+
+ For convenience, calling with one argument yields a single file instead of
+ a tuple with one item.
+
+ """
+ def __init__(self, *names, base_dir=test_dir):
+ self.paths = [os.path.join(base_dir, file_pattern(name))
+ for name in names]
+
+ def __enter__(self):
+ if len(self.paths) == 1:
+ return self.paths[0]
+ else:
+ return self.paths
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ for path in self.paths:
+ try:
+ os.remove(path)
+ except OSError:
+ pass
+ return False
+
+
+def try_remove(img):
+ try:
+ os.remove(img)
+ except OSError:
+ pass
+
+def file_path_remover():
+ for path in reversed(file_path_remover.paths):
+ try_remove(path)
+
+
+def file_path(*names, base_dir=test_dir):
+ ''' Another way to get auto-generated filename that cleans itself up.
+
+ Use is as simple as:
+
+ img_a, img_b = file_path('a.img', 'b.img')
+ sock = file_path('socket')
+ '''
+
+ if not hasattr(file_path_remover, 'paths'):
+ file_path_remover.paths = []
+ atexit.register(file_path_remover)
+
+ paths = []
+ for name in names:
+ filename = file_pattern(name)
+ path = os.path.join(base_dir, filename)
+ file_path_remover.paths.append(path)
+ paths.append(path)
+
+ return paths[0] if len(paths) == 1 else paths
+
+def remote_filename(path):
+ if imgproto == 'file':
+ return path
+ elif imgproto == 'ssh':
+ return "ssh://%s@127.0.0.1:22%s" % (os.environ.get('USER'), path)
+ else:
+ raise Exception("Protocol %s not supported" % (imgproto))
+
+class VM(qtest.QEMUQtestMachine):
+ '''A QEMU VM'''
+
+ def __init__(self, path_suffix=''):
+ name = "qemu%s-%d" % (path_suffix, os.getpid())
+ timer = 15.0 if not (qemu_gdb or qemu_valgrind) else None
+ if qemu_gdb and qemu_valgrind:
+ sys.stderr.write('gdb and valgrind are mutually exclusive\n')
+ sys.exit(1)
+ wrapper = qemu_gdb if qemu_gdb else qemu_valgrind
+ super().__init__(qemu_prog, qemu_opts, wrapper=wrapper,
+ name=name,
+ base_temp_dir=test_dir,
+ sock_dir=sock_dir, qmp_timer=timer)
+ self._num_drives = 0
+
+ def _post_shutdown(self) -> None:
+ super()._post_shutdown()
+ if not qemu_valgrind or not self._popen:
+ return
+ valgrind_filename = f"{test_dir}/{self._popen.pid}.valgrind"
+ if self.exitcode() == 99:
+ with open(valgrind_filename, encoding='utf-8') as f:
+ print(f.read())
+ else:
+ os.remove(valgrind_filename)
+
+ def _pre_launch(self) -> None:
+ super()._pre_launch()
+ if qemu_print:
+ # set QEMU binary output to stdout
+ self._close_qemu_log_file()
+
+ def add_object(self, opts):
+ self._args.append('-object')
+ self._args.append(opts)
+ return self
+
+ def add_device(self, opts):
+ self._args.append('-device')
+ self._args.append(opts)
+ return self
+
+ def add_drive_raw(self, opts):
+ self._args.append('-drive')
+ self._args.append(opts)
+ return self
+
+ def add_drive(self, path, opts='', interface='virtio', img_format=imgfmt):
+ '''Add a virtio-blk drive to the VM'''
+ options = ['if=%s' % interface,
+ 'id=drive%d' % self._num_drives]
+
+ if path is not None:
+ options.append('file=%s' % path)
+ options.append('format=%s' % img_format)
+ options.append('cache=%s' % cachemode)
+ options.append('aio=%s' % aiomode)
+
+ if opts:
+ options.append(opts)
+
+ if img_format == 'luks' and 'key-secret' not in opts:
+ # default luks support
+ if luks_default_secret_object not in self._args:
+ self.add_object(luks_default_secret_object)
+
+ options.append(luks_default_key_secret_opt)
+
+ self._args.append('-drive')
+ self._args.append(','.join(options))
+ self._num_drives += 1
+ return self
+
+ def add_blockdev(self, opts):
+ self._args.append('-blockdev')
+ if isinstance(opts, str):
+ self._args.append(opts)
+ else:
+ self._args.append(','.join(opts))
+ return self
+
+ def add_incoming(self, addr):
+ self._args.append('-incoming')
+ self._args.append(addr)
+ return self
+
+ def hmp(self, command_line: str, use_log: bool = False) -> QMPMessage:
+ cmd = 'human-monitor-command'
+ kwargs: Dict[str, Any] = {'command-line': command_line}
+ if use_log:
+ return self.qmp_log(cmd, **kwargs)
+ else:
+ return self.qmp(cmd, **kwargs)
+
+ def pause_drive(self, drive: str, event: Optional[str] = None) -> None:
+ """Pause drive r/w operations"""
+ if not event:
+ self.pause_drive(drive, "read_aio")
+ self.pause_drive(drive, "write_aio")
+ return
+ self.hmp(f'qemu-io {drive} "break {event} bp_{drive}"')
+
+ def resume_drive(self, drive: str) -> None:
+ """Resume drive r/w operations"""
+ self.hmp(f'qemu-io {drive} "remove_break bp_{drive}"')
+
+ def hmp_qemu_io(self, drive: str, cmd: str,
+ use_log: bool = False, qdev: bool = False) -> QMPMessage:
+ """Write to a given drive using an HMP command"""
+ d = '-d ' if qdev else ''
+ return self.hmp(f'qemu-io {d}{drive} "{cmd}"', use_log=use_log)
+
+ def flatten_qmp_object(self, obj, output=None, basestr=''):
+ if output is None:
+ output = {}
+ if isinstance(obj, list):
+ for i, item in enumerate(obj):
+ self.flatten_qmp_object(item, output, basestr + str(i) + '.')
+ elif isinstance(obj, dict):
+ for key in obj:
+ self.flatten_qmp_object(obj[key], output, basestr + key + '.')
+ else:
+ output[basestr[:-1]] = obj # Strip trailing '.'
+ return output
+
+ def qmp_to_opts(self, obj):
+ obj = self.flatten_qmp_object(obj)
+ output_list = []
+ for key in obj:
+ output_list += [key + '=' + obj[key]]
+ return ','.join(output_list)
+
+ def get_qmp_events_filtered(self, wait=60.0):
+ result = []
+ for ev in self.get_qmp_events(wait=wait):
+ result.append(filter_qmp_event(ev))
+ return result
+
+ def qmp_log(self, cmd, filters=(), indent=None, **kwargs):
+ full_cmd = OrderedDict((
+ ("execute", cmd),
+ ("arguments", ordered_qmp(kwargs))
+ ))
+ log(full_cmd, filters, indent=indent)
+ result = self.qmp(cmd, **kwargs)
+ log(result, filters, indent=indent)
+ return result
+
+ # Returns None on success, and an error string on failure
+ def run_job(self, job, auto_finalize=True, auto_dismiss=False,
+ pre_finalize=None, cancel=False, wait=60.0):
+ """
+ run_job moves a job from creation through to dismissal.
+
+ :param job: String. ID of recently-launched job
+ :param auto_finalize: Bool. True if the job was launched with
+ auto_finalize. Defaults to True.
+ :param auto_dismiss: Bool. True if the job was launched with
+ auto_dismiss=True. Defaults to False.
+ :param pre_finalize: Callback. A callable that takes no arguments to be
+ invoked prior to issuing job-finalize, if any.
+ :param cancel: Bool. When true, cancels the job after the pre_finalize
+ callback.
+ :param wait: Float. Timeout value specifying how long to wait for any
+ event, in seconds. Defaults to 60.0.
+ """
+ match_device = {'data': {'device': job}}
+ match_id = {'data': {'id': job}}
+ events = [
+ ('BLOCK_JOB_COMPLETED', match_device),
+ ('BLOCK_JOB_CANCELLED', match_device),
+ ('BLOCK_JOB_ERROR', match_device),
+ ('BLOCK_JOB_READY', match_device),
+ ('BLOCK_JOB_PENDING', match_id),
+ ('JOB_STATUS_CHANGE', match_id)
+ ]
+ error = None
+ while True:
+ ev = filter_qmp_event(self.events_wait(events, timeout=wait))
+ if ev['event'] != 'JOB_STATUS_CHANGE':
+ log(ev)
+ continue
+ status = ev['data']['status']
+ if status == 'aborting':
+ result = self.qmp('query-jobs')
+ for j in result['return']:
+ if j['id'] == job:
+ error = j['error']
+ log('Job failed: %s' % (j['error']))
+ elif status == 'ready':
+ self.qmp_log('job-complete', id=job)
+ elif status == 'pending' and not auto_finalize:
+ if pre_finalize:
+ pre_finalize()
+ if cancel:
+ self.qmp_log('job-cancel', id=job)
+ else:
+ self.qmp_log('job-finalize', id=job)
+ elif status == 'concluded' and not auto_dismiss:
+ self.qmp_log('job-dismiss', id=job)
+ elif status == 'null':
+ return error
+
+ # Returns None on success, and an error string on failure
+ def blockdev_create(self, options, job_id='job0', filters=None):
+ if filters is None:
+ filters = [filter_qmp_testfiles]
+ result = self.qmp_log('blockdev-create', filters=filters,
+ job_id=job_id, options=options)
+
+ if 'return' in result:
+ assert result['return'] == {}
+ job_result = self.run_job(job_id)
+ else:
+ job_result = result['error']
+
+ log("")
+ return job_result
+
+ def enable_migration_events(self, name):
+ log('Enabling migration QMP events on %s...' % name)
+ log(self.qmp('migrate-set-capabilities', capabilities=[
+ {
+ 'capability': 'events',
+ 'state': True
+ }
+ ]))
+
+ def wait_migration(self, expect_runstate: Optional[str]) -> bool:
+ while True:
+ event = self.event_wait('MIGRATION')
+ # We use the default timeout, and with a timeout, event_wait()
+ # never returns None
+ assert event
+
+ log(event, filters=[filter_qmp_event])
+ if event['data']['status'] in ('completed', 'failed'):
+ break
+
+ if event['data']['status'] == 'completed':
+ # The event may occur in finish-migrate, so wait for the expected
+ # post-migration runstate
+ runstate = None
+ while runstate != expect_runstate:
+ runstate = self.qmp('query-status')['return']['status']
+ return True
+ else:
+ return False
+
+ def node_info(self, node_name):
+ nodes = self.qmp('query-named-block-nodes')
+ for x in nodes['return']:
+ if x['node-name'] == node_name:
+ return x
+ return None
+
+ def query_bitmaps(self):
+ res = self.qmp("query-named-block-nodes")
+ return {device['node-name']: device['dirty-bitmaps']
+ for device in res['return'] if 'dirty-bitmaps' in device}
+
+ def get_bitmap(self, node_name, bitmap_name, recording=None, bitmaps=None):
+ """
+ get a specific bitmap from the object returned by query_bitmaps.
+ :param recording: If specified, filter results by the specified value.
+ :param bitmaps: If specified, use it instead of call query_bitmaps()
+ """
+ if bitmaps is None:
+ bitmaps = self.query_bitmaps()
+
+ for bitmap in bitmaps[node_name]:
+ if bitmap.get('name', '') == bitmap_name:
+ if recording is None or bitmap.get('recording') == recording:
+ return bitmap
+ return None
+
+ def check_bitmap_status(self, node_name, bitmap_name, fields):
+ ret = self.get_bitmap(node_name, bitmap_name)
+
+ return fields.items() <= ret.items()
+
+ def assert_block_path(self, root, path, expected_node, graph=None):
+ """
+ Check whether the node under the given path in the block graph
+ is @expected_node.
+
+ @root is the node name of the node where the @path is rooted.
+
+ @path is a string that consists of child names separated by
+ slashes. It must begin with a slash.
+
+ Examples for @root + @path:
+ - root="qcow2-node", path="/backing/file"
+ - root="quorum-node", path="/children.2/file"
+
+ Hypothetically, @path could be empty, in which case it would
+ point to @root. However, in practice this case is not useful
+ and hence not allowed.
+
+ @expected_node may be None. (All elements of the path but the
+ leaf must still exist.)
+
+ @graph may be None or the result of an x-debug-query-block-graph
+ call that has already been performed.
+ """
+ if graph is None:
+ graph = self.qmp('x-debug-query-block-graph')['return']
+
+ iter_path = iter(path.split('/'))
+
+ # Must start with a /
+ assert next(iter_path) == ''
+
+ node = next((node for node in graph['nodes'] if node['name'] == root),
+ None)
+
+ # An empty @path is not allowed, so the root node must be present
+ assert node is not None, 'Root node %s not found' % root
+
+ for child_name in iter_path:
+ assert node is not None, 'Cannot follow path %s%s' % (root, path)
+
+ try:
+ node_id = next(edge['child'] for edge in graph['edges']
+ if (edge['parent'] == node['id'] and
+ edge['name'] == child_name))
+
+ node = next(node for node in graph['nodes']
+ if node['id'] == node_id)
+
+ except StopIteration:
+ node = None
+
+ if node is None:
+ assert expected_node is None, \
+ 'No node found under %s (but expected %s)' % \
+ (path, expected_node)
+ else:
+ assert node['name'] == expected_node, \
+ 'Found node %s under %s (but expected %s)' % \
+ (node['name'], path, expected_node)
+
+index_re = re.compile(r'([^\[]+)\[([^\]]+)\]')
+
+class QMPTestCase(unittest.TestCase):
+ '''Abstract base class for QMP test cases'''
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ # Many users of this class set a VM property we rely on heavily
+ # in the methods below.
+ self.vm = None
+
+ def dictpath(self, d, path):
+ '''Traverse a path in a nested dict'''
+ for component in path.split('/'):
+ m = index_re.match(component)
+ if m:
+ component, idx = m.groups()
+ idx = int(idx)
+
+ if not isinstance(d, dict) or component not in d:
+ self.fail(f'failed path traversal for "{path}" in "{d}"')
+ d = d[component]
+
+ if m:
+ if not isinstance(d, list):
+ self.fail(f'path component "{component}" in "{path}" '
+ f'is not a list in "{d}"')
+ try:
+ d = d[idx]
+ except IndexError:
+ self.fail(f'invalid index "{idx}" in path "{path}" '
+ f'in "{d}"')
+ return d
+
+ def assert_qmp_absent(self, d, path):
+ try:
+ result = self.dictpath(d, path)
+ except AssertionError:
+ return
+ self.fail('path "%s" has value "%s"' % (path, str(result)))
+
+ def assert_qmp(self, d, path, value):
+ '''Assert that the value for a specific path in a QMP dict
+ matches. When given a list of values, assert that any of
+ them matches.'''
+
+ result = self.dictpath(d, path)
+
+ # [] makes no sense as a list of valid values, so treat it as
+ # an actual single value.
+ if isinstance(value, list) and value != []:
+ for v in value:
+ if result == v:
+ return
+ self.fail('no match for "%s" in %s' % (str(result), str(value)))
+ else:
+ self.assertEqual(result, value,
+ '"%s" is "%s", expected "%s"'
+ % (path, str(result), str(value)))
+
+ def assert_no_active_block_jobs(self):
+ result = self.vm.qmp('query-block-jobs')
+ self.assert_qmp(result, 'return', [])
+
+ def assert_has_block_node(self, node_name=None, file_name=None):
+ """Issue a query-named-block-nodes and assert node_name and/or
+ file_name is present in the result"""
+ def check_equal_or_none(a, b):
+ return a is None or b is None or a == b
+ assert node_name or file_name
+ result = self.vm.qmp('query-named-block-nodes')
+ for x in result["return"]:
+ if check_equal_or_none(x.get("node-name"), node_name) and \
+ check_equal_or_none(x.get("file"), file_name):
+ return
+ self.fail("Cannot find %s %s in result:\n%s" %
+ (node_name, file_name, result))
+
+ def assert_json_filename_equal(self, json_filename, reference):
+ '''Asserts that the given filename is a json: filename and that its
+ content is equal to the given reference object'''
+ self.assertEqual(json_filename[:5], 'json:')
+ self.assertEqual(
+ self.vm.flatten_qmp_object(json.loads(json_filename[5:])),
+ self.vm.flatten_qmp_object(reference)
+ )
+
+ def cancel_and_wait(self, drive='drive0', force=False,
+ resume=False, wait=60.0):
+ '''Cancel a block job and wait for it to finish, returning the event'''
+ result = self.vm.qmp('block-job-cancel', device=drive, force=force)
+ self.assert_qmp(result, 'return', {})
+
+ if resume:
+ self.vm.resume_drive(drive)
+
+ cancelled = False
+ result = None
+ while not cancelled:
+ for event in self.vm.get_qmp_events(wait=wait):
+ if event['event'] == 'BLOCK_JOB_COMPLETED' or \
+ event['event'] == 'BLOCK_JOB_CANCELLED':
+ self.assert_qmp(event, 'data/device', drive)
+ result = event
+ cancelled = True
+ elif event['event'] == 'JOB_STATUS_CHANGE':
+ self.assert_qmp(event, 'data/id', drive)
+
+
+ self.assert_no_active_block_jobs()
+ return result
+
+ def wait_until_completed(self, drive='drive0', check_offset=True,
+ wait=60.0, error=None):
+ '''Wait for a block job to finish, returning the event'''
+ while True:
+ for event in self.vm.get_qmp_events(wait=wait):
+ if event['event'] == 'BLOCK_JOB_COMPLETED':
+ self.assert_qmp(event, 'data/device', drive)
+ if error is None:
+ self.assert_qmp_absent(event, 'data/error')
+ if check_offset:
+ self.assert_qmp(event, 'data/offset',
+ event['data']['len'])
+ else:
+ self.assert_qmp(event, 'data/error', error)
+ self.assert_no_active_block_jobs()
+ return event
+ if event['event'] == 'JOB_STATUS_CHANGE':
+ self.assert_qmp(event, 'data/id', drive)
+
+ def wait_ready(self, drive='drive0'):
+ """Wait until a BLOCK_JOB_READY event, and return the event."""
+ return self.vm.events_wait([
+ ('BLOCK_JOB_READY',
+ {'data': {'type': 'mirror', 'device': drive}}),
+ ('BLOCK_JOB_READY',
+ {'data': {'type': 'commit', 'device': drive}})
+ ])
+
+ def wait_ready_and_cancel(self, drive='drive0'):
+ self.wait_ready(drive=drive)
+ event = self.cancel_and_wait(drive=drive)
+ self.assertEqual(event['event'], 'BLOCK_JOB_COMPLETED')
+ self.assert_qmp(event, 'data/type', 'mirror')
+ self.assert_qmp(event, 'data/offset', event['data']['len'])
+
+ def complete_and_wait(self, drive='drive0', wait_ready=True,
+ completion_error=None):
+ '''Complete a block job and wait for it to finish'''
+ if wait_ready:
+ self.wait_ready(drive=drive)
+
+ result = self.vm.qmp('block-job-complete', device=drive)
+ self.assert_qmp(result, 'return', {})
+
+ event = self.wait_until_completed(drive=drive, error=completion_error)
+ self.assertTrue(event['data']['type'] in ['mirror', 'commit'])
+
+ def pause_wait(self, job_id='job0'):
+ with Timeout(3, "Timeout waiting for job to pause"):
+ while True:
+ result = self.vm.qmp('query-block-jobs')
+ found = False
+ for job in result['return']:
+ if job['device'] == job_id:
+ found = True
+ if job['paused'] and not job['busy']:
+ return job
+ break
+ assert found
+
+ def pause_job(self, job_id='job0', wait=True):
+ result = self.vm.qmp('block-job-pause', device=job_id)
+ self.assert_qmp(result, 'return', {})
+ if wait:
+ return self.pause_wait(job_id)
+ return result
+
+ def case_skip(self, reason):
+ '''Skip this test case'''
+ case_notrun(reason)
+ self.skipTest(reason)
+
+
+def notrun(reason):
+ '''Skip this test suite'''
+ # Each test in qemu-iotests has a number ("seq")
+ seq = os.path.basename(sys.argv[0])
+
+ with open('%s/%s.notrun' % (output_dir, seq), 'w', encoding='utf-8') \
+ as outfile:
+ outfile.write(reason + '\n')
+ logger.warning("%s not run: %s", seq, reason)
+ sys.exit(0)
+
+def case_notrun(reason):
+ '''Mark this test case as not having been run (without actually
+ skipping it, that is left to the caller). See
+ QMPTestCase.case_skip() for a variant that actually skips the
+ current test case.'''
+
+ # Each test in qemu-iotests has a number ("seq")
+ seq = os.path.basename(sys.argv[0])
+
+ with open('%s/%s.casenotrun' % (output_dir, seq), 'a', encoding='utf-8') \
+ as outfile:
+ outfile.write(' [case not run] ' + reason + '\n')
+
+def _verify_image_format(supported_fmts: Sequence[str] = (),
+ unsupported_fmts: Sequence[str] = ()) -> None:
+ if 'generic' in supported_fmts and \
+ os.environ.get('IMGFMT_GENERIC', 'true') == 'true':
+ # similar to
+ # _supported_fmt generic
+ # for bash tests
+ supported_fmts = ()
+
+ not_sup = supported_fmts and (imgfmt not in supported_fmts)
+ if not_sup or (imgfmt in unsupported_fmts):
+ notrun('not suitable for this image format: %s' % imgfmt)
+
+ if imgfmt == 'luks':
+ verify_working_luks()
+
+def _verify_protocol(supported: Sequence[str] = (),
+ unsupported: Sequence[str] = ()) -> None:
+ assert not (supported and unsupported)
+
+ if 'generic' in supported:
+ return
+
+ not_sup = supported and (imgproto not in supported)
+ if not_sup or (imgproto in unsupported):
+ notrun('not suitable for this protocol: %s' % imgproto)
+
+def _verify_platform(supported: Sequence[str] = (),
+ unsupported: Sequence[str] = ()) -> None:
+ if any((sys.platform.startswith(x) for x in unsupported)):
+ notrun('not suitable for this OS: %s' % sys.platform)
+
+ if supported:
+ if not any((sys.platform.startswith(x) for x in supported)):
+ notrun('not suitable for this OS: %s' % sys.platform)
+
+def _verify_cache_mode(supported_cache_modes: Sequence[str] = ()) -> None:
+ if supported_cache_modes and (cachemode not in supported_cache_modes):
+ notrun('not suitable for this cache mode: %s' % cachemode)
+
+def _verify_aio_mode(supported_aio_modes: Sequence[str] = ()) -> None:
+ if supported_aio_modes and (aiomode not in supported_aio_modes):
+ notrun('not suitable for this aio mode: %s' % aiomode)
+
+def _verify_formats(required_formats: Sequence[str] = ()) -> None:
+ usf_list = list(set(required_formats) - set(supported_formats()))
+ if usf_list:
+ notrun(f'formats {usf_list} are not whitelisted')
+
+
+def _verify_virtio_blk() -> None:
+ out = qemu_pipe('-M', 'none', '-device', 'help')
+ if 'virtio-blk' not in out:
+ notrun('Missing virtio-blk in QEMU binary')
+
+def _verify_virtio_scsi_pci_or_ccw() -> None:
+ out = qemu_pipe('-M', 'none', '-device', 'help')
+ if 'virtio-scsi-pci' not in out and 'virtio-scsi-ccw' not in out:
+ notrun('Missing virtio-scsi-pci or virtio-scsi-ccw in QEMU binary')
+
+
+def supports_quorum():
+ return 'quorum' in qemu_img_pipe('--help')
+
+def verify_quorum():
+ '''Skip test suite if quorum support is not available'''
+ if not supports_quorum():
+ notrun('quorum support missing')
+
+def has_working_luks() -> Tuple[bool, str]:
+ """
+ Check whether our LUKS driver can actually create images
+ (this extends to LUKS encryption for qcow2).
+
+ If not, return the reason why.
+ """
+
+ img_file = f'{test_dir}/luks-test.luks'
+ (output, status) = \
+ qemu_img_pipe_and_status('create', '-f', 'luks',
+ '--object', luks_default_secret_object,
+ '-o', luks_default_key_secret_opt,
+ '-o', 'iter-time=10',
+ img_file, '1G')
+ try:
+ os.remove(img_file)
+ except OSError:
+ pass
+
+ if status != 0:
+ reason = output
+ for line in output.splitlines():
+ if img_file + ':' in line:
+ reason = line.split(img_file + ':', 1)[1].strip()
+ break
+
+ return (False, reason)
+ else:
+ return (True, '')
+
+def verify_working_luks():
+ """
+ Skip test suite if LUKS does not work
+ """
+ (working, reason) = has_working_luks()
+ if not working:
+ notrun(reason)
+
+def qemu_pipe(*args: str) -> str:
+ """
+ Run qemu with an option to print something and exit (e.g. a help option).
+
+ :return: QEMU's stdout output.
+ """
+ full_args = [qemu_prog] + qemu_opts + list(args)
+ output, _ = qemu_tool_pipe_and_status('qemu', full_args)
+ return output
+
+def supported_formats(read_only=False):
+ '''Set 'read_only' to True to check ro-whitelist
+ Otherwise, rw-whitelist is checked'''
+
+ if not hasattr(supported_formats, "formats"):
+ supported_formats.formats = {}
+
+ if read_only not in supported_formats.formats:
+ format_message = qemu_pipe("-drive", "format=help")
+ line = 1 if read_only else 0
+ supported_formats.formats[read_only] = \
+ format_message.splitlines()[line].split(":")[1].split()
+
+ return supported_formats.formats[read_only]
+
+def skip_if_unsupported(required_formats=(), read_only=False):
+ '''Skip Test Decorator
+ Runs the test if all the required formats are whitelisted'''
+ def skip_test_decorator(func):
+ def func_wrapper(test_case: QMPTestCase, *args: List[Any],
+ **kwargs: Dict[str, Any]) -> None:
+ if callable(required_formats):
+ fmts = required_formats(test_case)
+ else:
+ fmts = required_formats
+
+ usf_list = list(set(fmts) - set(supported_formats(read_only)))
+ if usf_list:
+ msg = f'{test_case}: formats {usf_list} are not whitelisted'
+ test_case.case_skip(msg)
+ else:
+ func(test_case, *args, **kwargs)
+ return func_wrapper
+ return skip_test_decorator
+
+def skip_for_formats(formats: Sequence[str] = ()) \
+ -> Callable[[Callable[[QMPTestCase, List[Any], Dict[str, Any]], None]],
+ Callable[[QMPTestCase, List[Any], Dict[str, Any]], None]]:
+ '''Skip Test Decorator
+ Skips the test for the given formats'''
+ def skip_test_decorator(func):
+ def func_wrapper(test_case: QMPTestCase, *args: List[Any],
+ **kwargs: Dict[str, Any]) -> None:
+ if imgfmt in formats:
+ msg = f'{test_case}: Skipped for format {imgfmt}'
+ test_case.case_skip(msg)
+ else:
+ func(test_case, *args, **kwargs)
+ return func_wrapper
+ return skip_test_decorator
+
+def skip_if_user_is_root(func):
+ '''Skip Test Decorator
+ Runs the test only without root permissions'''
+ def func_wrapper(*args, **kwargs):
+ if os.getuid() == 0:
+ case_notrun('{}: cannot be run as root'.format(args[0]))
+ return None
+ else:
+ return func(*args, **kwargs)
+ return func_wrapper
+
+# We need to filter out the time taken from the output so that
+# qemu-iotest can reliably diff the results against master output,
+# and hide skipped tests from the reference output.
+
+class ReproducibleTestResult(unittest.TextTestResult):
+ def addSkip(self, test, reason):
+ # Same as TextTestResult, but print dot instead of "s"
+ unittest.TestResult.addSkip(self, test, reason)
+ if self.showAll:
+ self.stream.writeln("skipped {0!r}".format(reason))
+ elif self.dots:
+ self.stream.write(".")
+ self.stream.flush()
+
+class ReproducibleStreamWrapper:
+ def __init__(self, stream: TextIO):
+ self.stream = stream
+
+ def __getattr__(self, attr):
+ if attr in ('stream', '__getstate__'):
+ raise AttributeError(attr)
+ return getattr(self.stream, attr)
+
+ def write(self, arg=None):
+ arg = re.sub(r'Ran (\d+) tests? in [\d.]+s', r'Ran \1 tests', arg)
+ arg = re.sub(r' \(skipped=\d+\)', r'', arg)
+ self.stream.write(arg)
+
+class ReproducibleTestRunner(unittest.TextTestRunner):
+ def __init__(self, stream: Optional[TextIO] = None,
+ resultclass: Type[unittest.TestResult] =
+ ReproducibleTestResult,
+ **kwargs: Any) -> None:
+ rstream = ReproducibleStreamWrapper(stream or sys.stdout)
+ super().__init__(stream=rstream, # type: ignore
+ descriptions=True,
+ resultclass=resultclass,
+ **kwargs)
+
+def execute_unittest(argv: List[str], debug: bool = False) -> None:
+ """Executes unittests within the calling module."""
+
+ # Some tests have warnings, especially ResourceWarnings for unclosed
+ # files and sockets. Ignore them for now to ensure reproducibility of
+ # the test output.
+ unittest.main(argv=argv,
+ testRunner=ReproducibleTestRunner,
+ verbosity=2 if debug else 1,
+ warnings=None if sys.warnoptions else 'ignore')
+
+def execute_setup_common(supported_fmts: Sequence[str] = (),
+ supported_platforms: Sequence[str] = (),
+ supported_cache_modes: Sequence[str] = (),
+ supported_aio_modes: Sequence[str] = (),
+ unsupported_fmts: Sequence[str] = (),
+ supported_protocols: Sequence[str] = (),
+ unsupported_protocols: Sequence[str] = (),
+ required_fmts: Sequence[str] = ()) -> bool:
+ """
+ Perform necessary setup for either script-style or unittest-style tests.
+
+ :return: Bool; Whether or not debug mode has been requested via the CLI.
+ """
+ # Note: Python 3.6 and pylint do not like 'Collection' so use 'Sequence'.
+
+ debug = '-d' in sys.argv
+ if debug:
+ sys.argv.remove('-d')
+ logging.basicConfig(level=(logging.DEBUG if debug else logging.WARN))
+
+ _verify_image_format(supported_fmts, unsupported_fmts)
+ _verify_protocol(supported_protocols, unsupported_protocols)
+ _verify_platform(supported=supported_platforms)
+ _verify_cache_mode(supported_cache_modes)
+ _verify_aio_mode(supported_aio_modes)
+ _verify_formats(required_fmts)
+ _verify_virtio_blk()
+
+ return debug
+
+def execute_test(*args, test_function=None, **kwargs):
+ """Run either unittest or script-style tests."""
+
+ debug = execute_setup_common(*args, **kwargs)
+ if not test_function:
+ execute_unittest(sys.argv, debug)
+ else:
+ test_function()
+
+def activate_logging():
+ """Activate iotests.log() output to stdout for script-style tests."""
+ handler = logging.StreamHandler(stream=sys.stdout)
+ formatter = logging.Formatter('%(message)s')
+ handler.setFormatter(formatter)
+ test_logger.addHandler(handler)
+ test_logger.setLevel(logging.INFO)
+ test_logger.propagate = False
+
+# This is called from script-style iotests without a single point of entry
+def script_initialize(*args, **kwargs):
+ """Initialize script-style tests without running any tests."""
+ activate_logging()
+ execute_setup_common(*args, **kwargs)
+
+# This is called from script-style iotests with a single point of entry
+def script_main(test_function, *args, **kwargs):
+ """Run script-style tests outside of the unittest framework"""
+ activate_logging()
+ execute_test(*args, test_function=test_function, **kwargs)
+
+# This is called from unittest style iotests
+def main(*args, **kwargs):
+ """Run tests using the unittest framework"""
+ execute_test(*args, **kwargs)