Merge remote-tracking branch 'remotes/maxreitz/tags/pull-block-2019-07-15' into staging
[qemu/ar7.git] / tests / qemu-iotests / iotests.py
blobce74177ab164c97bc1490ec1b55f673bcb1ac94a
1 from __future__ import print_function
2 # Common utilities and Python wrappers for qemu-iotests
4 # Copyright (C) 2012 IBM Corp.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 import errno
21 import os
22 import re
23 import subprocess
24 import string
25 import unittest
26 import sys
27 import struct
28 import json
29 import signal
30 import logging
31 import atexit
32 import io
33 from collections import OrderedDict
35 sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python'))
36 from qemu import qtest
39 # This will not work if arguments contain spaces but is necessary if we
40 # want to support the override options that ./check supports.
41 qemu_img_args = [os.environ.get('QEMU_IMG_PROG', 'qemu-img')]
42 if os.environ.get('QEMU_IMG_OPTIONS'):
43 qemu_img_args += os.environ['QEMU_IMG_OPTIONS'].strip().split(' ')
45 qemu_io_args = [os.environ.get('QEMU_IO_PROG', 'qemu-io')]
46 if os.environ.get('QEMU_IO_OPTIONS'):
47 qemu_io_args += os.environ['QEMU_IO_OPTIONS'].strip().split(' ')
49 qemu_nbd_args = [os.environ.get('QEMU_NBD_PROG', 'qemu-nbd')]
50 if os.environ.get('QEMU_NBD_OPTIONS'):
51 qemu_nbd_args += os.environ['QEMU_NBD_OPTIONS'].strip().split(' ')
53 qemu_prog = os.environ.get('QEMU_PROG', 'qemu')
54 qemu_opts = os.environ.get('QEMU_OPTIONS', '').strip().split(' ')
56 imgfmt = os.environ.get('IMGFMT', 'raw')
57 imgproto = os.environ.get('IMGPROTO', 'file')
58 test_dir = os.environ.get('TEST_DIR')
59 output_dir = os.environ.get('OUTPUT_DIR', '.')
60 cachemode = os.environ.get('CACHEMODE')
61 qemu_default_machine = os.environ.get('QEMU_DEFAULT_MACHINE')
63 socket_scm_helper = os.environ.get('SOCKET_SCM_HELPER', 'socket_scm_helper')
64 debug = False
66 luks_default_secret_object = 'secret,id=keysec0,data=' + \
67 os.environ.get('IMGKEYSECRET', '')
68 luks_default_key_secret_opt = 'key-secret=keysec0'
71 def qemu_img(*args):
72 '''Run qemu-img and return the exit code'''
73 devnull = open('/dev/null', 'r+')
74 exitcode = subprocess.call(qemu_img_args + list(args), stdin=devnull, stdout=devnull)
75 if exitcode < 0:
76 sys.stderr.write('qemu-img received signal %i: %s\n' % (-exitcode, ' '.join(qemu_img_args + list(args))))
77 return exitcode
79 def ordered_qmp(qmsg, conv_keys=True):
80 # Dictionaries are not ordered prior to 3.6, therefore:
81 if isinstance(qmsg, list):
82 return [ordered_qmp(atom) for atom in qmsg]
83 if isinstance(qmsg, dict):
84 od = OrderedDict()
85 for k, v in sorted(qmsg.items()):
86 if conv_keys:
87 k = k.replace('_', '-')
88 od[k] = ordered_qmp(v, conv_keys=False)
89 return od
90 return qmsg
92 def qemu_img_create(*args):
93 args = list(args)
95 # default luks support
96 if '-f' in args and args[args.index('-f') + 1] == 'luks':
97 if '-o' in args:
98 i = args.index('-o')
99 if 'key-secret' not in args[i + 1]:
100 args[i + 1].append(luks_default_key_secret_opt)
101 args.insert(i + 2, '--object')
102 args.insert(i + 3, luks_default_secret_object)
103 else:
104 args = ['-o', luks_default_key_secret_opt,
105 '--object', luks_default_secret_object] + args
107 args.insert(0, 'create')
109 return qemu_img(*args)
111 def qemu_img_verbose(*args):
112 '''Run qemu-img without suppressing its output and return the exit code'''
113 exitcode = subprocess.call(qemu_img_args + list(args))
114 if exitcode < 0:
115 sys.stderr.write('qemu-img received signal %i: %s\n' % (-exitcode, ' '.join(qemu_img_args + list(args))))
116 return exitcode
118 def qemu_img_pipe(*args):
119 '''Run qemu-img and return its output'''
120 subp = subprocess.Popen(qemu_img_args + list(args),
121 stdout=subprocess.PIPE,
122 stderr=subprocess.STDOUT,
123 universal_newlines=True)
124 exitcode = subp.wait()
125 if exitcode < 0:
126 sys.stderr.write('qemu-img received signal %i: %s\n' % (-exitcode, ' '.join(qemu_img_args + list(args))))
127 return subp.communicate()[0]
129 def qemu_img_log(*args):
130 result = qemu_img_pipe(*args)
131 log(result, filters=[filter_testfiles])
132 return result
134 def img_info_log(filename, filter_path=None, imgopts=False, extra_args=[]):
135 args = [ 'info' ]
136 if imgopts:
137 args.append('--image-opts')
138 else:
139 args += [ '-f', imgfmt ]
140 args += extra_args
141 args.append(filename)
143 output = qemu_img_pipe(*args)
144 if not filter_path:
145 filter_path = filename
146 log(filter_img_info(output, filter_path))
148 def qemu_io(*args):
149 '''Run qemu-io and return the stdout data'''
150 args = qemu_io_args + list(args)
151 subp = subprocess.Popen(args, stdout=subprocess.PIPE,
152 stderr=subprocess.STDOUT,
153 universal_newlines=True)
154 exitcode = subp.wait()
155 if exitcode < 0:
156 sys.stderr.write('qemu-io received signal %i: %s\n' % (-exitcode, ' '.join(args)))
157 return subp.communicate()[0]
159 def qemu_io_silent(*args):
160 '''Run qemu-io and return the exit code, suppressing stdout'''
161 args = qemu_io_args + list(args)
162 exitcode = subprocess.call(args, stdout=open('/dev/null', 'w'))
163 if exitcode < 0:
164 sys.stderr.write('qemu-io received signal %i: %s\n' %
165 (-exitcode, ' '.join(args)))
166 return exitcode
169 class QemuIoInteractive:
170 def __init__(self, *args):
171 self.args = qemu_io_args + list(args)
172 self._p = subprocess.Popen(self.args, stdin=subprocess.PIPE,
173 stdout=subprocess.PIPE,
174 stderr=subprocess.STDOUT,
175 universal_newlines=True)
176 assert self._p.stdout.read(9) == 'qemu-io> '
178 def close(self):
179 self._p.communicate('q\n')
181 def _read_output(self):
182 pattern = 'qemu-io> '
183 n = len(pattern)
184 pos = 0
185 s = []
186 while pos != n:
187 c = self._p.stdout.read(1)
188 # check unexpected EOF
189 assert c != ''
190 s.append(c)
191 if c == pattern[pos]:
192 pos += 1
193 else:
194 pos = 0
196 return ''.join(s[:-n])
198 def cmd(self, cmd):
199 # quit command is in close(), '\n' is added automatically
200 assert '\n' not in cmd
201 cmd = cmd.strip()
202 assert cmd != 'q' and cmd != 'quit'
203 self._p.stdin.write(cmd + '\n')
204 self._p.stdin.flush()
205 return self._read_output()
208 def qemu_nbd(*args):
209 '''Run qemu-nbd in daemon mode and return the parent's exit code'''
210 return subprocess.call(qemu_nbd_args + ['--fork'] + list(args))
212 def qemu_nbd_early_pipe(*args):
213 '''Run qemu-nbd in daemon mode and return both the parent's exit code
214 and its output in case of an error'''
215 subp = subprocess.Popen(qemu_nbd_args + ['--fork'] + list(args),
216 stdout=subprocess.PIPE,
217 stderr=subprocess.STDOUT,
218 universal_newlines=True)
219 exitcode = subp.wait()
220 if exitcode < 0:
221 sys.stderr.write('qemu-nbd received signal %i: %s\n' %
222 (-exitcode,
223 ' '.join(qemu_nbd_args + ['--fork'] + list(args))))
224 if exitcode == 0:
225 return exitcode, ''
226 else:
227 return exitcode, subp.communicate()[0]
229 def compare_images(img1, img2, fmt1=imgfmt, fmt2=imgfmt):
230 '''Return True if two image files are identical'''
231 return qemu_img('compare', '-f', fmt1,
232 '-F', fmt2, img1, img2) == 0
234 def create_image(name, size):
235 '''Create a fully-allocated raw image with sector markers'''
236 file = open(name, 'wb')
237 i = 0
238 while i < size:
239 sector = struct.pack('>l504xl', i // 512, i // 512)
240 file.write(sector)
241 i = i + 512
242 file.close()
244 def image_size(img):
245 '''Return image's virtual size'''
246 r = qemu_img_pipe('info', '--output=json', '-f', imgfmt, img)
247 return json.loads(r)['virtual-size']
249 def is_str(val):
250 if sys.version_info.major >= 3:
251 return isinstance(val, str)
252 else:
253 return isinstance(val, str) or isinstance(val, unicode)
255 test_dir_re = re.compile(r"%s" % test_dir)
256 def filter_test_dir(msg):
257 return test_dir_re.sub("TEST_DIR", msg)
259 win32_re = re.compile(r"\r")
260 def filter_win32(msg):
261 return win32_re.sub("", msg)
263 qemu_io_re = re.compile(r"[0-9]* ops; [0-9\/:. sec]* \([0-9\/.inf]* [EPTGMKiBbytes]*\/sec and [0-9\/.inf]* ops\/sec\)")
264 def filter_qemu_io(msg):
265 msg = filter_win32(msg)
266 return qemu_io_re.sub("X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)", msg)
268 chown_re = re.compile(r"chown [0-9]+:[0-9]+")
269 def filter_chown(msg):
270 return chown_re.sub("chown UID:GID", msg)
272 def filter_qmp_event(event):
273 '''Filter a QMP event dict'''
274 event = dict(event)
275 if 'timestamp' in event:
276 event['timestamp']['seconds'] = 'SECS'
277 event['timestamp']['microseconds'] = 'USECS'
278 return event
280 def filter_qmp(qmsg, filter_fn):
281 '''Given a string filter, filter a QMP object's values.
282 filter_fn takes a (key, value) pair.'''
283 # Iterate through either lists or dicts;
284 if isinstance(qmsg, list):
285 items = enumerate(qmsg)
286 else:
287 items = qmsg.items()
289 for k, v in items:
290 if isinstance(v, list) or isinstance(v, dict):
291 qmsg[k] = filter_qmp(v, filter_fn)
292 else:
293 qmsg[k] = filter_fn(k, v)
294 return qmsg
296 def filter_testfiles(msg):
297 prefix = os.path.join(test_dir, "%s-" % (os.getpid()))
298 return msg.replace(prefix, 'TEST_DIR/PID-')
300 def filter_qmp_testfiles(qmsg):
301 def _filter(key, value):
302 if is_str(value):
303 return filter_testfiles(value)
304 return value
305 return filter_qmp(qmsg, _filter)
307 def filter_generated_node_ids(msg):
308 return re.sub("#block[0-9]+", "NODE_NAME", msg)
310 def filter_img_info(output, filename):
311 lines = []
312 for line in output.split('\n'):
313 if 'disk size' in line or 'actual-size' in line:
314 continue
315 line = line.replace(filename, 'TEST_IMG') \
316 .replace(imgfmt, 'IMGFMT')
317 line = re.sub('iters: [0-9]+', 'iters: XXX', line)
318 line = re.sub('uuid: [-a-f0-9]+', 'uuid: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX', line)
319 line = re.sub('cid: [0-9]+', 'cid: XXXXXXXXXX', line)
320 lines.append(line)
321 return '\n'.join(lines)
323 def filter_imgfmt(msg):
324 return msg.replace(imgfmt, 'IMGFMT')
326 def filter_qmp_imgfmt(qmsg):
327 def _filter(key, value):
328 if is_str(value):
329 return filter_imgfmt(value)
330 return value
331 return filter_qmp(qmsg, _filter)
333 def log(msg, filters=[], indent=None):
334 '''Logs either a string message or a JSON serializable message (like QMP).
335 If indent is provided, JSON serializable messages are pretty-printed.'''
336 for flt in filters:
337 msg = flt(msg)
338 if isinstance(msg, dict) or isinstance(msg, list):
339 # Python < 3.4 needs to know not to add whitespace when pretty-printing:
340 separators = (', ', ': ') if indent is None else (',', ': ')
341 # Don't sort if it's already sorted
342 do_sort = not isinstance(msg, OrderedDict)
343 print(json.dumps(msg, sort_keys=do_sort,
344 indent=indent, separators=separators))
345 else:
346 print(msg)
348 class Timeout:
349 def __init__(self, seconds, errmsg = "Timeout"):
350 self.seconds = seconds
351 self.errmsg = errmsg
352 def __enter__(self):
353 signal.signal(signal.SIGALRM, self.timeout)
354 signal.setitimer(signal.ITIMER_REAL, self.seconds)
355 return self
356 def __exit__(self, type, value, traceback):
357 signal.setitimer(signal.ITIMER_REAL, 0)
358 return False
359 def timeout(self, signum, frame):
360 raise Exception(self.errmsg)
363 class FilePath(object):
364 '''An auto-generated filename that cleans itself up.
366 Use this context manager to generate filenames and ensure that the file
367 gets deleted::
369 with TestFilePath('test.img') as img_path:
370 qemu_img('create', img_path, '1G')
371 # migration_sock_path is automatically deleted
373 def __init__(self, name):
374 filename = '{0}-{1}'.format(os.getpid(), name)
375 self.path = os.path.join(test_dir, filename)
377 def __enter__(self):
378 return self.path
380 def __exit__(self, exc_type, exc_val, exc_tb):
381 try:
382 os.remove(self.path)
383 except OSError:
384 pass
385 return False
388 def file_path_remover():
389 for path in reversed(file_path_remover.paths):
390 try:
391 os.remove(path)
392 except OSError:
393 pass
396 def file_path(*names):
397 ''' Another way to get auto-generated filename that cleans itself up.
399 Use is as simple as:
401 img_a, img_b = file_path('a.img', 'b.img')
402 sock = file_path('socket')
405 if not hasattr(file_path_remover, 'paths'):
406 file_path_remover.paths = []
407 atexit.register(file_path_remover)
409 paths = []
410 for name in names:
411 filename = '{0}-{1}'.format(os.getpid(), name)
412 path = os.path.join(test_dir, filename)
413 file_path_remover.paths.append(path)
414 paths.append(path)
416 return paths[0] if len(paths) == 1 else paths
418 def remote_filename(path):
419 if imgproto == 'file':
420 return path
421 elif imgproto == 'ssh':
422 return "ssh://%s@127.0.0.1:22%s" % (os.environ.get('USER'), path)
423 else:
424 raise Exception("Protocol %s not supported" % (imgproto))
426 class VM(qtest.QEMUQtestMachine):
427 '''A QEMU VM'''
429 def __init__(self, path_suffix=''):
430 name = "qemu%s-%d" % (path_suffix, os.getpid())
431 super(VM, self).__init__(qemu_prog, qemu_opts, name=name,
432 test_dir=test_dir,
433 socket_scm_helper=socket_scm_helper)
434 self._num_drives = 0
436 def add_object(self, opts):
437 self._args.append('-object')
438 self._args.append(opts)
439 return self
441 def add_device(self, opts):
442 self._args.append('-device')
443 self._args.append(opts)
444 return self
446 def add_drive_raw(self, opts):
447 self._args.append('-drive')
448 self._args.append(opts)
449 return self
451 def add_drive(self, path, opts='', interface='virtio', format=imgfmt):
452 '''Add a virtio-blk drive to the VM'''
453 options = ['if=%s' % interface,
454 'id=drive%d' % self._num_drives]
456 if path is not None:
457 options.append('file=%s' % path)
458 options.append('format=%s' % format)
459 options.append('cache=%s' % cachemode)
461 if opts:
462 options.append(opts)
464 if format == 'luks' and 'key-secret' not in opts:
465 # default luks support
466 if luks_default_secret_object not in self._args:
467 self.add_object(luks_default_secret_object)
469 options.append(luks_default_key_secret_opt)
471 self._args.append('-drive')
472 self._args.append(','.join(options))
473 self._num_drives += 1
474 return self
476 def add_blockdev(self, opts):
477 self._args.append('-blockdev')
478 if isinstance(opts, str):
479 self._args.append(opts)
480 else:
481 self._args.append(','.join(opts))
482 return self
484 def add_incoming(self, addr):
485 self._args.append('-incoming')
486 self._args.append(addr)
487 return self
489 def pause_drive(self, drive, event=None):
490 '''Pause drive r/w operations'''
491 if not event:
492 self.pause_drive(drive, "read_aio")
493 self.pause_drive(drive, "write_aio")
494 return
495 self.qmp('human-monitor-command',
496 command_line='qemu-io %s "break %s bp_%s"' % (drive, event, drive))
498 def resume_drive(self, drive):
499 self.qmp('human-monitor-command',
500 command_line='qemu-io %s "remove_break bp_%s"' % (drive, drive))
502 def hmp_qemu_io(self, drive, cmd):
503 '''Write to a given drive using an HMP command'''
504 return self.qmp('human-monitor-command',
505 command_line='qemu-io %s "%s"' % (drive, cmd))
507 def flatten_qmp_object(self, obj, output=None, basestr=''):
508 if output is None:
509 output = dict()
510 if isinstance(obj, list):
511 for i in range(len(obj)):
512 self.flatten_qmp_object(obj[i], output, basestr + str(i) + '.')
513 elif isinstance(obj, dict):
514 for key in obj:
515 self.flatten_qmp_object(obj[key], output, basestr + key + '.')
516 else:
517 output[basestr[:-1]] = obj # Strip trailing '.'
518 return output
520 def qmp_to_opts(self, obj):
521 obj = self.flatten_qmp_object(obj)
522 output_list = list()
523 for key in obj:
524 output_list += [key + '=' + obj[key]]
525 return ','.join(output_list)
527 def get_qmp_events_filtered(self, wait=60.0):
528 result = []
529 for ev in self.get_qmp_events(wait=wait):
530 result.append(filter_qmp_event(ev))
531 return result
533 def qmp_log(self, cmd, filters=[], indent=None, **kwargs):
534 full_cmd = OrderedDict((
535 ("execute", cmd),
536 ("arguments", ordered_qmp(kwargs))
538 log(full_cmd, filters, indent=indent)
539 result = self.qmp(cmd, **kwargs)
540 log(result, filters, indent=indent)
541 return result
543 # Returns None on success, and an error string on failure
544 def run_job(self, job, auto_finalize=True, auto_dismiss=False,
545 pre_finalize=None, use_log=True, wait=60.0):
546 match_device = {'data': {'device': job}}
547 match_id = {'data': {'id': job}}
548 events = [
549 ('BLOCK_JOB_COMPLETED', match_device),
550 ('BLOCK_JOB_CANCELLED', match_device),
551 ('BLOCK_JOB_ERROR', match_device),
552 ('BLOCK_JOB_READY', match_device),
553 ('BLOCK_JOB_PENDING', match_id),
554 ('JOB_STATUS_CHANGE', match_id)
556 error = None
557 while True:
558 ev = filter_qmp_event(self.events_wait(events))
559 if ev['event'] != 'JOB_STATUS_CHANGE':
560 if use_log:
561 log(ev)
562 continue
563 status = ev['data']['status']
564 if status == 'aborting':
565 result = self.qmp('query-jobs')
566 for j in result['return']:
567 if j['id'] == job:
568 error = j['error']
569 if use_log:
570 log('Job failed: %s' % (j['error']))
571 elif status == 'pending' and not auto_finalize:
572 if pre_finalize:
573 pre_finalize()
574 if use_log:
575 self.qmp_log('job-finalize', id=job)
576 else:
577 self.qmp('job-finalize', id=job)
578 elif status == 'concluded' and not auto_dismiss:
579 if use_log:
580 self.qmp_log('job-dismiss', id=job)
581 else:
582 self.qmp('job-dismiss', id=job)
583 elif status == 'null':
584 return error
586 def node_info(self, node_name):
587 nodes = self.qmp('query-named-block-nodes')
588 for x in nodes['return']:
589 if x['node-name'] == node_name:
590 return x
591 return None
594 index_re = re.compile(r'([^\[]+)\[([^\]]+)\]')
596 class QMPTestCase(unittest.TestCase):
597 '''Abstract base class for QMP test cases'''
599 def dictpath(self, d, path):
600 '''Traverse a path in a nested dict'''
601 for component in path.split('/'):
602 m = index_re.match(component)
603 if m:
604 component, idx = m.groups()
605 idx = int(idx)
607 if not isinstance(d, dict) or component not in d:
608 self.fail('failed path traversal for "%s" in "%s"' % (path, str(d)))
609 d = d[component]
611 if m:
612 if not isinstance(d, list):
613 self.fail('path component "%s" in "%s" is not a list in "%s"' % (component, path, str(d)))
614 try:
615 d = d[idx]
616 except IndexError:
617 self.fail('invalid index "%s" in path "%s" in "%s"' % (idx, path, str(d)))
618 return d
620 def assert_qmp_absent(self, d, path):
621 try:
622 result = self.dictpath(d, path)
623 except AssertionError:
624 return
625 self.fail('path "%s" has value "%s"' % (path, str(result)))
627 def assert_qmp(self, d, path, value):
628 '''Assert that the value for a specific path in a QMP dict
629 matches. When given a list of values, assert that any of
630 them matches.'''
632 result = self.dictpath(d, path)
634 # [] makes no sense as a list of valid values, so treat it as
635 # an actual single value.
636 if isinstance(value, list) and value != []:
637 for v in value:
638 if result == v:
639 return
640 self.fail('no match for "%s" in %s' % (str(result), str(value)))
641 else:
642 self.assertEqual(result, value,
643 'values not equal "%s" and "%s"'
644 % (str(result), str(value)))
646 def assert_no_active_block_jobs(self):
647 result = self.vm.qmp('query-block-jobs')
648 self.assert_qmp(result, 'return', [])
650 def assert_has_block_node(self, node_name=None, file_name=None):
651 """Issue a query-named-block-nodes and assert node_name and/or
652 file_name is present in the result"""
653 def check_equal_or_none(a, b):
654 return a == None or b == None or a == b
655 assert node_name or file_name
656 result = self.vm.qmp('query-named-block-nodes')
657 for x in result["return"]:
658 if check_equal_or_none(x.get("node-name"), node_name) and \
659 check_equal_or_none(x.get("file"), file_name):
660 return
661 self.assertTrue(False, "Cannot find %s %s in result:\n%s" % \
662 (node_name, file_name, result))
664 def assert_json_filename_equal(self, json_filename, reference):
665 '''Asserts that the given filename is a json: filename and that its
666 content is equal to the given reference object'''
667 self.assertEqual(json_filename[:5], 'json:')
668 self.assertEqual(self.vm.flatten_qmp_object(json.loads(json_filename[5:])),
669 self.vm.flatten_qmp_object(reference))
671 def cancel_and_wait(self, drive='drive0', force=False, resume=False, wait=60.0):
672 '''Cancel a block job and wait for it to finish, returning the event'''
673 result = self.vm.qmp('block-job-cancel', device=drive, force=force)
674 self.assert_qmp(result, 'return', {})
676 if resume:
677 self.vm.resume_drive(drive)
679 cancelled = False
680 result = None
681 while not cancelled:
682 for event in self.vm.get_qmp_events(wait=wait):
683 if event['event'] == 'BLOCK_JOB_COMPLETED' or \
684 event['event'] == 'BLOCK_JOB_CANCELLED':
685 self.assert_qmp(event, 'data/device', drive)
686 result = event
687 cancelled = True
688 elif event['event'] == 'JOB_STATUS_CHANGE':
689 self.assert_qmp(event, 'data/id', drive)
692 self.assert_no_active_block_jobs()
693 return result
695 def wait_until_completed(self, drive='drive0', check_offset=True, wait=60.0):
696 '''Wait for a block job to finish, returning the event'''
697 while True:
698 for event in self.vm.get_qmp_events(wait=wait):
699 if event['event'] == 'BLOCK_JOB_COMPLETED':
700 self.assert_qmp(event, 'data/device', drive)
701 self.assert_qmp_absent(event, 'data/error')
702 if check_offset:
703 self.assert_qmp(event, 'data/offset', event['data']['len'])
704 self.assert_no_active_block_jobs()
705 return event
706 elif event['event'] == 'JOB_STATUS_CHANGE':
707 self.assert_qmp(event, 'data/id', drive)
709 def wait_ready(self, drive='drive0'):
710 '''Wait until a block job BLOCK_JOB_READY event'''
711 f = {'data': {'type': 'mirror', 'device': drive } }
712 event = self.vm.event_wait(name='BLOCK_JOB_READY', match=f)
714 def wait_ready_and_cancel(self, drive='drive0'):
715 self.wait_ready(drive=drive)
716 event = self.cancel_and_wait(drive=drive)
717 self.assertEqual(event['event'], 'BLOCK_JOB_COMPLETED')
718 self.assert_qmp(event, 'data/type', 'mirror')
719 self.assert_qmp(event, 'data/offset', event['data']['len'])
721 def complete_and_wait(self, drive='drive0', wait_ready=True):
722 '''Complete a block job and wait for it to finish'''
723 if wait_ready:
724 self.wait_ready(drive=drive)
726 result = self.vm.qmp('block-job-complete', device=drive)
727 self.assert_qmp(result, 'return', {})
729 event = self.wait_until_completed(drive=drive)
730 self.assert_qmp(event, 'data/type', 'mirror')
732 def pause_wait(self, job_id='job0'):
733 with Timeout(1, "Timeout waiting for job to pause"):
734 while True:
735 result = self.vm.qmp('query-block-jobs')
736 found = False
737 for job in result['return']:
738 if job['device'] == job_id:
739 found = True
740 if job['paused'] == True and job['busy'] == False:
741 return job
742 break
743 assert found
745 def pause_job(self, job_id='job0', wait=True):
746 result = self.vm.qmp('block-job-pause', device=job_id)
747 self.assert_qmp(result, 'return', {})
748 if wait:
749 return self.pause_wait(job_id)
750 return result
753 def notrun(reason):
754 '''Skip this test suite'''
755 # Each test in qemu-iotests has a number ("seq")
756 seq = os.path.basename(sys.argv[0])
758 open('%s/%s.notrun' % (output_dir, seq), 'w').write(reason + '\n')
759 print('%s not run: %s' % (seq, reason))
760 sys.exit(0)
762 def case_notrun(reason):
763 '''Skip this test case'''
764 # Each test in qemu-iotests has a number ("seq")
765 seq = os.path.basename(sys.argv[0])
767 open('%s/%s.casenotrun' % (output_dir, seq), 'a').write(
768 ' [case not run] ' + reason + '\n')
770 def verify_image_format(supported_fmts=[], unsupported_fmts=[]):
771 assert not (supported_fmts and unsupported_fmts)
773 if 'generic' in supported_fmts and \
774 os.environ.get('IMGFMT_GENERIC', 'true') == 'true':
775 # similar to
776 # _supported_fmt generic
777 # for bash tests
778 return
780 not_sup = supported_fmts and (imgfmt not in supported_fmts)
781 if not_sup or (imgfmt in unsupported_fmts):
782 notrun('not suitable for this image format: %s' % imgfmt)
784 def verify_protocol(supported=[], unsupported=[]):
785 assert not (supported and unsupported)
787 if 'generic' in supported:
788 return
790 not_sup = supported and (imgproto not in supported)
791 if not_sup or (imgproto in unsupported):
792 notrun('not suitable for this protocol: %s' % imgproto)
794 def verify_platform(supported_oses=['linux']):
795 if True not in [sys.platform.startswith(x) for x in supported_oses]:
796 notrun('not suitable for this OS: %s' % sys.platform)
798 def verify_cache_mode(supported_cache_modes=[]):
799 if supported_cache_modes and (cachemode not in supported_cache_modes):
800 notrun('not suitable for this cache mode: %s' % cachemode)
802 def supports_quorum():
803 return 'quorum' in qemu_img_pipe('--help')
805 def verify_quorum():
806 '''Skip test suite if quorum support is not available'''
807 if not supports_quorum():
808 notrun('quorum support missing')
810 def qemu_pipe(*args):
811 '''Run qemu with an option to print something and exit (e.g. a help option),
812 and return its output'''
813 args = [qemu_prog] + qemu_opts + list(args)
814 subp = subprocess.Popen(args, stdout=subprocess.PIPE,
815 stderr=subprocess.STDOUT,
816 universal_newlines=True)
817 exitcode = subp.wait()
818 if exitcode < 0:
819 sys.stderr.write('qemu received signal %i: %s\n' % (-exitcode,
820 ' '.join(args)))
821 return subp.communicate()[0]
823 def supported_formats(read_only=False):
824 '''Set 'read_only' to True to check ro-whitelist
825 Otherwise, rw-whitelist is checked'''
826 format_message = qemu_pipe("-drive", "format=help")
827 line = 1 if read_only else 0
828 return format_message.splitlines()[line].split(":")[1].split()
830 def skip_if_unsupported(required_formats=[], read_only=False):
831 '''Skip Test Decorator
832 Runs the test if all the required formats are whitelisted'''
833 def skip_test_decorator(func):
834 def func_wrapper(*args, **kwargs):
835 usf_list = list(set(required_formats) -
836 set(supported_formats(read_only)))
837 if usf_list:
838 case_notrun('{}: formats {} are not whitelisted'.format(
839 args[0], usf_list))
840 else:
841 return func(*args, **kwargs)
842 return func_wrapper
843 return skip_test_decorator
845 def main(supported_fmts=[], supported_oses=['linux'], supported_cache_modes=[],
846 unsupported_fmts=[]):
847 '''Run tests'''
849 global debug
851 # We are using TEST_DIR and QEMU_DEFAULT_MACHINE as proxies to
852 # indicate that we're not being run via "check". There may be
853 # other things set up by "check" that individual test cases rely
854 # on.
855 if test_dir is None or qemu_default_machine is None:
856 sys.stderr.write('Please run this test via the "check" script\n')
857 sys.exit(os.EX_USAGE)
859 debug = '-d' in sys.argv
860 verbosity = 1
861 verify_image_format(supported_fmts, unsupported_fmts)
862 verify_platform(supported_oses)
863 verify_cache_mode(supported_cache_modes)
865 if debug:
866 output = sys.stdout
867 verbosity = 2
868 sys.argv.remove('-d')
869 else:
870 # We need to filter out the time taken from the output so that
871 # qemu-iotest can reliably diff the results against master output.
872 if sys.version_info.major >= 3:
873 output = io.StringIO()
874 else:
875 # io.StringIO is for unicode strings, which is not what
876 # 2.x's test runner emits.
877 output = io.BytesIO()
879 logging.basicConfig(level=(logging.DEBUG if debug else logging.WARN))
881 class MyTestRunner(unittest.TextTestRunner):
882 def __init__(self, stream=output, descriptions=True, verbosity=verbosity):
883 unittest.TextTestRunner.__init__(self, stream, descriptions, verbosity)
885 # unittest.main() will use sys.exit() so expect a SystemExit exception
886 try:
887 unittest.main(testRunner=MyTestRunner)
888 finally:
889 if not debug:
890 sys.stderr.write(re.sub(r'Ran (\d+) tests? in [\d.]+s', r'Ran \1 tests', output.getvalue()))