From dfc7d6a609b30d2a2c32fe5a1ea5d935388d434c Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Mon, 13 Jul 2026 05:22:53 +0100 Subject: [PATCH] The harness grows a QMP channel (M21.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every case now gets a -qmp unix socket (additive; no case notices). A minimal client does the capabilities handshake and executes one command; the per-case qmp_after hook sends it N seconds after boot, retrying until the guest's socket is up. A case with a hook configured cannot pass until the hook delivered — and the smoke case now carries a harmless query-status hook, so the channel is proven end to end on every run. This is how the power scenarios inject the real ACPI power-button event (system_powerdown) in M21.1 and M21.3. --- docs/m21-plan.md | 10 +++++----- test/qemu_test.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/docs/m21-plan.md b/docs/m21-plan.md index 305e951..1c1629e 100644 --- a/docs/m21-plan.md +++ b/docs/m21-plan.md @@ -70,11 +70,11 @@ auto-merge to main when the branch is green; keep the branch; push everything. ## Status -- [ ] **M21.0** — baseline: rebase over anything newly merged (the dead-code - sweep touches acpi.zig); cut `feat/power-events`; add the QMP channel to - the harness (`-qmp unix:.../qmp.sock,server,nowait`, a small client with - the `qmp_capabilities` handshake, a per-case `qmp_after` hook that sends - a command N seconds after boot); existing suite stays green. +- [x] **M21.0** — baseline (dead-code sweep confirmed landed on main — no + acpi.zig conflict; `feat/power-events` cut; QMP channel in the harness: + always-on unix socket, client with the capabilities handshake, per-case + `qmp_after` hook, and a hook-must-deliver pass gate that the smoke case + now proves with a harmless query-status; suite 58/58). - [ ] **M21.1** — SCI + the power button: kernel appends the FADT as an acpi-tables memory resource; new `power-protocol` module + `ServiceId.power`; the acpi service converts to the harness, registers diff --git a/test/qemu_test.py b/test/qemu_test.py index 53d87b3..26bf613 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -18,9 +18,11 @@ Usage: """ import argparse +import json import os import re import shutil +import socket import subprocess import sys import time @@ -80,7 +82,10 @@ ARCHES = { # `expect`: a regex that must appear in serial output => pass. # `fail`: optional regex whose appearance => immediate fail. CASES = [ + # smoke also proves the QMP channel: the harmless query must be delivered + # (handshake + command) before the case may pass — see run_case. {"name": "smoke", + "qmp_after": {"delay": 2, "command": "query-status"}, "expect": r"DANOS-TEST-RESULT: PASS", "fail": r"DANOS-TEST-RESULT: FAIL"}, {"name": "discovery", @@ -418,6 +423,27 @@ def resolve_firmware(arch): + "\nInstall OVMF (edk2-ovmf / ovmf) or add its path above.") +def qmp_send(path, command): + """One QMP command: connect, capabilities handshake, execute. Raises on any + failure — the caller retries until the guest's socket is ready. This is how + a case injects a host-side event (system_powerdown = the ACPI power button) + into the running guest (docs/m21-plan.md).""" + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.settimeout(5) + try: + sock.connect(path) + stream = sock.makefile("rw") + stream.readline() # the QMP greeting + stream.write(json.dumps({"execute": "qmp_capabilities"}) + "\n") + stream.flush() + stream.readline() # {"return": {}} + stream.write(json.dumps({"execute": command}) + "\n") + stream.flush() + stream.readline() + finally: + sock.close() + + def run_case(arch, case): err = build(arch, case["name"]) if err: @@ -439,12 +465,27 @@ def run_case(arch, case): cmd += ["-smp", str(case["smp"])] if case.get("qemu_extra"): # extra qemu args, e.g. -device intel-iommu for the IOMMU case cmd += case["qemu_extra"] + # A QMP control socket, always present (additive): how a case's `qmp_after` + # hook injects host-side events into the guest mid-run. + qmp_path = os.path.join(WORK, "qmp.sock") + if os.path.exists(qmp_path): + os.remove(qmp_path) + cmd += ["-qmp", f"unix:{qmp_path},server,nowait"] + qmp_after = case.get("qmp_after") # {"delay": seconds, "command": "..."} + qmp_sent = False + started = time.monotonic() qemu = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) try: timeout = case.get("timeout", TIMEOUT) deadline = time.monotonic() + timeout while time.monotonic() < deadline: time.sleep(0.2) + if qmp_after and not qmp_sent and time.monotonic() - started >= qmp_after["delay"]: + try: + qmp_send(qmp_path, qmp_after["command"]) + qmp_sent = True + except OSError: + pass # socket not up yet; retry next tick text = "" if os.path.exists(serial): with open(serial, "r", errors="replace") as f: @@ -452,6 +493,8 @@ def run_case(arch, case): if fail and fail.search(text): return False, "hit failure marker" if expect.search(text): + if qmp_after and not qmp_sent: + continue # the hook must deliver before the case may pass return True, "matched " + repr(case["expect"]) if qemu.poll() is not None: # QEMU exited on its own if expect.search(text):