"""Real Godot / Rust / NATS smoke test; no physical hardware. Stop other labs first.""" import os from pathlib import Path import shutil import socket import subprocess import tempfile import time import urllib.error import lab ROOT = Path(__file__).resolve().parent PROJECT = ROOT.parents[1] def main(): for port in (*lab.PORTS, 18768): with socket.socket() as sock: sock.bind(("127.0.0.1", port)) nats = shutil.which("nats-server") godot = os.environ.get("GLITCH_LAB_GODOT") or str(PROJECT / "Godot_v4.7.2-stable_win64_console.exe") binary = ROOT / "target/debug" / ("glitch-odin-lab.exe" if os.name == "nt" else "glitch-odin-lab") if not nats or not Path(godot).is_file() or not binary.is_file(): raise RuntimeError("Need nats-server, built Rust lab, and Godot (or GLITCH_LAB_GODOT)") children = [] with tempfile.TemporaryFile(mode="w+b") as log: def spawn(args): p = subprocess.Popen(args, cwd=ROOT, stdout=log, stderr=log, creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0) children.append(p) return p def launch_game(inject): return spawn([godot, "--headless", "--path", str(PROJECT), "res://tests/odin_lab/cabinet.tscn", "--", "--glitch-lab", *(["--lab-inject-rfid"] if inject else [])]) def state(): deadline = time.monotonic() + 20 while time.monotonic() < deadline: try: return lab.api("/state")["state"] except (OSError, ValueError): time.sleep(.1) raise RuntimeError("No cabinet state after startup") try: spawn([nats, "-c", str(ROOT / "nats.conf")]) deadline = time.monotonic() + 10 while True: try: with socket.create_connection(("127.0.0.1", 14222), timeout=.2): break except OSError: if time.monotonic() > deadline: raise time.sleep(.1) game = launch_game(True) spawn([str(binary), "godot"]) spawn([str(binary), "controller"]) initial = state() assert initial["simulated"] is False and initial["phase"] == "powered_off" session = initial["session"] def send(action): return lab.api("/command", lab.command(action, session)) assert not send("start")["accepted"] assert send("power_on")["state"]["phase"] == "booting" assert not send("scan")["accepted"] deadline = time.monotonic() + 8 while state()["phase"] != "ready": assert time.monotonic() < deadline, "Boot did not finish" time.sleep(.1) assert not send("start")["accepted"] scan = lab.command("scan", session) assert lab.api("/command", scan)["state"]["credits"] == 1 replay = lab.api("/command", scan) assert replay["duplicate"] and replay["state"]["scans"] == 1 assert send("start")["state"]["phase"] == "playing" assert not send("win")["accepted"], "Remote synthetic win must be disabled" assert send("reset")["state"]["phase"] == "powered_off" game.terminate() game.wait(timeout=5) try: lab.api("/state") raise AssertionError("Offline cabinet falsely returned state") except urllib.error.HTTPError as error: assert error.code == 503 launch_game(False) restarted = state() assert restarted["session"] != session and not restarted["rfid_injection"] assert not send("power_on")["accepted"], "Old session accepted" session = restarted["session"] assert not send("scan")["accepted"] print("PASS: HTTP -> Rust -> NATS -> sidecar -> real Godot, boot/credit/start/reset, offline and restart") finally: for child in reversed(children): if child.poll() is None: child.terminate() for child in children: try: child.wait(timeout=5) except subprocess.TimeoutExpired: child.kill() child.wait() log.seek(0) print(log.read().decode("utf-8", errors="replace")[-4000:]) if __name__ == "__main__": main()