]>
Commit | Line | Data |
---|---|---|
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 | ||
19 | import os | |
20 | import re | |
21 | import subprocess | |
4f450568 | 22 | import string |
f345cfd0 | 23 | import unittest |
ed338bb0 FZ |
24 | import sys |
25 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts')) | |
26 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts', 'qmp')) | |
f345cfd0 | 27 | import qmp |
ed338bb0 | 28 | import qtest |
2499a096 | 29 | import struct |
f345cfd0 SH |
30 | |
31 | __all__ = ['imgfmt', 'imgproto', 'test_dir' 'qemu_img', 'qemu_io', | |
32 | 'VM', 'QMPTestCase', 'notrun', 'main'] | |
33 | ||
934659c4 | 34 | # This will not work if arguments contain spaces but is necessary if we |
f345cfd0 | 35 | # want to support the override options that ./check supports. |
934659c4 HR |
36 | qemu_img_args = [os.environ.get('QEMU_IMG_PROG', 'qemu-img')] |
37 | if os.environ.get('QEMU_IMG_OPTIONS'): | |
38 | qemu_img_args += os.environ['QEMU_IMG_OPTIONS'].strip().split(' ') | |
39 | ||
40 | qemu_io_args = [os.environ.get('QEMU_IO_PROG', 'qemu-io')] | |
41 | if os.environ.get('QEMU_IO_OPTIONS'): | |
42 | qemu_io_args += os.environ['QEMU_IO_OPTIONS'].strip().split(' ') | |
43 | ||
44 | qemu_args = [os.environ.get('QEMU_PROG', 'qemu')] | |
45 | if os.environ.get('QEMU_OPTIONS'): | |
46 | qemu_args += os.environ['QEMU_OPTIONS'].strip().split(' ') | |
f345cfd0 SH |
47 | |
48 | imgfmt = os.environ.get('IMGFMT', 'raw') | |
49 | imgproto = os.environ.get('IMGPROTO', 'file') | |
50 | test_dir = os.environ.get('TEST_DIR', '/var/tmp') | |
e8f8624d | 51 | output_dir = os.environ.get('OUTPUT_DIR', '.') |
58cc2ae1 | 52 | cachemode = os.environ.get('CACHEMODE') |
e166b414 | 53 | qemu_default_machine = os.environ.get('QEMU_DEFAULT_MACHINE') |
f345cfd0 | 54 | |
30b005d9 WX |
55 | socket_scm_helper = os.environ.get('SOCKET_SCM_HELPER', 'socket_scm_helper') |
56 | ||
f345cfd0 SH |
57 | def qemu_img(*args): |
58 | '''Run qemu-img and return the exit code''' | |
59 | devnull = open('/dev/null', 'r+') | |
60 | return subprocess.call(qemu_img_args + list(args), stdin=devnull, stdout=devnull) | |
61 | ||
d2ef210c | 62 | def qemu_img_verbose(*args): |
993d46ce | 63 | '''Run qemu-img without suppressing its output and return the exit code''' |
d2ef210c KW |
64 | return subprocess.call(qemu_img_args + list(args)) |
65 | ||
3677e6f6 HR |
66 | def qemu_img_pipe(*args): |
67 | '''Run qemu-img and return its output''' | |
68 | return subprocess.Popen(qemu_img_args + list(args), stdout=subprocess.PIPE).communicate()[0] | |
69 | ||
f345cfd0 SH |
70 | def qemu_io(*args): |
71 | '''Run qemu-io and return the stdout data''' | |
72 | args = qemu_io_args + list(args) | |
73 | return subprocess.Popen(args, stdout=subprocess.PIPE).communicate()[0] | |
74 | ||
3a3918c3 SH |
75 | def compare_images(img1, img2): |
76 | '''Return True if two image files are identical''' | |
77 | return qemu_img('compare', '-f', imgfmt, | |
78 | '-F', imgfmt, img1, img2) == 0 | |
79 | ||
2499a096 SH |
80 | def create_image(name, size): |
81 | '''Create a fully-allocated raw image with sector markers''' | |
82 | file = open(name, 'w') | |
83 | i = 0 | |
84 | while i < size: | |
85 | sector = struct.pack('>l504xl', i / 512, i / 512) | |
86 | file.write(sector) | |
87 | i = i + 512 | |
88 | file.close() | |
89 | ||
7898f74e JS |
90 | # Test if 'match' is a recursive subset of 'event' |
91 | def event_match(event, match=None): | |
92 | if match is None: | |
93 | return True | |
94 | ||
95 | for key in match: | |
96 | if key in event: | |
97 | if isinstance(event[key], dict): | |
98 | if not event_match(event[key], match[key]): | |
99 | return False | |
100 | elif event[key] != match[key]: | |
101 | return False | |
102 | else: | |
103 | return False | |
104 | ||
105 | return True | |
106 | ||
f345cfd0 SH |
107 | class VM(object): |
108 | '''A QEMU VM''' | |
109 | ||
110 | def __init__(self): | |
111 | self._monitor_path = os.path.join(test_dir, 'qemu-mon.%d' % os.getpid()) | |
112 | self._qemu_log_path = os.path.join(test_dir, 'qemu-log.%d' % os.getpid()) | |
ed338bb0 | 113 | self._qtest_path = os.path.join(test_dir, 'qemu-qtest.%d' % os.getpid()) |
f345cfd0 SH |
114 | self._args = qemu_args + ['-chardev', |
115 | 'socket,id=mon,path=' + self._monitor_path, | |
0fd05e8d | 116 | '-mon', 'chardev=mon,mode=control', |
ed338bb0 FZ |
117 | '-qtest', 'unix:path=' + self._qtest_path, |
118 | '-machine', 'accel=qtest', | |
0fd05e8d | 119 | '-display', 'none', '-vga', 'none'] |
f345cfd0 | 120 | self._num_drives = 0 |
7898f74e | 121 | self._events = [] |
f345cfd0 | 122 | |
30b005d9 WX |
123 | # This can be used to add an unused monitor instance. |
124 | def add_monitor_telnet(self, ip, port): | |
125 | args = 'tcp:%s:%d,server,nowait,telnet' % (ip, port) | |
126 | self._args.append('-monitor') | |
127 | self._args.append(args) | |
128 | ||
8e492253 | 129 | def add_drive(self, path, opts='', interface='virtio'): |
f345cfd0 | 130 | '''Add a virtio-blk drive to the VM''' |
8e492253 | 131 | options = ['if=%s' % interface, |
f345cfd0 | 132 | 'format=%s' % imgfmt, |
58cc2ae1 | 133 | 'cache=%s' % cachemode, |
f345cfd0 | 134 | 'id=drive%d' % self._num_drives] |
8e492253 HR |
135 | |
136 | if path is not None: | |
137 | options.append('file=%s' % path) | |
138 | ||
f345cfd0 SH |
139 | if opts: |
140 | options.append(opts) | |
141 | ||
142 | self._args.append('-drive') | |
143 | self._args.append(','.join(options)) | |
144 | self._num_drives += 1 | |
145 | return self | |
146 | ||
3cf53c77 FZ |
147 | def pause_drive(self, drive, event=None): |
148 | '''Pause drive r/w operations''' | |
149 | if not event: | |
150 | self.pause_drive(drive, "read_aio") | |
151 | self.pause_drive(drive, "write_aio") | |
152 | return | |
153 | self.qmp('human-monitor-command', | |
154 | command_line='qemu-io %s "break %s bp_%s"' % (drive, event, drive)) | |
155 | ||
156 | def resume_drive(self, drive): | |
157 | self.qmp('human-monitor-command', | |
158 | command_line='qemu-io %s "remove_break bp_%s"' % (drive, drive)) | |
159 | ||
e3409362 IM |
160 | def hmp_qemu_io(self, drive, cmd): |
161 | '''Write to a given drive using an HMP command''' | |
162 | return self.qmp('human-monitor-command', | |
163 | command_line='qemu-io %s "%s"' % (drive, cmd)) | |
164 | ||
23e956bf CB |
165 | def add_fd(self, fd, fdset, opaque, opts=''): |
166 | '''Pass a file descriptor to the VM''' | |
167 | options = ['fd=%d' % fd, | |
168 | 'set=%d' % fdset, | |
169 | 'opaque=%s' % opaque] | |
170 | if opts: | |
171 | options.append(opts) | |
172 | ||
173 | self._args.append('-add-fd') | |
174 | self._args.append(','.join(options)) | |
175 | return self | |
176 | ||
30b005d9 WX |
177 | def send_fd_scm(self, fd_file_path): |
178 | # In iotest.py, the qmp should always use unix socket. | |
179 | assert self._qmp.is_scm_available() | |
180 | bin = socket_scm_helper | |
181 | if os.path.exists(bin) == False: | |
182 | print "Scm help program does not present, path '%s'." % bin | |
183 | return -1 | |
184 | fd_param = ["%s" % bin, | |
185 | "%d" % self._qmp.get_sock_fd(), | |
186 | "%s" % fd_file_path] | |
187 | devnull = open('/dev/null', 'rb') | |
188 | p = subprocess.Popen(fd_param, stdin=devnull, stdout=sys.stdout, | |
189 | stderr=sys.stderr) | |
190 | return p.wait() | |
191 | ||
f345cfd0 SH |
192 | def launch(self): |
193 | '''Launch the VM and establish a QMP connection''' | |
194 | devnull = open('/dev/null', 'rb') | |
195 | qemulog = open(self._qemu_log_path, 'wb') | |
196 | try: | |
197 | self._qmp = qmp.QEMUMonitorProtocol(self._monitor_path, server=True) | |
ed338bb0 | 198 | self._qtest = qtest.QEMUQtestProtocol(self._qtest_path, server=True) |
f345cfd0 SH |
199 | self._popen = subprocess.Popen(self._args, stdin=devnull, stdout=qemulog, |
200 | stderr=subprocess.STDOUT) | |
201 | self._qmp.accept() | |
ed338bb0 | 202 | self._qtest.accept() |
f345cfd0 SH |
203 | except: |
204 | os.remove(self._monitor_path) | |
205 | raise | |
206 | ||
207 | def shutdown(self): | |
208 | '''Terminate the VM and clean up''' | |
863a5d04 PB |
209 | if not self._popen is None: |
210 | self._qmp.cmd('quit') | |
211 | self._popen.wait() | |
212 | os.remove(self._monitor_path) | |
ed338bb0 | 213 | os.remove(self._qtest_path) |
863a5d04 PB |
214 | os.remove(self._qemu_log_path) |
215 | self._popen = None | |
f345cfd0 | 216 | |
4f450568 | 217 | underscore_to_dash = string.maketrans('_', '-') |
df89d112 | 218 | def qmp(self, cmd, conv_keys=True, **args): |
f345cfd0 | 219 | '''Invoke a QMP command and return the result dict''' |
4f450568 PB |
220 | qmp_args = dict() |
221 | for k in args.keys(): | |
df89d112 FZ |
222 | if conv_keys: |
223 | qmp_args[k.translate(self.underscore_to_dash)] = args[k] | |
224 | else: | |
225 | qmp_args[k] = args[k] | |
4f450568 PB |
226 | |
227 | return self._qmp.cmd(cmd, args=qmp_args) | |
f345cfd0 | 228 | |
ed338bb0 FZ |
229 | def qtest(self, cmd): |
230 | '''Send a qtest command to guest''' | |
231 | return self._qtest.cmd(cmd) | |
232 | ||
9dfa9f59 PB |
233 | def get_qmp_event(self, wait=False): |
234 | '''Poll for one queued QMP events and return it''' | |
7898f74e JS |
235 | if len(self._events) > 0: |
236 | return self._events.pop(0) | |
9dfa9f59 PB |
237 | return self._qmp.pull_event(wait=wait) |
238 | ||
f345cfd0 SH |
239 | def get_qmp_events(self, wait=False): |
240 | '''Poll for queued QMP events and return a list of dicts''' | |
241 | events = self._qmp.get_events(wait=wait) | |
7898f74e JS |
242 | events.extend(self._events) |
243 | del self._events[:] | |
f345cfd0 SH |
244 | self._qmp.clear_events() |
245 | return events | |
246 | ||
7898f74e JS |
247 | def event_wait(self, name='BLOCK_JOB_COMPLETED', timeout=60.0, match=None): |
248 | # Search cached events | |
249 | for event in self._events: | |
250 | if (event['event'] == name) and event_match(event, match): | |
251 | self._events.remove(event) | |
252 | return event | |
253 | ||
254 | # Poll for new events | |
255 | while True: | |
256 | event = self._qmp.pull_event(wait=timeout) | |
257 | if (event['event'] == name) and event_match(event, match): | |
258 | return event | |
259 | self._events.append(event) | |
260 | ||
261 | return None | |
262 | ||
f345cfd0 SH |
263 | index_re = re.compile(r'([^\[]+)\[([^\]]+)\]') |
264 | ||
265 | class QMPTestCase(unittest.TestCase): | |
266 | '''Abstract base class for QMP test cases''' | |
267 | ||
268 | def dictpath(self, d, path): | |
269 | '''Traverse a path in a nested dict''' | |
270 | for component in path.split('/'): | |
271 | m = index_re.match(component) | |
272 | if m: | |
273 | component, idx = m.groups() | |
274 | idx = int(idx) | |
275 | ||
276 | if not isinstance(d, dict) or component not in d: | |
277 | self.fail('failed path traversal for "%s" in "%s"' % (path, str(d))) | |
278 | d = d[component] | |
279 | ||
280 | if m: | |
281 | if not isinstance(d, list): | |
282 | self.fail('path component "%s" in "%s" is not a list in "%s"' % (component, path, str(d))) | |
283 | try: | |
284 | d = d[idx] | |
285 | except IndexError: | |
286 | self.fail('invalid index "%s" in path "%s" in "%s"' % (idx, path, str(d))) | |
287 | return d | |
288 | ||
90f0b711 PB |
289 | def assert_qmp_absent(self, d, path): |
290 | try: | |
291 | result = self.dictpath(d, path) | |
292 | except AssertionError: | |
293 | return | |
294 | self.fail('path "%s" has value "%s"' % (path, str(result))) | |
295 | ||
f345cfd0 SH |
296 | def assert_qmp(self, d, path, value): |
297 | '''Assert that the value for a specific path in a QMP dict matches''' | |
298 | result = self.dictpath(d, path) | |
299 | self.assertEqual(result, value, 'values not equal "%s" and "%s"' % (str(result), str(value))) | |
300 | ||
ecc1c88e SH |
301 | def assert_no_active_block_jobs(self): |
302 | result = self.vm.qmp('query-block-jobs') | |
303 | self.assert_qmp(result, 'return', []) | |
304 | ||
3cf53c77 | 305 | def cancel_and_wait(self, drive='drive0', force=False, resume=False): |
2575fe16 SH |
306 | '''Cancel a block job and wait for it to finish, returning the event''' |
307 | result = self.vm.qmp('block-job-cancel', device=drive, force=force) | |
308 | self.assert_qmp(result, 'return', {}) | |
309 | ||
3cf53c77 FZ |
310 | if resume: |
311 | self.vm.resume_drive(drive) | |
312 | ||
2575fe16 SH |
313 | cancelled = False |
314 | result = None | |
315 | while not cancelled: | |
316 | for event in self.vm.get_qmp_events(wait=True): | |
317 | if event['event'] == 'BLOCK_JOB_COMPLETED' or \ | |
318 | event['event'] == 'BLOCK_JOB_CANCELLED': | |
319 | self.assert_qmp(event, 'data/device', drive) | |
320 | result = event | |
321 | cancelled = True | |
322 | ||
323 | self.assert_no_active_block_jobs() | |
324 | return result | |
325 | ||
9974ad40 | 326 | def wait_until_completed(self, drive='drive0', check_offset=True): |
0dbe8a1b SH |
327 | '''Wait for a block job to finish, returning the event''' |
328 | completed = False | |
329 | while not completed: | |
330 | for event in self.vm.get_qmp_events(wait=True): | |
331 | if event['event'] == 'BLOCK_JOB_COMPLETED': | |
332 | self.assert_qmp(event, 'data/device', drive) | |
333 | self.assert_qmp_absent(event, 'data/error') | |
9974ad40 | 334 | if check_offset: |
1d3ba15a | 335 | self.assert_qmp(event, 'data/offset', event['data']['len']) |
0dbe8a1b SH |
336 | completed = True |
337 | ||
338 | self.assert_no_active_block_jobs() | |
339 | return event | |
340 | ||
866323f3 FZ |
341 | def wait_ready(self, drive='drive0'): |
342 | '''Wait until a block job BLOCK_JOB_READY event''' | |
d7b25297 FZ |
343 | f = {'data': {'type': 'mirror', 'device': drive } } |
344 | event = self.vm.event_wait(name='BLOCK_JOB_READY', match=f) | |
866323f3 FZ |
345 | |
346 | def wait_ready_and_cancel(self, drive='drive0'): | |
347 | self.wait_ready(drive=drive) | |
348 | event = self.cancel_and_wait(drive=drive) | |
349 | self.assertEquals(event['event'], 'BLOCK_JOB_COMPLETED') | |
350 | self.assert_qmp(event, 'data/type', 'mirror') | |
351 | self.assert_qmp(event, 'data/offset', event['data']['len']) | |
352 | ||
353 | def complete_and_wait(self, drive='drive0', wait_ready=True): | |
354 | '''Complete a block job and wait for it to finish''' | |
355 | if wait_ready: | |
356 | self.wait_ready(drive=drive) | |
357 | ||
358 | result = self.vm.qmp('block-job-complete', device=drive) | |
359 | self.assert_qmp(result, 'return', {}) | |
360 | ||
361 | event = self.wait_until_completed(drive=drive) | |
362 | self.assert_qmp(event, 'data/type', 'mirror') | |
363 | ||
f345cfd0 SH |
364 | def notrun(reason): |
365 | '''Skip this test suite''' | |
366 | # Each test in qemu-iotests has a number ("seq") | |
367 | seq = os.path.basename(sys.argv[0]) | |
368 | ||
e8f8624d | 369 | open('%s/%s.notrun' % (output_dir, seq), 'wb').write(reason + '\n') |
f345cfd0 SH |
370 | print '%s not run: %s' % (seq, reason) |
371 | sys.exit(0) | |
372 | ||
bc521696 | 373 | def main(supported_fmts=[], supported_oses=['linux']): |
f345cfd0 SH |
374 | '''Run tests''' |
375 | ||
aa4f592a FZ |
376 | debug = '-d' in sys.argv |
377 | verbosity = 1 | |
f345cfd0 SH |
378 | if supported_fmts and (imgfmt not in supported_fmts): |
379 | notrun('not suitable for this image format: %s' % imgfmt) | |
380 | ||
79e7a019 | 381 | if True not in [sys.platform.startswith(x) for x in supported_oses]: |
bc521696 FZ |
382 | notrun('not suitable for this OS: %s' % sys.platform) |
383 | ||
f345cfd0 SH |
384 | # We need to filter out the time taken from the output so that qemu-iotest |
385 | # can reliably diff the results against master output. | |
386 | import StringIO | |
aa4f592a FZ |
387 | if debug: |
388 | output = sys.stdout | |
389 | verbosity = 2 | |
390 | sys.argv.remove('-d') | |
391 | else: | |
392 | output = StringIO.StringIO() | |
f345cfd0 SH |
393 | |
394 | class MyTestRunner(unittest.TextTestRunner): | |
aa4f592a | 395 | def __init__(self, stream=output, descriptions=True, verbosity=verbosity): |
f345cfd0 SH |
396 | unittest.TextTestRunner.__init__(self, stream, descriptions, verbosity) |
397 | ||
398 | # unittest.main() will use sys.exit() so expect a SystemExit exception | |
399 | try: | |
400 | unittest.main(testRunner=MyTestRunner) | |
401 | finally: | |
aa4f592a FZ |
402 | if not debug: |
403 | sys.stderr.write(re.sub(r'Ran (\d+) tests? in [\d.]+s', r'Ran \1 tests', output.getvalue())) |