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