Skip to content

Commit 5431761

Browse files
authored
Validate GOP test output with screenshots (#37)
- Can now validate screenshots of QEMU against a reference file during tests - This allows unattended testing of the GOP - Moved "qemu-f4-exit" into a more general "qemu" feature - Enabled communication with QEMU's monitor in the test runner - Removed stalls from unattended tests
1 parent c1067f5 commit 5431761

File tree

7 files changed

+143
-41
lines changed

7 files changed

+143
-41
lines changed

uefi-services/Cargo.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,5 @@ uefi-logger = { path = "../uefi-logger" }
1515
log = { version = "0.4", default-features = false }
1616

1717
[features]
18-
# Signals the implementation that QEMU's exit port hack is enabled and assigned
19-
# to the f4 CPU port.
20-
qemu-f4-exit = []
18+
# Enable QEMU-specific functionality
19+
qemu = []

uefi-services/src/lib.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,8 @@ fn panic_handler(info: &core::panic::PanicInfo) -> ! {
121121
}
122122
}
123123

124-
// If running inside of QEMU and the f4 port hack is enabled, use it to
125-
// signal the error to the parent shell and exit
126-
if cfg!(feature = "qemu-f4-exit") {
124+
// If running in QEMU, use the f4 exit port to signal the error and exit
125+
if cfg!(feature = "qemu") {
127126
use x86_64::instructions::port::Port;
128127
let mut port = Port::<u32>::new(0xf4);
129128
unsafe {

uefi-test-runner/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ uefi-exts = { path = "../uefi-exts" }
1313
log = { version = "0.4", default-features = false }
1414

1515
[features]
16-
qemu-f4-exit = ["uefi-services/qemu-f4-exit"]
16+
qemu = ["uefi-services/qemu"]

uefi-test-runner/build.py

Lines changed: 79 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
'Script used to build, run, and test the code on all supported platforms.'
44

55
import argparse
6+
import filecmp
7+
import json
68
import os
79
from pathlib import Path
810
import re
@@ -100,7 +102,7 @@ def run_qemu():
100102
'Runs the code in QEMU.'
101103

102104
# Rebuild all the changes.
103-
build('--features', 'qemu-f4-exit')
105+
build('--features', 'qemu')
104106

105107
ovmf_dir = SETTINGS['ovmf_dir']
106108
ovmf_code, ovmf_vars = ovmf_dir / 'OVMF_CODE.fd', ovmf_dir / 'OVMF_VARS.fd'
@@ -110,6 +112,8 @@ def run_qemu():
110112

111113
examples_dir = build_dir() / 'examples'
112114

115+
qemu_monitor_pipe = 'qemu-monitor'
116+
113117
qemu_flags = [
114118
# Disable default devices.
115119
# QEMU by defaults enables a ton of devices which slow down boot.
@@ -134,6 +138,16 @@ def run_qemu():
134138
# Connect the serial port to the host. OVMF is kind enough to connect
135139
# the UEFI stdout and stdin to that port too.
136140
'-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',
137151
]
138152

139153
# When running in headless mode we don't have video, but we can still have
@@ -143,16 +157,6 @@ def run_qemu():
143157
# Do not attach a window to QEMU's display
144158
qemu_flags.extend(['-display', 'none'])
145159

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-
156160
cmd = [SETTINGS['qemu_binary']] + qemu_flags
157161

158162
if SETTINGS['verbose']:
@@ -162,25 +166,71 @@ def run_qemu():
162166
# analyzing the output of the test runner.
163167
ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]')
164168

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)
167174

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)
184234

185235
def main():
186236
'Runs the user-requested actions.'

uefi-test-runner/screenshots/gop_test.ppm

Lines changed: 4 additions & 0 deletions
Large diffs are not rendered by default.

uefi-test-runner/src/main.rs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ extern crate log;
1010
extern crate alloc;
1111

1212
use uefi::prelude::*;
13+
use uefi::proto::console::serial::Serial;
14+
use uefi_exts::BootServicesExt;
1315

1416
mod boot;
1517
mod proto;
@@ -50,15 +52,64 @@ fn check_revision(rev: uefi::table::Revision) {
5052
);
5153
}
5254

55+
/// Ask the test runner to check the current screen output against a reference
56+
///
57+
/// This functionality is very specific to our QEMU-based test runner. Outside
58+
/// of it, we just pause the tests for a couple of seconds to allow visual
59+
/// inspection of the output.
60+
///
61+
fn check_screenshot(bt: &BootServices, name: &str) {
62+
if cfg!(feature = "qemu") {
63+
// Access the serial port (in a QEMU environment, it should always be there)
64+
let mut serial = bt
65+
.find_protocol::<Serial>()
66+
.expect("Could not find serial port");
67+
let serial = unsafe { serial.as_mut() };
68+
69+
// Set a large timeout to avoid problems
70+
let mut io_mode = *serial.io_mode();
71+
io_mode.timeout = 1_000_000;
72+
serial
73+
.set_attributes(&io_mode)
74+
.expect("Failed to configure serial port timeout");
75+
76+
// Send a screenshot request to the host
77+
let mut len = serial
78+
.write(b"SCREENSHOT: ")
79+
.expect("Failed to send request");
80+
assert_eq!(len, 12, "Screenshot request timed out");
81+
let name_bytes = name.as_bytes();
82+
len = serial.write(name_bytes).expect("Failed to send request");
83+
assert_eq!(len, name_bytes.len(), "Screenshot request timed out");
84+
len = serial.write(b"\n").expect("Failed to send request");
85+
assert_eq!(len, 1, "Screenshot request timed out");
86+
87+
// Wait for the host's acknowledgement before moving forward
88+
let mut reply = [0; 3];
89+
let read_size = serial
90+
.read(&mut reply[..])
91+
.expect("Failed to read host reply");
92+
assert_eq!(read_size, 3, "Screenshot request timed out");
93+
assert_eq!(&reply[..], b"OK\n", "Unexpected screenshot request reply");
94+
} else {
95+
// Outside of QEMU, give the user some time to inspect the output
96+
bt.stall(3_000_000);
97+
}
98+
}
99+
53100
fn shutdown(st: &SystemTable) -> ! {
54101
use uefi::table::runtime::ResetType;
55102

56103
// Get our text output back.
57104
st.stdout().reset(false).unwrap();
58105

59-
// Inform the user.
60-
info!("Testing complete, shutting down in 3 seconds...");
61-
st.boot.stall(3_000_000);
106+
// Inform the user, and give him time to read on real hardware
107+
if cfg!(not(feature = "qemu")) {
108+
info!("Testing complete, shutting down in 3 seconds...");
109+
st.boot.stall(3_000_000);
110+
} else {
111+
info!("Testing complete, shutting down...");
112+
}
62113

63114
let rt = st.runtime;
64115
rt.reset(ResetType::Shutdown, Status::Success, None);

uefi-test-runner/src/proto/console/gop.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ pub fn test(bt: &BootServices) {
1111
fill_color(gop);
1212
draw_fb(gop);
1313

14-
// TODO: For now, allow the user to inspect the visual output.
15-
bt.stall(1_000_000);
14+
crate::check_screenshot(bt, "gop_test");
1615
} else {
1716
// No tests can be run.
1817
warn!("UEFI Graphics Output Protocol is not supported");

0 commit comments

Comments
 (0)