mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-10-15 17:19:55 -07:00
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 since30c0a05and setup.machine-name since70e13bd. This deletion adds none to the owed list. releases/ and TEST-AUDIT.md are unchanged.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user