commissioning: the layer, the acceptance tests, the harness rule, the docs, and the bench drills

meta-forgefirm: the forgefirm-users init replays the account at boot;
sshd refuses root and empty passwords and runs only while the panel
turns it on; the release image keeps an empty root password for the
console; the console banner; avahi announces forgefirm.local; https in
libmicrohttpd and ulfius; the panel on 80 and 443; the license bundle on
the rootfs; release.sh checks the root policy on the built rootfs.

forgetest: the commission suites (commission, commission_dark,
commission_sheet: 23 cases); the runner turns cloud mode on with the
typed phrase for a test that declares it; the baseline's motor_lock is
0; the log-export test checks the bundle for the camera key; the record
helpers write bytes as given and join the daemon's paths as POSIX. The
stream harness gains rule 24: a hold verdict is held again after a
resume. Bench drills: lens_travel.py and lens_stop_accel.py.

Docs: BRINGUP carries the present state; CAMPAIGN-LOG carries the dated
record.
This commit is contained in:
ScottW514
2026-09-06 19:56:05 -04:00
parent ff9796cde6
commit 97287aa6a9
59 changed files with 15113 additions and 9139 deletions
+13 -1
View File
@@ -108,6 +108,7 @@ class FakeForgectrl:
}
self.posts = []
self.on_post = None
self.on_get = None # (path, query) -> (status, body) | (status, bytes, ctype) | None
fake = self
class H(http.server.BaseHTTPRequestHandler):
@@ -123,7 +124,18 @@ class FakeForgectrl:
self.wfile.write(data)
def do_GET(self):
path = self.path.split("?", 1)[0]
path, _, query = self.path.partition("?")
if fake.on_get:
r = fake.on_get(path, dict(_up.parse_qsl(query, keep_blank_values=True)))
if r is not None:
if isinstance(r[1], (bytes, bytearray)):
self.send_response(r[0])
self.send_header("Content-Type", r[2] if len(r) > 2 else "application/octet-stream")
self.send_header("Content-Length", str(len(r[1])))
self.end_headers()
self.wfile.write(r[1])
return
return self._send(r[0], r[1])
key = {"/mode": "mode", "/status": "status", "/cool/status": "cool", "/cam/status": "cam",
"/diag/status": "diag", "/settings": "settings",
"/logs/tail": "logs_tail"}.get(path)
+19 -5
View File
@@ -82,11 +82,11 @@ class BaselineTests(unittest.TestCase):
self.assertEqual(set(items), {"cnc/motor_lock", "cnc/step_freq", "cnc/streaming"})
for x in left:
self.assertEqual(x.action, "restored", str(x))
self.assertEqual(self._read("cnc/motor_lock"), "8")
self.assertEqual(self._read("cnc/motor_lock"), "0")
self.assertEqual(self._read("cnc/step_freq"), "28160")
self.assertEqual(self._read("cnc/streaming"), "0")
self.assertEqual(items["cnc/motor_lock"].found, "15")
self.assertEqual(items["cnc/motor_lock"].expected, "8")
self.assertEqual(items["cnc/motor_lock"].expected, "0")
def test_unlocked_latch_is_relocked(self):
self._attr("cnc/interlock_circuit", "5") # bit 3 clear = unlocked
@@ -194,7 +194,7 @@ class BaselineTests(unittest.TestCase):
else:
# a young host: the reference is taken from the fake tree
self.assertIsNotNone(ref)
self.assertEqual(ref["sysfs"]["cnc/motor_lock"], "8")
self.assertEqual(ref["sysfs"]["cnc/motor_lock"], "0")
self.assertTrue(os.path.exists(os.path.join(self.tmp, "boot-test-boot.json")))
# and loaded back the second time
self.lines[:] = []
@@ -205,7 +205,7 @@ class BaselineTests(unittest.TestCase):
os.environ.pop("FORGETEST_BOOT_ID", None)
def test_fixed_constants_checked_against_a_dump(self):
ref = {"sysfs": {"cnc/motor_lock": "8", "cnc/step_freq": "10000"}}
ref = {"sysfs": {"cnc/motor_lock": "0", "cnc/step_freq": "10000"}}
diffs = baseline.check_fixed_against(ref, self.lines.append)
self.assertEqual(diffs, ["cnc/step_freq: boot=10000 constant=28160"])
@@ -286,7 +286,7 @@ class BaselineTests(unittest.TestCase):
self.assertEqual(ref["ts"], "old")
else:
self.assertTrue(any("retaking" in l for l in self.lines))
self.assertEqual(ref["sysfs"]["cnc/motor_lock"], "8")
self.assertEqual(ref["sysfs"]["cnc/motor_lock"], "0")
finally:
os.environ.pop("FORGETEST_BOOT_ID", None)
@@ -325,6 +325,7 @@ class BaselineModeTests(BaselineTests):
The fake client takes the marker down as gfcloud does, first thing
at its start; with refuse the POST is refused and no client starts."""
baseline.NOHUNT_MARKER = os.path.join(self.tmp, "nohunt-marker")
self.fc.state["settings"].setdefault("cloud_enabled", "1")
seen = []
def on_post(path, form):
@@ -355,6 +356,19 @@ class BaselineModeTests(BaselineTests):
self.assertEqual(seen, [("/mode", {"controller": "cloud"}, True)])
self.assertFalse(os.path.exists(baseline.NOHUNT_MARKER)) # one start, never left behind
def test_a_switch_to_cloud_turns_cloud_mode_on_when_it_is_off(self):
# A test that declares cloud mode gets it: cloud_enabled goes to 1
# first, with the typed phrase the daemon asks for and a log line,
# and only then the mode switch.
lines = []
seen = self.nohunt_marker()
self.fc.state["settings"]["cloud_enabled"] = "0"
ok, detail = baseline.Baseline(lines.append).switch_mode("cloud")
self.assertTrue(ok, detail)
self.assertEqual(seen[0], ("/settings", {"cloud_enabled": "1", "phrase": "I UNDERSTAND"}, False))
self.assertEqual(seen[1], ("/mode", {"controller": "cloud"}, True))
self.assertTrue(any("cloud mode turned on for the test" in ln for ln in lines))
def test_a_switch_to_grbl_sets_no_marker(self):
seen = self.nohunt_marker()
self.cloud()
+7 -5
View File
@@ -650,11 +650,13 @@ class CloudSuiteTests(unittest.TestCase):
threading.Thread(target=self.wait_home_command, daemon=True).start()
self.assertFails(cloud.mode_switch, "no 'homing complete' line", hooks=hooks)
def test_mode_switch_precheck_needs_gfcloud_homing(self):
self.fc.state["settings"]["homing_mode"] = "switches"
self.assertIn("needs gfcloud", cloud.homing_mode_is_gfcloud())
self.fc.state["settings"]["homing_mode"] = "gfcloud"
self.assertIsNone(cloud.homing_mode_is_gfcloud())
def test_mode_switch_needs_no_setting_from_the_operator(self):
# The test sets the gfcloud homing itself and restores it: no precheck
# names a setting the operator should change first.
from forgetest import catalog
t = catalog.load_suite()["cloud.mode-switch"]
self.assertIsNone(getattr(t, "precheck", None))
self.assertNotIn("homing_mode_is_gfcloud", dir(cloud))
# -- pause / resume ---------------------------------------------------------
def replay_print(self, name, at_run, at_end, tail_delay=0.3):
+116
View File
@@ -0,0 +1,116 @@
"""The setup's checks in the catalog: registered as automatic tests with
no precheck, the takeover ones marked so, every covers map naming the
check runner; and the check driver itself against the fake daemon: a
start, a prompt answered, the result taken, the record read at version
1, and the settings a check may write put back as found."""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(__file__))
import helpers # noqa: E402
from forgetest import catalog # noqa: E402
from forgetest.runner import Context, Run # noqa: E402
IDS = ["commission.check-switches", "commission.check-sensors", "commission.check-airflow",
"commission.check-cameras", "commission.check-motion", "commission.check-flow-verify"]
class RegistrationTests(unittest.TestCase):
def setUp(self):
self.reg = catalog.load_suite()
def test_every_check_runs_unattended_with_the_fixture(self):
# The lid and button checks name their machine actions, which the
# bench fixture performs; the rest need no hands at all.
for tid in IDS:
self.assertIn(tid, self.reg)
for tid in ("commission.check-switches", "commission.check-cameras"):
self.assertEqual(self.reg[tid].kind, "operator", tid)
self.assertTrue(self.reg[tid].fixture_runnable(("button", "lid", "interlock")), tid)
for tid in ("commission.check-sensors", "commission.check-airflow", "commission.check-motion",
"commission.check-flow-verify"):
self.assertEqual(self.reg[tid].kind, "auto", tid)
def test_no_check_asks_the_operator_to_prepare_anything(self):
for tid in IDS:
self.assertIsNone(getattr(self.reg[tid], "precheck", None), tid)
def test_the_checks_that_take_the_machine_say_so(self):
for tid in ("commission.check-airflow", "commission.check-motion", "commission.check-flow-verify"):
self.assertEqual(self.reg[tid].hardware, "takeover", tid)
def test_every_check_covers_the_runner_and_the_page(self):
for tid in IDS:
covers = set(self.reg[tid].covers)
self.assertIn(("forgectrl", "src/wizdark.*"), covers, tid)
self.assertIn(("forgectrl", "src/ui/wizard.*"), covers, tid)
class SensorsCheckTests(unittest.TestCase):
"""The check driver against a scripted daemon."""
RESULT = {"coolant_down_c": 22.4, "coolant_up_c": 21.9, "chassis_c": 28.0, "soc_c": 45.2,
"lid_ir_max": [110, 120, 105, 118], "accel_events": 0, "laser_pgood": 1,
"hv_current_max": 0, "exhaust_rpm_idle": 3400, "intake_rpm_idle": 2200}
def setUp(self):
self.fake = helpers.FakeForgectrl().start()
self.fake.state["settings"].update({"cool_temp_offset_c": "0.5"})
self.dark = {"id": "", "running": False, "phase": "", "progress": 0, "elapsed_s": 0,
"log": [], "prompt": None, "result": None, "error": "",
"shots": {"lid": False, "head": False}}
self.versions = {}
self.answers = []
def on_get(path, q):
if path == "/wiz/dark":
return 200, dict(self.dark)
if path == "/wiz":
return 200, {"versions": dict(self.versions), "required": [], "completed": True}
return None
def on_post(path, form):
s = self.fake.state["settings"]
if path == "/wiz/sensors/start":
self.dark.update({"id": "sensors", "running": True, "phase": "sampling",
"prompt": {"seq": 7, "id": "room-temp", "kind": "number",
"text": "Optional: the room temperature.",
"options": ["Set", "Skip"]}})
return 200, {"started": True, "id": "sensors"}
if path == "/wiz/sensors/answer":
self.answers.append(dict(form))
if form.get("seq") != "7":
return 409, {"error": "no such prompt is open"}
if form.get("value") == "Skip":
self.dark.update({"running": False, "prompt": None, "progress": 100,
"result": dict(self.RESULT)})
self.versions["sensors"] = 1
return 200, {"ok": True}
if path == "/settings":
for k, v in form.items():
if v == "":
s.pop(k, None)
else:
s[k] = v
return 200, dict(s)
return None
self.fake.on_get = on_get
self.fake.on_post = on_post
def tearDown(self):
self.fake.stop()
def test_start_prompt_answer_result_and_restore(self):
t = catalog.load_suite()["commission.check-sensors"]
run = Run("test", t.id, t.title)
t.fn(Context(run, None, t))
self.assertIn(("/wiz/sensors/start", {}), [(p, f) for p, f in self.fake.posts])
self.assertEqual(self.answers, [{"seq": "7", "value": "Skip"}])
self.assertEqual(run.evidence["result"]["laser_pgood"], 1)
self.assertEqual(run.evidence["settings_before"], {"cool_temp_offset_c": "0.5"})
self.assertEqual(self.fake.state["settings"]["cool_temp_offset_c"], "0.5")
if __name__ == "__main__":
unittest.main()
+266
View File
@@ -0,0 +1,266 @@
"""The setup's sheet in the catalog: one live test on the button with a
piece of wood at hand, its covers map naming the live runner, the
renderer, the sender, the page, and the driver's laser module; and the
whole run against a scripted daemon: the placement's jogs and origin,
then each card's preview and program read, its start, the arm prompt and
the pick answered, the witnesses judged, the record read at version 1,
every card's evidence filed under its own key, and the settings the cards
wrote put back as found at the end."""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(__file__))
import helpers # noqa: E402
from forgetest import catalog # noqa: E402
from forgetest.runner import Context, Run # noqa: E402
from forgetest.suite import commission_sheet # noqa: E402
LIVE = ["sheet.frame", "laser.focus", "laser.floor", "laser.dose-curve", "laser.corner", "cooling.flow-load"]
EMISSION = {"hv_max": 610, "laser_on_samples": 900, "thermopile_delta": 600, "lit_s": 30.0}
# Each wizard as the daemon runs it: its prompts in order (id, kind,
# options) and the result it ends with. A "jog" prompt repeats until the
# origin is set.
SCRIPT = {
"sheet.place": ([("place", "jog", ["X-10", "X+10", "Set origin"]),
("sheet-kind", "choice", ["Full sheet", "One card"]),
("thickness", "number", ["Set", "Skip"])],
{"origin_x": 100, "origin_y": 200, "origin_z": 0, "steps_per_mm": {"x": 40.0, "y": 40.0},
"z_referenced": True, "thickness_mm": 3.2, "alone": False}),
"sheet.frame": ([("frame-arm", "continue", ["Continue"]), ("frame-ok", "choice", ["Yes", "No", "Again"])],
{"mark_s": 400, "mark_feed": 3000, "emission": EMISSION}),
"laser.focus": ([("focus-arm", "continue", ["Continue"]),
("focus-pick", "multichoice", [str(i) for i in range(1, 13)]),
("thickness", "number", ["Keep", "Set"])],
{"pick": 11, "thickness_mm": 3.2, "pick_half_steps": -11, "edge_z_mm": 6.96,
"steps_per_mm": 2.922, "max_height_mm": 13.81,
"focus_range_mm": {"min": 2.17, "max": 13.81},
"stops": {"found": True, "below": 14, "above": 20, "contact_below": 16,
"contact_above": 22, "why": ""}, "emission": EMISSION}),
"laser.floor": ([("floor-arm", "continue", ["Continue"]),
("floor-pick", "choice", ["2", "4", "6", "8", "None"])],
{"faintest_density": 8.0, "floor_density": 10.0, "emission": EMISSION}),
"laser.dose-curve": ([("dose-arm", "continue", ["Continue"])],
{"points": [[10, 0.44], [100, 100]], "curve": "10:0.44,100:100", "emission": EMISSION}),
"laser.corner": ([("corner-arm", "continue", ["Continue"]),
("corner-pick", "choice", ["1.00", "1.25", "1.50", "1.75", "2.00"])],
{"gamma": 1.5, "emission": EMISSION}),
"cooling.flow-load": ([("load-arm", "continue", ["Continue"])],
{"lit_s": 30.0, "dose_raw_s": 12000, "peak_c": 0.72, "k_density": 2.14e-5,
"k_cw": 2.78e-5, "emission": EMISSION}),
}
# What each card writes when it ends.
WRITES = {
"laser.focus": {"lens_hall_edge_z_mm": "6.96", "lens_stop_below_steps": "14",
"lens_stop_above_steps": "20"},
"laser.floor": {"laser_floor_density": "10"},
"laser.dose-curve": {"laser_dose_curve": "10:0.44,100:100"},
"laser.corner": {"laser_corner_gamma": "1.5"},
"cooling.flow-load": {"cool_laser_heat_density": "2.14e-5", "cool_laser_heat_cw": "2.78e-5"},
}
FOUND = {"lens_hall_edge_z_mm": "3.35", "lens_stop_below_steps": "10", "lens_stop_above_steps": "12",
"laser_floor_density": "12",
"laser_dose_curve": "off", "laser_corner_gamma": "2", "cool_laser_heat_density": "1e-5",
"cool_laser_heat_cw": "1e-5"}
class RegistrationTests(unittest.TestCase):
def setUp(self):
self.t = catalog.load_suite()["commission.sheet"]
def test_one_live_test_on_the_button_with_the_sheet_at_hand(self):
t = self.t
self.assertEqual(t.kind, "live")
self.assertEqual(t.mode, "grbl")
self.assertEqual(t.hardware, "takeover")
self.assertIn("button", t.actions)
self.assertTrue(t.hands)
self.assertFalse(t.fixture_runnable(("button", "lid", "interlock")))
self.assertIsNone(getattr(t, "precheck", None))
for want in ("commission.check-motion", "laser.emission-witness", "cooling.flow-verify"):
self.assertIn(want, t.requires)
def test_no_other_sheet_test_remains(self):
ids = [i for i in catalog.load_suite() if i.startswith("commission.sheet")]
self.assertEqual(ids, ["commission.sheet"])
def test_covers_the_live_runner_the_renderer_the_sender_the_page_and_the_driver(self):
covers = set(self.t.covers)
for want in (("forgectrl", "src/wizlive.*"), ("forgectrl", "src/sheet.*"), ("forgectrl", "src/jobstream.*"),
("forgectrl", "src/curverec.*"), ("forgectrl", "src/font_hershey.*"),
("forgectrl", "src/ui/wizard.*"), ("forgectrl", "tools/hershey_gen.py"),
("grblhal-glowforge", "src/glowforge_laser.c")):
self.assertIn(want, covers, "lacks %s" % (want,))
def test_the_steps_ask_for_one_press_and_no_page(self):
text = " ".join(self.t.steps).lower()
self.assertIn("press the machine's button once", text)
self.assertNotIn("when the page", text)
self.assertEqual(len(commission_sheet.CARDS), 6)
class ScriptedDaemon:
"""The wizard runner as the suite sees it: /wiz/<id>/start opens the
first prompt, each answer opens the next or ends the wizard with its
result and its writes, /wiz/dark reports it, GET /wiz carries the
versions, the sheet preview and program are served per card."""
def __init__(self, fake):
self.fake = fake
fake.state["settings"].update(FOUND)
self.dark = {"id": "", "running": False, "phase": "", "progress": 0, "elapsed_s": 0,
"log": [], "prompt": None, "result": None, "error": "",
"shots": {"lid": False, "head": False}}
self.versions = {}
self.answers = {} # wid -> [values]
self.order = [] # the wizards as started
self.previews = []
self.queue = []
self.seq = 0
fake.on_get = self.on_get
fake.on_post = self.on_post
def open_next(self):
if not self.queue:
return False
pid, kind, options = self.queue[0]
self.seq += 1
self.dark["prompt"] = {"seq": self.seq, "id": pid, "kind": kind, "text": pid, "options": options}
return True
def on_get(self, path, q):
if path == "/wiz/dark":
return 200, dict(self.dark)
if path == "/wiz":
return 200, {"versions": dict(self.versions), "required": [], "completed": True}
if path == "/wiz/sheet.svg":
self.previews.append(q.get("card"))
if q.get("card") not in LIVE:
return 404, {"error": "no such card"}
return 200, b"<svg xmlns='http://www.w3.org/2000/svg'></svg>", "image/svg+xml"
if path == "/wiz/sheet.gcode":
# the frame and the text are M4 at the mark dose, as the daemon serves them
return 200, b"; mock\nM4 S400\nG0 X60 Y42\nG1 X120 Y42 F3000\nM5\n", "text/plain"
return None
def on_post(self, path, form):
s = self.fake.state["settings"]
if path.startswith("/wiz/") and path.endswith("/start"):
wid = path[len("/wiz/"):-len("/start")]
if wid not in SCRIPT:
return 404, {"error": "no such wizard"}
self.order.append(wid)
self.queue = list(SCRIPT[wid][0])
self.dark.update({"id": wid, "running": True, "phase": "starting", "result": None, "error": ""})
self.open_next()
return 200, {"started": True, "id": wid}
if path.startswith("/wiz/") and path.endswith("/answer"):
wid = path[len("/wiz/"):-len("/answer")]
p = self.dark.get("prompt")
if not p or form.get("seq") != str(p["seq"]):
return 409, {"error": "no such prompt is open"}
self.answers.setdefault(wid, []).append(form.get("value"))
if not (p["kind"] == "jog" and form.get("value") != "Set origin"):
self.queue.pop(0)
if not self.open_next():
s.update(WRITES.get(wid, {}))
self.dark.update({"running": False, "prompt": None, "progress": 100,
"result": dict(SCRIPT[wid][1])})
self.versions[wid] = 1
return 200, {"ok": True}
if path == "/settings":
for k, v in form.items():
if v == "":
s.pop(k, None)
else:
s[k] = v
return 200, dict(s)
return None
class SheetRunTests(unittest.TestCase):
def setUp(self):
self.fake = helpers.FakeForgectrl().start()
self.daemon = ScriptedDaemon(self.fake)
def tearDown(self):
self.fake.stop()
def test_the_whole_sheet_on_one_piece(self):
t = catalog.load_suite()["commission.sheet"]
run = Run("test", t.id, t.title)
run.unattended = True # the ready gate passes; the arm cue is a notice
t.fn(Context(run, None, t))
d = self.daemon
self.assertEqual(d.order, ["sheet.place"] + LIVE)
self.assertEqual(d.answers["sheet.place"], ["X+10", "X-10", "Set origin", "Full sheet", "3.2"])
self.assertEqual(d.answers["sheet.frame"], ["Continue", "Yes"])
self.assertEqual(d.answers["laser.focus"], ["Continue", "11", "Keep"])
self.assertEqual(d.answers["laser.floor"], ["Continue", "8"])
self.assertEqual(d.answers["laser.corner"], ["Continue", "1.50"])
self.assertEqual(d.previews, LIVE)
cards = run.evidence["cards"]
self.assertEqual(sorted(cards), sorted(["sheet.place"] + LIVE))
for wid in LIVE:
self.assertEqual(cards[wid]["emission"]["hv_max"], 610, wid)
self.assertEqual(cards[wid]["result"], SCRIPT[wid][1], wid)
self.assertNotIn("emission", run.evidence)
self.assertEqual(cards["sheet.place"]["result"]["origin_x"], 100)
self.assertEqual(run.evidence["settings_before"], FOUND)
for k, v in FOUND.items():
self.assertEqual(self.fake.state["settings"][k], v, k) # restored
def test_imperial_units_answer_the_thickness_in_inches(self):
# The number prompts read ui_units: on an imperial machine the
# placement's thickness is 0.125 in and the record carries 3.175 mm.
self.fake.state["settings"]["ui_units"] = "imperial"
place_prompts, place_result = SCRIPT["sheet.place"]
SCRIPT["sheet.place"] = (place_prompts, dict(place_result, thickness_mm=3.175))
try:
t = catalog.load_suite()["commission.sheet"]
run = Run("test", t.id, t.title)
run.unattended = True
t.fn(Context(run, None, t))
self.assertEqual(self.daemon.answers["sheet.place"][-1], "0.125")
self.assertEqual(run.evidence["thickness_answer"], "0.125")
finally:
SCRIPT["sheet.place"] = (place_prompts, place_result)
def test_a_metric_answer_on_an_imperial_machine_fails_the_placement(self):
# The defect the bench found: 3.2 read as inches is over the 30 mm
# cap and the wizard records 0; the test must not read that as a pass.
from forgetest.runner import Failed
place_prompts, place_result = SCRIPT["sheet.place"]
SCRIPT["sheet.place"] = (place_prompts, dict(place_result, thickness_mm=0.0))
try:
t = catalog.load_suite()["commission.sheet"]
run = Run("test", t.id, t.title)
run.unattended = True
with self.assertRaises(Failed) as cm:
t.fn(Context(run, None, t))
self.assertIn("thickness 0.0 mm", str(cm.exception))
self.assertEqual(self.daemon.order, ["sheet.place"]) # nothing burned
finally:
SCRIPT["sheet.place"] = (place_prompts, place_result)
def test_a_dark_burn_fails_the_run_and_still_restores(self):
from forgetest.runner import Failed
SCRIPT_DARK = dict(SCRIPT["laser.floor"][1])
SCRIPT_DARK["emission"] = {"hv_max": 0, "laser_on_samples": 0, "thermopile_delta": 0}
SCRIPT["laser.floor"] = (SCRIPT["laser.floor"][0], SCRIPT_DARK)
try:
t = catalog.load_suite()["commission.sheet"]
run = Run("test", t.id, t.title)
run.unattended = True
with self.assertRaises(Failed):
t.fn(Context(run, None, t))
self.assertEqual(self.daemon.order, ["sheet.place", "sheet.frame", "laser.focus", "laser.floor"])
for k, v in FOUND.items():
self.assertEqual(self.fake.state["settings"][k], v, k)
finally:
SCRIPT["laser.floor"] = (SCRIPT["laser.floor"][0], dict(SCRIPT_DARK, emission=EMISSION))
if __name__ == "__main__":
unittest.main()
+415
View File
@@ -0,0 +1,415 @@
"""The commission.* suite on the host: the registration (ids, kinds, the
takeover tests, the operator tests' hands), the record builders, the
mDNS packet code, the cookie parsing, the LED cue, the settle rule for a
gated supervisor, and the cloud-off surface test driven end to end
against the fake daemon."""
import json
import os
import shutil
import socket
import struct
import tempfile
import unittest
import helpers
from forgetest import baseline, catalog
from forgetest.runner import Context, Run
from forgetest.suite import commission
IDS = ("commission.gate-blocks-controllers", "commission.override-until-reboot",
"commission.advisories-rehash", "commission.account-login", "commission.https-only-writes",
"commission.ssh-until-reboot", "commission.cloud-disabled-surface",
"commission.factory-return", "commission.mdns-announce", "commission.first-run-flow",
"commission.first-run-page", "commission.what-changed", "commission.record-export",
"commission.mirror")
OPERATOR = ("commission.first-run-flow", "commission.first-run-page")
class RegistrationTests(unittest.TestCase):
def setUp(self):
self.reg = catalog.load_suite()
def test_every_test_is_registered_with_its_kind(self):
kinds = {tid: self.reg[tid].kind for tid in IDS if tid in self.reg}
self.assertEqual(sorted(kinds), sorted(IDS))
for tid in OPERATOR:
self.assertEqual(kinds[tid], "operator", tid)
for tid in IDS:
if tid not in OPERATOR:
self.assertEqual(kinds[tid], "auto", tid)
self.assertNotIn("commission.root-ssh-refused", self.reg) # folded into ssh-until-reboot
self.assertNotIn("commission.wizard-first-run", self.reg) # split into the flow and the page
def test_the_record_swapping_tests_are_takeovers(self):
for tid in ("commission.gate-blocks-controllers", "commission.advisories-rehash",
"commission.first-run-flow", "commission.first-run-page",
"commission.what-changed", "commission.mirror"):
self.assertEqual(self.reg[tid].hardware, "takeover", tid)
self.assertEqual(self.reg["commission.record-export"].hardware, "api")
def test_the_lifecycle_tests_cover_their_files(self):
changed = set(self.reg["commission.what-changed"].covers)
export = set(self.reg["commission.record-export"].covers)
mirror = set(self.reg["commission.mirror"].covers)
self.assertIn(("forgectrl", "src/ui/panel.js"), changed)
self.assertIn(("forgectrl", "src/recordhtml.*"), export)
self.assertIn(("forgectrl", "src/logs.*"), export)
self.assertIn(("forgectrl", "src/wizdark.*"), mirror)
self.assertIn("commission.check-sensors", self.reg["commission.mirror"].requires)
def test_flag_of_reads_the_status_document(self):
w = {"required": ["airflow", {"id": "laser.floor", "reason": "the tube was replaced"}],
"recommended": [{"id": "laser.focus", "reason": "the tray was replaced"}]}
self.assertEqual(commission.flag_of(w, "required", "laser.floor"), "the tube was replaced")
self.assertEqual(commission.flag_of(w, "recommended", "laser.focus"), "the tray was replaced")
self.assertIsNone(commission.flag_of(w, "required", "airflow")) # a plain id is the table, not a flag
self.assertIsNone(commission.flag_of(w, "required", "laser.focus"))
self.assertIsNone(commission.flag_of({}, "recommended", "x"))
def test_the_first_run_flow_runs_unattended_with_the_fixture_and_the_page_walk_does_not(self):
flow = self.reg["commission.first-run-flow"]
self.assertEqual(list(flow.actions), ["button"])
self.assertFalse(flow.hands)
self.assertTrue(flow.fixture_runnable(("button", "lid", "interlock")))
page = self.reg["commission.first-run-page"]
self.assertTrue(page.hands)
self.assertFalse(page.fixture_runnable(("button", "lid", "interlock")))
self.assertIn("commission.first-run-flow", page.requires)
def test_the_first_run_split_keeps_the_backend_and_the_page_apart(self):
flow = set(self.reg["commission.first-run-flow"].covers)
page = set(self.reg["commission.first-run-page"].covers)
for want in (("forgectrl", "src/wiz.*"), ("forgectrl", "src/commission.*"), ("forgectrl", "src/users.*"),
("forgectrl", "src/button.*"), ("forgectrl", "src/led.*"), ("forgectrl", "src/advisories.*")):
self.assertIn(want, flow, want)
self.assertNotIn(want, page, want)
for want in (("forgectrl", "src/ui/wizard.*"), ("forgectrl", "src/ui/md.js"), ("forgectrl", "src/tls.*")):
self.assertIn(want, page, want)
def test_the_only_attended_tests_need_a_workstation_or_the_sheet(self):
# With the bench actuator up, three commissioning tests keep a
# person: the page walk, the print from the Glowforge app, and
# the sheet. Everything else runs from the queue.
attended = sorted(tid for tid, t in self.reg.items() if tid.startswith("commission.")
and t.kind != "auto" and not t.fixture_runnable(("button", "lid", "interlock")))
self.assertEqual(attended, ["commission.cloud-header-capture", "commission.first-run-page",
"commission.sheet"])
def test_the_factory_return_never_runs_the_return(self):
import inspect
t = self.reg["commission.factory-return"]
src = inspect.getsource(t.fn)
self.assertNotIn('"confirm": "1"', src) # the only argument that starts the return
self.assertIn('"confirm": "0"', src)
self.assertEqual(t.kind, "auto") # the return itself is a bench drill, not a test
self.assertFalse(t.hands)
def test_mdns_covers_nothing_by_design(self):
self.assertEqual(self.reg["commission.mdns-announce"].covers, ())
def test_the_login_test_makes_its_own_account(self):
# No bench credentials, no precheck: the test installs a temporary
# account under a takeover and restores the real one.
t = self.reg["commission.account-login"]
self.assertIsNone(getattr(t, "precheck", None))
self.assertEqual(t.hardware, "takeover")
with open(commission.__file__) as f:
src = f.read()
for k in ("FORGETEST_LOGIN_NAME", "FORGETEST_LOGIN_PASSWORD"):
self.assertNotIn(k, src)
class RecordTests(unittest.TestCase):
STATUS = {"sheet_id": "ABCDE-FGHIJ",
"documents": [{"id": "safety-and-risk", "hash": "a" * 64, "consent": "typed"},
{"id": "licenses", "hash": "b" * 64, "consent": "check"}],
"wizards": [{"id": "advisories", "version": 1}, {"id": "account", "version": 1},
{"id": "machine", "version": 2}]}
def test_complete_record_counts_as_commissioned(self):
rec = commission.complete_record(self.STATUS)
self.assertEqual(rec["schema"], 1)
self.assertEqual(rec["advisories"]["safety-and-risk"]["hash"], "a" * 64)
self.assertEqual(rec["advisories"]["safety-and-risk"]["method"], "typed")
self.assertIn("pressed_at", rec["acceptance"])
self.assertEqual(rec["account"]["name"], "bench")
self.assertEqual(rec["wizards"]["machine"]["version"], 2)
self.assertEqual(rec["sheet_id"], "ABCDE-FGHIJ")
self.assertTrue(rec["completed"])
json.dumps(rec)
def test_complete_record_keeps_the_base_account_and_machine(self):
base = {"account": {"name": "owner", "uid": 1000, "created": "x"}, "machine": {"model": "pro"},
"sheet_id": "KKKKK-LLLLL", "created": "then"}
rec = commission.complete_record(self.STATUS, base)
self.assertEqual(rec["account"]["name"], "owner")
self.assertEqual(rec["machine"], {"model": "pro"})
self.assertEqual(rec["sheet_id"], "KKKKK-LLLLL")
self.assertEqual(rec["created"], "then")
def test_without_wizards_keeps_consent_and_account(self):
rec = commission.complete_record(self.STATUS)
rec["flags"] = {"machine": {"level": "required", "reason": "x"}}
out = commission.record_without_wizards(rec)
self.assertEqual(out["wizards"], {})
self.assertEqual(out["flags"], {})
self.assertEqual(out["advisories"], rec["advisories"])
self.assertEqual(out["account"], rec["account"])
self.assertEqual(rec["flags"], {"machine": {"level": "required", "reason": "x"}}) # a copy
def test_write_file_and_remove(self):
tmp = tempfile.mkdtemp(prefix="forgetest-comm-")
try:
p = os.path.join(tmp, "sub", "commissioning.json")
commission.write_file(p, b"{}\n")
self.assertEqual(commission.read_file(p), b"{}\n")
if os.name == "posix": # a mode means nothing on a Windows host
self.assertEqual(oct(os.stat(p).st_mode & 0o777), oct(0o600))
commission.write_file(p, None)
self.assertIsNone(commission.read_file(p))
commission.write_file(p, None) # a second remove is silent
finally:
shutil.rmtree(tmp, ignore_errors=True)
def test_paths_follow_the_daemon_environment(self):
os.environ["FORGECTRL_DATA_DIR"] = "/tmp/ffdata"
os.environ["GF_RUN_DIR"] = "/tmp/ffrun"
try:
self.assertEqual(commission.record_path(), "/tmp/ffdata/commissioning.json")
self.assertEqual(commission.users_path(), "/tmp/ffdata/users")
self.assertEqual(commission.override_path(), "/tmp/ffrun/commissioning-override")
self.assertEqual(commission.ssh_flag_path(), "/tmp/ffrun/ssh-enabled")
finally:
os.environ.pop("FORGECTRL_DATA_DIR", None)
os.environ.pop("GF_RUN_DIR", None)
class MdnsTests(unittest.TestCase):
def test_query_is_a_unicast_response_question(self):
q = commission.mdns_query("forgefirm.local", qid=0x1234)
qid, flags, qd, an, ns, ar = struct.unpack(">HHHHHH", q[:12])
self.assertEqual((qid, flags, qd, an, ns, ar), (0x1234, 0, 1, 0, 0, 0))
self.assertEqual(q[12:], b"\x09forgefirm\x05local\x00" + struct.pack(">HH", 1, 0x8001))
def _response(self, qid, name_bytes, addr, flags=0x8400, extra=b""):
q = b"\x09forgefirm\x05local\x00" + struct.pack(">HH", 1, 1)
rr = name_bytes + struct.pack(">HHIH", 1, 0x8001, 120, 4) + socket.inet_aton(addr)
return struct.pack(">HHHHHH", qid, flags, 1, 1, 0, 0) + q + rr + extra
def test_answers_follow_a_compression_pointer(self):
pkt = self._response(7, b"\xc0\x0c", "192.168.1.9")
self.assertEqual(commission.mdns_answers(pkt, 7), [("forgefirm.local", "192.168.1.9")])
def test_answers_with_the_name_spelled_out(self):
pkt = self._response(0, b"\x09forgefirm\x05local\x00", "10.0.0.5")
self.assertEqual(commission.mdns_answers(pkt), [("forgefirm.local", "10.0.0.5")])
def test_wrong_id_or_a_query_yields_nothing(self):
pkt = self._response(7, b"\xc0\x0c", "192.168.1.9")
self.assertEqual(commission.mdns_answers(pkt, 8), [])
self.assertEqual(commission.mdns_answers(self._response(7, b"\xc0\x0c", "1.2.3.4", flags=0), 7), [])
self.assertEqual(commission.mdns_answers(b"\x00" * 5), [])
def test_a_truncated_packet_yields_nothing(self):
pkt = self._response(7, b"\xc0\x0c", "192.168.1.9")
self.assertEqual(commission.mdns_answers(pkt[:20], 7), [])
class SmallHelpersTests(unittest.TestCase):
def test_cookie_parsing(self):
sid = "ab" * 32
value = "ffsid=%s; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=43200" % sid
self.assertEqual(commission.session_from_cookie(value), sid)
self.assertEqual(commission.cookie_flags(value), {"path", "httponly", "secure", "samesite", "max-age"})
self.assertIsNone(commission.session_from_cookie("ffsid=; Path=/; Max-Age=0"))
self.assertIsNone(commission.session_from_cookie(""))
def test_the_teal_breathe_cue(self):
self.assertTrue(commission.breathes_teal({"target": [0, 180, 200], "pulse_on": [0, 1400, 1400],
"pulse_off": [0, 1400, 1400]}))
self.assertFalse(commission.breathes_teal({"target": [0, 255, 40], "pulse_on": [0, 0, 0],
"pulse_off": [0, 0, 0]})) # solid green
self.assertFalse(commission.breathes_teal({"target": [200, 200, 200], "pulse_on": [1800, 1800, 1800],
"pulse_off": [1800] * 3})) # white
self.assertFalse(commission.breathes_teal({"target": [None, None, None], "pulse_on": [None] * 3}))
self.assertFalse(commission.breathes_teal({}))
def test_decode(self):
self.assertEqual(commission.decode(b'{"a": 1}'), {"a": 1})
self.assertEqual(commission.decode(b"cloud mode is not enabled"), "cloud mode is not enabled")
class GatedSettleTests(unittest.TestCase):
"""A supervisor that reports the commissioning gate closed is settled:
it spawns nothing until the gate opens, so a takeover that installs
a gating record must not wait the whole settle timeout for it."""
def setUp(self):
self.fake = helpers.FakeForgectrl().start()
baseline.Baseline._unreachable_until = 0.0
def tearDown(self):
self.fake.stop()
def test_gated_returns_at_once(self):
self.fake.state["mode"] = {"mode": "grbl", "controller": "gated", "pid": 0, "motion": "unverified",
"gated": True, "why": "commissioning required: machine"}
lines = []
body = baseline.Baseline(lines.append).wait_settled(timeout=8)
self.assertEqual(body["controller"], "gated")
self.assertTrue(any("controller=gated" in ln for ln in lines))
class FirstRunSeedTests(unittest.TestCase):
"""The record the first-run tests install: a first run behind the
checks and the sheet."""
STATUS = {"wizards": [{"id": "advisories", "version": 1, "class": "form"},
{"id": "account", "version": 1, "class": "form"},
{"id": "cloud", "version": 1, "class": "form"},
{"id": "switches", "version": 1, "class": "dark"},
{"id": "motion", "version": 2, "class": "dark"},
{"id": "sheet.frame", "version": 1, "class": "live"}]}
def test_no_consent_no_account_not_complete_and_every_check_done(self):
base = {"created": "2026-09-01T00:00:00Z", "completed": "2026-09-02T00:00:00Z",
"advisories": {"safety-and-risk": {"hash": "x"}}, "acceptance": {"pressed_at": "t"},
"account": {"name": "scott", "uid": 1000}, "flags": {"flow_thin": True},
"wizards": {"advisories": {"version": 1}, "switches": {"version": 1, "result": {"lid": True}}}}
rec = commission.first_run_record(self.STATUS, base)
self.assertNotIn("completed", rec)
self.assertNotIn("acceptance", rec)
self.assertNotIn("account", rec)
self.assertEqual(rec["advisories"], {})
self.assertEqual(rec["created"], "2026-09-01T00:00:00Z")
self.assertEqual(rec["flags"], {"flow_thin": True})
self.assertEqual(sorted(rec["wizards"]), ["motion", "sheet.frame", "switches"])
self.assertEqual(rec["wizards"]["switches"]["result"], {"lid": True}) # kept from the base
self.assertEqual(rec["wizards"]["motion"]["version"], 2) # written at the catalog version
def test_without_a_base_record(self):
rec = commission.first_run_record(self.STATUS, None)
self.assertEqual(sorted(rec["wizards"]), ["motion", "sheet.frame", "switches"])
self.assertEqual(rec["wizards"]["sheet.frame"]["version"], 1)
class SshdPolicyTests(unittest.TestCase):
def test_the_three_keys_out_of_sshd_t(self):
from forgetest import hw
calls = []
def fake_run(cmd, timeout=60):
calls.append(cmd)
return 0, "port 22\npermitrootlogin no\npasswordauthentication yes\npermitemptypasswords no\nx11forwarding no\n"
real = hw.run
hw.run = fake_run
try:
self.assertEqual(commission.sshd_policy(), {"permitrootlogin": "no", "passwordauthentication": "yes",
"permitemptypasswords": "no"})
finally:
hw.run = real
self.assertEqual(calls, [["/usr/sbin/sshd", "-T"]])
def test_a_failing_sshd_reports_the_error(self):
from forgetest import hw
real = hw.run
hw.run = lambda cmd, timeout=60: (1, "sshd: no hostkeys available")
try:
self.assertEqual(commission.sshd_policy(), {"error": "sshd: no hostkeys available"})
finally:
hw.run = real
class CloudDisabledSurfaceTests(unittest.TestCase):
"""The cloud-off surface test against the fake daemon: cloud_enabled
is turned off with one write that sweeps the cloud choices, the three
cloud-pointing writes and the phrase-less cloud_enabled=1 are refused,
the settings are left alone, and the prior values come back."""
def setUp(self):
self.fake = helpers.FakeForgectrl().start()
self.fake.state["settings"].update({"controller_mode": "grbl", "homing_mode": "none",
"cloud_enabled": "1", "ui_units": "metric"})
def on_post(path, form):
s = self.fake.state["settings"]
if path == "/settings":
# as the daemon rules it: on from off takes the typed phrase
if form.get("cloud_enabled") == "1" and s.get("cloud_enabled") != "1" \
and form.get("phrase") != "I UNDERSTAND":
return 400, "type I UNDERSTAND to turn cloud mode on"
# the request's own cloud choices, as main.c checks them
enabled = form.get("cloud_enabled", s.get("cloud_enabled")) == "1"
if not enabled and form.get("controller_mode") == "cloud":
return 409, "cloud mode is not enabled on this machine"
if not enabled and form.get("homing_mode") == "gfcloud":
return 409, "cloud homing needs cloud mode enabled"
for k, v in form.items():
if k == "phrase":
continue
if v == "":
s.pop(k, None)
else:
s[k] = v
# off sweeps what pointed at the cloud, as the cloud step does
if form.get("cloud_enabled") == "0":
if "homing_mode" not in form and s.get("homing_mode") == "gfcloud":
s["homing_mode"] = "none"
if "controller_mode" not in form and s.get("controller_mode") == "cloud":
s["controller_mode"] = "grbl"
return 200, dict(s)
if path == "/mode" and form.get("controller") == "cloud" and s.get("cloud_enabled") != "1":
return 409, "cloud mode is not enabled on this machine"
return None
self.fake.on_post = on_post
def tearDown(self):
self.fake.stop()
def run_test(self):
t = catalog.load_suite()["commission.cloud-disabled-surface"]
run = Run("test", t.id, t.title)
t.fn(Context(run, None, t))
return run
def test_refusals_and_restore(self):
run = self.run_test()
s = self.fake.state["settings"]
self.assertEqual(s["cloud_enabled"], "1") # restored, with the phrase
self.assertEqual(s["controller_mode"], "grbl")
self.assertEqual(s["homing_mode"], "none")
self.assertNotIn("phrase", s)
posts = [(p, f) for p, f in self.fake.posts]
self.assertIn(("/settings", {"cloud_enabled": "0"}), posts)
self.assertIn(("/settings", {"cloud_enabled": "1"}), posts) # refused: no phrase
self.assertIn(("/settings", {"cloud_enabled": "1", "phrase": "I UNDERSTAND"}), posts)
self.assertIn(("/mode", {"controller": "cloud"}), posts)
self.assertEqual(run.evidence["mode_cloud"]["status"], 409)
self.assertEqual(run.evidence["settings cloud_enabled=1 no phrase"], 400)
self.assertEqual(run.evidence["found"]["cloud_enabled"], "1")
def test_cloud_homing_and_boot_mode_are_swept_by_the_one_write_and_go_back(self):
# As the daemon rules it: cloud_enabled=0 takes the gfcloud homing
# and the cloud boot mode down with it, so the test writes the one
# key, checks the sweep, and puts the three back, cloud mode first.
self.fake.state["settings"].update({"controller_mode": "cloud", "homing_mode": "gfcloud"})
run = self.run_test()
s = self.fake.state["settings"]
self.assertEqual((s["cloud_enabled"], s["homing_mode"], s["controller_mode"]), ("1", "gfcloud", "cloud"))
posts = [(p, f) for p, f in self.fake.posts if p == "/settings"]
order = [f for _, f in posts]
self.assertEqual(order[:1], [{"cloud_enabled": "0"}])
self.assertEqual(order[-3:], [{"cloud_enabled": "1", "phrase": "I UNDERSTAND"},
{"homing_mode": "gfcloud"}, {"controller_mode": "cloud"}])
self.assertEqual(run.evidence["found"], {"cloud_enabled": "1", "homing_mode": "gfcloud",
"controller_mode": "cloud"})
def test_an_unset_value_is_cleared_back(self):
self.fake.state["settings"].pop("cloud_enabled")
self.run_test()
self.assertNotIn("cloud_enabled", self.fake.state["settings"])
self.assertIn(("/settings", {"cloud_enabled": ""}), self.fake.posts)
if __name__ == "__main__":
unittest.main()
+27
View File
@@ -586,6 +586,33 @@ class RoutingTests(unittest.TestCase):
finally:
runner_mod.hw.button_lit = saved
def test_the_arm_press_waits_as_long_as_the_card_says_for_the_light(self):
"""A card that settles the coolant before it lights the button
takes minutes; the caller names the wait, and the actuator presses
when the light comes instead of handing the press to a person at
the default 60 s (found on the bench by the flow-load card)."""
run = Run("test", "r.live", "r.live")
ctx = Context(run, self.runner, self.reg["r.live"])
self.runner.probe_fixture(force=True)
run.fixture_takeover = True
lit_at = time.time() + 0.6
saved = runner_mod.hw.button_lit
runner_mod.hw.button_lit = lambda: time.time() >= lit_at
seen = []
run.set_notice = lambda text: seen.append(text)
try:
self.assertTrue(ctx.arm_press(lit_timeout=5))
deadline = time.time() + 5
while ("button", "press") not in self.stub.acts and time.time() < deadline:
time.sleep(0.05)
self.assertIn(("button", "press"), self.stub.acts)
self.assertEqual([n for n in seen if n], []) # nobody was asked
rec = run.evidence["actions"][-1]
self.assertEqual(rec["by"], "fixture")
self.assertGreaterEqual(rec["took_s"], 0.5)
finally:
runner_mod.hw.button_lit = saved
def test_an_actuator_lost_after_the_takeover_is_said_out_loud(self):
"""Falling back to the operator without a word is how one dropped
actuator becomes a press nobody can account for afterwards."""