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