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