xref: /openbmc/qemu/tests/qemu-iotests/iotests.py (revision 19f4ed36)
1# Common utilities and Python wrappers for qemu-iotests
2#
3# Copyright (C) 2012 IBM Corp.
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program.  If not, see <http://www.gnu.org/licenses/>.
17#
18
19import atexit
20import bz2
21from collections import OrderedDict
22import faulthandler
23import io
24import json
25import logging
26import os
27import re
28import shutil
29import signal
30import struct
31import subprocess
32import sys
33import time
34from typing import (Any, Callable, Dict, Iterable,
35                    List, Optional, Sequence, Tuple, TypeVar)
36import unittest
37
38from contextlib import contextmanager
39
40# pylint: disable=import-error, wrong-import-position
41sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python'))
42from qemu import qtest
43from qemu.qmp import QMPMessage
44
45# Use this logger for logging messages directly from the iotests module
46logger = logging.getLogger('qemu.iotests')
47logger.addHandler(logging.NullHandler())
48
49# Use this logger for messages that ought to be used for diff output.
50test_logger = logging.getLogger('qemu.iotests.diff_io')
51
52
53faulthandler.enable()
54
55# This will not work if arguments contain spaces but is necessary if we
56# want to support the override options that ./check supports.
57qemu_img_args = [os.environ.get('QEMU_IMG_PROG', 'qemu-img')]
58if os.environ.get('QEMU_IMG_OPTIONS'):
59    qemu_img_args += os.environ['QEMU_IMG_OPTIONS'].strip().split(' ')
60
61qemu_io_args = [os.environ.get('QEMU_IO_PROG', 'qemu-io')]
62if os.environ.get('QEMU_IO_OPTIONS'):
63    qemu_io_args += os.environ['QEMU_IO_OPTIONS'].strip().split(' ')
64
65qemu_io_args_no_fmt = [os.environ.get('QEMU_IO_PROG', 'qemu-io')]
66if os.environ.get('QEMU_IO_OPTIONS_NO_FMT'):
67    qemu_io_args_no_fmt += \
68        os.environ['QEMU_IO_OPTIONS_NO_FMT'].strip().split(' ')
69
70qemu_nbd_prog = os.environ.get('QEMU_NBD_PROG', 'qemu-nbd')
71qemu_nbd_args = [qemu_nbd_prog]
72if os.environ.get('QEMU_NBD_OPTIONS'):
73    qemu_nbd_args += os.environ['QEMU_NBD_OPTIONS'].strip().split(' ')
74
75qemu_prog = os.environ.get('QEMU_PROG', 'qemu')
76qemu_opts = os.environ.get('QEMU_OPTIONS', '').strip().split(' ')
77
78imgfmt = os.environ.get('IMGFMT', 'raw')
79imgproto = os.environ.get('IMGPROTO', 'file')
80output_dir = os.environ.get('OUTPUT_DIR', '.')
81
82try:
83    test_dir = os.environ['TEST_DIR']
84    sock_dir = os.environ['SOCK_DIR']
85    cachemode = os.environ['CACHEMODE']
86    aiomode = os.environ['AIOMODE']
87    qemu_default_machine = os.environ['QEMU_DEFAULT_MACHINE']
88except KeyError:
89    # We are using these variables as proxies to indicate that we're
90    # not being run via "check". There may be other things set up by
91    # "check" that individual test cases rely on.
92    sys.stderr.write('Please run this test via the "check" script\n')
93    sys.exit(os.EX_USAGE)
94
95socket_scm_helper = os.environ.get('SOCKET_SCM_HELPER', 'socket_scm_helper')
96
97luks_default_secret_object = 'secret,id=keysec0,data=' + \
98                             os.environ.get('IMGKEYSECRET', '')
99luks_default_key_secret_opt = 'key-secret=keysec0'
100
101sample_img_dir = os.environ['SAMPLE_IMG_DIR']
102
103
104def unarchive_sample_image(sample, fname):
105    sample_fname = os.path.join(sample_img_dir, sample + '.bz2')
106    with bz2.open(sample_fname) as f_in, open(fname, 'wb') as f_out:
107        shutil.copyfileobj(f_in, f_out)
108
109
110def qemu_tool_pipe_and_status(tool: str, args: Sequence[str],
111                              connect_stderr: bool = True) -> Tuple[str, int]:
112    """
113    Run a tool and return both its output and its exit code
114    """
115    stderr = subprocess.STDOUT if connect_stderr else None
116    subp = subprocess.Popen(args,
117                            stdout=subprocess.PIPE,
118                            stderr=stderr,
119                            universal_newlines=True)
120    output = subp.communicate()[0]
121    if subp.returncode < 0:
122        cmd = ' '.join(args)
123        sys.stderr.write(f'{tool} received signal {-subp.returncode}: {cmd}\n')
124    return (output, subp.returncode)
125
126def qemu_img_pipe_and_status(*args: str) -> Tuple[str, int]:
127    """
128    Run qemu-img and return both its output and its exit code
129    """
130    full_args = qemu_img_args + list(args)
131    return qemu_tool_pipe_and_status('qemu-img', full_args)
132
133def qemu_img(*args: str) -> int:
134    '''Run qemu-img and return the exit code'''
135    return qemu_img_pipe_and_status(*args)[1]
136
137def ordered_qmp(qmsg, conv_keys=True):
138    # Dictionaries are not ordered prior to 3.6, therefore:
139    if isinstance(qmsg, list):
140        return [ordered_qmp(atom) for atom in qmsg]
141    if isinstance(qmsg, dict):
142        od = OrderedDict()
143        for k, v in sorted(qmsg.items()):
144            if conv_keys:
145                k = k.replace('_', '-')
146            od[k] = ordered_qmp(v, conv_keys=False)
147        return od
148    return qmsg
149
150def qemu_img_create(*args):
151    args = list(args)
152
153    # default luks support
154    if '-f' in args and args[args.index('-f') + 1] == 'luks':
155        if '-o' in args:
156            i = args.index('-o')
157            if 'key-secret' not in args[i + 1]:
158                args[i + 1].append(luks_default_key_secret_opt)
159                args.insert(i + 2, '--object')
160                args.insert(i + 3, luks_default_secret_object)
161        else:
162            args = ['-o', luks_default_key_secret_opt,
163                    '--object', luks_default_secret_object] + args
164
165    args.insert(0, 'create')
166
167    return qemu_img(*args)
168
169def qemu_img_measure(*args):
170    return json.loads(qemu_img_pipe("measure", "--output", "json", *args))
171
172def qemu_img_check(*args):
173    return json.loads(qemu_img_pipe("check", "--output", "json", *args))
174
175def qemu_img_verbose(*args):
176    '''Run qemu-img without suppressing its output and return the exit code'''
177    exitcode = subprocess.call(qemu_img_args + list(args))
178    if exitcode < 0:
179        sys.stderr.write('qemu-img received signal %i: %s\n'
180                         % (-exitcode, ' '.join(qemu_img_args + list(args))))
181    return exitcode
182
183def qemu_img_pipe(*args: str) -> str:
184    '''Run qemu-img and return its output'''
185    return qemu_img_pipe_and_status(*args)[0]
186
187def qemu_img_log(*args):
188    result = qemu_img_pipe(*args)
189    log(result, filters=[filter_testfiles])
190    return result
191
192def img_info_log(filename, filter_path=None, imgopts=False, extra_args=()):
193    args = ['info']
194    if imgopts:
195        args.append('--image-opts')
196    else:
197        args += ['-f', imgfmt]
198    args += extra_args
199    args.append(filename)
200
201    output = qemu_img_pipe(*args)
202    if not filter_path:
203        filter_path = filename
204    log(filter_img_info(output, filter_path))
205
206def qemu_io(*args):
207    '''Run qemu-io and return the stdout data'''
208    args = qemu_io_args + list(args)
209    return qemu_tool_pipe_and_status('qemu-io', args)[0]
210
211def qemu_io_log(*args):
212    result = qemu_io(*args)
213    log(result, filters=[filter_testfiles, filter_qemu_io])
214    return result
215
216def qemu_io_silent(*args):
217    '''Run qemu-io and return the exit code, suppressing stdout'''
218    if '-f' in args or '--image-opts' in args:
219        default_args = qemu_io_args_no_fmt
220    else:
221        default_args = qemu_io_args
222
223    args = default_args + list(args)
224    exitcode = subprocess.call(args, stdout=open('/dev/null', 'w'))
225    if exitcode < 0:
226        sys.stderr.write('qemu-io received signal %i: %s\n' %
227                         (-exitcode, ' '.join(args)))
228    return exitcode
229
230def qemu_io_silent_check(*args):
231    '''Run qemu-io and return the true if subprocess returned 0'''
232    args = qemu_io_args + list(args)
233    exitcode = subprocess.call(args, stdout=open('/dev/null', 'w'),
234                               stderr=subprocess.STDOUT)
235    return exitcode == 0
236
237class QemuIoInteractive:
238    def __init__(self, *args):
239        self.args = qemu_io_args_no_fmt + list(args)
240        self._p = subprocess.Popen(self.args, stdin=subprocess.PIPE,
241                                   stdout=subprocess.PIPE,
242                                   stderr=subprocess.STDOUT,
243                                   universal_newlines=True)
244        out = self._p.stdout.read(9)
245        if out != 'qemu-io> ':
246            # Most probably qemu-io just failed to start.
247            # Let's collect the whole output and exit.
248            out += self._p.stdout.read()
249            self._p.wait(timeout=1)
250            raise ValueError(out)
251
252    def close(self):
253        self._p.communicate('q\n')
254
255    def _read_output(self):
256        pattern = 'qemu-io> '
257        n = len(pattern)
258        pos = 0
259        s = []
260        while pos != n:
261            c = self._p.stdout.read(1)
262            # check unexpected EOF
263            assert c != ''
264            s.append(c)
265            if c == pattern[pos]:
266                pos += 1
267            else:
268                pos = 0
269
270        return ''.join(s[:-n])
271
272    def cmd(self, cmd):
273        # quit command is in close(), '\n' is added automatically
274        assert '\n' not in cmd
275        cmd = cmd.strip()
276        assert cmd not in ('q', 'quit')
277        self._p.stdin.write(cmd + '\n')
278        self._p.stdin.flush()
279        return self._read_output()
280
281
282def qemu_nbd(*args):
283    '''Run qemu-nbd in daemon mode and return the parent's exit code'''
284    return subprocess.call(qemu_nbd_args + ['--fork'] + list(args))
285
286def qemu_nbd_early_pipe(*args: str) -> Tuple[int, str]:
287    '''Run qemu-nbd in daemon mode and return both the parent's exit code
288       and its output in case of an error'''
289    full_args = qemu_nbd_args + ['--fork'] + list(args)
290    output, returncode = qemu_tool_pipe_and_status('qemu-nbd', full_args,
291                                                   connect_stderr=False)
292    return returncode, output if returncode else ''
293
294def qemu_nbd_list_log(*args: str) -> str:
295    '''Run qemu-nbd to list remote exports'''
296    full_args = [qemu_nbd_prog, '-L'] + list(args)
297    output, _ = qemu_tool_pipe_and_status('qemu-nbd', full_args)
298    log(output, filters=[filter_testfiles, filter_nbd_exports])
299    return output
300
301@contextmanager
302def qemu_nbd_popen(*args):
303    '''Context manager running qemu-nbd within the context'''
304    pid_file = file_path("qemu_nbd_popen-nbd-pid-file")
305
306    assert not os.path.exists(pid_file)
307
308    cmd = list(qemu_nbd_args)
309    cmd.extend(('--persistent', '--pid-file', pid_file))
310    cmd.extend(args)
311
312    log('Start NBD server')
313    p = subprocess.Popen(cmd)
314    try:
315        while not os.path.exists(pid_file):
316            if p.poll() is not None:
317                raise RuntimeError(
318                    "qemu-nbd terminated with exit code {}: {}"
319                    .format(p.returncode, ' '.join(cmd)))
320
321            time.sleep(0.01)
322        yield
323    finally:
324        if os.path.exists(pid_file):
325            os.remove(pid_file)
326        log('Kill NBD server')
327        p.kill()
328        p.wait()
329
330def compare_images(img1, img2, fmt1=imgfmt, fmt2=imgfmt):
331    '''Return True if two image files are identical'''
332    return qemu_img('compare', '-f', fmt1,
333                    '-F', fmt2, img1, img2) == 0
334
335def create_image(name, size):
336    '''Create a fully-allocated raw image with sector markers'''
337    file = open(name, 'wb')
338    i = 0
339    while i < size:
340        sector = struct.pack('>l504xl', i // 512, i // 512)
341        file.write(sector)
342        i = i + 512
343    file.close()
344
345def image_size(img):
346    '''Return image's virtual size'''
347    r = qemu_img_pipe('info', '--output=json', '-f', imgfmt, img)
348    return json.loads(r)['virtual-size']
349
350def is_str(val):
351    return isinstance(val, str)
352
353test_dir_re = re.compile(r"%s" % test_dir)
354def filter_test_dir(msg):
355    return test_dir_re.sub("TEST_DIR", msg)
356
357win32_re = re.compile(r"\r")
358def filter_win32(msg):
359    return win32_re.sub("", msg)
360
361qemu_io_re = re.compile(r"[0-9]* ops; [0-9\/:. sec]* "
362                        r"\([0-9\/.inf]* [EPTGMKiBbytes]*\/sec "
363                        r"and [0-9\/.inf]* ops\/sec\)")
364def filter_qemu_io(msg):
365    msg = filter_win32(msg)
366    return qemu_io_re.sub("X ops; XX:XX:XX.X "
367                          "(XXX YYY/sec and XXX ops/sec)", msg)
368
369chown_re = re.compile(r"chown [0-9]+:[0-9]+")
370def filter_chown(msg):
371    return chown_re.sub("chown UID:GID", msg)
372
373def filter_qmp_event(event):
374    '''Filter a QMP event dict'''
375    event = dict(event)
376    if 'timestamp' in event:
377        event['timestamp']['seconds'] = 'SECS'
378        event['timestamp']['microseconds'] = 'USECS'
379    return event
380
381def filter_qmp(qmsg, filter_fn):
382    '''Given a string filter, filter a QMP object's values.
383    filter_fn takes a (key, value) pair.'''
384    # Iterate through either lists or dicts;
385    if isinstance(qmsg, list):
386        items = enumerate(qmsg)
387    else:
388        items = qmsg.items()
389
390    for k, v in items:
391        if isinstance(v, (dict, list)):
392            qmsg[k] = filter_qmp(v, filter_fn)
393        else:
394            qmsg[k] = filter_fn(k, v)
395    return qmsg
396
397def filter_testfiles(msg):
398    pref1 = os.path.join(test_dir, "%s-" % (os.getpid()))
399    pref2 = os.path.join(sock_dir, "%s-" % (os.getpid()))
400    return msg.replace(pref1, 'TEST_DIR/PID-').replace(pref2, 'SOCK_DIR/PID-')
401
402def filter_qmp_testfiles(qmsg):
403    def _filter(_key, value):
404        if is_str(value):
405            return filter_testfiles(value)
406        return value
407    return filter_qmp(qmsg, _filter)
408
409def filter_virtio_scsi(output: str) -> str:
410    return re.sub(r'(virtio-scsi)-(ccw|pci)', r'\1', output)
411
412def filter_qmp_virtio_scsi(qmsg):
413    def _filter(_key, value):
414        if is_str(value):
415            return filter_virtio_scsi(value)
416        return value
417    return filter_qmp(qmsg, _filter)
418
419def filter_generated_node_ids(msg):
420    return re.sub("#block[0-9]+", "NODE_NAME", msg)
421
422def filter_img_info(output, filename):
423    lines = []
424    for line in output.split('\n'):
425        if 'disk size' in line or 'actual-size' in line:
426            continue
427        line = line.replace(filename, 'TEST_IMG')
428        line = filter_testfiles(line)
429        line = line.replace(imgfmt, 'IMGFMT')
430        line = re.sub('iters: [0-9]+', 'iters: XXX', line)
431        line = re.sub('uuid: [-a-f0-9]+',
432                      'uuid: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX',
433                      line)
434        line = re.sub('cid: [0-9]+', 'cid: XXXXXXXXXX', line)
435        lines.append(line)
436    return '\n'.join(lines)
437
438def filter_imgfmt(msg):
439    return msg.replace(imgfmt, 'IMGFMT')
440
441def filter_qmp_imgfmt(qmsg):
442    def _filter(_key, value):
443        if is_str(value):
444            return filter_imgfmt(value)
445        return value
446    return filter_qmp(qmsg, _filter)
447
448def filter_nbd_exports(output: str) -> str:
449    return re.sub(r'((min|opt|max) block): [0-9]+', r'\1: XXX', output)
450
451
452Msg = TypeVar('Msg', Dict[str, Any], List[Any], str)
453
454def log(msg: Msg,
455        filters: Iterable[Callable[[Msg], Msg]] = (),
456        indent: Optional[int] = None) -> None:
457    """
458    Logs either a string message or a JSON serializable message (like QMP).
459    If indent is provided, JSON serializable messages are pretty-printed.
460    """
461    for flt in filters:
462        msg = flt(msg)
463    if isinstance(msg, (dict, list)):
464        # Don't sort if it's already sorted
465        do_sort = not isinstance(msg, OrderedDict)
466        test_logger.info(json.dumps(msg, sort_keys=do_sort, indent=indent))
467    else:
468        test_logger.info(msg)
469
470class Timeout:
471    def __init__(self, seconds, errmsg="Timeout"):
472        self.seconds = seconds
473        self.errmsg = errmsg
474    def __enter__(self):
475        signal.signal(signal.SIGALRM, self.timeout)
476        signal.setitimer(signal.ITIMER_REAL, self.seconds)
477        return self
478    def __exit__(self, exc_type, value, traceback):
479        signal.setitimer(signal.ITIMER_REAL, 0)
480        return False
481    def timeout(self, signum, frame):
482        raise Exception(self.errmsg)
483
484def file_pattern(name):
485    return "{0}-{1}".format(os.getpid(), name)
486
487class FilePath:
488    """
489    Context manager generating multiple file names. The generated files are
490    removed when exiting the context.
491
492    Example usage:
493
494        with FilePath('a.img', 'b.img') as (img_a, img_b):
495            # Use img_a and img_b here...
496
497        # a.img and b.img are automatically removed here.
498
499    By default images are created in iotests.test_dir. To create sockets use
500    iotests.sock_dir:
501
502       with FilePath('a.sock', base_dir=iotests.sock_dir) as sock:
503
504    For convenience, calling with one argument yields a single file instead of
505    a tuple with one item.
506
507    """
508    def __init__(self, *names, base_dir=test_dir):
509        self.paths = [os.path.join(base_dir, file_pattern(name))
510                      for name in names]
511
512    def __enter__(self):
513        if len(self.paths) == 1:
514            return self.paths[0]
515        else:
516            return self.paths
517
518    def __exit__(self, exc_type, exc_val, exc_tb):
519        for path in self.paths:
520            try:
521                os.remove(path)
522            except OSError:
523                pass
524        return False
525
526
527def try_remove(img):
528    try:
529        os.remove(img)
530    except OSError:
531        pass
532
533def file_path_remover():
534    for path in reversed(file_path_remover.paths):
535        try_remove(path)
536
537
538def file_path(*names, base_dir=test_dir):
539    ''' Another way to get auto-generated filename that cleans itself up.
540
541    Use is as simple as:
542
543    img_a, img_b = file_path('a.img', 'b.img')
544    sock = file_path('socket')
545    '''
546
547    if not hasattr(file_path_remover, 'paths'):
548        file_path_remover.paths = []
549        atexit.register(file_path_remover)
550
551    paths = []
552    for name in names:
553        filename = file_pattern(name)
554        path = os.path.join(base_dir, filename)
555        file_path_remover.paths.append(path)
556        paths.append(path)
557
558    return paths[0] if len(paths) == 1 else paths
559
560def remote_filename(path):
561    if imgproto == 'file':
562        return path
563    elif imgproto == 'ssh':
564        return "ssh://%s@127.0.0.1:22%s" % (os.environ.get('USER'), path)
565    else:
566        raise Exception("Protocol %s not supported" % (imgproto))
567
568class VM(qtest.QEMUQtestMachine):
569    '''A QEMU VM'''
570
571    def __init__(self, path_suffix=''):
572        name = "qemu%s-%d" % (path_suffix, os.getpid())
573        super().__init__(qemu_prog, qemu_opts, name=name,
574                         test_dir=test_dir,
575                         socket_scm_helper=socket_scm_helper,
576                         sock_dir=sock_dir)
577        self._num_drives = 0
578
579    def add_object(self, opts):
580        self._args.append('-object')
581        self._args.append(opts)
582        return self
583
584    def add_device(self, opts):
585        self._args.append('-device')
586        self._args.append(opts)
587        return self
588
589    def add_drive_raw(self, opts):
590        self._args.append('-drive')
591        self._args.append(opts)
592        return self
593
594    def add_drive(self, path, opts='', interface='virtio', img_format=imgfmt):
595        '''Add a virtio-blk drive to the VM'''
596        options = ['if=%s' % interface,
597                   'id=drive%d' % self._num_drives]
598
599        if path is not None:
600            options.append('file=%s' % path)
601            options.append('format=%s' % img_format)
602            options.append('cache=%s' % cachemode)
603            options.append('aio=%s' % aiomode)
604
605        if opts:
606            options.append(opts)
607
608        if img_format == 'luks' and 'key-secret' not in opts:
609            # default luks support
610            if luks_default_secret_object not in self._args:
611                self.add_object(luks_default_secret_object)
612
613            options.append(luks_default_key_secret_opt)
614
615        self._args.append('-drive')
616        self._args.append(','.join(options))
617        self._num_drives += 1
618        return self
619
620    def add_blockdev(self, opts):
621        self._args.append('-blockdev')
622        if isinstance(opts, str):
623            self._args.append(opts)
624        else:
625            self._args.append(','.join(opts))
626        return self
627
628    def add_incoming(self, addr):
629        self._args.append('-incoming')
630        self._args.append(addr)
631        return self
632
633    def hmp(self, command_line: str, use_log: bool = False) -> QMPMessage:
634        cmd = 'human-monitor-command'
635        kwargs: Dict[str, Any] = {'command-line': command_line}
636        if use_log:
637            return self.qmp_log(cmd, **kwargs)
638        else:
639            return self.qmp(cmd, **kwargs)
640
641    def pause_drive(self, drive: str, event: Optional[str] = None) -> None:
642        """Pause drive r/w operations"""
643        if not event:
644            self.pause_drive(drive, "read_aio")
645            self.pause_drive(drive, "write_aio")
646            return
647        self.hmp(f'qemu-io {drive} "break {event} bp_{drive}"')
648
649    def resume_drive(self, drive: str) -> None:
650        """Resume drive r/w operations"""
651        self.hmp(f'qemu-io {drive} "remove_break bp_{drive}"')
652
653    def hmp_qemu_io(self, drive: str, cmd: str,
654                    use_log: bool = False) -> QMPMessage:
655        """Write to a given drive using an HMP command"""
656        return self.hmp(f'qemu-io {drive} "{cmd}"', use_log=use_log)
657
658    def flatten_qmp_object(self, obj, output=None, basestr=''):
659        if output is None:
660            output = dict()
661        if isinstance(obj, list):
662            for i, item in enumerate(obj):
663                self.flatten_qmp_object(item, output, basestr + str(i) + '.')
664        elif isinstance(obj, dict):
665            for key in obj:
666                self.flatten_qmp_object(obj[key], output, basestr + key + '.')
667        else:
668            output[basestr[:-1]] = obj # Strip trailing '.'
669        return output
670
671    def qmp_to_opts(self, obj):
672        obj = self.flatten_qmp_object(obj)
673        output_list = list()
674        for key in obj:
675            output_list += [key + '=' + obj[key]]
676        return ','.join(output_list)
677
678    def get_qmp_events_filtered(self, wait=60.0):
679        result = []
680        for ev in self.get_qmp_events(wait=wait):
681            result.append(filter_qmp_event(ev))
682        return result
683
684    def qmp_log(self, cmd, filters=(), indent=None, **kwargs):
685        full_cmd = OrderedDict((
686            ("execute", cmd),
687            ("arguments", ordered_qmp(kwargs))
688        ))
689        log(full_cmd, filters, indent=indent)
690        result = self.qmp(cmd, **kwargs)
691        log(result, filters, indent=indent)
692        return result
693
694    # Returns None on success, and an error string on failure
695    def run_job(self, job, auto_finalize=True, auto_dismiss=False,
696                pre_finalize=None, cancel=False, wait=60.0):
697        """
698        run_job moves a job from creation through to dismissal.
699
700        :param job: String. ID of recently-launched job
701        :param auto_finalize: Bool. True if the job was launched with
702                              auto_finalize. Defaults to True.
703        :param auto_dismiss: Bool. True if the job was launched with
704                             auto_dismiss=True. Defaults to False.
705        :param pre_finalize: Callback. A callable that takes no arguments to be
706                             invoked prior to issuing job-finalize, if any.
707        :param cancel: Bool. When true, cancels the job after the pre_finalize
708                       callback.
709        :param wait: Float. Timeout value specifying how long to wait for any
710                     event, in seconds. Defaults to 60.0.
711        """
712        match_device = {'data': {'device': job}}
713        match_id = {'data': {'id': job}}
714        events = [
715            ('BLOCK_JOB_COMPLETED', match_device),
716            ('BLOCK_JOB_CANCELLED', match_device),
717            ('BLOCK_JOB_ERROR', match_device),
718            ('BLOCK_JOB_READY', match_device),
719            ('BLOCK_JOB_PENDING', match_id),
720            ('JOB_STATUS_CHANGE', match_id)
721        ]
722        error = None
723        while True:
724            ev = filter_qmp_event(self.events_wait(events, timeout=wait))
725            if ev['event'] != 'JOB_STATUS_CHANGE':
726                log(ev)
727                continue
728            status = ev['data']['status']
729            if status == 'aborting':
730                result = self.qmp('query-jobs')
731                for j in result['return']:
732                    if j['id'] == job:
733                        error = j['error']
734                        log('Job failed: %s' % (j['error']))
735            elif status == 'ready':
736                self.qmp_log('job-complete', id=job)
737            elif status == 'pending' and not auto_finalize:
738                if pre_finalize:
739                    pre_finalize()
740                if cancel:
741                    self.qmp_log('job-cancel', id=job)
742                else:
743                    self.qmp_log('job-finalize', id=job)
744            elif status == 'concluded' and not auto_dismiss:
745                self.qmp_log('job-dismiss', id=job)
746            elif status == 'null':
747                return error
748
749    # Returns None on success, and an error string on failure
750    def blockdev_create(self, options, job_id='job0', filters=None):
751        if filters is None:
752            filters = [filter_qmp_testfiles]
753        result = self.qmp_log('blockdev-create', filters=filters,
754                              job_id=job_id, options=options)
755
756        if 'return' in result:
757            assert result['return'] == {}
758            job_result = self.run_job(job_id)
759        else:
760            job_result = result['error']
761
762        log("")
763        return job_result
764
765    def enable_migration_events(self, name):
766        log('Enabling migration QMP events on %s...' % name)
767        log(self.qmp('migrate-set-capabilities', capabilities=[
768            {
769                'capability': 'events',
770                'state': True
771            }
772        ]))
773
774    def wait_migration(self, expect_runstate: Optional[str]) -> bool:
775        while True:
776            event = self.event_wait('MIGRATION')
777            # We use the default timeout, and with a timeout, event_wait()
778            # never returns None
779            assert event
780
781            log(event, filters=[filter_qmp_event])
782            if event['data']['status'] in ('completed', 'failed'):
783                break
784
785        if event['data']['status'] == 'completed':
786            # The event may occur in finish-migrate, so wait for the expected
787            # post-migration runstate
788            runstate = None
789            while runstate != expect_runstate:
790                runstate = self.qmp('query-status')['return']['status']
791            return True
792        else:
793            return False
794
795    def node_info(self, node_name):
796        nodes = self.qmp('query-named-block-nodes')
797        for x in nodes['return']:
798            if x['node-name'] == node_name:
799                return x
800        return None
801
802    def query_bitmaps(self):
803        res = self.qmp("query-named-block-nodes")
804        return {device['node-name']: device['dirty-bitmaps']
805                for device in res['return'] if 'dirty-bitmaps' in device}
806
807    def get_bitmap(self, node_name, bitmap_name, recording=None, bitmaps=None):
808        """
809        get a specific bitmap from the object returned by query_bitmaps.
810        :param recording: If specified, filter results by the specified value.
811        :param bitmaps: If specified, use it instead of call query_bitmaps()
812        """
813        if bitmaps is None:
814            bitmaps = self.query_bitmaps()
815
816        for bitmap in bitmaps[node_name]:
817            if bitmap.get('name', '') == bitmap_name:
818                if recording is None or bitmap.get('recording') == recording:
819                    return bitmap
820        return None
821
822    def check_bitmap_status(self, node_name, bitmap_name, fields):
823        ret = self.get_bitmap(node_name, bitmap_name)
824
825        return fields.items() <= ret.items()
826
827    def assert_block_path(self, root, path, expected_node, graph=None):
828        """
829        Check whether the node under the given path in the block graph
830        is @expected_node.
831
832        @root is the node name of the node where the @path is rooted.
833
834        @path is a string that consists of child names separated by
835        slashes.  It must begin with a slash.
836
837        Examples for @root + @path:
838          - root="qcow2-node", path="/backing/file"
839          - root="quorum-node", path="/children.2/file"
840
841        Hypothetically, @path could be empty, in which case it would
842        point to @root.  However, in practice this case is not useful
843        and hence not allowed.
844
845        @expected_node may be None.  (All elements of the path but the
846        leaf must still exist.)
847
848        @graph may be None or the result of an x-debug-query-block-graph
849        call that has already been performed.
850        """
851        if graph is None:
852            graph = self.qmp('x-debug-query-block-graph')['return']
853
854        iter_path = iter(path.split('/'))
855
856        # Must start with a /
857        assert next(iter_path) == ''
858
859        node = next((node for node in graph['nodes'] if node['name'] == root),
860                    None)
861
862        # An empty @path is not allowed, so the root node must be present
863        assert node is not None, 'Root node %s not found' % root
864
865        for child_name in iter_path:
866            assert node is not None, 'Cannot follow path %s%s' % (root, path)
867
868            try:
869                node_id = next(edge['child'] for edge in graph['edges']
870                               if (edge['parent'] == node['id'] and
871                                   edge['name'] == child_name))
872
873                node = next(node for node in graph['nodes']
874                            if node['id'] == node_id)
875
876            except StopIteration:
877                node = None
878
879        if node is None:
880            assert expected_node is None, \
881                   'No node found under %s (but expected %s)' % \
882                   (path, expected_node)
883        else:
884            assert node['name'] == expected_node, \
885                   'Found node %s under %s (but expected %s)' % \
886                   (node['name'], path, expected_node)
887
888index_re = re.compile(r'([^\[]+)\[([^\]]+)\]')
889
890class QMPTestCase(unittest.TestCase):
891    '''Abstract base class for QMP test cases'''
892
893    def __init__(self, *args, **kwargs):
894        super().__init__(*args, **kwargs)
895        # Many users of this class set a VM property we rely on heavily
896        # in the methods below.
897        self.vm = None
898
899    def dictpath(self, d, path):
900        '''Traverse a path in a nested dict'''
901        for component in path.split('/'):
902            m = index_re.match(component)
903            if m:
904                component, idx = m.groups()
905                idx = int(idx)
906
907            if not isinstance(d, dict) or component not in d:
908                self.fail(f'failed path traversal for "{path}" in "{d}"')
909            d = d[component]
910
911            if m:
912                if not isinstance(d, list):
913                    self.fail(f'path component "{component}" in "{path}" '
914                              f'is not a list in "{d}"')
915                try:
916                    d = d[idx]
917                except IndexError:
918                    self.fail(f'invalid index "{idx}" in path "{path}" '
919                              f'in "{d}"')
920        return d
921
922    def assert_qmp_absent(self, d, path):
923        try:
924            result = self.dictpath(d, path)
925        except AssertionError:
926            return
927        self.fail('path "%s" has value "%s"' % (path, str(result)))
928
929    def assert_qmp(self, d, path, value):
930        '''Assert that the value for a specific path in a QMP dict
931           matches.  When given a list of values, assert that any of
932           them matches.'''
933
934        result = self.dictpath(d, path)
935
936        # [] makes no sense as a list of valid values, so treat it as
937        # an actual single value.
938        if isinstance(value, list) and value != []:
939            for v in value:
940                if result == v:
941                    return
942            self.fail('no match for "%s" in %s' % (str(result), str(value)))
943        else:
944            self.assertEqual(result, value,
945                             '"%s" is "%s", expected "%s"'
946                             % (path, str(result), str(value)))
947
948    def assert_no_active_block_jobs(self):
949        result = self.vm.qmp('query-block-jobs')
950        self.assert_qmp(result, 'return', [])
951
952    def assert_has_block_node(self, node_name=None, file_name=None):
953        """Issue a query-named-block-nodes and assert node_name and/or
954        file_name is present in the result"""
955        def check_equal_or_none(a, b):
956            return a is None or b is None or a == b
957        assert node_name or file_name
958        result = self.vm.qmp('query-named-block-nodes')
959        for x in result["return"]:
960            if check_equal_or_none(x.get("node-name"), node_name) and \
961                    check_equal_or_none(x.get("file"), file_name):
962                return
963        self.fail("Cannot find %s %s in result:\n%s" %
964                  (node_name, file_name, result))
965
966    def assert_json_filename_equal(self, json_filename, reference):
967        '''Asserts that the given filename is a json: filename and that its
968           content is equal to the given reference object'''
969        self.assertEqual(json_filename[:5], 'json:')
970        self.assertEqual(
971            self.vm.flatten_qmp_object(json.loads(json_filename[5:])),
972            self.vm.flatten_qmp_object(reference)
973        )
974
975    def cancel_and_wait(self, drive='drive0', force=False,
976                        resume=False, wait=60.0):
977        '''Cancel a block job and wait for it to finish, returning the event'''
978        result = self.vm.qmp('block-job-cancel', device=drive, force=force)
979        self.assert_qmp(result, 'return', {})
980
981        if resume:
982            self.vm.resume_drive(drive)
983
984        cancelled = False
985        result = None
986        while not cancelled:
987            for event in self.vm.get_qmp_events(wait=wait):
988                if event['event'] == 'BLOCK_JOB_COMPLETED' or \
989                   event['event'] == 'BLOCK_JOB_CANCELLED':
990                    self.assert_qmp(event, 'data/device', drive)
991                    result = event
992                    cancelled = True
993                elif event['event'] == 'JOB_STATUS_CHANGE':
994                    self.assert_qmp(event, 'data/id', drive)
995
996
997        self.assert_no_active_block_jobs()
998        return result
999
1000    def wait_until_completed(self, drive='drive0', check_offset=True,
1001                             wait=60.0, error=None):
1002        '''Wait for a block job to finish, returning the event'''
1003        while True:
1004            for event in self.vm.get_qmp_events(wait=wait):
1005                if event['event'] == 'BLOCK_JOB_COMPLETED':
1006                    self.assert_qmp(event, 'data/device', drive)
1007                    if error is None:
1008                        self.assert_qmp_absent(event, 'data/error')
1009                        if check_offset:
1010                            self.assert_qmp(event, 'data/offset',
1011                                            event['data']['len'])
1012                    else:
1013                        self.assert_qmp(event, 'data/error', error)
1014                    self.assert_no_active_block_jobs()
1015                    return event
1016                if event['event'] == 'JOB_STATUS_CHANGE':
1017                    self.assert_qmp(event, 'data/id', drive)
1018
1019    def wait_ready(self, drive='drive0'):
1020        """Wait until a BLOCK_JOB_READY event, and return the event."""
1021        return self.vm.events_wait([
1022            ('BLOCK_JOB_READY',
1023             {'data': {'type': 'mirror', 'device': drive}}),
1024            ('BLOCK_JOB_READY',
1025             {'data': {'type': 'commit', 'device': drive}})
1026        ])
1027
1028    def wait_ready_and_cancel(self, drive='drive0'):
1029        self.wait_ready(drive=drive)
1030        event = self.cancel_and_wait(drive=drive)
1031        self.assertEqual(event['event'], 'BLOCK_JOB_COMPLETED')
1032        self.assert_qmp(event, 'data/type', 'mirror')
1033        self.assert_qmp(event, 'data/offset', event['data']['len'])
1034
1035    def complete_and_wait(self, drive='drive0', wait_ready=True,
1036                          completion_error=None):
1037        '''Complete a block job and wait for it to finish'''
1038        if wait_ready:
1039            self.wait_ready(drive=drive)
1040
1041        result = self.vm.qmp('block-job-complete', device=drive)
1042        self.assert_qmp(result, 'return', {})
1043
1044        event = self.wait_until_completed(drive=drive, error=completion_error)
1045        self.assertTrue(event['data']['type'] in ['mirror', 'commit'])
1046
1047    def pause_wait(self, job_id='job0'):
1048        with Timeout(3, "Timeout waiting for job to pause"):
1049            while True:
1050                result = self.vm.qmp('query-block-jobs')
1051                found = False
1052                for job in result['return']:
1053                    if job['device'] == job_id:
1054                        found = True
1055                        if job['paused'] and not job['busy']:
1056                            return job
1057                        break
1058                assert found
1059
1060    def pause_job(self, job_id='job0', wait=True):
1061        result = self.vm.qmp('block-job-pause', device=job_id)
1062        self.assert_qmp(result, 'return', {})
1063        if wait:
1064            return self.pause_wait(job_id)
1065        return result
1066
1067    def case_skip(self, reason):
1068        '''Skip this test case'''
1069        case_notrun(reason)
1070        self.skipTest(reason)
1071
1072
1073def notrun(reason):
1074    '''Skip this test suite'''
1075    # Each test in qemu-iotests has a number ("seq")
1076    seq = os.path.basename(sys.argv[0])
1077
1078    open('%s/%s.notrun' % (output_dir, seq), 'w').write(reason + '\n')
1079    logger.warning("%s not run: %s", seq, reason)
1080    sys.exit(0)
1081
1082def case_notrun(reason):
1083    '''Mark this test case as not having been run (without actually
1084    skipping it, that is left to the caller).  See
1085    QMPTestCase.case_skip() for a variant that actually skips the
1086    current test case.'''
1087
1088    # Each test in qemu-iotests has a number ("seq")
1089    seq = os.path.basename(sys.argv[0])
1090
1091    open('%s/%s.casenotrun' % (output_dir, seq), 'a').write(
1092        '    [case not run] ' + reason + '\n')
1093
1094def _verify_image_format(supported_fmts: Sequence[str] = (),
1095                         unsupported_fmts: Sequence[str] = ()) -> None:
1096    if 'generic' in supported_fmts and \
1097            os.environ.get('IMGFMT_GENERIC', 'true') == 'true':
1098        # similar to
1099        #   _supported_fmt generic
1100        # for bash tests
1101        supported_fmts = ()
1102
1103    not_sup = supported_fmts and (imgfmt not in supported_fmts)
1104    if not_sup or (imgfmt in unsupported_fmts):
1105        notrun('not suitable for this image format: %s' % imgfmt)
1106
1107    if imgfmt == 'luks':
1108        verify_working_luks()
1109
1110def _verify_protocol(supported: Sequence[str] = (),
1111                     unsupported: Sequence[str] = ()) -> None:
1112    assert not (supported and unsupported)
1113
1114    if 'generic' in supported:
1115        return
1116
1117    not_sup = supported and (imgproto not in supported)
1118    if not_sup or (imgproto in unsupported):
1119        notrun('not suitable for this protocol: %s' % imgproto)
1120
1121def _verify_platform(supported: Sequence[str] = (),
1122                     unsupported: Sequence[str] = ()) -> None:
1123    if any((sys.platform.startswith(x) for x in unsupported)):
1124        notrun('not suitable for this OS: %s' % sys.platform)
1125
1126    if supported:
1127        if not any((sys.platform.startswith(x) for x in supported)):
1128            notrun('not suitable for this OS: %s' % sys.platform)
1129
1130def _verify_cache_mode(supported_cache_modes: Sequence[str] = ()) -> None:
1131    if supported_cache_modes and (cachemode not in supported_cache_modes):
1132        notrun('not suitable for this cache mode: %s' % cachemode)
1133
1134def _verify_aio_mode(supported_aio_modes: Sequence[str] = ()) -> None:
1135    if supported_aio_modes and (aiomode not in supported_aio_modes):
1136        notrun('not suitable for this aio mode: %s' % aiomode)
1137
1138def _verify_formats(required_formats: Sequence[str] = ()) -> None:
1139    usf_list = list(set(required_formats) - set(supported_formats()))
1140    if usf_list:
1141        notrun(f'formats {usf_list} are not whitelisted')
1142
1143
1144def _verify_virtio_blk() -> None:
1145    out = qemu_pipe('-M', 'none', '-device', 'help')
1146    if 'virtio-blk' not in out:
1147        notrun('Missing virtio-blk in QEMU binary')
1148
1149def _verify_virtio_scsi_pci_or_ccw() -> None:
1150    out = qemu_pipe('-M', 'none', '-device', 'help')
1151    if 'virtio-scsi-pci' not in out and 'virtio-scsi-ccw' not in out:
1152        notrun('Missing virtio-scsi-pci or virtio-scsi-ccw in QEMU binary')
1153
1154
1155def supports_quorum():
1156    return 'quorum' in qemu_img_pipe('--help')
1157
1158def verify_quorum():
1159    '''Skip test suite if quorum support is not available'''
1160    if not supports_quorum():
1161        notrun('quorum support missing')
1162
1163def has_working_luks() -> Tuple[bool, str]:
1164    """
1165    Check whether our LUKS driver can actually create images
1166    (this extends to LUKS encryption for qcow2).
1167
1168    If not, return the reason why.
1169    """
1170
1171    img_file = f'{test_dir}/luks-test.luks'
1172    (output, status) = \
1173        qemu_img_pipe_and_status('create', '-f', 'luks',
1174                                 '--object', luks_default_secret_object,
1175                                 '-o', luks_default_key_secret_opt,
1176                                 '-o', 'iter-time=10',
1177                                 img_file, '1G')
1178    try:
1179        os.remove(img_file)
1180    except OSError:
1181        pass
1182
1183    if status != 0:
1184        reason = output
1185        for line in output.splitlines():
1186            if img_file + ':' in line:
1187                reason = line.split(img_file + ':', 1)[1].strip()
1188                break
1189
1190        return (False, reason)
1191    else:
1192        return (True, '')
1193
1194def verify_working_luks():
1195    """
1196    Skip test suite if LUKS does not work
1197    """
1198    (working, reason) = has_working_luks()
1199    if not working:
1200        notrun(reason)
1201
1202def qemu_pipe(*args: str) -> str:
1203    """
1204    Run qemu with an option to print something and exit (e.g. a help option).
1205
1206    :return: QEMU's stdout output.
1207    """
1208    full_args = [qemu_prog] + qemu_opts + list(args)
1209    output, _ = qemu_tool_pipe_and_status('qemu', full_args)
1210    return output
1211
1212def supported_formats(read_only=False):
1213    '''Set 'read_only' to True to check ro-whitelist
1214       Otherwise, rw-whitelist is checked'''
1215
1216    if not hasattr(supported_formats, "formats"):
1217        supported_formats.formats = {}
1218
1219    if read_only not in supported_formats.formats:
1220        format_message = qemu_pipe("-drive", "format=help")
1221        line = 1 if read_only else 0
1222        supported_formats.formats[read_only] = \
1223            format_message.splitlines()[line].split(":")[1].split()
1224
1225    return supported_formats.formats[read_only]
1226
1227def skip_if_unsupported(required_formats=(), read_only=False):
1228    '''Skip Test Decorator
1229       Runs the test if all the required formats are whitelisted'''
1230    def skip_test_decorator(func):
1231        def func_wrapper(test_case: QMPTestCase, *args: List[Any],
1232                         **kwargs: Dict[str, Any]) -> None:
1233            if callable(required_formats):
1234                fmts = required_formats(test_case)
1235            else:
1236                fmts = required_formats
1237
1238            usf_list = list(set(fmts) - set(supported_formats(read_only)))
1239            if usf_list:
1240                msg = f'{test_case}: formats {usf_list} are not whitelisted'
1241                test_case.case_skip(msg)
1242            else:
1243                func(test_case, *args, **kwargs)
1244        return func_wrapper
1245    return skip_test_decorator
1246
1247def skip_for_formats(formats: Sequence[str] = ()) \
1248    -> Callable[[Callable[[QMPTestCase, List[Any], Dict[str, Any]], None]],
1249                Callable[[QMPTestCase, List[Any], Dict[str, Any]], None]]:
1250    '''Skip Test Decorator
1251       Skips the test for the given formats'''
1252    def skip_test_decorator(func):
1253        def func_wrapper(test_case: QMPTestCase, *args: List[Any],
1254                         **kwargs: Dict[str, Any]) -> None:
1255            if imgfmt in formats:
1256                msg = f'{test_case}: Skipped for format {imgfmt}'
1257                test_case.case_skip(msg)
1258            else:
1259                func(test_case, *args, **kwargs)
1260        return func_wrapper
1261    return skip_test_decorator
1262
1263def skip_if_user_is_root(func):
1264    '''Skip Test Decorator
1265       Runs the test only without root permissions'''
1266    def func_wrapper(*args, **kwargs):
1267        if os.getuid() == 0:
1268            case_notrun('{}: cannot be run as root'.format(args[0]))
1269            return None
1270        else:
1271            return func(*args, **kwargs)
1272    return func_wrapper
1273
1274def execute_unittest(debug=False):
1275    """Executes unittests within the calling module."""
1276
1277    verbosity = 2 if debug else 1
1278
1279    if debug:
1280        output = sys.stdout
1281    else:
1282        # We need to filter out the time taken from the output so that
1283        # qemu-iotest can reliably diff the results against master output.
1284        output = io.StringIO()
1285
1286    runner = unittest.TextTestRunner(stream=output, descriptions=True,
1287                                     verbosity=verbosity)
1288    try:
1289        # unittest.main() will use sys.exit(); so expect a SystemExit
1290        # exception
1291        unittest.main(testRunner=runner)
1292    finally:
1293        # We need to filter out the time taken from the output so that
1294        # qemu-iotest can reliably diff the results against master output.
1295        if not debug:
1296            out = output.getvalue()
1297            out = re.sub(r'Ran (\d+) tests? in [\d.]+s', r'Ran \1 tests', out)
1298
1299            # Hide skipped tests from the reference output
1300            out = re.sub(r'OK \(skipped=\d+\)', 'OK', out)
1301            out_first_line, out_rest = out.split('\n', 1)
1302            out = out_first_line.replace('s', '.') + '\n' + out_rest
1303
1304            sys.stderr.write(out)
1305
1306def execute_setup_common(supported_fmts: Sequence[str] = (),
1307                         supported_platforms: Sequence[str] = (),
1308                         supported_cache_modes: Sequence[str] = (),
1309                         supported_aio_modes: Sequence[str] = (),
1310                         unsupported_fmts: Sequence[str] = (),
1311                         supported_protocols: Sequence[str] = (),
1312                         unsupported_protocols: Sequence[str] = (),
1313                         required_fmts: Sequence[str] = ()) -> bool:
1314    """
1315    Perform necessary setup for either script-style or unittest-style tests.
1316
1317    :return: Bool; Whether or not debug mode has been requested via the CLI.
1318    """
1319    # Note: Python 3.6 and pylint do not like 'Collection' so use 'Sequence'.
1320
1321    debug = '-d' in sys.argv
1322    if debug:
1323        sys.argv.remove('-d')
1324    logging.basicConfig(level=(logging.DEBUG if debug else logging.WARN))
1325
1326    _verify_image_format(supported_fmts, unsupported_fmts)
1327    _verify_protocol(supported_protocols, unsupported_protocols)
1328    _verify_platform(supported=supported_platforms)
1329    _verify_cache_mode(supported_cache_modes)
1330    _verify_aio_mode(supported_aio_modes)
1331    _verify_formats(required_fmts)
1332    _verify_virtio_blk()
1333
1334    return debug
1335
1336def execute_test(*args, test_function=None, **kwargs):
1337    """Run either unittest or script-style tests."""
1338
1339    debug = execute_setup_common(*args, **kwargs)
1340    if not test_function:
1341        execute_unittest(debug)
1342    else:
1343        test_function()
1344
1345def activate_logging():
1346    """Activate iotests.log() output to stdout for script-style tests."""
1347    handler = logging.StreamHandler(stream=sys.stdout)
1348    formatter = logging.Formatter('%(message)s')
1349    handler.setFormatter(formatter)
1350    test_logger.addHandler(handler)
1351    test_logger.setLevel(logging.INFO)
1352    test_logger.propagate = False
1353
1354# This is called from script-style iotests without a single point of entry
1355def script_initialize(*args, **kwargs):
1356    """Initialize script-style tests without running any tests."""
1357    activate_logging()
1358    execute_setup_common(*args, **kwargs)
1359
1360# This is called from script-style iotests with a single point of entry
1361def script_main(test_function, *args, **kwargs):
1362    """Run script-style tests outside of the unittest framework"""
1363    activate_logging()
1364    execute_test(*args, test_function=test_function, **kwargs)
1365
1366# This is called from unittest style iotests
1367def main(*args, **kwargs):
1368    """Run tests using the unittest framework"""
1369    execute_test(*args, **kwargs)
1370