3
3
'Script used to build, run, and test the code on all supported platforms.'
4
4
5
5
import argparse
6
+ import filecmp
7
+ import json
6
8
import os
7
9
from pathlib import Path
8
10
import re
@@ -100,7 +102,7 @@ def run_qemu():
100
102
'Runs the code in QEMU.'
101
103
102
104
# Rebuild all the changes.
103
- build ('--features' , 'qemu-f4-exit ' )
105
+ build ('--features' , 'qemu' )
104
106
105
107
ovmf_dir = SETTINGS ['ovmf_dir' ]
106
108
ovmf_code , ovmf_vars = ovmf_dir / 'OVMF_CODE.fd' , ovmf_dir / 'OVMF_VARS.fd'
@@ -110,6 +112,8 @@ def run_qemu():
110
112
111
113
examples_dir = build_dir () / 'examples'
112
114
115
+ qemu_monitor_pipe = 'qemu-monitor'
116
+
113
117
qemu_flags = [
114
118
# Disable default devices.
115
119
# QEMU by defaults enables a ton of devices which slow down boot.
@@ -134,6 +138,16 @@ def run_qemu():
134
138
# Connect the serial port to the host. OVMF is kind enough to connect
135
139
# the UEFI stdout and stdin to that port too.
136
140
'-serial' , 'stdio' ,
141
+
142
+ # Map the QEMU exit signal to port f4
143
+ '-device' , 'isa-debug-exit,iobase=0xf4,iosize=0x04' ,
144
+
145
+ # Map the QEMU monitor to a pair of named pipes
146
+ '-qmp' , f'pipe:{ qemu_monitor_pipe } ' ,
147
+
148
+ # OVMF debug builds can output information to a serial `debugcon`.
149
+ # Only enable when debugging UEFI boot:
150
+ #'-debugcon', 'file:debug.log', '-global', 'isa-debugcon.iobase=0x402',
137
151
]
138
152
139
153
# When running in headless mode we don't have video, but we can still have
@@ -143,16 +157,6 @@ def run_qemu():
143
157
# Do not attach a window to QEMU's display
144
158
qemu_flags .extend (['-display' , 'none' ])
145
159
146
- # Add other devices
147
- qemu_flags .extend ([
148
- # Map the QEMU exit signal to port f4
149
- '-device' , 'isa-debug-exit,iobase=0xf4,iosize=0x04' ,
150
-
151
- # OVMF debug builds can output information to a serial `debugcon`.
152
- # Only enable when debugging UEFI boot:
153
- #'-debugcon', 'file:debug.log', '-global', 'isa-debugcon.iobase=0x402',
154
- ])
155
-
156
160
cmd = [SETTINGS ['qemu_binary' ]] + qemu_flags
157
161
158
162
if SETTINGS ['verbose' ]:
@@ -162,25 +166,71 @@ def run_qemu():
162
166
# analyzing the output of the test runner.
163
167
ansi_escape = re .compile (r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]' )
164
168
165
- # Start QEMU
166
- qemu = sp .Popen (cmd , stdout = sp .PIPE , universal_newlines = True )
169
+ # Setup named pipes as a communication channel with QEMU's monitor
170
+ monitor_input_path = f'{ qemu_monitor_pipe } .in'
171
+ os .mkfifo (monitor_input_path )
172
+ monitor_output_path = f'{ qemu_monitor_pipe } .out'
173
+ os .mkfifo (monitor_output_path )
167
174
168
- # Iterate over stdout...
169
- for line in qemu .stdout :
170
- # Strip ending and trailing whitespace + ANSI escape codes for analysis
171
- stripped = ansi_escape .sub ('' , line .strip ())
172
-
173
- # Skip empty lines
174
- if not stripped :
175
- continue
176
-
177
- # Print out the processed QEMU output to allow logging & inspection
178
- print (stripped )
179
-
180
- # Wait for QEMU to finish, then abort if that fails
181
- status = qemu .wait ()
182
- if status != 0 :
183
- raise sp .CalledProcessError (cmd = cmd , returncode = status )
175
+ # Start QEMU
176
+ qemu = sp .Popen (cmd , stdin = sp .PIPE , stdout = sp .PIPE , universal_newlines = True )
177
+ try :
178
+ # Connect to the QEMU monitor
179
+ with open (monitor_input_path , mode = 'w' ) as monitor_input , \
180
+ open (monitor_output_path , mode = 'r' ) as monitor_output :
181
+ # Execute the QEMU monitor handshake, doing basic sanity checks
182
+ assert monitor_output .readline ().startswith ('{"QMP":' )
183
+ print ('{"execute": "qmp_capabilities"}' , file = monitor_input , flush = True )
184
+ assert monitor_output .readline () == '{"return": {}}\n '
185
+
186
+ # Iterate over stdout...
187
+ for line in qemu .stdout :
188
+ # Strip ending and trailing whitespace + ANSI escape codes
189
+ # (This simplifies log analysis and keeps the terminal clean)
190
+ stripped = ansi_escape .sub ('' , line .strip ())
191
+
192
+ # Skip lines which contain nothing else
193
+ if not stripped :
194
+ continue
195
+
196
+ # Print out the processed QEMU output for logging & inspection
197
+ print (stripped )
198
+
199
+ # If the app requests a screenshot, take it
200
+ if stripped .startswith ("SCREENSHOT: " ):
201
+ reference_name = stripped [12 :]
202
+
203
+ # Ask QEMU to take a screenshot
204
+ monitor_command = '{"execute": "screendump", "arguments": {"filename": "screenshot.ppm"}}'
205
+ print (monitor_command , file = monitor_input , flush = True )
206
+
207
+ # Wait for QEMU's acknowledgement, ignoring events
208
+ reply = json .loads (monitor_output .readline ())
209
+ while "event" in reply :
210
+ reply = json .loads (monitor_output .readline ())
211
+ assert reply == {"return" : {}}
212
+
213
+ # Tell the VM that the screenshot was taken
214
+ print ('OK' , file = qemu .stdin , flush = True )
215
+
216
+ # Compare screenshot to the reference file specified by the user
217
+ # TODO: Add an operating mode where the reference is created if it doesn't exist
218
+ reference_file = WORKSPACE_DIR / 'uefi-test-runner' / 'screenshots' / (reference_name + '.ppm' )
219
+ assert filecmp .cmp ('screenshot.ppm' , reference_file )
220
+
221
+ # Delete the screenshot once done
222
+ os .remove ('screenshot.ppm' )
223
+ finally :
224
+ # Wait for QEMU to finish
225
+ status = qemu .wait ()
226
+
227
+ # Delete the monitor pipes
228
+ os .remove (monitor_input_path )
229
+ os .remove (monitor_output_path )
230
+
231
+ # Throw an exception if QEMU failed
232
+ if status != 0 :
233
+ raise sp .CalledProcessError (cmd = cmd , returncode = status )
184
234
185
235
def main ():
186
236
'Runs the user-requested actions.'
0 commit comments