mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-02 11:07:31 +00:00
The probe scripts, run.sh, trace_run.sh, webui.py and two emulator tests each named the same hardcoded firmware from a source tree that is not in this repository. They now resolve QEMU and the firmware the way tools/uvk5_testenv.py does -- environment, then PATH, then whatever the checkout has -- and skip with a reason when there is nothing. webui.py's --qemu and --elf lost their author defaults too: a bare qemu-system-arm through PATH, and no firmware until one is uploaded, which the page already reports.
183 lines
6.2 KiB
Python
Executable File
183 lines
6.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""End-to-end check: real QEMU, real HTTP, real keypress.
|
|
|
|
Boots its own emulator on a private QMP socket and HTTP port, so it does not
|
|
disturb a run.sh session or fight it for the QMP socket. Then drives the server
|
|
over HTTP and asserts the LCD actually changed.
|
|
|
|
The unit tests stub QMP, so they cannot catch a wrong QMP command -- which is a
|
|
real risk here: pmemsave and memsave differ only in whether the address is
|
|
treated as physical or virtual, and pmemsave fails by silently returning zeros.
|
|
This test is what notices.
|
|
|
|
Run: python3 tools/test_webui_e2e.py
|
|
"""
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
SIM = os.path.dirname(HERE)
|
|
sys.path.insert(0, HERE)
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import uvk5_testenv
|
|
|
|
QEMU = uvk5_testenv.qemu()
|
|
ELF = uvk5_testenv.firmware()
|
|
FLASH = os.path.join(SIM, "assets", "flash.img")
|
|
QMP = "/tmp/uvk5-webui-e2e.sock"
|
|
HTTP_PORT = 8099
|
|
FRAME_ADDR, STATUS_ADDR = 0x200013DC, 0x2000175C
|
|
|
|
BASE = f"http://127.0.0.1:{HTTP_PORT}"
|
|
|
|
|
|
def post(path, payload=None):
|
|
data = json.dumps(payload).encode() if payload is not None else b"{}"
|
|
req = urllib.request.Request(BASE + path, data=data,
|
|
headers={"Content-Type": "application/json"},
|
|
method="POST")
|
|
with urllib.request.urlopen(req, timeout=5) as r:
|
|
return json.load(r)
|
|
|
|
|
|
def get_frame():
|
|
with urllib.request.urlopen(BASE + "/frame.png", timeout=5) as r:
|
|
return r.read()
|
|
|
|
|
|
def get_json(path):
|
|
with urllib.request.urlopen(BASE + path, timeout=5) as r:
|
|
return json.load(r)
|
|
|
|
|
|
def tap(key, hold=0.20, gap=0.30):
|
|
"""A real short press: two edges, duration decided here, not by the server."""
|
|
post("/api/key", {"key": key, "action": "down"})
|
|
time.sleep(hold)
|
|
post("/api/key", {"key": key, "action": "up"})
|
|
time.sleep(gap)
|
|
|
|
|
|
def main():
|
|
for path in (QMP,):
|
|
if os.path.exists(path):
|
|
os.unlink(path)
|
|
for path, what in ((QEMU, "QEMU binary"), (ELF, "firmware ELF"),
|
|
(FLASH, "flash image")):
|
|
if not os.path.exists(path):
|
|
sys.exit(f"missing {what}: {path}")
|
|
|
|
qemu = subprocess.Popen(
|
|
[QEMU, "-M", f"uv-k5-v3,flash-image={FLASH}", "-nographic",
|
|
"-monitor", "none", "-qmp", f"unix:{QMP},server=on,wait=off",
|
|
"-kernel", ELF],
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
failures = []
|
|
try:
|
|
for _ in range(150):
|
|
if os.path.exists(QMP):
|
|
break
|
|
if qemu.poll() is not None:
|
|
sys.exit("QEMU exited during startup")
|
|
time.sleep(0.1)
|
|
else:
|
|
sys.exit(f"QMP socket never appeared at {QMP}")
|
|
|
|
from uvk5_qmp import QmpClient
|
|
import webui
|
|
|
|
client = QmpClient(QMP)
|
|
app = webui.create_app(client, FRAME_ADDR, STATUS_ADDR)
|
|
threading.Thread(
|
|
target=lambda: app.run(host="127.0.0.1", port=HTTP_PORT,
|
|
threaded=True, use_reloader=False),
|
|
daemon=True).start()
|
|
|
|
# Boot is ~5 s; power save engages ~6 s in. Waiting past it also proves
|
|
# keys work once the radio is asleep.
|
|
time.sleep(16)
|
|
|
|
# 1. a frame renders, and is not blank
|
|
before = get_frame()
|
|
if not before.startswith(b"\x89PNG\r\n\x1a\n"):
|
|
failures.append("frame.png did not return a PNG")
|
|
else:
|
|
print(f"PASS frame.png returned {len(before)} bytes of PNG")
|
|
|
|
# 2. the screen has real content. A blank screen is exactly how the
|
|
# pmemsave mistake presented, so assert lit pixels rather than trust
|
|
# that a PNG came back at all.
|
|
from uvk5_lcd import FrameGrabber, unpack
|
|
status, frame = FrameGrabber(client, FRAME_ADDR, STATUS_ADDR).raw()
|
|
lit = sum(sum(row) for row in unpack(status, frame))
|
|
if lit == 0:
|
|
failures.append("screen is blank: 0 lit pixels, check the QMP "
|
|
"memory command and the framebuffer addresses")
|
|
else:
|
|
print(f"PASS screen has content ({lit} lit pixels)")
|
|
|
|
# 3. a tap opens the menu, which must visibly change the screen
|
|
tap("MENU")
|
|
time.sleep(0.6)
|
|
after = get_frame()
|
|
if after == before:
|
|
failures.append("screen did not change after a MENU tap")
|
|
else:
|
|
print("PASS screen changed after a MENU tap")
|
|
|
|
# 4. the guest is still running: streaming must not pause it
|
|
st = get_json("/api/status")
|
|
if not st.get("running"):
|
|
failures.append(f"guest not running after streaming: {st}")
|
|
else:
|
|
print("PASS guest still running after reading frames")
|
|
|
|
# 5. an invalid key is rejected, not forwarded to QMP
|
|
try:
|
|
post("/api/key", {"key": "PTT", "action": "tap"})
|
|
failures.append("PTT was accepted; it should be rejected")
|
|
except urllib.error.HTTPError as err:
|
|
if err.code != 400:
|
|
failures.append(f"PTT rejected with {err.code}, expected 400")
|
|
else:
|
|
print("PASS PTT rejected with 400")
|
|
|
|
# 6. the stream really is multipart with a PNG part
|
|
with urllib.request.urlopen(BASE + "/stream?limit=1", timeout=10) as r:
|
|
ctype = r.headers.get("Content-Type", "")
|
|
chunk = r.read(4096)
|
|
if "multipart/x-mixed-replace" not in ctype:
|
|
failures.append(f"stream Content-Type was {ctype!r}")
|
|
elif b"\x89PNG\r\n\x1a\n" not in chunk:
|
|
failures.append("stream did not contain a PNG part")
|
|
else:
|
|
print("PASS /stream is multipart and carries a PNG")
|
|
finally:
|
|
qemu.terminate()
|
|
try:
|
|
qemu.wait(timeout=10)
|
|
except subprocess.TimeoutExpired:
|
|
qemu.kill()
|
|
if os.path.exists(QMP):
|
|
os.unlink(QMP)
|
|
|
|
print()
|
|
if failures:
|
|
for f in failures:
|
|
print("FAIL", f)
|
|
return 1
|
|
print("all end-to-end checks passed")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|