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