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