Catalog: forgectrl.lease leaves

forgectrl.lease leaves for forgectrl's tests/host_lease.sh (0086e65),
which passed as build.yml runs it: with the gate open and cnc/state idle,
the switches check held at its first wait names wizard:switches, kind
hardware, as the holder; POST /diag/flow-verify, /curve/record,
/logs/export, /mode with the mode in force, /controller/start and
/settings are each 409 in the holder's name, none for an idle machine or
a closed gate; the abort frees the lease and the same settings write is
200; lease.changed carries the take before the free. lease_test and
super_test case K hold the lease's own rules. The catalog goes from 81 to
80 (80 @test in forgetest/forgetest/suite/).

_EventStream, _lease and _text, which only it read, go with it, and so
does urllib.parse, which only _EventStream used; socket stays for lan_ip.
No test requires the id, and no unit test names it.

The coverage lint on the tree manifest then named forgectrl src/events.c
and src/events.h, and nothing else beside setup.check-lens's empty
src/lensfind.* entry. One HOST_TESTED row takes them, to
forgectrl:tests/events_test.c on forgectrl:.github/workflows/build.yml,
which the pin c274f19 runs (./build/events_test). The lint then finds
no uncovered path and no host-tested row problem across the 80 tests;
its one finding is setup.check-lens's. test_manifest.py (34) passes, and
pyflakes reports nothing on forgectrl.py or manifest.py. The full
forgetest suite was not run.

Fingerprints, measured against the parent: 26 tests move with
forgectrl.py's shared text: the 4 left in it (auth, panel-serves,
settings-bounds, tokens), the 5 left in exthost.py and the 17 of the
setup family, whose modules import lan_ip from it (exthost.py and
setup.py, and through setup.py the rest). The pending pin bumps from
forgectrl c274f19 and forgeext 2d0391e already owe 24 of them;
exthost.platform has been owed since 30c0a05 and setup.machine-name
since 70e13bd. This deletion adds none to the owed list. releases/ and
TEST-AUDIT.md are unchanged.
This commit is contained in:
ScottW514
2026-10-14 17:33:16 -04:00
parent 70e13bdd88
commit 40c478d56c
2 changed files with 3 additions and 154 deletions
+3
View File
@@ -159,6 +159,9 @@ HOST_TESTED = [
# the closed list of actions, the host's words, the catalog's fetch (the # the closed list of actions, the host's words, the catalog's fetch (the
# routes against a live host's stand-in are tests/host_ext.sh). # routes against a live host's stand-in are tests/host_ext.sh).
("forgectrl", "src/extpkg.*", "forgectrl:tests/extpkg_test.c", "forgectrl:.github/workflows/build.yml"), ("forgectrl", "src/extpkg.*", "forgectrl:tests/extpkg_test.c", "forgectrl:.github/workflows/build.yml"),
# The event stream: a button edge, a lease changing hands, telemetry
# (the live order, the take before the free, is tests/host_lease.sh).
("forgectrl", "src/events.*", "forgectrl:tests/events_test.c", "forgectrl:.github/workflows/build.yml"),
] ]
# This repository's root, when the package runs from a checkout of it; a # This repository's root, when the package runs from a checkout of it; a
-154
View File
@@ -8,7 +8,6 @@ import json
import os import os
import socket import socket
import time import time
import urllib.parse
from ..catalog import test from ..catalog import test
from .. import hw from .. import hw
@@ -455,159 +454,6 @@ def panel_serves(ctx):
"GET /curve/ladder.gcode -> %s without the ladder", st) "GET /curve/ladder.gcode -> %s without the ladder", st)
class _EventStream:
"""GET /events over a raw socket bound to a chosen loopback source
address. Every address in 127.0.0.0/8 is this host, so the test can be
several peers at once, and the daemon counts streams per peer address."""
def __init__(self, base, source, timeout=5.0):
u = urllib.parse.urlsplit(base)
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.bind((source, 0))
self.sock.settimeout(timeout)
self.sock.connect((u.hostname or "127.0.0.1", u.port or 80))
self.sock.sendall(b"GET /events HTTP/1.1\r\nHost: %s\r\nAccept: text/event-stream\r\n"
b"Connection: close\r\n\r\n" % (u.netloc or "127.0.0.1").encode())
self.buf = b""
self.eof = False # the response is over: the last chunk, or the socket closed
head = self._until(b"\r\n\r\n")
self.status = int(head.split(b" ", 2)[1]) if head.startswith(b"HTTP/") else 0
self.head = head.decode("latin-1", "replace")
def _fill(self):
try:
chunk = self.sock.recv(4096)
except socket.timeout:
return False
if not chunk:
self.eof = True
return False
self.buf += chunk
return True
def _until(self, mark):
while mark not in self.buf:
if not self._fill():
break
i = self.buf.find(mark)
if i < 0:
out, self.buf = self.buf, b""
return out
out, self.buf = self.buf[:i], self.buf[i + len(mark):]
return out
def text(self, seconds):
"""Everything the stream says in the next `seconds` (chunk framing
and all: the checks look for event names inside it)."""
end = time.time() + seconds
self.sock.settimeout(0.2) # the window is the caller's, not one long read
while time.time() < end and not self.eof:
self._fill()
if self.buf.endswith(b"\r\n0\r\n\r\n"):
self.eof = True
out, self.buf = self.buf, b""
return out.decode("utf-8", "replace")
def close(self):
try:
self.sock.close()
except OSError:
pass
def _lease(fc):
return (fc.status().get("lease") or {})
def _text(body):
return body if isinstance(body, str) else json.dumps(body)
@test("forgectrl.lease", title="The machine lease: a holder refuses everything else, by name",
subsystem="forgectrl", kind="auto", est_min=2,
covers=[("forgectrl", "src/lease.*"), ("forgectrl", "src/main.c"), ("forgectrl", "src/status.*"),
("forgectrl", "src/wizdark.*"), ("forgectrl", "src/diag.*"), ("forgectrl", "src/curverec.*"),
("forgectrl", "src/jobrun.*"), ("forgectrl", "src/update.*"), ("forgectrl", "src/logs.*"), ("forgectrl", "src/super.*"),
("forgectrl", "src/events.*")],
description="The switches check is started and left waiting at its first prompt: it moves "
"nothing, and it holds the machine lease for as long as it waits. /status must "
"name it as the holder (wizard:switches, kind hardware), the event stream must "
"report lease.changed, and everything that asks the lease must be refused with "
"409 and the holder's name: a diagnostic, the dose-curve recorder, a log export, a "
"mode switch to the mode already in force, POST /controller/start, and POST "
"/settings (with 'settings are locked'). Only requests that would do no harm if "
"the lease failed are made: no reboot, no boot-slot change, no update job. The "
"check is then aborted, and the lease must read free, lease.changed must say so, "
"and POST /settings must be accepted again. Whatever a failed refusal started is "
"stopped on the way out.")
def lease(ctx):
from .setup_dark import dark
ev = ctx.evidence
fc = hw.Forgectrl()
wid, owner = "switches", "wizard:switches"
rest = _lease(fc)
ctx.check("holder" in rest and rest["holder"] is None, "the machine is not free at the start: %s", rest)
ctx.check(set(rest.get("observed") or {}) == {"sender", "motors_released"},
"what is observed outside the lease is not reported: %s", rest)
mode = (fc.get("/mode")[1] or {}).get("mode")
units = fc.settings().get("ui_units") or "metric"
stream = _EventStream(fc.base, "127.0.0.6")
ctx.check(stream.status == 200, "GET /events -> %s", stream.status)
stream.text(0.5)
started = False
try:
st, body = fc.post("/wiz/%s/start" % wid)
ctx.check(st == 200, "the %s check did not start (%s %s)", wid, st, _text(body))
started = True
ok = ctx.wait_for(lambda: (_lease(fc).get("holder") or {}).get("owner") == owner, 10, poll=0.2)
held = _lease(fc).get("holder") or {}
ev["holder"] = held
ctx.log("the lease while the check waits: %s", held)
ctx.check(ok is not None and held.get("kind") == "hardware" and "switches" in (held.get("words") or ""),
"the lease does not name the running check: %s", held)
refused = {}
for name, path, kw in (
("a diagnostic", "/diag/flow-verify", {}),
("the recorder", "/curve/record", {}),
("a log export", "/logs/export", {}),
("a mode switch", "/mode", {"params": {"controller": mode}}),
("the controller's start", "/controller/start", {}),
("a settings write", "/settings", {"params": {"ui_units": units}})):
st, body = fc.post(path, **kw)
refused[name] = [st, _text(body)[:160]]
ctx.check(st == 409 and "holds the machine" in _text(body) and "switches" in _text(body),
"%s under the wizard's hold -> %s %s", name, st, _text(body)[:160])
ctx.check("settings are locked" in refused["a settings write"][1],
"the settings refusal does not say they are locked: %s", refused["a settings write"])
ev["refused"] = refused
ctx.check((_lease(fc).get("holder") or {}).get("owner") == owner,
"a refused request took the lease away: %s", _lease(fc))
finally:
if started:
fc.post("/wiz/%s/abort" % wid)
ctx.wait_for(lambda: not dark(fc).get("running"), 30)
# Nothing a failed refusal may have started is left running.
fc.post("/diag/abort")
fc.post("/curve/stop")
ok = ctx.wait_for(lambda: _lease(fc).get("holder") is None, 30, poll=0.5)
ctx.check(ok is not None, "the lease was not given back after the abort: %s", _lease(fc))
st, body = fc.post("/settings", params={"ui_units": units})
ctx.check(st == 200, "POST /settings with the machine free again -> %s %s", st, _text(body)[:120])
text = stream.text(2.0)
stream.close()
changes = [l for l in text.splitlines() if l.startswith("data:") and "owner" in l]
ev["lease_changed"] = changes
ctx.log("the event stream: %s", changes)
i_take, i_free = text.find('"owner":"%s"' % owner), text.find('"owner":null')
ctx.check("event: lease.changed" in text and 0 <= i_take < i_free,
"lease.changed did not report the hold and then its end: %r", text[-300:])
ctx.log("PASS: %s held the machine, six requests were refused by name, the abort freed it, and the "
"event stream said both", owner)
# ---------------------------------------------------------------- scoped tokens # ---------------------------------------------------------------- scoped tokens
def _words_of(body): def _words_of(body):