Add --pass option
[ci.git] / htest / vm / qemu.py
blob87c51b4f61f2b4546fc889ac6a5e6ae7baed77ad
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
35 from PIL import Image
37 from htest.utils import retries, format_command, format_command_pipe
38 from htest.vm.controller import VMController
40 class QemuVMController(VMController):
41 """
42 QEMU VM controller.
43 """
45 config = {
46 'amd64': [
47 'qemu-system-x86_64',
48 '-cdrom', '{BOOT}',
49 '-m', '{MEMORY}',
50 '-usb',
52 'arm32/integratorcp': [
53 'qemu-system-arm',
54 '-M', 'integratorcp',
55 '-usb',
56 '-kernel', '{BOOT}',
57 '-m', '{MEMORY}',
59 'ia32': [
60 'qemu-system-i386',
61 '-cdrom', '{BOOT}',
62 '-m', '{MEMORY}',
63 '-usb',
65 'ppc32': [
66 'qemu-system-ppc',
67 '-usb',
68 '-boot', 'd',
69 '-cdrom', '{BOOT}',
70 '-m', '{MEMORY}',
74 def __init__(self, arch, name, boot_image):
75 VMController.__init__(self, 'QEMU-' + arch)
76 self.arch = arch
77 self.booted = False
78 self.name = name
79 self.boot_image = boot_image
80 self.logger = logging.getLogger('qemu-{}'.format(name))
82 def is_supported(arch):
83 return arch in QemuVMController.config
85 def _get_image_dimensions(self, filename):
86 im = Image.open(filename)
87 width, height = im.size
88 im.close()
89 return ( width, height )
91 def _check_is_up(self):
92 if not self.booted:
93 raise Exception("Machine not launched")
95 def _send_command(self, command):
96 self._check_is_up()
97 self.logger.debug("Sending command '{}'".format(command))
98 command = command + '\n'
99 self.monitor.sendall(command.encode('utf-8'))
101 def _run_command(self, command):
102 proc = subprocess.Popen(command)
103 proc.wait()
104 if proc.returncode != 0:
105 raise Exception("Command {} failed.".format(command))
107 def _run_pipe(self, commands):
108 self.logger.debug("Running pipe {}".format(format_command_pipe(commands)))
109 procs = []
110 for command in commands:
111 inp = None
112 if len(procs) > 0:
113 inp = procs[-1].stdout
114 proc = subprocess.Popen(command, stdout=subprocess.PIPE, stdin=inp)
115 procs.append(proc)
116 procs[-1].communicate()
119 def boot(self, **kwargs):
120 self.monitor_file = self.get_temp('monitor')
121 cmd = []
122 for opt in QemuVMController.config[self.arch]:
123 if opt == '{BOOT}':
124 opt = self.boot_image
125 elif opt == '{MEMORY}':
126 opt = '{}'.format(self.memory)
127 cmd.append(opt)
128 cmd.append('-monitor')
129 cmd.append('unix:{},server,nowait'.format(self.monitor_file))
130 for opt in self.extra_options:
131 cmd.append(opt)
132 self.logger.debug("Starting QEMU: {}".format(format_command(cmd)))
134 self.proc = subprocess.Popen(cmd)
135 self.monitor = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
136 for xxx in retries(timeout=30, interval=2, name="ctl-socket", message="Failed to connect to QEMU control socket."):
137 try:
138 self.monitor.connect(self.monitor_file)
139 break
140 except FileNotFoundError:
141 pass
142 except ConnectionRefusedError:
143 pass
144 if self.proc.poll():
145 raise Exception("QEMU not started, aborting.")
147 self.booted = True
148 self.logger.info("Machine started.")
150 # Skip past GRUB
151 self.type('\n')
153 uspace_booted = False
154 for xxx in retries(timeout=3*60, interval=5, name="vterm", message="Failed to boot into userspace"):
155 self.vterm = []
156 self.capture_vterm()
157 for l in self.vterm:
158 if l.find('to see a few survival tips') != -1:
159 uspace_booted = True
160 break
161 if uspace_booted:
162 break
164 assert uspace_booted
165 self.full_vterm = self.vterm
167 self.logger.info("Machine booted into userspace.")
169 return
171 def capture_vterm_impl(self):
172 screenshot_full = self.get_temp('screen-full.ppm')
173 screenshot_term = self.get_temp('screen-term.png')
174 screenshot_text = self.get_temp('screen-term.txt')
176 self._send_command('screendump ' + screenshot_full)
178 for xxx in retries(timeout=5, interval=1, name="scrdump", message="Failed to capture screen"):
179 try:
180 self._run_command([
181 'convert',
182 screenshot_full,
183 '-crop', '640x480+4+24',
184 '+repage',
185 '-colors', '2',
186 '-monochrome',
187 screenshot_term
189 break
190 except:
191 pass
193 width, height = self._get_image_dimensions(screenshot_term)
194 cols = width // 8
195 rows = height // 16
196 self._run_pipe([
198 'convert',
199 screenshot_term,
200 '-crop', '{}x{}'.format(cols * 8, rows * 16),
201 '+repage',
202 '-crop', '8x16',
203 '+repage',
204 '+adjoin',
205 'txt:-',
208 'sed',
209 '-e', 's|[0-9]*,[0-9]*: ([^)]*)[ ]*#\\([0-9A-Fa-f]\\{6\\}\\).*|\\1|',
210 '-e', 's:^#.*:@:',
211 '-e', 's#000000#0#g',
212 '-e', 's#FFFFFF#F#',
214 [ 'tee', self.get_temp('1.txt') ],
216 'sed',
217 '-e', ':a',
218 '-e', 'N;s#\\n##;s#^@##;/@$/{s#@$##p;d}',
219 '-e', 't a',
221 [ 'tee', self.get_temp('2.txt') ],
223 'sed',
224 '-f', 'ocr.sed',
227 'sed',
228 '/../s#.*#?#',
230 [ 'tee', self.get_temp('3.txt') ],
232 'paste',
233 '-sd', '',
236 'fold',
237 '-w', '{}'.format(cols),
239 [ 'tee', self.get_temp('4.txt') ],
241 'head',
242 '-n', '{}'.format(rows),
245 'tee',
246 screenshot_text,
250 self.screenshot_filename = screenshot_full
252 with open(screenshot_text, 'r') as f:
253 lines = [ l.strip('\n') for l in f.readlines() ]
254 self.logger.debug("Captured text:")
255 for l in lines:
256 self.logger.debug("| " + l)
257 return lines
259 def terminate(self):
260 if not self.booted:
261 return
262 self._send_command('quit')
263 VMController.terminate(self)
265 def type(self, what):
266 translations = {
267 ' ': 'spc',
268 '.': 'dot',
269 '-': 'minus',
270 '/': 'slash',
271 '\n': 'ret',
272 '_': 'shift-minus',
274 for letter in what:
275 if letter in translations:
276 letter = translations[letter]
277 self._send_command('sendkey ' + letter)
278 pass