Promotes the throwaway harness that verified the installer into tools/:
- tools/vm/qmp.py QMP keystroke injection / typing / quit
- tools/vm/vncshot.py GL-safe screenshots via VNC readback (QMP
screendump shows "no surface" with virtio-vga-gl)
- tools/vm/gap-analysis.py drv-graph diff that converged the offline
pin set; run it when offline installs build
from source
- tools/test-install.sh the full offline regression: build ISO, boot
offline, unattended LUKS+swap install via a
config disk (typed long commands drop keys),
wait for poweroff, boot the installed disk,
screenshot the first boot for visual verdict
docs/TESTING.md §4 now points at the script.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
106 lines
3.4 KiB
Python
Executable File
106 lines
3.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Minimal QMP driver for the Nomarchy test VMs.
|
|
|
|
Drives a QEMU instance through its QMP socket: inject keystrokes, type
|
|
text, take screendumps, power off. Used by tools/test-install.sh; also
|
|
handy interactively while debugging a VM that has no SSH.
|
|
|
|
Usage:
|
|
qmp.py key <combo> # e.g. meta_l-ret, ctrl-alt-f2, ret
|
|
qmp.py type <text...> # types literal text (US layout)
|
|
qmp.py screenshot <out.png> # NOTE: with virtio-vga-gl this fails
|
|
# ("no surface") once the guest uses GL —
|
|
# use vncshot.py instead for desktops
|
|
qmp.py quit # hard power-off
|
|
|
|
The socket path comes from $NOMARCHY_QMP_SOCK
|
|
(default /tmp/nomarchy-vm/qmp.sock).
|
|
|
|
Keystroke pacing: the guest drops keys when busy (especially during
|
|
boot). $NOMARCHY_TYPE_DELAY (seconds between keys, default 0.08) trades
|
|
speed for reliability; verify long typed lines with a screenshot before
|
|
pressing enter.
|
|
"""
|
|
import json
|
|
import os
|
|
import socket
|
|
import sys
|
|
import time
|
|
|
|
SOCK = os.environ.get("NOMARCHY_QMP_SOCK", "/tmp/nomarchy-vm/qmp.sock")
|
|
TYPE_DELAY = float(os.environ.get("NOMARCHY_TYPE_DELAY", "0.08"))
|
|
|
|
CHAR_KEYS = {
|
|
" ": "spc", "-": "minus", "=": "equal", "[": "bracket_left",
|
|
"]": "bracket_right", ";": "semicolon", "'": "apostrophe",
|
|
"`": "grave_accent", "\\": "backslash", ",": "comma", ".": "dot",
|
|
"/": "slash", "\n": "ret", "\t": "tab",
|
|
}
|
|
SHIFT_CHARS = {
|
|
"!": "1", "@": "2", "#": "3", "$": "4", "%": "5", "^": "6",
|
|
"&": "7", "*": "8", "(": "9", ")": "0", "_": "minus", "+": "equal",
|
|
"{": "bracket_left", "}": "bracket_right", ":": "semicolon",
|
|
'"': "apostrophe", "~": "grave_accent", "|": "backslash",
|
|
"<": "comma", ">": "dot", "?": "slash",
|
|
}
|
|
|
|
|
|
class QMP:
|
|
def __init__(self):
|
|
self.s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
self.s.connect(SOCK)
|
|
self.f = self.s.makefile("rw")
|
|
self._read() # greeting
|
|
self.cmd("qmp_capabilities")
|
|
|
|
def _read(self):
|
|
while True:
|
|
msg = json.loads(self.f.readline())
|
|
if "event" in msg:
|
|
continue
|
|
return msg
|
|
|
|
def cmd(self, name, **args):
|
|
self.f.write(json.dumps({"execute": name, "arguments": args}) + "\n")
|
|
self.f.flush()
|
|
resp = self._read()
|
|
if "error" in resp:
|
|
raise RuntimeError(f"{name}: {resp['error']}")
|
|
return resp
|
|
|
|
def send_keys(self, names, hold=40):
|
|
keys = [{"type": "qcode", "data": n} for n in names]
|
|
self.cmd("send-key", keys=keys, **{"hold-time": hold})
|
|
|
|
def type_text(self, text):
|
|
for ch in text:
|
|
if ch.isalpha() and ch.lower() != ch:
|
|
self.send_keys(["shift", ch.lower()])
|
|
elif ch in SHIFT_CHARS:
|
|
self.send_keys(["shift", SHIFT_CHARS[ch]])
|
|
elif ch in CHAR_KEYS:
|
|
self.send_keys([CHAR_KEYS[ch]])
|
|
else:
|
|
self.send_keys([ch])
|
|
time.sleep(TYPE_DELAY)
|
|
|
|
|
|
def main():
|
|
q = QMP()
|
|
cmd = sys.argv[1]
|
|
if cmd == "screenshot":
|
|
q.cmd("screendump", filename=sys.argv[2], format="png")
|
|
elif cmd == "key":
|
|
q.send_keys(sys.argv[2].split("-"))
|
|
elif cmd == "type":
|
|
q.type_text(" ".join(sys.argv[2:]))
|
|
elif cmd == "quit":
|
|
q.f.write(json.dumps({"execute": "quit"}) + "\n")
|
|
q.f.flush()
|
|
else:
|
|
sys.exit(f"unknown command {cmd}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|