mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-28 09:11:11 -07:00
forgetest: cloud tests stay in cloud mode; the page can ignore prerequisites
The cloud job tests (lid-abort, lid-during-button-wait, hunt-lid-open,
pause-resume) run in cloud mode and leave the machine there: enter_cloud
reuses a live session (pid-scoped from the client's own websocket state
lines) and switches once from GRBL mode, declaring the change to the
baseline; nothing switches back. Each test judges the log from its own
window, the print by its own "print [id]: finished" line, and waits the
service's follow-up moves out (wait_quiet) before it ends. hunt-lid-open
restarts the cloud client through the supervisor's stop/start lever for
a fresh connect. The former switch-back is what failed the last bench run
of hunt-lid-open (409 machine is not idle: the service was still
re-finding the head after the lid closed).
The baseline is mode-aware: in cloud mode the client owns the GRBL
controller's init values, the lid lamp, and the position counters; the
mode itself is preserved unless the run declared the change
(Context.mode_changed); controller_mode is never handed back as a bare
setting (a bare write left the persisted mode out of step with the live
one). The undeclared-change restore now uses the captured state, which
the post pass never saw before.
The acceptance page gets an "Ignore prerequisites" switch (remembered by
the browser): POST /start {ignore_requires} starts a test whose requires
are unmet, and the run records the unmet prerequisites in its evidence
and log; they stay required for the release. The cloud tests' requires
no longer chain through cloud.mode-switch.
Proof: tests/test_cloud_suite.py replays the four tests on the bench's
own gfcloud excerpts (fixtures/) and the run loop's emitted pause/resume
lines under the real runner Context against a fake forgectrl; baseline
mode tests and the server override test; the whole suite (98) and the
coverage lint pass. Catalog consequence: cloud.* fingerprints move with
the module; the catalog hash moves with the requires.
This commit is contained in:
@@ -76,3 +76,89 @@ def make_test(id, covers, always=False, requires=(), kind="auto", fn=None, subsy
|
||||
|
||||
def registry(*tests):
|
||||
return {t.id: t for t in tests}
|
||||
|
||||
|
||||
# ------------------------------------------------------- fake forgectrl
|
||||
|
||||
class FakeForgectrl:
|
||||
"""A stand-in for the machine-services daemon on localhost: canned
|
||||
JSON for the endpoints the suite reads, a mutable `state` the test
|
||||
scripts, every POST recorded, and an optional `on_post(path, form)`
|
||||
hook returning (status, body) to script the daemon's reactions.
|
||||
Point the suite at it with FORGECTRL_URL (see start/stop)."""
|
||||
|
||||
def __init__(self):
|
||||
import http.server
|
||||
import json as _json
|
||||
import threading as _threading
|
||||
import urllib.parse as _up
|
||||
self.state = {
|
||||
"mode": {"mode": "grbl", "controller": "running", "pid": 100, "motion": "verified"},
|
||||
"status": {"state": "idle", "homed": False, "diag": False, "laser_locked": True,
|
||||
"pos": {"x": 0.0, "y": 0.0, "z": 0.0},
|
||||
"switches": {"lid": True, "button": False, "interlock_ok": True,
|
||||
"head": True, "hv_enable": False}},
|
||||
"cool": {"phase": "idle", "armed": False, "hold": False},
|
||||
"cam": {"running": False, "clients": 0},
|
||||
"diag": {"running": False},
|
||||
"settings": {"controller_mode": "grbl", "lid_lamp_idle": ""},
|
||||
}
|
||||
self.posts = []
|
||||
self.on_post = None
|
||||
fake = self
|
||||
|
||||
class H(http.server.BaseHTTPRequestHandler):
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
def _send(self, st, body):
|
||||
data = _json.dumps(body).encode()
|
||||
self.send_response(st)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def do_GET(self):
|
||||
path = self.path.split("?", 1)[0]
|
||||
key = {"/mode": "mode", "/status": "status", "/cool/status": "cool", "/cam/status": "cam",
|
||||
"/diag/status": "diag", "/settings": "settings"}.get(path)
|
||||
if key is None:
|
||||
return self._send(404, {"error": "no " + path})
|
||||
self._send(200, fake.state[key])
|
||||
|
||||
def do_POST(self):
|
||||
path, _, query = self.path.partition("?")
|
||||
n = int(self.headers.get("Content-Length") or 0)
|
||||
raw = self.rfile.read(n).decode() if n else ""
|
||||
form = dict(_up.parse_qsl(raw)) if raw else dict(_up.parse_qsl(query))
|
||||
fake.posts.append((path, form))
|
||||
if fake.on_post:
|
||||
r = fake.on_post(path, form)
|
||||
if r is not None:
|
||||
return self._send(*r)
|
||||
if path == "/mode" and form.get("controller"):
|
||||
fake.state["mode"] = dict(fake.state["mode"], mode=form["controller"], controller="running",
|
||||
pid=fake.state["mode"].get("pid", 0) + 1)
|
||||
fake.state["settings"]["controller_mode"] = form["controller"]
|
||||
elif path == "/settings":
|
||||
fake.state["settings"].update(form)
|
||||
self._send(200, {"ok": True})
|
||||
|
||||
self._srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), H)
|
||||
self._srv.daemon_threads = True
|
||||
self._srv.block_on_close = False # never wait on a lingering connection
|
||||
self.url = "http://127.0.0.1:%d" % self._srv.server_address[1]
|
||||
self._th = _threading.Thread(target=self._srv.serve_forever, daemon=True)
|
||||
|
||||
def start(self):
|
||||
self._th.start()
|
||||
os.environ["FORGECTRL_URL"] = self.url
|
||||
os.environ["FORGECTRL_TOKEN_FILE"] = os.devnull
|
||||
return self
|
||||
|
||||
def stop(self):
|
||||
self._srv.shutdown()
|
||||
self._srv.server_close()
|
||||
os.environ.pop("FORGECTRL_URL", None)
|
||||
os.environ.pop("FORGECTRL_TOKEN_FILE", None)
|
||||
|
||||
Reference in New Issue
Block a user