Tester: enable soundcard in QEMU
[ci.git] / htest / vm / qemu.py
blobe8eea80ccf1bb03e17d60aafc41b0480d5014703
1 #!/usr/bin/env python3
4 # Copyright (c) 2018 Vojtech Horky
5 # All rights reserved.
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions
9 # are met:
11 # - Redistributions of source code must retain the above copyright
12 # notice, this list of conditions and the following disclaimer.
13 # - Redistributions in binary form must reproduce the above copyright
14 # notice, this list of conditions and the following disclaimer in the
15 # documentation and/or other materials provided with the distribution.
16 # - The name of the author may not be used to endorse or promote products
17 # derived from this software without specific prior written permission.
19 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
20 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
21 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22 # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
23 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
24 # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 import subprocess
32 import socket
33 import logging
34 import os
35 import sys
37 from PIL import Image
39 from htest.utils import retries, format_command, format_command_pipe
40 from htest.vm.controller import VMController
42 class QemuVMController(VMController):
43 """
44 QEMU VM controller.
45 """
47 config = {
48 'amd64': [
49 'qemu-system-x86_64',
50 '-cdrom', '{BOOT}',
51 '-m', '{MEMORY}',
52 '-usb',
53 '-device', 'intel-hda', '-device', 'hda-duplex',
55 'arm32/integratorcp': [
56 'qemu-system-arm',
57 '-M', 'integratorcp',
58 '-usb',
59 '-kernel', '{BOOT}',
60 '-m', '{MEMORY}',
62 'ia32': [
63 'qemu-system-i386',
64 '-cdrom', '{BOOT}',
65 '-m', '{MEMORY}',
66 '-usb',
67 '-device', 'intel-hda', '-device', 'hda-duplex',
69 'ppc32': [
70 'qemu-system-ppc',
71 '-usb',
72 '-boot', 'd',
73 '-cdrom', '{BOOT}',
74 '-m', '{MEMORY}',
78 ocr_sed = os.path.join(
79 os.path.dirname(os.path.realpath(sys.argv[0])),
80 'ocr.sed'
83 def __init__(self, arch, name, boot_image):
84 VMController.__init__(self, 'QEMU-' + arch)
85 self.arch = arch
86 self.booted = False
87 self.name = name
88 self.boot_image = boot_image
90 def is_supported(arch):
91 return arch in QemuVMController.config
93 def _get_image_dimensions(self, filename):
94 im = Image.open(filename)
95 width, height = im.size
96 im.close()
97 return ( width, height )
99 def _check_is_up(self):
100 if not self.booted:
101 raise Exception("Machine not launched")
103 def _send_command(self, command):
104 self._check_is_up()
105 self.logger.debug("Sending command '{}'".format(command))
106 command = command + '\n'
107 self.monitor.sendall(command.encode('utf-8'))
109 def _run_command(self, command):
110 proc = subprocess.Popen(command)
111 proc.wait()
112 if proc.returncode != 0:
113 raise Exception("Command {} failed.".format(command))
115 def _run_pipe(self, commands):
116 self.logger.debug("Running pipe {}".format(format_command_pipe(commands)))
117 procs = []
118 for command in commands:
119 inp = None
120 if len(procs) > 0:
121 inp = procs[-1].stdout
122 proc = subprocess.Popen(command, stdout=subprocess.PIPE, stdin=inp)
123 procs.append(proc)
124 procs[-1].communicate()
127 def boot(self, **kwargs):
128 self.monitor_file = self.get_temp('monitor')
129 cmd = []
130 for opt in QemuVMController.config[self.arch]:
131 if opt == '{BOOT}':
132 opt = self.boot_image
133 elif opt == '{MEMORY}':
134 opt = '{}'.format(self.memory)
135 cmd.append(opt)
136 if self.is_headless:
137 cmd.append('-display')
138 cmd.append('none')
139 cmd.append('-monitor')
140 cmd.append('unix:{},server,nowait'.format(self.monitor_file))
141 for opt in self.extra_options:
142 cmd.append(opt)
143 self.logger.debug("Starting QEMU: {}".format(format_command(cmd)))
145 self.proc = subprocess.Popen(cmd)
146 self.monitor = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
147 for xxx in retries(timeout=30, interval=2, name="ctl-socket", message="Failed to connect to QEMU control socket."):
148 try:
149 self.monitor.connect(self.monitor_file)
150 break
151 except FileNotFoundError:
152 pass
153 except ConnectionRefusedError:
154 pass
155 if self.proc.poll():
156 raise Exception("QEMU not started, aborting.")
158 self.booted = True
159 self.logger.info("Machine started.")
161 # Skip past GRUB
162 self.type('\n')
164 uspace_booted = False
165 for xxx in retries(timeout=3*60, interval=5, name="vterm", message="Failed to boot into userspace"):
166 self.vterm = []
167 self.capture_vterm()
168 for l in self.vterm:
169 if l.find('to see a few survival tips') != -1:
170 uspace_booted = True
171 break
172 if uspace_booted:
173 break
175 assert uspace_booted
176 self.full_vterm = self.vterm
178 self.logger.info("Machine booted into userspace.")
180 return
182 def capture_vterm_impl(self):
183 screenshot_full = self.get_temp('screen-full.ppm')
184 screenshot_term = self.get_temp('screen-term.png')
185 screenshot_text = self.get_temp('screen-term.txt')
187 self._send_command('screendump ' + screenshot_full)
189 for xxx in retries(timeout=5, interval=1, name="scrdump", message="Failed to capture screen"):
190 try:
191 self._run_command([
192 'convert',
193 screenshot_full,
194 '-crop', '640x480+4+24',
195 '+repage',
196 '-colors', '2',
197 '-monochrome',
198 screenshot_term
200 break
201 except:
202 pass
204 width, height = self._get_image_dimensions(screenshot_term)
205 cols = width // 8
206 rows = height // 16
207 self._run_pipe([
209 'convert',
210 screenshot_term,
211 '-crop', '{}x{}'.format(cols * 8, rows * 16),
212 '+repage',
213 '-crop', '8x16',
214 '+repage',
215 '+adjoin',
216 'txt:-',
219 'sed',
220 '-e', 's|[0-9]*,[0-9]*: ([^)]*)[ ]*#\\([0-9A-Fa-f]\\{6\\}\\).*|\\1|',
221 '-e', 's:^#.*:@:',
222 '-e', 's#000000#0#g',
223 '-e', 's#FFFFFF#F#',
225 [ 'tee', self.get_temp('1.txt') ],
227 'sed',
228 '-e', ':a',
229 '-e', 'N;s#\\n##;s#^@##;/@$/{s#@$##p;d}',
230 '-e', 't a',
232 [ 'tee', self.get_temp('2.txt') ],
234 'sed',
235 '-f', QemuVMController.ocr_sed,
238 'sed',
239 '/../s#.*#?#',
241 [ 'tee', self.get_temp('3.txt') ],
243 'paste',
244 '-sd', '',
247 'fold',
248 '-w', '{}'.format(cols),
250 [ 'tee', self.get_temp('4.txt') ],
252 'head',
253 '-n', '{}'.format(rows),
256 'tee',
257 screenshot_text,
261 self.screenshot_filename = screenshot_full
263 with open(screenshot_text, 'r') as f:
264 lines = [ l.strip('\n') for l in f.readlines() ]
265 self.logger.debug("Captured text:")
266 for l in lines:
267 self.logger.debug("| " + l)
268 return lines
270 def terminate(self):
271 if not self.booted:
272 return
273 self._send_command('quit')
274 VMController.terminate(self)
276 def type(self, what):
277 translations = {
278 ' ': 'spc',
279 '.': 'dot',
280 '-': 'minus',
281 '/': 'slash',
282 '\n': 'ret',
283 '_': 'shift-minus',
284 '|': 'shift-backslash',
285 '=': 'equal',
287 for letter in what:
288 if letter.isupper():
289 letter = 'shift-' + letter.lower()
290 if letter in translations:
291 letter = translations[letter]
292 self._send_command('sendkey ' + letter)
293 pass