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