]>
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 SH |
23 | import unittest |
24 | import sys; sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'QMP')) | |
25 | import qmp | |
2499a096 | 26 | import struct |
f345cfd0 SH |
27 | |
28 | __all__ = ['imgfmt', 'imgproto', 'test_dir' 'qemu_img', 'qemu_io', | |
29 | 'VM', 'QMPTestCase', 'notrun', 'main'] | |
30 | ||
31 | # This will not work if arguments or path contain spaces but is necessary if we | |
32 | # want to support the override options that ./check supports. | |
c68b039a PB |
33 | qemu_img_args = os.environ.get('QEMU_IMG', 'qemu-img').strip().split(' ') |
34 | qemu_io_args = os.environ.get('QEMU_IO', 'qemu-io').strip().split(' ') | |
35 | qemu_args = os.environ.get('QEMU', 'qemu').strip().split(' ') | |
f345cfd0 SH |
36 | |
37 | imgfmt = os.environ.get('IMGFMT', 'raw') | |
38 | imgproto = os.environ.get('IMGPROTO', 'file') | |
39 | test_dir = os.environ.get('TEST_DIR', '/var/tmp') | |
40 | ||
41 | def qemu_img(*args): | |
42 | '''Run qemu-img and return the exit code''' | |
43 | devnull = open('/dev/null', 'r+') | |
44 | return subprocess.call(qemu_img_args + list(args), stdin=devnull, stdout=devnull) | |
45 | ||
d2ef210c | 46 | def qemu_img_verbose(*args): |
993d46ce | 47 | '''Run qemu-img without suppressing its output and return the exit code''' |
d2ef210c KW |
48 | return subprocess.call(qemu_img_args + list(args)) |
49 | ||
f345cfd0 SH |
50 | def qemu_io(*args): |
51 | '''Run qemu-io and return the stdout data''' | |
52 | args = qemu_io_args + list(args) | |
53 | return subprocess.Popen(args, stdout=subprocess.PIPE).communicate()[0] | |
54 | ||
3a3918c3 SH |
55 | def compare_images(img1, img2): |
56 | '''Return True if two image files are identical''' | |
57 | return qemu_img('compare', '-f', imgfmt, | |
58 | '-F', imgfmt, img1, img2) == 0 | |
59 | ||
2499a096 SH |
60 | def create_image(name, size): |
61 | '''Create a fully-allocated raw image with sector markers''' | |
62 | file = open(name, 'w') | |
63 | i = 0 | |
64 | while i < size: | |
65 | sector = struct.pack('>l504xl', i / 512, i / 512) | |
66 | file.write(sector) | |
67 | i = i + 512 | |
68 | file.close() | |
69 | ||
f345cfd0 SH |
70 | class VM(object): |
71 | '''A QEMU VM''' | |
72 | ||
73 | def __init__(self): | |
74 | self._monitor_path = os.path.join(test_dir, 'qemu-mon.%d' % os.getpid()) | |
75 | self._qemu_log_path = os.path.join(test_dir, 'qemu-log.%d' % os.getpid()) | |
76 | self._args = qemu_args + ['-chardev', | |
77 | 'socket,id=mon,path=' + self._monitor_path, | |
0fd05e8d PB |
78 | '-mon', 'chardev=mon,mode=control', |
79 | '-qtest', 'stdio', '-machine', 'accel=qtest', | |
80 | '-display', 'none', '-vga', 'none'] | |
f345cfd0 SH |
81 | self._num_drives = 0 |
82 | ||
83 | def add_drive(self, path, opts=''): | |
84 | '''Add a virtio-blk drive to the VM''' | |
85 | options = ['if=virtio', | |
86 | 'format=%s' % imgfmt, | |
87 | 'cache=none', | |
88 | 'file=%s' % path, | |
89 | 'id=drive%d' % self._num_drives] | |
90 | if opts: | |
91 | options.append(opts) | |
92 | ||
93 | self._args.append('-drive') | |
94 | self._args.append(','.join(options)) | |
95 | self._num_drives += 1 | |
96 | return self | |
97 | ||
e3409362 IM |
98 | def hmp_qemu_io(self, drive, cmd): |
99 | '''Write to a given drive using an HMP command''' | |
100 | return self.qmp('human-monitor-command', | |
101 | command_line='qemu-io %s "%s"' % (drive, cmd)) | |
102 | ||
23e956bf CB |
103 | def add_fd(self, fd, fdset, opaque, opts=''): |
104 | '''Pass a file descriptor to the VM''' | |
105 | options = ['fd=%d' % fd, | |
106 | 'set=%d' % fdset, | |
107 | 'opaque=%s' % opaque] | |
108 | if opts: | |
109 | options.append(opts) | |
110 | ||
111 | self._args.append('-add-fd') | |
112 | self._args.append(','.join(options)) | |
113 | return self | |
114 | ||
f345cfd0 SH |
115 | def launch(self): |
116 | '''Launch the VM and establish a QMP connection''' | |
117 | devnull = open('/dev/null', 'rb') | |
118 | qemulog = open(self._qemu_log_path, 'wb') | |
119 | try: | |
120 | self._qmp = qmp.QEMUMonitorProtocol(self._monitor_path, server=True) | |
121 | self._popen = subprocess.Popen(self._args, stdin=devnull, stdout=qemulog, | |
122 | stderr=subprocess.STDOUT) | |
123 | self._qmp.accept() | |
124 | except: | |
125 | os.remove(self._monitor_path) | |
126 | raise | |
127 | ||
128 | def shutdown(self): | |
129 | '''Terminate the VM and clean up''' | |
863a5d04 PB |
130 | if not self._popen is None: |
131 | self._qmp.cmd('quit') | |
132 | self._popen.wait() | |
133 | os.remove(self._monitor_path) | |
134 | os.remove(self._qemu_log_path) | |
135 | self._popen = None | |
f345cfd0 | 136 | |
4f450568 | 137 | underscore_to_dash = string.maketrans('_', '-') |
f345cfd0 SH |
138 | def qmp(self, cmd, **args): |
139 | '''Invoke a QMP command and return the result dict''' | |
4f450568 PB |
140 | qmp_args = dict() |
141 | for k in args.keys(): | |
142 | qmp_args[k.translate(self.underscore_to_dash)] = args[k] | |
143 | ||
144 | return self._qmp.cmd(cmd, args=qmp_args) | |
f345cfd0 | 145 | |
9dfa9f59 PB |
146 | def get_qmp_event(self, wait=False): |
147 | '''Poll for one queued QMP events and return it''' | |
148 | return self._qmp.pull_event(wait=wait) | |
149 | ||
f345cfd0 SH |
150 | def get_qmp_events(self, wait=False): |
151 | '''Poll for queued QMP events and return a list of dicts''' | |
152 | events = self._qmp.get_events(wait=wait) | |
153 | self._qmp.clear_events() | |
154 | return events | |
155 | ||
156 | index_re = re.compile(r'([^\[]+)\[([^\]]+)\]') | |
157 | ||
158 | class QMPTestCase(unittest.TestCase): | |
159 | '''Abstract base class for QMP test cases''' | |
160 | ||
161 | def dictpath(self, d, path): | |
162 | '''Traverse a path in a nested dict''' | |
163 | for component in path.split('/'): | |
164 | m = index_re.match(component) | |
165 | if m: | |
166 | component, idx = m.groups() | |
167 | idx = int(idx) | |
168 | ||
169 | if not isinstance(d, dict) or component not in d: | |
170 | self.fail('failed path traversal for "%s" in "%s"' % (path, str(d))) | |
171 | d = d[component] | |
172 | ||
173 | if m: | |
174 | if not isinstance(d, list): | |
175 | self.fail('path component "%s" in "%s" is not a list in "%s"' % (component, path, str(d))) | |
176 | try: | |
177 | d = d[idx] | |
178 | except IndexError: | |
179 | self.fail('invalid index "%s" in path "%s" in "%s"' % (idx, path, str(d))) | |
180 | return d | |
181 | ||
90f0b711 PB |
182 | def assert_qmp_absent(self, d, path): |
183 | try: | |
184 | result = self.dictpath(d, path) | |
185 | except AssertionError: | |
186 | return | |
187 | self.fail('path "%s" has value "%s"' % (path, str(result))) | |
188 | ||
f345cfd0 SH |
189 | def assert_qmp(self, d, path, value): |
190 | '''Assert that the value for a specific path in a QMP dict matches''' | |
191 | result = self.dictpath(d, path) | |
192 | self.assertEqual(result, value, 'values not equal "%s" and "%s"' % (str(result), str(value))) | |
193 | ||
ecc1c88e SH |
194 | def assert_no_active_block_jobs(self): |
195 | result = self.vm.qmp('query-block-jobs') | |
196 | self.assert_qmp(result, 'return', []) | |
197 | ||
2575fe16 SH |
198 | def cancel_and_wait(self, drive='drive0', force=False): |
199 | '''Cancel a block job and wait for it to finish, returning the event''' | |
200 | result = self.vm.qmp('block-job-cancel', device=drive, force=force) | |
201 | self.assert_qmp(result, 'return', {}) | |
202 | ||
203 | cancelled = False | |
204 | result = None | |
205 | while not cancelled: | |
206 | for event in self.vm.get_qmp_events(wait=True): | |
207 | if event['event'] == 'BLOCK_JOB_COMPLETED' or \ | |
208 | event['event'] == 'BLOCK_JOB_CANCELLED': | |
209 | self.assert_qmp(event, 'data/device', drive) | |
210 | result = event | |
211 | cancelled = True | |
212 | ||
213 | self.assert_no_active_block_jobs() | |
214 | return result | |
215 | ||
0dbe8a1b SH |
216 | def wait_until_completed(self, drive='drive0'): |
217 | '''Wait for a block job to finish, returning the event''' | |
218 | completed = False | |
219 | while not completed: | |
220 | for event in self.vm.get_qmp_events(wait=True): | |
221 | if event['event'] == 'BLOCK_JOB_COMPLETED': | |
222 | self.assert_qmp(event, 'data/device', drive) | |
223 | self.assert_qmp_absent(event, 'data/error') | |
224 | self.assert_qmp(event, 'data/offset', self.image_len) | |
225 | self.assert_qmp(event, 'data/len', self.image_len) | |
226 | completed = True | |
227 | ||
228 | self.assert_no_active_block_jobs() | |
229 | return event | |
230 | ||
f345cfd0 SH |
231 | def notrun(reason): |
232 | '''Skip this test suite''' | |
233 | # Each test in qemu-iotests has a number ("seq") | |
234 | seq = os.path.basename(sys.argv[0]) | |
235 | ||
236 | open('%s.notrun' % seq, 'wb').write(reason + '\n') | |
237 | print '%s not run: %s' % (seq, reason) | |
238 | sys.exit(0) | |
239 | ||
240 | def main(supported_fmts=[]): | |
241 | '''Run tests''' | |
242 | ||
243 | if supported_fmts and (imgfmt not in supported_fmts): | |
244 | notrun('not suitable for this image format: %s' % imgfmt) | |
245 | ||
246 | # We need to filter out the time taken from the output so that qemu-iotest | |
247 | # can reliably diff the results against master output. | |
248 | import StringIO | |
249 | output = StringIO.StringIO() | |
250 | ||
251 | class MyTestRunner(unittest.TextTestRunner): | |
252 | def __init__(self, stream=output, descriptions=True, verbosity=1): | |
253 | unittest.TextTestRunner.__init__(self, stream, descriptions, verbosity) | |
254 | ||
255 | # unittest.main() will use sys.exit() so expect a SystemExit exception | |
256 | try: | |
257 | unittest.main(testRunner=MyTestRunner) | |
258 | finally: | |
d2ef210c | 259 | sys.stderr.write(re.sub(r'Ran (\d+) tests? in [\d.]+s', r'Ran \1 tests', output.getvalue())) |