]> git.proxmox.com Git - mirror_qemu.git/blame - tests/qemu-iotests/iotests.py
Merge tag 'for-upstream' of https://repo.or.cz/qemu/kevin into staging
[mirror_qemu.git] / tests / qemu-iotests / iotests.py
CommitLineData
f345cfd0
SH
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
22e29bce 19import argparse
b404b13b 20import atexit
55b11630 21import bz2
b404b13b
JS
22from collections import OrderedDict
23import faulthandler
b404b13b
JS
24import json
25import logging
f345cfd0
SH
26import os
27import re
55b11630 28import shutil
b404b13b
JS
29import signal
30import struct
f345cfd0 31import subprocess
ed338bb0 32import sys
b7719bca 33import time
206dc475 34from typing import (Any, Callable, Dict, Iterable, Iterator,
f29f4c25 35 List, Optional, Sequence, TextIO, Tuple, Type, TypeVar)
b404b13b 36import unittest
f345cfd0 37
b7719bca
NS
38from contextlib import contextmanager
39
beb6b57b 40from qemu.machine import qtest
37094b6d 41from qemu.qmp.legacy import QMPMessage, QEMUMonitorProtocol
2882ccf8 42from qemu.utils import VerboseProcessError
02f3a911 43
52ea799e
JS
44# Use this logger for logging messages directly from the iotests module
45logger = logging.getLogger('qemu.iotests')
46logger.addHandler(logging.NullHandler())
47
48# Use this logger for messages that ought to be used for diff output.
49test_logger = logging.getLogger('qemu.iotests.diff_io')
50
51
aa1cbeb8
KW
52faulthandler.enable()
53
934659c4 54# This will not work if arguments contain spaces but is necessary if we
f345cfd0 55# want to support the override options that ./check supports.
934659c4
HR
56qemu_img_args = [os.environ.get('QEMU_IMG_PROG', 'qemu-img')]
57if os.environ.get('QEMU_IMG_OPTIONS'):
58 qemu_img_args += os.environ['QEMU_IMG_OPTIONS'].strip().split(' ')
59
60qemu_io_args = [os.environ.get('QEMU_IO_PROG', 'qemu-io')]
61if os.environ.get('QEMU_IO_OPTIONS'):
62 qemu_io_args += os.environ['QEMU_IO_OPTIONS'].strip().split(' ')
63
a4d925f8
AS
64qemu_io_args_no_fmt = [os.environ.get('QEMU_IO_PROG', 'qemu-io')]
65if os.environ.get('QEMU_IO_OPTIONS_NO_FMT'):
66 qemu_io_args_no_fmt += \
67 os.environ['QEMU_IO_OPTIONS_NO_FMT'].strip().split(' ')
68
81b6b2bc
KW
69qemu_nbd_prog = os.environ.get('QEMU_NBD_PROG', 'qemu-nbd')
70qemu_nbd_args = [qemu_nbd_prog]
bec87774
HR
71if os.environ.get('QEMU_NBD_OPTIONS'):
72 qemu_nbd_args += os.environ['QEMU_NBD_OPTIONS'].strip().split(' ')
73
4c44b4a4 74qemu_prog = os.environ.get('QEMU_PROG', 'qemu')
66613974 75qemu_opts = os.environ.get('QEMU_OPTIONS', '').strip().split(' ')
f345cfd0 76
091dc7b2
HR
77qsd_prog = os.environ.get('QSD_PROG', 'qemu-storage-daemon')
78
cfb9b0b7
EGE
79gdb_qemu_env = os.environ.get('GDB_OPTIONS')
80qemu_gdb = []
81if gdb_qemu_env:
82 qemu_gdb = ['gdbserver'] + gdb_qemu_env.strip().split(' ')
83
eb7a91d0
EGE
84qemu_print = os.environ.get('PRINT_QEMU', False)
85
f345cfd0
SH
86imgfmt = os.environ.get('IMGFMT', 'raw')
87imgproto = os.environ.get('IMGPROTO', 'file')
3e0105e0
HR
88
89try:
90 test_dir = os.environ['TEST_DIR']
91 sock_dir = os.environ['SOCK_DIR']
92 cachemode = os.environ['CACHEMODE']
93 aiomode = os.environ['AIOMODE']
94 qemu_default_machine = os.environ['QEMU_DEFAULT_MACHINE']
95except KeyError:
96 # We are using these variables as proxies to indicate that we're
97 # not being run via "check". There may be other things set up by
98 # "check" that individual test cases rely on.
99 sys.stderr.write('Please run this test via the "check" script\n')
100 sys.exit(os.EX_USAGE)
f345cfd0 101
a9b4c6bb
EGE
102qemu_valgrind = []
103if os.environ.get('VALGRIND_QEMU') == "y" and \
104 os.environ.get('NO_VALGRIND') != "y":
105 valgrind_logfile = "--log-file=" + test_dir
106 # %p allows to put the valgrind process PID, since
107 # we don't know it a priori (subprocess.Popen is
108 # not yet invoked)
109 valgrind_logfile += "/%p.valgrind"
110
111 qemu_valgrind = ['valgrind', valgrind_logfile, '--error-exitcode=99']
112
85a353a0 113luks_default_secret_object = 'secret,id=keysec0,data=' + \
58ebcb65 114 os.environ.get('IMGKEYSECRET', '')
85a353a0
VSO
115luks_default_key_secret_opt = 'key-secret=keysec0'
116
55b11630
VSO
117sample_img_dir = os.environ['SAMPLE_IMG_DIR']
118
119
206dc475
JS
120@contextmanager
121def change_log_level(
122 logger_name: str, level: int = logging.CRITICAL) -> Iterator[None]:
123 """
124 Utility function for temporarily changing the log level of a logger.
125
126 This can be used to silence errors that are expected or uninteresting.
127 """
128 _logger = logging.getLogger(logger_name)
129 current_level = _logger.level
130 _logger.setLevel(level)
131
132 try:
133 yield
134 finally:
135 _logger.setLevel(current_level)
136
137
55b11630
VSO
138def unarchive_sample_image(sample, fname):
139 sample_fname = os.path.join(sample_img_dir, sample + '.bz2')
140 with bz2.open(sample_fname) as f_in, open(fname, 'wb') as f_out:
141 shutil.copyfileobj(f_in, f_out)
142
85a353a0 143
c34ec513
VSO
144def qemu_tool_popen(args: Sequence[str],
145 connect_stderr: bool = True) -> 'subprocess.Popen[str]':
146 stderr = subprocess.STDOUT if connect_stderr else None
147 # pylint: disable=consider-using-with
148 return subprocess.Popen(args,
149 stdout=subprocess.PIPE,
150 stderr=stderr,
151 universal_newlines=True)
152
153
91efbae9 154def qemu_tool_pipe_and_status(tool: str, args: Sequence[str],
a70eeb3d
VSO
155 connect_stderr: bool = True,
156 drop_successful_output: bool = False) \
157 -> Tuple[str, int]:
d849acab 158 """
91efbae9 159 Run a tool and return both its output and its exit code
d849acab 160 """
c34ec513 161 with qemu_tool_popen(args, connect_stderr) as subp:
ac4e14f5
EGE
162 output = subp.communicate()[0]
163 if subp.returncode < 0:
164 cmd = ' '.join(args)
165 sys.stderr.write(f'{tool} received signal \
166 {-subp.returncode}: {cmd}\n')
a70eeb3d
VSO
167 if drop_successful_output and subp.returncode == 0:
168 output = ''
ac4e14f5 169 return (output, subp.returncode)
d849acab 170
22e29bce
VSO
171def qemu_img_create_prepare_args(args: List[str]) -> List[str]:
172 if not args or args[0] != 'create':
173 return list(args)
174 args = args[1:]
175
176 p = argparse.ArgumentParser(allow_abbrev=False)
28a5ad93
VSO
177 # -o option may be specified several times
178 p.add_argument('-o', action='append', default=[])
22e29bce
VSO
179 p.add_argument('-f')
180 parsed, remaining = p.parse_known_args(args)
181
28a5ad93
VSO
182 opts_list = parsed.o
183
22e29bce
VSO
184 result = ['create']
185 if parsed.f is not None:
186 result += ['-f', parsed.f]
187
188 # IMGOPTS most probably contain options specific for the selected format,
189 # like extended_l2 or compression_type for qcow2. Test may want to create
190 # additional images in other formats that doesn't support these options.
191 # So, use IMGOPTS only for images created in imgfmt format.
28a5ad93
VSO
192 imgopts = os.environ.get('IMGOPTS')
193 if imgopts and parsed.f == imgfmt:
194 opts_list.insert(0, imgopts)
195
196 # default luks support
197 if parsed.f == 'luks' and \
198 all('key-secret' not in opts for opts in opts_list):
199 result += ['--object', luks_default_secret_object]
200 opts_list.append(luks_default_key_secret_opt)
201
202 for opts in opts_list:
203 result += ['-o', opts]
22e29bce
VSO
204
205 result += remaining
206
207 return result
208
2882ccf8 209
b2d68a8e
JS
210def qemu_tool(*args: str, check: bool = True, combine_stdio: bool = True
211 ) -> 'subprocess.CompletedProcess[str]':
212 """
213 Run a qemu tool and return its status code and console output.
2882ccf8 214
b2d68a8e 215 :param args: full command line to run.
2882ccf8
JS
216 :param check: Enforce a return code of zero.
217 :param combine_stdio: set to False to keep stdout/stderr separated.
218
219 :raise VerboseProcessError:
220 When the return code is negative, or on any non-zero exit code
221 when 'check=True' was provided (the default). This exception has
222 'stdout', 'stderr', and 'returncode' properties that may be
223 inspected to show greater detail. If this exception is not
224 handled, the command-line, return code, and all console output
225 will be included at the bottom of the stack trace.
226
227 :return:
228 a CompletedProcess. This object has args, returncode, and stdout
229 properties. If streams are not combined, it will also have a
230 stderr property.
231 """
2882ccf8 232 subp = subprocess.run(
b2d68a8e 233 args,
2882ccf8
JS
234 stdout=subprocess.PIPE,
235 stderr=subprocess.STDOUT if combine_stdio else subprocess.PIPE,
236 universal_newlines=True,
237 check=False
238 )
239
240 if check and subp.returncode or (subp.returncode < 0):
241 raise VerboseProcessError(
b2d68a8e 242 subp.returncode, args,
2882ccf8
JS
243 output=subp.stdout,
244 stderr=subp.stderr,
245 )
246
247 return subp
248
f345cfd0 249
b2d68a8e
JS
250def qemu_img(*args: str, check: bool = True, combine_stdio: bool = True
251 ) -> 'subprocess.CompletedProcess[str]':
252 """
253 Run QEMU_IMG_PROG and return its status code and console output.
254
255 This function always prepends QEMU_IMG_OPTIONS and may further alter
256 the args for 'create' commands.
257
258 See `qemu_tool()` for greater detail.
259 """
260 full_args = qemu_img_args + qemu_img_create_prepare_args(list(args))
261 return qemu_tool(*full_args, check=check, combine_stdio=combine_stdio)
262
263
8a57a4be 264def ordered_qmp(qmsg, conv_keys=True):
039be85c
JS
265 # Dictionaries are not ordered prior to 3.6, therefore:
266 if isinstance(qmsg, list):
267 return [ordered_qmp(atom) for atom in qmsg]
268 if isinstance(qmsg, dict):
269 od = OrderedDict()
270 for k, v in sorted(qmsg.items()):
8a57a4be
HR
271 if conv_keys:
272 k = k.replace('_', '-')
273 od[k] = ordered_qmp(v, conv_keys=False)
039be85c
JS
274 return od
275 return qmsg
0706e87d 276
2882ccf8 277def qemu_img_create(*args: str) -> 'subprocess.CompletedProcess[str]':
28a5ad93 278 return qemu_img('create', *args)
85a353a0 279
29768d04
JS
280def qemu_img_json(*args: str) -> Any:
281 """
282 Run qemu-img and return its output as deserialized JSON.
283
284 :raise CalledProcessError:
285 When qemu-img crashes, or returns a non-zero exit code without
286 producing a valid JSON document to stdout.
287 :raise JSONDecoderError:
288 When qemu-img returns 0, but failed to produce a valid JSON document.
289
290 :return: A deserialized JSON object; probably a dict[str, Any].
291 """
292 try:
293 res = qemu_img(*args, combine_stdio=False)
294 except subprocess.CalledProcessError as exc:
295 # Terminated due to signal. Don't bother.
296 if exc.returncode < 0:
297 raise
298
299 # Commands like 'check' can return failure (exit codes 2 and 3)
300 # to indicate command completion, but with errors found. For
301 # multi-command flexibility, ignore the exact error codes and
302 # *try* to load JSON.
303 try:
304 return json.loads(exc.stdout)
305 except json.JSONDecodeError:
306 # Nope. This thing is toast. Raise the /process/ error.
307 pass
308 raise
309
310 return json.loads(res.stdout)
311
0f7d7d72
JS
312def qemu_img_measure(*args: str) -> Any:
313 return qemu_img_json("measure", "--output", "json", *args)
4b914b01 314
0f7d7d72
JS
315def qemu_img_check(*args: str) -> Any:
316 return qemu_img_json("check", "--output", "json", *args)
4b914b01 317
9ebb2b76
JS
318def qemu_img_info(*args: str) -> Any:
319 return qemu_img_json('info', "--output", "json", *args)
320
1670ae7a
JS
321def qemu_img_map(*args: str) -> Any:
322 return qemu_img_json('map', "--output", "json", *args)
323
8f685ac3
JS
324def qemu_img_log(*args: str, check: bool = True
325 ) -> 'subprocess.CompletedProcess[str]':
326 result = qemu_img(*args, check=check)
f400e14d 327 log(result.stdout, filters=[filter_testfiles])
ac6fb43e
KW
328 return result
329
f400e14d
JS
330def img_info_log(filename: str, filter_path: Optional[str] = None,
331 use_image_opts: bool = False, extra_args: Sequence[str] = (),
bcc6777a 332 check: bool = True, drop_child_info: bool = True,
f400e14d 333 ) -> None:
6a96d87c 334 args = ['info']
3bd2b942 335 if use_image_opts:
5ba141dc
KW
336 args.append('--image-opts')
337 else:
6a96d87c 338 args += ['-f', imgfmt]
5ba141dc
KW
339 args += extra_args
340 args.append(filename)
341
8f685ac3 342 output = qemu_img(*args, check=check).stdout
6b605ade
KW
343 if not filter_path:
344 filter_path = filename
bcc6777a 345 log(filter_img_info(output, filter_path, drop_child_info))
6b605ade 346
94a781f2
VSO
347def qemu_io_wrap_args(args: Sequence[str]) -> List[str]:
348 if '-f' in args or '--image-opts' in args:
349 return qemu_io_args_no_fmt + list(args)
350 else:
351 return qemu_io_args + list(args)
352
75c90eee
VSO
353def qemu_io_popen(*args):
354 return qemu_tool_popen(qemu_io_wrap_args(args))
355
6dede6a4
JS
356def qemu_io(*args: str, check: bool = True, combine_stdio: bool = True
357 ) -> 'subprocess.CompletedProcess[str]':
358 """
359 Run QEMU_IO_PROG and return the status code and console output.
360
361 This function always prepends either QEMU_IO_OPTIONS or
362 QEMU_IO_OPTIONS_NO_FMT.
363 """
364 return qemu_tool(*qemu_io_wrap_args(args),
365 check=check, combine_stdio=combine_stdio)
f345cfd0 366
40bfeae1
JS
367def qemu_io_log(*args: str, check: bool = True
368 ) -> 'subprocess.CompletedProcess[str]':
369 result = qemu_io(*args, check=check)
6dede6a4 370 log(result.stdout, filters=[filter_testfiles, filter_qemu_io])
a96f0350
KW
371 return result
372
9fa90eec
VSO
373class QemuIoInteractive:
374 def __init__(self, *args):
94a781f2 375 self.args = qemu_io_wrap_args(args)
ac4e14f5
EGE
376 # We need to keep the Popen objext around, and not
377 # close it immediately. Therefore, disable the pylint check:
378 # pylint: disable=consider-using-with
9fa90eec
VSO
379 self._p = subprocess.Popen(self.args, stdin=subprocess.PIPE,
380 stdout=subprocess.PIPE,
8eb5e674
HR
381 stderr=subprocess.STDOUT,
382 universal_newlines=True)
1f4b774a
VSO
383 out = self._p.stdout.read(9)
384 if out != 'qemu-io> ':
385 # Most probably qemu-io just failed to start.
386 # Let's collect the whole output and exit.
387 out += self._p.stdout.read()
388 self._p.wait(timeout=1)
389 raise ValueError(out)
9fa90eec
VSO
390
391 def close(self):
392 self._p.communicate('q\n')
393
394 def _read_output(self):
395 pattern = 'qemu-io> '
396 n = len(pattern)
397 pos = 0
398 s = []
399 while pos != n:
400 c = self._p.stdout.read(1)
401 # check unexpected EOF
402 assert c != ''
403 s.append(c)
404 if c == pattern[pos]:
405 pos += 1
406 else:
407 pos = 0
408
409 return ''.join(s[:-n])
410
411 def cmd(self, cmd):
412 # quit command is in close(), '\n' is added automatically
413 assert '\n' not in cmd
414 cmd = cmd.strip()
6a96d87c 415 assert cmd not in ('q', 'quit')
9fa90eec 416 self._p.stdin.write(cmd + '\n')
f544adf7 417 self._p.stdin.flush()
9fa90eec
VSO
418 return self._read_output()
419
420
091dc7b2 421class QemuStorageDaemon:
ec88eed8
HR
422 _qmp: Optional[QEMUMonitorProtocol] = None
423 _qmpsock: Optional[str] = None
424 # Python < 3.8 would complain if this type were not a string literal
425 # (importing `annotations` from `__future__` would work; but not on <= 3.6)
426 _p: 'Optional[subprocess.Popen[bytes]]' = None
427
428 def __init__(self, *args: str, instance_id: str = 'a', qmp: bool = False):
091dc7b2
HR
429 assert '--pidfile' not in args
430 self.pidfile = os.path.join(test_dir, f'qsd-{instance_id}-pid')
431 all_args = [qsd_prog] + list(args) + ['--pidfile', self.pidfile]
432
ec88eed8
HR
433 if qmp:
434 self._qmpsock = os.path.join(sock_dir, f'qsd-{instance_id}.sock')
435 all_args += ['--chardev',
436 f'socket,id=qmp-sock,path={self._qmpsock}',
437 '--monitor', 'qmp-sock']
438
439 self._qmp = QEMUMonitorProtocol(self._qmpsock, server=True)
440
091dc7b2
HR
441 # Cannot use with here, we want the subprocess to stay around
442 # pylint: disable=consider-using-with
443 self._p = subprocess.Popen(all_args)
ec88eed8
HR
444 if self._qmp is not None:
445 self._qmp.accept()
091dc7b2
HR
446 while not os.path.exists(self.pidfile):
447 if self._p.poll() is not None:
448 cmd = ' '.join(all_args)
449 raise RuntimeError(
450 'qemu-storage-daemon terminated with exit code ' +
451 f'{self._p.returncode}: {cmd}')
452
453 time.sleep(0.01)
454
455 with open(self.pidfile, encoding='utf-8') as f:
456 self._pid = int(f.read().strip())
457
458 assert self._pid == self._p.pid
459
ec88eed8
HR
460 def qmp(self, cmd: str, args: Optional[Dict[str, object]] = None) \
461 -> QMPMessage:
462 assert self._qmp is not None
463 return self._qmp.cmd(cmd, args)
464
95fdd8db
KW
465 def get_qmp(self) -> QEMUMonitorProtocol:
466 assert self._qmp is not None
467 return self._qmp
468
091dc7b2
HR
469 def stop(self, kill_signal=15):
470 self._p.send_signal(kill_signal)
471 self._p.wait()
472 self._p = None
473
ec88eed8
HR
474 if self._qmp:
475 self._qmp.close()
476
477 if self._qmpsock is not None:
478 try:
479 os.remove(self._qmpsock)
480 except OSError:
481 pass
091dc7b2
HR
482 try:
483 os.remove(self.pidfile)
484 except OSError:
485 pass
486
487 def __del__(self):
488 if self._p is not None:
489 self.stop(kill_signal=9)
490
491
bec87774
HR
492def qemu_nbd(*args):
493 '''Run qemu-nbd in daemon mode and return the parent's exit code'''
494 return subprocess.call(qemu_nbd_args + ['--fork'] + list(args))
495
91efbae9 496def qemu_nbd_early_pipe(*args: str) -> Tuple[int, str]:
e1e6eccd 497 '''Run qemu-nbd in daemon mode and return both the parent's exit code
6177b584 498 and its output in case of an error'''
91efbae9
KW
499 full_args = qemu_nbd_args + ['--fork'] + list(args)
500 output, returncode = qemu_tool_pipe_and_status('qemu-nbd', full_args,
501 connect_stderr=False)
502 return returncode, output if returncode else ''
e1e6eccd 503
81b6b2bc
KW
504def qemu_nbd_list_log(*args: str) -> str:
505 '''Run qemu-nbd to list remote exports'''
506 full_args = [qemu_nbd_prog, '-L'] + list(args)
507 output, _ = qemu_tool_pipe_and_status('qemu-nbd', full_args)
508 log(output, filters=[filter_testfiles, filter_nbd_exports])
509 return output
510
b7719bca 511@contextmanager
23ee0ec2 512def qemu_nbd_popen(*args):
b7719bca 513 '''Context manager running qemu-nbd within the context'''
3f7db418
VSO
514 pid_file = file_path("qemu_nbd_popen-nbd-pid-file")
515
516 assert not os.path.exists(pid_file)
b7719bca
NS
517
518 cmd = list(qemu_nbd_args)
519 cmd.extend(('--persistent', '--pid-file', pid_file))
520 cmd.extend(args)
521
522 log('Start NBD server')
ac4e14f5
EGE
523 with subprocess.Popen(cmd) as p:
524 try:
525 while not os.path.exists(pid_file):
526 if p.poll() is not None:
527 raise RuntimeError(
528 "qemu-nbd terminated with exit code {}: {}"
529 .format(p.returncode, ' '.join(cmd)))
530
531 time.sleep(0.01)
532 yield
533 finally:
534 if os.path.exists(pid_file):
535 os.remove(pid_file)
536 log('Kill NBD server')
537 p.kill()
538 p.wait()
23ee0ec2 539
569131d5
JS
540def compare_images(img1: str, img2: str,
541 fmt1: str = imgfmt, fmt2: str = imgfmt) -> bool:
542 """
543 Compare two images with QEMU_IMG; return True if they are identical.
544
545 :raise CalledProcessError:
546 when qemu-img crashes or returns a status code of anything other
547 than 0 (identical) or 1 (different).
548 """
549 try:
550 qemu_img('compare', '-f', fmt1, '-F', fmt2, img1, img2)
551 return True
552 except subprocess.CalledProcessError as exc:
553 if exc.returncode == 1:
554 return False
555 raise
3a3918c3 556
2499a096
SH
557def create_image(name, size):
558 '''Create a fully-allocated raw image with sector markers'''
ac4e14f5
EGE
559 with open(name, 'wb') as file:
560 i = 0
561 while i < size:
562 sector = struct.pack('>l504xl', i // 512, i // 512)
563 file.write(sector)
564 i = i + 512
2499a096 565
9ebb2b76
JS
566def image_size(img: str) -> int:
567 """Return image's virtual size"""
568 value = qemu_img_info('-f', imgfmt, img)['virtual-size']
569 if not isinstance(value, int):
570 type_name = type(value).__name__
571 raise TypeError("Expected 'int' for 'virtual-size', "
572 f"got '{value}' of type '{type_name}'")
573 return value
74f69050 574
011a5761 575def is_str(val):
7e693a05 576 return isinstance(val, str)
011a5761 577
a2d1c8fd
DB
578test_dir_re = re.compile(r"%s" % test_dir)
579def filter_test_dir(msg):
580 return test_dir_re.sub("TEST_DIR", msg)
581
582win32_re = re.compile(r"\r")
583def filter_win32(msg):
584 return win32_re.sub("", msg)
585
b031e9a5
JS
586qemu_io_re = re.compile(r"[0-9]* ops; [0-9\/:. sec]* "
587 r"\([0-9\/.inf]* [EPTGMKiBbytes]*\/sec "
588 r"and [0-9\/.inf]* ops\/sec\)")
a2d1c8fd
DB
589def filter_qemu_io(msg):
590 msg = filter_win32(msg)
b031e9a5
JS
591 return qemu_io_re.sub("X ops; XX:XX:XX.X "
592 "(XXX YYY/sec and XXX ops/sec)", msg)
a2d1c8fd
DB
593
594chown_re = re.compile(r"chown [0-9]+:[0-9]+")
595def filter_chown(msg):
596 return chown_re.sub("chown UID:GID", msg)
597
12314f2d
SH
598def filter_qmp_event(event):
599 '''Filter a QMP event dict'''
600 event = dict(event)
601 if 'timestamp' in event:
602 event['timestamp']['seconds'] = 'SECS'
603 event['timestamp']['microseconds'] = 'USECS'
604 return event
605
08fcd611
JS
606def filter_qmp(qmsg, filter_fn):
607 '''Given a string filter, filter a QMP object's values.
608 filter_fn takes a (key, value) pair.'''
609 # Iterate through either lists or dicts;
610 if isinstance(qmsg, list):
611 items = enumerate(qmsg)
da9d88d8 612 elif isinstance(qmsg, dict):
08fcd611 613 items = qmsg.items()
da9d88d8
HR
614 else:
615 return filter_fn(None, qmsg)
08fcd611
JS
616
617 for k, v in items:
6a96d87c 618 if isinstance(v, (dict, list)):
08fcd611
JS
619 qmsg[k] = filter_qmp(v, filter_fn)
620 else:
621 qmsg[k] = filter_fn(k, v)
622 return qmsg
623
e234398a 624def filter_testfiles(msg):
df0e032b
VSO
625 pref1 = os.path.join(test_dir, "%s-" % (os.getpid()))
626 pref2 = os.path.join(sock_dir, "%s-" % (os.getpid()))
627 return msg.replace(pref1, 'TEST_DIR/PID-').replace(pref2, 'SOCK_DIR/PID-')
e234398a 628
08fcd611 629def filter_qmp_testfiles(qmsg):
6a96d87c 630 def _filter(_key, value):
56a6e5d0 631 if is_str(value):
08fcd611
JS
632 return filter_testfiles(value)
633 return value
634 return filter_qmp(qmsg, _filter)
635
2daba442
ML
636def filter_virtio_scsi(output: str) -> str:
637 return re.sub(r'(virtio-scsi)-(ccw|pci)', r'\1', output)
638
639def filter_qmp_virtio_scsi(qmsg):
640 def _filter(_key, value):
641 if is_str(value):
642 return filter_virtio_scsi(value)
643 return value
644 return filter_qmp(qmsg, _filter)
645
fa1151f8
JS
646def filter_generated_node_ids(msg):
647 return re.sub("#block[0-9]+", "NODE_NAME", msg)
648
bcc6777a
HR
649def filter_img_info(output: str, filename: str,
650 drop_child_info: bool = True) -> str:
6b605ade 651 lines = []
bcc6777a 652 drop_indented = False
6b605ade
KW
653 for line in output.split('\n'):
654 if 'disk size' in line or 'actual-size' in line:
655 continue
bcc6777a
HR
656
657 # Drop child node info
658 if drop_indented:
659 if line.startswith(' '):
660 continue
661 drop_indented = False
662 if drop_child_info and "Child node '/" in line:
663 drop_indented = True
664 continue
665
fd586ce8
KW
666 line = line.replace(filename, 'TEST_IMG')
667 line = filter_testfiles(line)
668 line = line.replace(imgfmt, 'IMGFMT')
6b605ade 669 line = re.sub('iters: [0-9]+', 'iters: XXX', line)
b031e9a5
JS
670 line = re.sub('uuid: [-a-f0-9]+',
671 'uuid: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX',
672 line)
bab4feb2 673 line = re.sub('cid: [0-9]+', 'cid: XXXXXXXXXX', line)
e877bba3
VSO
674 line = re.sub('(compression type: )(zlib|zstd)', r'\1COMPRESSION_TYPE',
675 line)
6b605ade
KW
676 lines.append(line)
677 return '\n'.join(lines)
678
f2ea0b20
HR
679def filter_imgfmt(msg):
680 return msg.replace(imgfmt, 'IMGFMT')
681
682def filter_qmp_imgfmt(qmsg):
6a96d87c 683 def _filter(_key, value):
f2ea0b20
HR
684 if is_str(value):
685 return filter_imgfmt(value)
686 return value
687 return filter_qmp(qmsg, _filter)
688
81b6b2bc
KW
689def filter_nbd_exports(output: str) -> str:
690 return re.sub(r'((min|opt|max) block): [0-9]+', r'\1: XXX', output)
691
1cd0dbfc
JS
692
693Msg = TypeVar('Msg', Dict[str, Any], List[Any], str)
694
695def log(msg: Msg,
696 filters: Iterable[Callable[[Msg], Msg]] = (),
697 indent: Optional[int] = None) -> None:
698 """
699 Logs either a string message or a JSON serializable message (like QMP).
700 If indent is provided, JSON serializable messages are pretty-printed.
701 """
a2d1c8fd
DB
702 for flt in filters:
703 msg = flt(msg)
6a96d87c 704 if isinstance(msg, (dict, list)):
0706e87d
JS
705 # Don't sort if it's already sorted
706 do_sort = not isinstance(msg, OrderedDict)
52ea799e 707 test_logger.info(json.dumps(msg, sort_keys=do_sort, indent=indent))
e21b5f34 708 else:
52ea799e 709 test_logger.info(msg)
a2d1c8fd 710
2c93c5cb 711class Timeout:
6a96d87c 712 def __init__(self, seconds, errmsg="Timeout"):
2c93c5cb
KW
713 self.seconds = seconds
714 self.errmsg = errmsg
715 def __enter__(self):
d0c34326 716 if qemu_gdb or qemu_valgrind:
d3ec2022 717 return self
2c93c5cb
KW
718 signal.signal(signal.SIGALRM, self.timeout)
719 signal.setitimer(signal.ITIMER_REAL, self.seconds)
720 return self
6a96d87c 721 def __exit__(self, exc_type, value, traceback):
d0c34326 722 if qemu_gdb or qemu_valgrind:
d3ec2022 723 return False
2c93c5cb
KW
724 signal.setitimer(signal.ITIMER_REAL, 0)
725 return False
726 def timeout(self, signum, frame):
aef633e7 727 raise TimeoutError(self.errmsg)
2c93c5cb 728
de263986
JS
729def file_pattern(name):
730 return "{0}-{1}".format(os.getpid(), name)
f4844ac0 731
3192fad7 732class FilePath:
de263986 733 """
f765af87
NS
734 Context manager generating multiple file names. The generated files are
735 removed when exiting the context.
f4844ac0 736
f765af87
NS
737 Example usage:
738
3192fad7 739 with FilePath('a.img', 'b.img') as (img_a, img_b):
f765af87
NS
740 # Use img_a and img_b here...
741
742 # a.img and b.img are automatically removed here.
743
744 By default images are created in iotests.test_dir. To create sockets use
745 iotests.sock_dir:
746
3192fad7
NS
747 with FilePath('a.sock', base_dir=iotests.sock_dir) as sock:
748
749 For convenience, calling with one argument yields a single file instead of
750 a tuple with one item.
f4844ac0 751
de263986 752 """
a242b19e 753 def __init__(self, *names, base_dir=test_dir):
7cc002a0
NS
754 self.paths = [os.path.join(base_dir, file_pattern(name))
755 for name in names]
f4844ac0
SH
756
757 def __enter__(self):
3192fad7
NS
758 if len(self.paths) == 1:
759 return self.paths[0]
760 else:
761 return self.paths
f4844ac0
SH
762
763 def __exit__(self, exc_type, exc_val, exc_tb):
a7971702
NS
764 for path in self.paths:
765 try:
de263986 766 os.remove(path)
a7971702
NS
767 except OSError:
768 pass
f4844ac0
SH
769 return False
770
771
c5ff5a3c
HR
772def try_remove(img):
773 try:
774 os.remove(img)
775 except OSError:
776 pass
777
ef6e9228
VSO
778def file_path_remover():
779 for path in reversed(file_path_remover.paths):
c5ff5a3c 780 try_remove(path)
ef6e9228
VSO
781
782
93b78ea5 783def file_path(*names, base_dir=test_dir):
ef6e9228
VSO
784 ''' Another way to get auto-generated filename that cleans itself up.
785
786 Use is as simple as:
787
788 img_a, img_b = file_path('a.img', 'b.img')
789 sock = file_path('socket')
790 '''
791
792 if not hasattr(file_path_remover, 'paths'):
793 file_path_remover.paths = []
794 atexit.register(file_path_remover)
795
796 paths = []
797 for name in names:
de263986 798 filename = file_pattern(name)
93b78ea5 799 path = os.path.join(base_dir, filename)
ef6e9228
VSO
800 file_path_remover.paths.append(path)
801 paths.append(path)
802
803 return paths[0] if len(paths) == 1 else paths
804
5a259e86
KW
805def remote_filename(path):
806 if imgproto == 'file':
807 return path
808 elif imgproto == 'ssh':
b8c1f901 809 return "ssh://%s@127.0.0.1:22%s" % (os.environ.get('USER'), path)
5a259e86 810 else:
aef633e7 811 raise ValueError("Protocol %s not supported" % (imgproto))
ef6e9228 812
4c44b4a4 813class VM(qtest.QEMUQtestMachine):
f345cfd0
SH
814 '''A QEMU VM'''
815
5fcbdf50
HR
816 def __init__(self, path_suffix=''):
817 name = "qemu%s-%d" % (path_suffix, os.getpid())
d0c34326 818 timer = 15.0 if not (qemu_gdb or qemu_valgrind) else None
4032d1f6
EGE
819 if qemu_gdb and qemu_valgrind:
820 sys.stderr.write('gdb and valgrind are mutually exclusive\n')
821 sys.exit(1)
822 wrapper = qemu_gdb if qemu_gdb else qemu_valgrind
823 super().__init__(qemu_prog, qemu_opts, wrapper=wrapper,
776b9974 824 name=name,
2ca6e26c 825 base_temp_dir=test_dir,
e2f948a8 826 sock_dir=sock_dir, qmp_timer=timer)
f345cfd0 827 self._num_drives = 0
30b005d9 828
d792c863
EGE
829 def _post_shutdown(self) -> None:
830 super()._post_shutdown()
831 if not qemu_valgrind or not self._popen:
832 return
87e4d4a2 833 valgrind_filename = f"{test_dir}/{self._popen.pid}.valgrind"
d792c863 834 if self.exitcode() == 99:
81dcb9ca 835 with open(valgrind_filename, encoding='utf-8') as f:
d792c863
EGE
836 print(f.read())
837 else:
838 os.remove(valgrind_filename)
839
eb7a91d0
EGE
840 def _pre_launch(self) -> None:
841 super()._pre_launch()
842 if qemu_print:
843 # set QEMU binary output to stdout
844 self._close_qemu_log_file()
845
ccc15f7d
SH
846 def add_object(self, opts):
847 self._args.append('-object')
848 self._args.append(opts)
849 return self
850
486b88bd
KW
851 def add_device(self, opts):
852 self._args.append('-device')
853 self._args.append(opts)
854 return self
855
78b666f4
FZ
856 def add_drive_raw(self, opts):
857 self._args.append('-drive')
858 self._args.append(opts)
859 return self
860
1d3d4b63 861 def add_drive(self, path, opts='', interface='virtio', img_format=imgfmt):
f345cfd0 862 '''Add a virtio-blk drive to the VM'''
8e492253 863 options = ['if=%s' % interface,
f345cfd0 864 'id=drive%d' % self._num_drives]
8e492253
HR
865
866 if path is not None:
867 options.append('file=%s' % path)
1d3d4b63 868 options.append('format=%s' % img_format)
fc17c259 869 options.append('cache=%s' % cachemode)
7156ca48 870 options.append('aio=%s' % aiomode)
8e492253 871
f345cfd0
SH
872 if opts:
873 options.append(opts)
874
1d3d4b63 875 if img_format == 'luks' and 'key-secret' not in opts:
85a353a0
VSO
876 # default luks support
877 if luks_default_secret_object not in self._args:
878 self.add_object(luks_default_secret_object)
879
880 options.append(luks_default_key_secret_opt)
881
f345cfd0
SH
882 self._args.append('-drive')
883 self._args.append(','.join(options))
884 self._num_drives += 1
885 return self
886
5694923a
HR
887 def add_blockdev(self, opts):
888 self._args.append('-blockdev')
889 if isinstance(opts, str):
890 self._args.append(opts)
891 else:
892 self._args.append(','.join(opts))
893 return self
894
12314f2d
SH
895 def add_incoming(self, addr):
896 self._args.append('-incoming')
897 self._args.append(addr)
898 return self
899
2012453d 900 def hmp(self, command_line: str, use_log: bool = False) -> QMPMessage:
239bbcc0 901 cmd = 'human-monitor-command'
090744d5 902 kwargs: Dict[str, Any] = {'command-line': command_line}
239bbcc0
JS
903 if use_log:
904 return self.qmp_log(cmd, **kwargs)
905 else:
906 return self.qmp(cmd, **kwargs)
907
908 def pause_drive(self, drive: str, event: Optional[str] = None) -> None:
909 """Pause drive r/w operations"""
3cf53c77
FZ
910 if not event:
911 self.pause_drive(drive, "read_aio")
912 self.pause_drive(drive, "write_aio")
913 return
239bbcc0
JS
914 self.hmp(f'qemu-io {drive} "break {event} bp_{drive}"')
915
916 def resume_drive(self, drive: str) -> None:
917 """Resume drive r/w operations"""
918 self.hmp(f'qemu-io {drive} "remove_break bp_{drive}"')
919
920 def hmp_qemu_io(self, drive: str, cmd: str,
e89c0c8d 921 use_log: bool = False, qdev: bool = False) -> QMPMessage:
239bbcc0 922 """Write to a given drive using an HMP command"""
e89c0c8d
VSO
923 d = '-d ' if qdev else ''
924 return self.hmp(f'qemu-io {d}{drive} "{cmd}"', use_log=use_log)
e3409362 925
62a94288
KW
926 def flatten_qmp_object(self, obj, output=None, basestr=''):
927 if output is None:
cc16153f 928 output = {}
62a94288 929 if isinstance(obj, list):
6a96d87c
JS
930 for i, item in enumerate(obj):
931 self.flatten_qmp_object(item, output, basestr + str(i) + '.')
62a94288
KW
932 elif isinstance(obj, dict):
933 for key in obj:
934 self.flatten_qmp_object(obj[key], output, basestr + key + '.')
935 else:
936 output[basestr[:-1]] = obj # Strip trailing '.'
937 return output
938
939 def qmp_to_opts(self, obj):
940 obj = self.flatten_qmp_object(obj)
cc16153f 941 output_list = []
62a94288
KW
942 for key in obj:
943 output_list += [key + '=' + obj[key]]
944 return ','.join(output_list)
945
8b6f5f8b 946 def get_qmp_events_filtered(self, wait=60.0):
5ad1dbf7
KW
947 result = []
948 for ev in self.get_qmp_events(wait=wait):
949 result.append(filter_qmp_event(ev))
950 return result
62a94288 951
4eabe051 952 def qmp_log(self, cmd, filters=(), indent=None, **kwargs):
0706e87d
JS
953 full_cmd = OrderedDict((
954 ("execute", cmd),
039be85c 955 ("arguments", ordered_qmp(kwargs))
0706e87d 956 ))
55cd64ea 957 log(full_cmd, filters, indent=indent)
e234398a 958 result = self.qmp(cmd, **kwargs)
55cd64ea 959 log(result, filters, indent=indent)
e234398a
KW
960 return result
961
6a4e88e1 962 # Returns None on success, and an error string on failure
da9d88d8
HR
963 def run_job(self, job: str, auto_finalize: bool = True,
964 auto_dismiss: bool = False,
965 pre_finalize: Optional[Callable[[], None]] = None,
966 cancel: bool = False, wait: float = 60.0,
967 filters: Iterable[Callable[[Any], Any]] = (),
968 ) -> Optional[str]:
d443b74b
JS
969 """
970 run_job moves a job from creation through to dismissal.
971
972 :param job: String. ID of recently-launched job
973 :param auto_finalize: Bool. True if the job was launched with
974 auto_finalize. Defaults to True.
975 :param auto_dismiss: Bool. True if the job was launched with
976 auto_dismiss=True. Defaults to False.
977 :param pre_finalize: Callback. A callable that takes no arguments to be
978 invoked prior to issuing job-finalize, if any.
979 :param cancel: Bool. When true, cancels the job after the pre_finalize
980 callback.
d443b74b
JS
981 :param wait: Float. Timeout value specifying how long to wait for any
982 event, in seconds. Defaults to 60.0.
983 """
d6a79af0
JS
984 match_device = {'data': {'device': job}}
985 match_id = {'data': {'id': job}}
986 events = [
987 ('BLOCK_JOB_COMPLETED', match_device),
988 ('BLOCK_JOB_CANCELLED', match_device),
989 ('BLOCK_JOB_ERROR', match_device),
990 ('BLOCK_JOB_READY', match_device),
991 ('BLOCK_JOB_PENDING', match_id),
992 ('JOB_STATUS_CHANGE', match_id)
993 ]
6a4e88e1 994 error = None
fc47d851 995 while True:
55824e09 996 ev = filter_qmp_event(self.events_wait(events, timeout=wait))
d6a79af0 997 if ev['event'] != 'JOB_STATUS_CHANGE':
da9d88d8 998 log(ev, filters=filters)
d6a79af0
JS
999 continue
1000 status = ev['data']['status']
1001 if status == 'aborting':
1002 result = self.qmp('query-jobs')
1003 for j in result['return']:
1004 if j['id'] == job:
1005 error = j['error']
da9d88d8 1006 log('Job failed: %s' % (j['error']), filters=filters)
4688c4e3 1007 elif status == 'ready':
da9d88d8 1008 self.qmp_log('job-complete', id=job, filters=filters)
d6a79af0
JS
1009 elif status == 'pending' and not auto_finalize:
1010 if pre_finalize:
1011 pre_finalize()
52ea799e 1012 if cancel:
da9d88d8 1013 self.qmp_log('job-cancel', id=job, filters=filters)
15427f63 1014 else:
da9d88d8 1015 self.qmp_log('job-finalize', id=job, filters=filters)
d6a79af0 1016 elif status == 'concluded' and not auto_dismiss:
da9d88d8 1017 self.qmp_log('job-dismiss', id=job, filters=filters)
d6a79af0
JS
1018 elif status == 'null':
1019 return error
fc47d851 1020
e9dbd1ca
KW
1021 # Returns None on success, and an error string on failure
1022 def blockdev_create(self, options, job_id='job0', filters=None):
1023 if filters is None:
1024 filters = [filter_qmp_testfiles]
1025 result = self.qmp_log('blockdev-create', filters=filters,
1026 job_id=job_id, options=options)
1027
1028 if 'return' in result:
1029 assert result['return'] == {}
da9d88d8 1030 job_result = self.run_job(job_id, filters=filters)
e9dbd1ca
KW
1031 else:
1032 job_result = result['error']
1033
1034 log("")
1035 return job_result
1036
980448f1
KW
1037 def enable_migration_events(self, name):
1038 log('Enabling migration QMP events on %s...' % name)
1039 log(self.qmp('migrate-set-capabilities', capabilities=[
1040 {
1041 'capability': 'events',
1042 'state': True
1043 }
1044 ]))
1045
4bf63c80 1046 def wait_migration(self, expect_runstate: Optional[str]) -> bool:
980448f1
KW
1047 while True:
1048 event = self.event_wait('MIGRATION')
503c2b31
KW
1049 # We use the default timeout, and with a timeout, event_wait()
1050 # never returns None
1051 assert event
1052
980448f1 1053 log(event, filters=[filter_qmp_event])
4bf63c80 1054 if event['data']['status'] in ('completed', 'failed'):
980448f1 1055 break
4bf63c80
HR
1056
1057 if event['data']['status'] == 'completed':
1058 # The event may occur in finish-migrate, so wait for the expected
1059 # post-migration runstate
1060 runstate = None
1061 while runstate != expect_runstate:
1062 runstate = self.qmp('query-status')['return']['status']
1063 return True
1064 else:
1065 return False
980448f1 1066
ef7afd63
HR
1067 def node_info(self, node_name):
1068 nodes = self.qmp('query-named-block-nodes')
1069 for x in nodes['return']:
1070 if x['node-name'] == node_name:
1071 return x
1072 return None
1073
5c4343b8
VSO
1074 def query_bitmaps(self):
1075 res = self.qmp("query-named-block-nodes")
1076 return {device['node-name']: device['dirty-bitmaps']
1077 for device in res['return'] if 'dirty-bitmaps' in device}
1078
1079 def get_bitmap(self, node_name, bitmap_name, recording=None, bitmaps=None):
1080 """
1081 get a specific bitmap from the object returned by query_bitmaps.
1082 :param recording: If specified, filter results by the specified value.
1083 :param bitmaps: If specified, use it instead of call query_bitmaps()
1084 """
1085 if bitmaps is None:
1086 bitmaps = self.query_bitmaps()
1087
1088 for bitmap in bitmaps[node_name]:
1089 if bitmap.get('name', '') == bitmap_name:
6a96d87c 1090 if recording is None or bitmap.get('recording') == recording:
5c4343b8
VSO
1091 return bitmap
1092 return None
1093
1094 def check_bitmap_status(self, node_name, bitmap_name, fields):
1095 ret = self.get_bitmap(node_name, bitmap_name)
1096
1097 return fields.items() <= ret.items()
1098
6a3d0f1e
HR
1099 def assert_block_path(self, root, path, expected_node, graph=None):
1100 """
1101 Check whether the node under the given path in the block graph
1102 is @expected_node.
1103
1104 @root is the node name of the node where the @path is rooted.
1105
1106 @path is a string that consists of child names separated by
1107 slashes. It must begin with a slash.
1108
1109 Examples for @root + @path:
1110 - root="qcow2-node", path="/backing/file"
1111 - root="quorum-node", path="/children.2/file"
1112
1113 Hypothetically, @path could be empty, in which case it would
1114 point to @root. However, in practice this case is not useful
1115 and hence not allowed.
1116
1117 @expected_node may be None. (All elements of the path but the
1118 leaf must still exist.)
1119
1120 @graph may be None or the result of an x-debug-query-block-graph
1121 call that has already been performed.
1122 """
1123 if graph is None:
1124 graph = self.qmp('x-debug-query-block-graph')['return']
1125
1126 iter_path = iter(path.split('/'))
1127
1128 # Must start with a /
1129 assert next(iter_path) == ''
1130
1131 node = next((node for node in graph['nodes'] if node['name'] == root),
1132 None)
1133
1134 # An empty @path is not allowed, so the root node must be present
1135 assert node is not None, 'Root node %s not found' % root
1136
1137 for child_name in iter_path:
1138 assert node is not None, 'Cannot follow path %s%s' % (root, path)
1139
1140 try:
6a96d87c
JS
1141 node_id = next(edge['child'] for edge in graph['edges']
1142 if (edge['parent'] == node['id'] and
1143 edge['name'] == child_name))
1144
1145 node = next(node for node in graph['nodes']
1146 if node['id'] == node_id)
6a3d0f1e 1147
6a3d0f1e
HR
1148 except StopIteration:
1149 node = None
1150
1151 if node is None:
1152 assert expected_node is None, \
1153 'No node found under %s (but expected %s)' % \
1154 (path, expected_node)
1155 else:
1156 assert node['name'] == expected_node, \
1157 'Found node %s under %s (but expected %s)' % \
1158 (node['name'], path, expected_node)
7898f74e 1159
f345cfd0
SH
1160index_re = re.compile(r'([^\[]+)\[([^\]]+)\]')
1161
1162class QMPTestCase(unittest.TestCase):
1163 '''Abstract base class for QMP test cases'''
1164
6a96d87c
JS
1165 def __init__(self, *args, **kwargs):
1166 super().__init__(*args, **kwargs)
1167 # Many users of this class set a VM property we rely on heavily
1168 # in the methods below.
1169 self.vm = None
1170
f345cfd0
SH
1171 def dictpath(self, d, path):
1172 '''Traverse a path in a nested dict'''
1173 for component in path.split('/'):
1174 m = index_re.match(component)
1175 if m:
1176 component, idx = m.groups()
1177 idx = int(idx)
1178
1179 if not isinstance(d, dict) or component not in d:
b031e9a5 1180 self.fail(f'failed path traversal for "{path}" in "{d}"')
f345cfd0
SH
1181 d = d[component]
1182
1183 if m:
1184 if not isinstance(d, list):
b031e9a5
JS
1185 self.fail(f'path component "{component}" in "{path}" '
1186 f'is not a list in "{d}"')
f345cfd0
SH
1187 try:
1188 d = d[idx]
1189 except IndexError:
b031e9a5
JS
1190 self.fail(f'invalid index "{idx}" in path "{path}" '
1191 f'in "{d}"')
f345cfd0
SH
1192 return d
1193
90f0b711
PB
1194 def assert_qmp_absent(self, d, path):
1195 try:
1196 result = self.dictpath(d, path)
1197 except AssertionError:
1198 return
1199 self.fail('path "%s" has value "%s"' % (path, str(result)))
1200
f345cfd0 1201 def assert_qmp(self, d, path, value):
a93a42bd
HR
1202 '''Assert that the value for a specific path in a QMP dict
1203 matches. When given a list of values, assert that any of
1204 them matches.'''
1205
f345cfd0 1206 result = self.dictpath(d, path)
a93a42bd
HR
1207
1208 # [] makes no sense as a list of valid values, so treat it as
1209 # an actual single value.
1210 if isinstance(value, list) and value != []:
1211 for v in value:
1212 if result == v:
1213 return
1214 self.fail('no match for "%s" in %s' % (str(result), str(value)))
1215 else:
1216 self.assertEqual(result, value,
dbf231d7 1217 '"%s" is "%s", expected "%s"'
6a96d87c 1218 % (path, str(result), str(value)))
f345cfd0 1219
ecc1c88e
SH
1220 def assert_no_active_block_jobs(self):
1221 result = self.vm.qmp('query-block-jobs')
1222 self.assert_qmp(result, 'return', [])
1223
e71fc0ba
FZ
1224 def assert_has_block_node(self, node_name=None, file_name=None):
1225 """Issue a query-named-block-nodes and assert node_name and/or
1226 file_name is present in the result"""
1227 def check_equal_or_none(a, b):
6a96d87c 1228 return a is None or b is None or a == b
e71fc0ba
FZ
1229 assert node_name or file_name
1230 result = self.vm.qmp('query-named-block-nodes')
1231 for x in result["return"]:
1232 if check_equal_or_none(x.get("node-name"), node_name) and \
1233 check_equal_or_none(x.get("file"), file_name):
1234 return
6a96d87c
JS
1235 self.fail("Cannot find %s %s in result:\n%s" %
1236 (node_name, file_name, result))
e71fc0ba 1237
e07375f5
HR
1238 def assert_json_filename_equal(self, json_filename, reference):
1239 '''Asserts that the given filename is a json: filename and that its
1240 content is equal to the given reference object'''
1241 self.assertEqual(json_filename[:5], 'json:')
b031e9a5
JS
1242 self.assertEqual(
1243 self.vm.flatten_qmp_object(json.loads(json_filename[5:])),
1244 self.vm.flatten_qmp_object(reference)
1245 )
e07375f5 1246
b031e9a5
JS
1247 def cancel_and_wait(self, drive='drive0', force=False,
1248 resume=False, wait=60.0):
2575fe16
SH
1249 '''Cancel a block job and wait for it to finish, returning the event'''
1250 result = self.vm.qmp('block-job-cancel', device=drive, force=force)
1251 self.assert_qmp(result, 'return', {})
1252
3cf53c77
FZ
1253 if resume:
1254 self.vm.resume_drive(drive)
1255
2575fe16
SH
1256 cancelled = False
1257 result = None
1258 while not cancelled:
8b6f5f8b 1259 for event in self.vm.get_qmp_events(wait=wait):
2575fe16
SH
1260 if event['event'] == 'BLOCK_JOB_COMPLETED' or \
1261 event['event'] == 'BLOCK_JOB_CANCELLED':
1262 self.assert_qmp(event, 'data/device', drive)
1263 result = event
1264 cancelled = True
1dac83f1
KW
1265 elif event['event'] == 'JOB_STATUS_CHANGE':
1266 self.assert_qmp(event, 'data/id', drive)
1267
2575fe16
SH
1268
1269 self.assert_no_active_block_jobs()
1270 return result
1271
b031e9a5
JS
1272 def wait_until_completed(self, drive='drive0', check_offset=True,
1273 wait=60.0, error=None):
0dbe8a1b 1274 '''Wait for a block job to finish, returning the event'''
c3988519 1275 while True:
8b6f5f8b 1276 for event in self.vm.get_qmp_events(wait=wait):
0dbe8a1b
SH
1277 if event['event'] == 'BLOCK_JOB_COMPLETED':
1278 self.assert_qmp(event, 'data/device', drive)
216656f5
HR
1279 if error is None:
1280 self.assert_qmp_absent(event, 'data/error')
1281 if check_offset:
1282 self.assert_qmp(event, 'data/offset',
1283 event['data']['len'])
1284 else:
1285 self.assert_qmp(event, 'data/error', error)
c3988519
PX
1286 self.assert_no_active_block_jobs()
1287 return event
6a96d87c 1288 if event['event'] == 'JOB_STATUS_CHANGE':
1dac83f1 1289 self.assert_qmp(event, 'data/id', drive)
0dbe8a1b 1290
866323f3 1291 def wait_ready(self, drive='drive0'):
6a96d87c 1292 """Wait until a BLOCK_JOB_READY event, and return the event."""
c682bf18
HR
1293 return self.vm.events_wait([
1294 ('BLOCK_JOB_READY',
1295 {'data': {'type': 'mirror', 'device': drive}}),
1296 ('BLOCK_JOB_READY',
1297 {'data': {'type': 'commit', 'device': drive}})
1298 ])
866323f3
FZ
1299
1300 def wait_ready_and_cancel(self, drive='drive0'):
1301 self.wait_ready(drive=drive)
1302 event = self.cancel_and_wait(drive=drive)
fa1cfb40 1303 self.assertEqual(event['event'], 'BLOCK_JOB_COMPLETED')
866323f3
FZ
1304 self.assert_qmp(event, 'data/type', 'mirror')
1305 self.assert_qmp(event, 'data/offset', event['data']['len'])
1306
216656f5
HR
1307 def complete_and_wait(self, drive='drive0', wait_ready=True,
1308 completion_error=None):
866323f3
FZ
1309 '''Complete a block job and wait for it to finish'''
1310 if wait_ready:
1311 self.wait_ready(drive=drive)
1312
1313 result = self.vm.qmp('block-job-complete', device=drive)
1314 self.assert_qmp(result, 'return', {})
1315
216656f5 1316 event = self.wait_until_completed(drive=drive, error=completion_error)
c682bf18 1317 self.assertTrue(event['data']['type'] in ['mirror', 'commit'])
866323f3 1318
f03d9d24 1319 def pause_wait(self, job_id='job0'):
e1df89bb 1320 with Timeout(3, "Timeout waiting for job to pause"):
2c93c5cb
KW
1321 while True:
1322 result = self.vm.qmp('query-block-jobs')
c1bac161 1323 found = False
2c93c5cb 1324 for job in result['return']:
c1bac161
VSO
1325 if job['device'] == job_id:
1326 found = True
6a96d87c 1327 if job['paused'] and not job['busy']:
c1bac161
VSO
1328 return job
1329 break
1330 assert found
2c93c5cb 1331
f03d9d24
JS
1332 def pause_job(self, job_id='job0', wait=True):
1333 result = self.vm.qmp('block-job-pause', device=job_id)
1334 self.assert_qmp(result, 'return', {})
1335 if wait:
1336 return self.pause_wait(job_id)
1337 return result
1338
6be01225
HR
1339 def case_skip(self, reason):
1340 '''Skip this test case'''
1341 case_notrun(reason)
1342 self.skipTest(reason)
1343
2c93c5cb 1344
f345cfd0
SH
1345def notrun(reason):
1346 '''Skip this test suite'''
1347 # Each test in qemu-iotests has a number ("seq")
1348 seq = os.path.basename(sys.argv[0])
1349
1a8fcca0 1350 with open('%s/%s.notrun' % (test_dir, seq), 'w', encoding='utf-8') \
81dcb9ca 1351 as outfile:
06aad78b 1352 outfile.write(reason + '\n')
52ea799e 1353 logger.warning("%s not run: %s", seq, reason)
f345cfd0
SH
1354 sys.exit(0)
1355
57ed557f 1356def case_notrun(reason):
6be01225
HR
1357 '''Mark this test case as not having been run (without actually
1358 skipping it, that is left to the caller). See
1359 QMPTestCase.case_skip() for a variant that actually skips the
1360 current test case.'''
1361
57ed557f
AS
1362 # Each test in qemu-iotests has a number ("seq")
1363 seq = os.path.basename(sys.argv[0])
1364
1a8fcca0 1365 with open('%s/%s.casenotrun' % (test_dir, seq), 'a', encoding='utf-8') \
81dcb9ca 1366 as outfile:
06aad78b 1367 outfile.write(' [case not run] ' + reason + '\n')
57ed557f 1368
59c29869
JS
1369def _verify_image_format(supported_fmts: Sequence[str] = (),
1370 unsupported_fmts: Sequence[str] = ()) -> None:
f48351d2
VSO
1371 if 'generic' in supported_fmts and \
1372 os.environ.get('IMGFMT_GENERIC', 'true') == 'true':
1373 # similar to
1374 # _supported_fmt generic
1375 # for bash tests
f7125522 1376 supported_fmts = ()
f48351d2
VSO
1377
1378 not_sup = supported_fmts and (imgfmt not in supported_fmts)
1379 if not_sup or (imgfmt in unsupported_fmts):
3f5c4076 1380 notrun('not suitable for this image format: %s' % imgfmt)
f345cfd0 1381
d2a839ed
HR
1382 if imgfmt == 'luks':
1383 verify_working_luks()
1384
59c29869
JS
1385def _verify_protocol(supported: Sequence[str] = (),
1386 unsupported: Sequence[str] = ()) -> None:
5a259e86
KW
1387 assert not (supported and unsupported)
1388
1389 if 'generic' in supported:
1390 return
1391
1392 not_sup = supported and (imgproto not in supported)
1393 if not_sup or (imgproto in unsupported):
1394 notrun('not suitable for this protocol: %s' % imgproto)
1395
59c29869
JS
1396def _verify_platform(supported: Sequence[str] = (),
1397 unsupported: Sequence[str] = ()) -> None:
7d814059
JS
1398 if any((sys.platform.startswith(x) for x in unsupported)):
1399 notrun('not suitable for this OS: %s' % sys.platform)
72b29030 1400
7d814059 1401 if supported:
72b29030
JS
1402 if not any((sys.platform.startswith(x) for x in supported)):
1403 notrun('not suitable for this OS: %s' % sys.platform)
bc521696 1404
59c29869 1405def _verify_cache_mode(supported_cache_modes: Sequence[str] = ()) -> None:
ac8bd439
VSO
1406 if supported_cache_modes and (cachemode not in supported_cache_modes):
1407 notrun('not suitable for this cache mode: %s' % cachemode)
1408
cd8f5b75 1409def _verify_aio_mode(supported_aio_modes: Sequence[str] = ()) -> None:
7156ca48
AM
1410 if supported_aio_modes and (aiomode not in supported_aio_modes):
1411 notrun('not suitable for this aio mode: %s' % aiomode)
1412
18654716
VSO
1413def _verify_formats(required_formats: Sequence[str] = ()) -> None:
1414 usf_list = list(set(required_formats) - set(supported_formats()))
1415 if usf_list:
1416 notrun(f'formats {usf_list} are not whitelisted')
1417
f203080b
VSO
1418
1419def _verify_virtio_blk() -> None:
1420 out = qemu_pipe('-M', 'none', '-device', 'help')
1421 if 'virtio-blk' not in out:
1422 notrun('Missing virtio-blk in QEMU binary')
1423
bc05c638 1424def verify_virtio_scsi_pci_or_ccw() -> None:
359a8562
LV
1425 out = qemu_pipe('-M', 'none', '-device', 'help')
1426 if 'virtio-scsi-pci' not in out and 'virtio-scsi-ccw' not in out:
1427 notrun('Missing virtio-scsi-pci or virtio-scsi-ccw in QEMU binary')
1428
f203080b 1429
7c15400c
VSO
1430def _verify_imgopts(unsupported: Sequence[str] = ()) -> None:
1431 imgopts = os.environ.get('IMGOPTS')
1432 # One of usage examples for IMGOPTS is "data_file=$TEST_IMG.ext_data_file"
1433 # but it supported only for bash tests. We don't have a concept of global
1434 # TEST_IMG in iotests.py, not saying about somehow parsing $variables.
1435 # So, for simplicity let's just not support any IMGOPTS with '$' inside.
1436 unsup = list(unsupported) + ['$']
1437 if imgopts and any(x in imgopts for x in unsup):
1438 notrun(f'not suitable for this imgopts: {imgopts}')
1439
1440
78d04761
JS
1441def supports_quorum() -> bool:
1442 return 'quorum' in qemu_img('--help').stdout
b0f90495 1443
3f647b51
SS
1444def verify_quorum():
1445 '''Skip test suite if quorum support is not available'''
b0f90495 1446 if not supports_quorum():
3f647b51
SS
1447 notrun('quorum support missing')
1448
6649f4bd
HR
1449def has_working_luks() -> Tuple[bool, str]:
1450 """
1451 Check whether our LUKS driver can actually create images
1452 (this extends to LUKS encryption for qcow2).
1453
1454 If not, return the reason why.
1455 """
1456
1457 img_file = f'{test_dir}/luks-test.luks'
97576f8c
JS
1458 res = qemu_img('create', '-f', 'luks',
1459 '--object', luks_default_secret_object,
1460 '-o', luks_default_key_secret_opt,
1461 '-o', 'iter-time=10',
1462 img_file, '1G',
1463 check=False)
6649f4bd
HR
1464 try:
1465 os.remove(img_file)
1466 except OSError:
1467 pass
1468
97576f8c
JS
1469 if res.returncode:
1470 reason = res.stdout
1471 for line in res.stdout.splitlines():
6649f4bd
HR
1472 if img_file + ':' in line:
1473 reason = line.split(img_file + ':', 1)[1].strip()
1474 break
1475
1476 return (False, reason)
1477 else:
1478 return (True, '')
1479
1480def verify_working_luks():
1481 """
1482 Skip test suite if LUKS does not work
1483 """
1484 (working, reason) = has_working_luks()
1485 if not working:
1486 notrun(reason)
1487
9ba271f0
HR
1488def supports_qcow2_zstd_compression() -> bool:
1489 img_file = f'{test_dir}/qcow2-zstd-test.qcow2'
1490 res = qemu_img('create', '-f', 'qcow2', '-o', 'compression_type=zstd',
1491 img_file, '0',
1492 check=False)
1493 try:
1494 os.remove(img_file)
1495 except OSError:
1496 pass
1497
1498 if res.returncode == 1 and \
1499 "'compression-type' does not accept value 'zstd'" in res.stdout:
1500 return False
1501 else:
1502 return True
1503
1504def verify_qcow2_zstd_compression():
1505 if not supports_qcow2_zstd_compression():
1506 notrun('zstd compression not supported')
1507
91efbae9 1508def qemu_pipe(*args: str) -> str:
b031e9a5
JS
1509 """
1510 Run qemu with an option to print something and exit (e.g. a help option).
1511
1512 :return: QEMU's stdout output.
1513 """
91efbae9
KW
1514 full_args = [qemu_prog] + qemu_opts + list(args)
1515 output, _ = qemu_tool_pipe_and_status('qemu', full_args)
49438972 1516 return output
57ed557f
AS
1517
1518def supported_formats(read_only=False):
1519 '''Set 'read_only' to True to check ro-whitelist
1520 Otherwise, rw-whitelist is checked'''
767de537
HR
1521
1522 if not hasattr(supported_formats, "formats"):
1523 supported_formats.formats = {}
1524
1525 if read_only not in supported_formats.formats:
1526 format_message = qemu_pipe("-drive", "format=help")
1527 line = 1 if read_only else 0
1528 supported_formats.formats[read_only] = \
1529 format_message.splitlines()[line].split(":")[1].split()
1530
1531 return supported_formats.formats[read_only]
57ed557f 1532
4eabe051 1533def skip_if_unsupported(required_formats=(), read_only=False):
57ed557f
AS
1534 '''Skip Test Decorator
1535 Runs the test if all the required formats are whitelisted'''
1536 def skip_test_decorator(func):
cd8f5b75
KW
1537 def func_wrapper(test_case: QMPTestCase, *args: List[Any],
1538 **kwargs: Dict[str, Any]) -> None:
7448be83
HR
1539 if callable(required_formats):
1540 fmts = required_formats(test_case)
1541 else:
1542 fmts = required_formats
1543
1544 usf_list = list(set(fmts) - set(supported_formats(read_only)))
57ed557f 1545 if usf_list:
b031e9a5
JS
1546 msg = f'{test_case}: formats {usf_list} are not whitelisted'
1547 test_case.case_skip(msg)
57ed557f 1548 else:
cd8f5b75 1549 func(test_case, *args, **kwargs)
57ed557f
AS
1550 return func_wrapper
1551 return skip_test_decorator
1552
ff3caf5a
HR
1553def skip_for_formats(formats: Sequence[str] = ()) \
1554 -> Callable[[Callable[[QMPTestCase, List[Any], Dict[str, Any]], None]],
1555 Callable[[QMPTestCase, List[Any], Dict[str, Any]], None]]:
1556 '''Skip Test Decorator
1557 Skips the test for the given formats'''
1558 def skip_test_decorator(func):
1559 def func_wrapper(test_case: QMPTestCase, *args: List[Any],
1560 **kwargs: Dict[str, Any]) -> None:
1561 if imgfmt in formats:
1562 msg = f'{test_case}: Skipped for format {imgfmt}'
1563 test_case.case_skip(msg)
1564 else:
1565 func(test_case, *args, **kwargs)
1566 return func_wrapper
1567 return skip_test_decorator
1568
d926f4dd
KW
1569def skip_if_user_is_root(func):
1570 '''Skip Test Decorator
1571 Runs the test only without root permissions'''
1572 def func_wrapper(*args, **kwargs):
1573 if os.getuid() == 0:
1574 case_notrun('{}: cannot be run as root'.format(args[0]))
6a96d87c 1575 return None
d926f4dd
KW
1576 else:
1577 return func(*args, **kwargs)
1578 return func_wrapper
1579
f29f4c25
PB
1580# We need to filter out the time taken from the output so that
1581# qemu-iotest can reliably diff the results against master output,
1582# and hide skipped tests from the reference output.
1583
1584class ReproducibleTestResult(unittest.TextTestResult):
1585 def addSkip(self, test, reason):
1586 # Same as TextTestResult, but print dot instead of "s"
1587 unittest.TestResult.addSkip(self, test, reason)
1588 if self.showAll:
1589 self.stream.writeln("skipped {0!r}".format(reason))
1590 elif self.dots:
1591 self.stream.write(".")
1592 self.stream.flush()
1593
1594class ReproducibleStreamWrapper:
1595 def __init__(self, stream: TextIO):
1596 self.stream = stream
1597
1598 def __getattr__(self, attr):
1599 if attr in ('stream', '__getstate__'):
1600 raise AttributeError(attr)
1601 return getattr(self.stream, attr)
1602
1603 def write(self, arg=None):
1604 arg = re.sub(r'Ran (\d+) tests? in [\d.]+s', r'Ran \1 tests', arg)
1605 arg = re.sub(r' \(skipped=\d+\)', r'', arg)
1606 self.stream.write(arg)
1607
1608class ReproducibleTestRunner(unittest.TextTestRunner):
1609 def __init__(self, stream: Optional[TextIO] = None,
87e4d4a2
EGE
1610 resultclass: Type[unittest.TestResult] =
1611 ReproducibleTestResult,
1612 **kwargs: Any) -> None:
f29f4c25
PB
1613 rstream = ReproducibleStreamWrapper(stream or sys.stdout)
1614 super().__init__(stream=rstream, # type: ignore
1615 descriptions=True,
1616 resultclass=resultclass,
1617 **kwargs)
1618
00dbc85e 1619def execute_unittest(argv: List[str], debug: bool = False) -> None:
7d814059
JS
1620 """Executes unittests within the calling module."""
1621
00dbc85e
PB
1622 # Some tests have warnings, especially ResourceWarnings for unclosed
1623 # files and sockets. Ignore them for now to ensure reproducibility of
1624 # the test output.
1625 unittest.main(argv=argv,
1626 testRunner=ReproducibleTestRunner,
1627 verbosity=2 if debug else 1,
1628 warnings=None if sys.warnoptions else 'ignore')
c6a92369 1629
7d814059
JS
1630def execute_setup_common(supported_fmts: Sequence[str] = (),
1631 supported_platforms: Sequence[str] = (),
1632 supported_cache_modes: Sequence[str] = (),
1633 supported_aio_modes: Sequence[str] = (),
1634 unsupported_fmts: Sequence[str] = (),
1635 supported_protocols: Sequence[str] = (),
18654716 1636 unsupported_protocols: Sequence[str] = (),
7c15400c
VSO
1637 required_fmts: Sequence[str] = (),
1638 unsupported_imgopts: Sequence[str] = ()) -> bool:
7d814059
JS
1639 """
1640 Perform necessary setup for either script-style or unittest-style tests.
1641
1642 :return: Bool; Whether or not debug mode has been requested via the CLI.
1643 """
1644 # Note: Python 3.6 and pylint do not like 'Collection' so use 'Sequence'.
c0088d79 1645
44a46a9c
JS
1646 debug = '-d' in sys.argv
1647 if debug:
1648 sys.argv.remove('-d')
1649 logging.basicConfig(level=(logging.DEBUG if debug else logging.WARN))
1650
59c29869
JS
1651 _verify_image_format(supported_fmts, unsupported_fmts)
1652 _verify_protocol(supported_protocols, unsupported_protocols)
1653 _verify_platform(supported=supported_platforms)
1654 _verify_cache_mode(supported_cache_modes)
1655 _verify_aio_mode(supported_aio_modes)
18654716 1656 _verify_formats(required_fmts)
f203080b 1657 _verify_virtio_blk()
7c15400c 1658 _verify_imgopts(unsupported_imgopts)
c6a92369 1659
7d814059
JS
1660 return debug
1661
1662def execute_test(*args, test_function=None, **kwargs):
1663 """Run either unittest or script-style tests."""
1664
1665 debug = execute_setup_common(*args, **kwargs)
456a2d5a 1666 if not test_function:
00dbc85e 1667 execute_unittest(sys.argv, debug)
456a2d5a
JS
1668 else:
1669 test_function()
1670
52ea799e
JS
1671def activate_logging():
1672 """Activate iotests.log() output to stdout for script-style tests."""
1673 handler = logging.StreamHandler(stream=sys.stdout)
1674 formatter = logging.Formatter('%(message)s')
1675 handler.setFormatter(formatter)
1676 test_logger.addHandler(handler)
1677 test_logger.setLevel(logging.INFO)
1678 test_logger.propagate = False
1679
7d814059
JS
1680# This is called from script-style iotests without a single point of entry
1681def script_initialize(*args, **kwargs):
1682 """Initialize script-style tests without running any tests."""
52ea799e 1683 activate_logging()
7d814059
JS
1684 execute_setup_common(*args, **kwargs)
1685
1686# This is called from script-style iotests with a single point of entry
456a2d5a
JS
1687def script_main(test_function, *args, **kwargs):
1688 """Run script-style tests outside of the unittest framework"""
52ea799e 1689 activate_logging()
7d814059 1690 execute_test(*args, test_function=test_function, **kwargs)
f345cfd0 1691
7d814059 1692# This is called from unittest style iotests
456a2d5a
JS
1693def main(*args, **kwargs):
1694 """Run tests using the unittest framework"""
7d814059 1695 execute_test(*args, **kwargs)